diff --git a/frontend/src/App.js b/frontend/src/App.js index 41be71a..6bec285 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,12 +1,15 @@ import React from 'react'; -import { Routes, Route } from 'react-router-dom'; +import { Routes, Route, useParams } from 'react-router-dom'; import { observer } from 'mobx-react-lite'; import styled from 'styled-components'; import { useAuthStore } from './stores/AuthStore'; +import { useUserStore } from './stores/UserStore'; import Layout from './components/common/Layout'; import Loading from './components/common/Loading'; import CitiesPage from './components/region/CitiesPage'; import CityDetailPage from './components/region/CityDetailPage'; +import ArticleDetailPage from './components/article/ArticleDetailPage'; +import ServiceDetailPage from './components/service/ServiceDetailPage'; const Container = styled.div` max-width: 1200px; @@ -41,6 +44,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> } /> @@ -48,6 +53,16 @@ function App() { ); } +const ArticleDetailPageWrapper = observer(() => { + const { articleId } = useParams(); + return ; +}); + +const ServiceDetailPageWrapper = observer(() => { + const { serviceId } = useParams(); + return ; +}); + const HomePage = observer(() => { return ( diff --git a/frontend/src/components/article/ArticleDetailPage.js b/frontend/src/components/article/ArticleDetailPage.js new file mode 100644 index 0000000..502a97d --- /dev/null +++ b/frontend/src/components/article/ArticleDetailPage.js @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { observer } from 'mobx-react-lite'; +import { useNavigate } from 'react-router-dom'; +import { useArticleStore } from '../../stores/ArticleStore'; +import { useInteractionStore } from '../../stores/InteractionStore'; +import Card from '../common/Card'; +import Loading from '../common/Loading'; +import ErrorMessage from '../common/ErrorMessage'; + +const Content = styled.div` + line-height: 1.8; + color: #333; + + h1, h2, h3 { + color: #2c3e50; + margin-top: 30px; + } + + p { + margin-bottom: 15px; + } +`; + +const Actions = styled.div` + display: flex; + gap: 10px; + margin: 20px 0; +`; + +const Button = styled.button` + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; + + &:hover { + opacity: 0.8; + } + + ${props => props.primary && ` + background: #667eea; + color: white; + `} + + ${props => props.secondary && ` + background: #6c757d; + color: white; + `} +`; + +const CommentsSection = styled.div` + margin-top: 40px; +`; + +const CommentForm = styled.form` + margin-bottom: 20px; + + textarea { + width: 100%; + min-height: 100px; + padding: 10px; + border: 1px solid #dee2e6; + border-radius: 5px; + resize: vertical; + } +`; + +const CommentList = styled.div` + display: flex; + flex-direction: column; + gap: 15px; +`; + +const CommentItem = styled.div` + background: #f8f9fa; + padding: 15px; + border-radius: 5px; +`; + +const ArticleDetailPage = observer(({ articleId }) => { + const articleStore = useArticleStore(); + const interactionStore = useInteractionStore(); + const navigate = useNavigate(); + const [comment, setComment] = useState(''); + const [liked, setLiked] = useState(false); + + useEffect(() => { + articleStore.fetchArticle(articleId); + articleStore.fetchArticleComments(articleId); + articleStore.fetchArticleStats(articleId); + }, [articleId, articleStore]); + + const handleLike = async () => { + const result = await articleStore.likeArticle(articleId); + if (result) { + setLiked(result.liked); + articleStore.fetchArticleStats(articleId); + } + }; + + const handleComment = async (e) => { + e.preventDefault(); + if (!comment.trim()) return; + + const result = await interactionStore.createComment('article', articleId, comment); + if (result.success) { + setComment(''); + articleStore.fetchArticleComments(articleId); + } + }; + + if (articleStore.loading) { + return ; + } + + if (articleStore.error) { + return ( + articleStore.error = null} + /> + ); + } + + if (!articleStore.currentArticle) { + return ; + } + + const article = articleStore.currentArticle; + + return ( +
+

{article.title}

+

作者: {article.author_username} | {article.article_type_display}

+ + + + + + + + + + +

评论 ({articleStore.currentArticle.comments_count})

+ + +