From 6b3fdce1d31bcceb3fd667f58a40645b7ff8de4e Mon Sep 17 00:00:00 2001 From: maoshen Date: Wed, 15 Apr 2026 05:16:32 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20React=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改进: - 新增 HomePage 组件,包含统计数据和内容展示 - 新增 ArticlesPage 和 ServicesPage 列表页,支持搜索和筛选 - 新增 UserProfilePage 个人中心页面 - 新增 NotFoundPage 404 页面 - 改进 Layout 组件,添加用户登录状态和动态导航 - 完善所有 Stores (AuthStore, UserStore, ArticleStore, ServiceStore) - 优化全局样式和响应式设计 - 添加环境变量配置 - 修复构建警告 技术栈: - React 18 + MobX + React Router v6 + Styled Components --- authentication/__init__.py | 0 authentication/admin.py | 3 + authentication/apps.py | 6 + authentication/migrations/__init__.py | 0 authentication/models.py | 3 + authentication/tests.py | 3 + authentication/views.py | 3 + frontend/src/App.js | 156 +----- .../components/article/ArticleDetailPage.js | 312 +++++++++--- .../src/components/article/ArticlesPage.js | 186 ++++++++ frontend/src/components/auth/RegisterPage.js | 55 ++- frontend/src/components/common/ChinaMap.js | 2 - frontend/src/components/common/Layout.js | 112 ++++- .../src/components/common/NotFoundPage.js | 85 ++++ frontend/src/components/home/HomePage.js | 220 +++++++++ .../src/components/region/CityDetailPage.js | 316 ++++++++----- .../components/service/ServiceDetailPage.js | 443 +++++++++++++----- .../src/components/service/ServicesPage.js | 201 ++++++++ .../src/components/user/UserProfilePage.js | 308 ++++++++++++ frontend/src/stores/ArticleStore.js | 298 +++++++----- frontend/src/stores/AuthStore.js | 102 +++- frontend/src/stores/ServiceStore.js | 304 +++++++----- frontend/src/stores/UserStore.js | 127 ++++- frontend/src/styles/global.js | 130 ++++- 24 files changed, 2680 insertions(+), 695 deletions(-) create mode 100644 authentication/__init__.py create mode 100644 authentication/admin.py create mode 100644 authentication/apps.py create mode 100644 authentication/migrations/__init__.py create mode 100644 authentication/models.py create mode 100644 authentication/tests.py create mode 100644 authentication/views.py create mode 100644 frontend/src/components/article/ArticlesPage.js create mode 100644 frontend/src/components/common/NotFoundPage.js create mode 100644 frontend/src/components/home/HomePage.js create mode 100644 frontend/src/components/service/ServicesPage.js create mode 100644 frontend/src/components/user/UserProfilePage.js diff --git a/authentication/__init__.py b/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authentication/admin.py b/authentication/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/authentication/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/authentication/apps.py b/authentication/apps.py new file mode 100644 index 0000000..8bab8df --- /dev/null +++ b/authentication/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'authentication' diff --git a/authentication/migrations/__init__.py b/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authentication/models.py b/authentication/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/authentication/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/authentication/tests.py b/authentication/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/authentication/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/authentication/views.py b/authentication/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/authentication/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/frontend/src/App.js b/frontend/src/App.js index d3eaebe..b874c8a 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,54 +1,29 @@ import React from 'react'; -import { Routes, Route, useParams, useNavigate } 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 { useRegionStore } from './stores/RegionStore'; import Layout from './components/common/Layout'; -import Loading from './components/common/Loading'; -import ChinaMap from './components/common/ChinaMap'; +import HomePage from './components/home/HomePage'; import CitiesPage from './components/region/CitiesPage'; import CityDetailPage from './components/region/CityDetailPage'; +import ArticlesPage from './components/article/ArticlesPage'; import ArticleDetailPage from './components/article/ArticleDetailPage'; +import ServicesPage from './components/service/ServicesPage'; import ServiceDetailPage from './components/service/ServiceDetailPage'; import LoginPage from './components/auth/LoginPage'; import RegisterPage from './components/auth/RegisterPage'; - -const Container = styled.div` - max-width: 1200px; - margin: 0 auto; - padding: 20px; -`; - -const Header = styled.header` - margin-bottom: 30px; - padding-bottom: 20px; - border-bottom: 2px solid #eee; -`; - -const Title = styled.h1` - margin: 0; - font-size: 28px; -`; +import UserProfilePage from './components/user/UserProfilePage'; +import NotFoundPage from './components/common/NotFoundPage'; function App() { - const authStore = useAuthStore(); - - // Fetch current user on app load - React.useEffect(() => { - if (authStore.isAuthenticated) { - authStore.fetchCurrentUser(); - } - }, [authStore]); - return ( } /> } /> - } /> + } /> + } /> } /> + } /> } /> } /> } /> @@ -59,6 +34,11 @@ function App() { ); } +const CityDetailPageWrapper = observer(() => { + const { regionId } = useParams(); + return ; +}); + const ArticleDetailPageWrapper = observer(() => { const { articleId } = useParams(); return ; @@ -69,110 +49,4 @@ const ServiceDetailPageWrapper = observer(() => { return ; }); -const HomePage = observer(() => { - const navigate = useNavigate(); - const regionStore = useRegionStore(); - const [loading, setLoading] = React.useState(false); - - const handleProvinceClick = async (geo) => { - const provinceName = geo.properties.name; - const provinceCode = geo.properties.code; - - setLoading(true); - try { - // 先获取所有省份列表,找到对应的 region ID - await regionStore.fetchProvinces(); - const province = regionStore.regions.find( - r => r.name === provinceName || r.code === provinceCode - ); - - if (province) { - navigate(`/cities/${province.id}`); - } else { - // 如果没有找到,跳转到城市列表页并带上省份名称 - navigate(`/cities?province=${encodeURIComponent(provinceName)}`); - } - } catch (error) { - console.error('Failed to navigate to province:', error); - } finally { - setLoading(false); - } - }; - - return ( - -
- 欢迎来到城市手册 -

探索每个城市的故事与特色

-
- - {loading ? ( - - ) : ( - - )} - -
-

📚 最新文章

-

即将推出...

-
-
- ); -}); - -const UserProfilePage = observer(() => { - const authStore = useAuthStore(); - const userStore = useUserStore(); - - React.useEffect(() => { - if (authStore.isAuthenticated) { - userStore.fetchCurrentUser(); - } - }, [authStore, userStore]); - - if (!authStore.isAuthenticated) { - return ( - -

请先登录

-
- ); - } - - if (userStore.loading) { - return ; - } - - return ( - -
- 个人中心 -
- {userStore.user && ( -
-

用户信息

-

用户名: {userStore.user.username}

-

邮箱: {userStore.user.email}

-

角色: {userStore.user.role_display}

- -

统计

-

文章数: {userStore.user.articles_count}

-

服务数: {userStore.user.services_count}

-

评论数: {userStore.user.comments_count}

-

点赞数: {userStore.user.likes_count}

-

收藏数: {userStore.user.favorites_count}

-
- )} -
- ); -}); - -const NotFoundPage = () => ( - -
- 404 -
-

页面未找到

-
-); - -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/components/article/ArticleDetailPage.js b/frontend/src/components/article/ArticleDetailPage.js index 502a97d..ddd9154 100644 --- a/frontend/src/components/article/ArticleDetailPage.js +++ b/frontend/src/components/article/ArticleDetailPage.js @@ -3,68 +3,179 @@ 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 { useAuthStore } from '../../stores/AuthStore'; import Loading from '../common/Loading'; import ErrorMessage from '../common/ErrorMessage'; +import Card from '../common/Card'; + +const Container = styled.div` + max-width: 900px; + margin: 0 auto; + padding: 20px; +`; + +const Header = styled.div` + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 2px solid #eee; +`; + +const Title = styled.h1` + font-size: 32px; + margin: 0 0 15px; + color: #333; + line-height: 1.4; +`; + +const Meta = styled.div` + display: flex; + gap: 20px; + color: #666; + font-size: 14px; + flex-wrap: wrap; +`; + +const MetaItem = styled.span` + display: flex; + align-items: center; + gap: 5px; +`; const Content = styled.div` line-height: 1.8; color: #333; + font-size: 16px; - h1, h2, h3 { + h1, h2, h3, h4 { color: #2c3e50; - margin-top: 30px; + margin: 30px 0 15px; } p { margin-bottom: 15px; } + + img { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 20px 0; + } + + ul, ol { + margin: 15px 0; + padding-left: 30px; + } + + blockquote { + border-left: 4px solid #667eea; + padding-left: 20px; + margin: 20px 0; + color: #666; + font-style: italic; + } + + code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 14px; + } + + pre { + background: #2d2d2d; + color: #f8f8f2; + padding: 20px; + border-radius: 8px; + overflow-x: auto; + margin: 20px 0; + + code { + background: none; + padding: 0; + color: inherit; + } + } `; const Actions = styled.div` display: flex; - gap: 10px; - margin: 20px 0; + gap: 15px; + margin: 30px 0; + padding: 20px 0; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; `; -const Button = styled.button` +const ActionButton = styled.button` padding: 10px 20px; border: none; - border-radius: 5px; + border-radius: 6px; cursor: pointer; font-size: 14px; + font-weight: 500; transition: all 0.2s; + display: flex; + align-items: center; + gap: 8px; - &:hover { - opacity: 0.8; - } - - ${props => props.primary && ` + ${(props) => + props.primary && + ` background: #667eea; color: white; + + &:hover { + background: #5568d3; + } `} - ${props => props.secondary && ` - background: #6c757d; - color: white; + ${(props) => + props.secondary && + ` + background: #f8f9fa; + color: #667eea; + border: 1px solid #667eea; + + &:hover { + background: #e9ecef; + } `} + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } `; const CommentsSection = styled.div` margin-top: 40px; `; +const SectionTitle = styled.h2` + font-size: 20px; + margin: 0 0 20px; + color: #333; +`; + const CommentForm = styled.form` - margin-bottom: 20px; + margin-bottom: 30px; textarea { width: 100%; min-height: 100px; - padding: 10px; + padding: 15px; border: 1px solid #dee2e6; - border-radius: 5px; + border-radius: 8px; resize: vertical; + font-family: inherit; + font-size: 14px; + + &:focus { + outline: none; + border-color: #667eea; + } } `; @@ -76,39 +187,70 @@ const CommentList = styled.div` const CommentItem = styled.div` background: #f8f9fa; - padding: 15px; - border-radius: 5px; + padding: 20px; + border-radius: 8px; + + .author { + font-weight: 600; + color: #333; + margin-bottom: 8px; + } + + .content { + color: #555; + line-height: 1.6; + margin-bottom: 10px; + } + + .date { + color: #999; + font-size: 12px; + } `; const ArticleDetailPage = observer(({ articleId }) => { - const articleStore = useArticleStore(); - const interactionStore = useInteractionStore(); const navigate = useNavigate(); + const articleStore = useArticleStore(); + const authStore = useAuthStore(); const [comment, setComment] = useState(''); const [liked, setLiked] = useState(false); useEffect(() => { articleStore.fetchArticle(articleId); - articleStore.fetchArticleComments(articleId); - articleStore.fetchArticleStats(articleId); }, [articleId, articleStore]); const handleLike = async () => { + if (!authStore.isAuthenticated) { + navigate('/login'); + return; + } const result = await articleStore.likeArticle(articleId); - if (result) { - setLiked(result.liked); - articleStore.fetchArticleStats(articleId); + if (result.success) { + setLiked(!liked); + articleStore.fetchArticle(articleId); } }; - const handleComment = async (e) => { - e.preventDefault(); - if (!comment.trim()) return; - - const result = await interactionStore.createComment('article', articleId, comment); + const handleFavorite = async () => { + if (!authStore.isAuthenticated) { + navigate('/login'); + return; + } + const result = await articleStore.favoriteArticle(articleId); if (result.success) { - setComment(''); - articleStore.fetchArticleComments(articleId); + alert('已收藏'); + } + }; + + const handleShare = () => { + if (navigator.share) { + navigator.share({ + title: articleStore.currentArticle?.title, + url: window.location.href, + }); + } else { + navigator.clipboard.writeText(window.location.href); + alert('链接已复制到剪贴板'); } }; @@ -120,7 +262,7 @@ const ArticleDetailPage = observer(({ articleId }) => { return ( articleStore.error = null} + onDismiss={() => articleStore.clearError()} /> ); } @@ -132,52 +274,82 @@ const ArticleDetailPage = observer(({ articleId }) => { const article = articleStore.currentArticle; return ( -
-

{article.title}

-

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

+ +
+ {article.title} + + + 👤 {article.author_username || '匿名用户'} + + + 📍 {article.region_name || '未知地区'} + + + 👁 {article.views || 0} 阅读 + + + ❤️ {article.likes_count || 0} 点赞 + + + 💬 {article.comments_count || 0} 评论 + + +
- + - - - + + {liked ? '❤️' : '🤍'} {liked ? '已点赞' : '点赞'} + + + ⭐ 收藏 + + + 🔗 分享 + -

评论 ({articleStore.currentArticle.comments_count})

+ 💬 评论 ({article.comments_count || 0}) - -