From 8d4eda17e0304154af395900ad26d70f18e604ff Mon Sep 17 00:00:00 2001 From: mashen Date: Thu, 9 Apr 2026 13:48:21 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1=E8=AF=A6=E6=83=85=E9=A1=B5=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArticleDetailPage(文章详情、评论、点赞) - ServiceDetailPage(服务详情、评分、评论、点赞) - 更新 App.js 路由配置 - 集成 ArticleStore 和 ServiceStore --- frontend/src/App.js | 17 +- .../components/article/ArticleDetailPage.js | 183 ++++++++++++++++ .../components/service/ServiceDetailPage.js | 204 ++++++++++++++++++ 3 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/article/ArticleDetailPage.js create mode 100644 frontend/src/components/service/ServiceDetailPage.js 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})

+ + +