Merge remote-tracking branch 'refs/remotes/origin/master'
Change-Id: I0879b29d705017b7d6df0ddaae7b1c4d56fa0a53
diff --git a/Merge/front/src/App.jsx b/Merge/front/src/App.jsx
index 3ab9fca..6dab985 100644
--- a/Merge/front/src/App.jsx
+++ b/Merge/front/src/App.jsx
@@ -1,19 +1,44 @@
-import React from 'react';
-import Header from './components/Header';
-import Sidebar from './components/Sidebar';
-import AppRoutes from './router/App';
-import './App.css';
+// src/App.jsx
+import React, { useState, useEffect } from 'react'
+import { useLocation } from 'react-router-dom'
+import Header from './components/Header'
+import Sidebar from './components/Sidebar'
+import AppRoutes from './router/App'
+import { getUserInfo } from './utils/auth'
+import './App.css'
export default function App() {
+ const location = useLocation()
+
+ // 初始 role = null (未登录或刚进来时)
+ const [role, setRole] = useState(null)
+
+ // 每次路由变化(含登录後 navigate),都重新从 storage 读一遍 userInfo
+ useEffect(() => {
+ const u = getUserInfo()
+ setRole(u?.role || null)
+ }, [location.pathname])
+
+ // 只有普通 user 才显示侧边栏
+ const showSidebar = role === 'user'
+
return (
<div className="app">
<Header />
- <Sidebar />
- <main className="main-content">
+
+ {showSidebar && <Sidebar />}
+
+ <main
+ className="main-content"
+ style={{
+ // 没侧边栏时去掉左边距
+ marginLeft: showSidebar ? undefined : 0
+ }}
+ >
<div className="content-wrapper">
<AppRoutes />
</div>
</main>
</div>
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/Merge/front/src/api/posts_wzy.js b/Merge/front/src/api/posts_wzy.js
index b449c43..d901500 100644
--- a/Merge/front/src/api/posts_wzy.js
+++ b/Merge/front/src/api/posts_wzy.js
@@ -1,15 +1,25 @@
// src/api/posts.js
-const BASE = 'http://10.126.59.25:5714/' // 如果有代理可以留空,否则填完整域名,如 'http://localhost:3000'
+const BASE = 'http://10.126.59.25:5714' // 如果有代理可以留空,否则填完整域名,如 'http://localhost:3000'
/**
- * 获取所有已发布的帖子列表
- * GET /posts
+ * 获取帖子列表
+ * - GET /posts
+ * - GET /posts?user_id=123
+ *
+ * @param {number?} userId 可选,传了就加 ?user_id= 用户 ID
+ * @returns Promise<[{ id, title, status, heat, created_at }, …]>
*/
-export async function fetchPosts() {
- const res = await fetch(`${BASE}/posts`)
- if (!res.ok) throw new Error(`fetchPosts: ${res.status}`)
- console.log('fetchPosts response:', res) // debug: inspect response
- return res.json() // 返回 [ { id, title, heat, created_at }, … ]
+export async function fetchPosts(userId) {
+ // 自动拼接 query
+ const url = userId != null
+ ? `${BASE}/posts?user_id=${encodeURIComponent(userId)}`
+ : `${BASE}/posts`
+
+ const res = await fetch(url)
+ if (!res.ok) {
+ throw new Error(`fetchPosts${userId != null ? `(user ${userId})` : ''}: ${res.status}`)
+ }
+ return res.json()
}
/**
diff --git a/Merge/front/src/components/Admin.js b/Merge/front/src/components/Admin.js
index 2d97495..f73e239 100644
--- a/Merge/front/src/components/Admin.js
+++ b/Merge/front/src/components/Admin.js
@@ -1,21 +1,32 @@
-import 'antd/dist/antd.css';
-import React, { useState, useEffect, useMemo, useCallback } from 'react';
-import { Layout, Tabs, Input, List, Card, Button, Tag, Spin, Typography, Divider } from 'antd';
-import '../style/Admin.css';
-import { fetchPosts, approvePost, rejectPost } from '../api/posts_trm';
+// src/components/Admin.jsx
+import React, { useState, useEffect, useCallback } from 'react'
+import { useParams } from 'react-router-dom'
+import 'antd/dist/antd.css'
+import {
+ Layout,
+ Tabs,
+ Input,
+ List,
+ Card,
+ Button,
+ Tag,
+ Spin,
+ Typography,
+ Divider
+} from 'antd'
+import '../style/Admin.css'
+import { fetchPosts, approvePost, rejectPost } from '../api/posts_trm'
-export default function Admin() {
- const ADMIN_USER_ID = 2;
- const [posts, setPosts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [hasPermission, setHasPermission] = useState(true);
- const [activeTab, setActiveTab] = useState('all');
- const [selectedPost, setSelectedPost] = useState(null);
- const [searchTerm, setSearchTerm] = useState('');
-
- // 新增:拖拽相关状态
- const [leftPanelWidth, setLeftPanelWidth] = useState(300);
- const [isResizing, setIsResizing] = useState(false);
+export default function AdminPage() {
+ const { userId } = useParams() // ← 从路由拿到
+ const [posts, setPosts] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [hasPermission, setHasPermission] = useState(true)
+ const [activeTab, setActiveTab] = useState('all')
+ const [selectedPost, setSelectedPost] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [leftPanelWidth, setLeftPanelWidth] = useState(300)
+ const [isResizing, setIsResizing] = useState(false)
const statusColors = {
draft: 'orange',
@@ -23,12 +34,12 @@
published: 'green',
deleted: 'gray',
rejected: 'red'
- };
+ }
useEffect(() => {
async function load() {
try {
- const list = await fetchPosts(ADMIN_USER_ID)
+ const list = await fetchPosts(userId) // ← 传入 userId
setPosts(list)
} catch (e) {
if (e.message === 'Unauthorized') {
@@ -41,10 +52,9 @@
}
}
load()
- }, [])
+ }, [userId])
- // 过滤并排序
- const sortedPosts = useMemo(() => {
+ const sortedPosts = React.useMemo(() => {
return [...posts].sort((a, b) => {
if (a.status === 'pending' && b.status !== 'pending') return -1
if (b.status === 'pending' && a.status !== 'pending') return 1
@@ -52,18 +62,10 @@
})
}, [posts])
- // 调整:根据 activeTab 及搜索关键词过滤
- const filteredPosts = useMemo(() => {
- let list
- switch (activeTab) {
- case 'pending':
- list = sortedPosts.filter(p => p.status === 'pending'); break
- case 'published':
- list = sortedPosts.filter(p => p.status === 'published'); break
- case 'rejected':
- list = sortedPosts.filter(p => p.status === 'rejected'); break
- default:
- list = sortedPosts
+ const filteredPosts = React.useMemo(() => {
+ let list = sortedPosts
+ if (activeTab !== 'all') {
+ list = sortedPosts.filter(p => p.status === activeTab)
}
return list.filter(p =>
p.title.toLowerCase().includes(searchTerm.toLowerCase())
@@ -71,84 +73,101 @@
}, [sortedPosts, activeTab, searchTerm])
const handleApprove = async id => {
- await approvePost(id, ADMIN_USER_ID)
- setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'published' } : x))
- // 同步更新选中的帖子状态
+ await approvePost(id, userId) // ← 传入 userId
+ setPosts(ps =>
+ ps.map(x => (x.id === id ? { ...x, status: 'published' } : x))
+ )
if (selectedPost?.id === id) {
- setSelectedPost(prev => ({ ...prev, status: 'published' }));
+ setSelectedPost(prev => ({ ...prev, status: 'published' }))
}
}
- const handleReject = async id => {
- await rejectPost(id, ADMIN_USER_ID)
- setPosts(ps => ps.map(x => x.id === id ? { ...x, status: 'rejected' } : x))
- // 同步更新选中的帖子状态
- if (selectedPost?.id === id) {
- setSelectedPost(prev => ({ ...prev, status: 'rejected' }));
- }
- }
- const handleSelect = post => setSelectedPost(post)
- // 修复:拖拽处理函数
- const handleMouseMove = useCallback((e) => {
- if (!isResizing) return;
-
- const newWidth = e.clientX;
- const minWidth = 200;
- const maxWidth = window.innerWidth - 300;
-
- if (newWidth >= minWidth && newWidth <= maxWidth) {
- setLeftPanelWidth(newWidth);
+ const handleReject = async id => {
+ await rejectPost(id, userId) // ← 传入 userId
+ setPosts(ps =>
+ ps.map(x => (x.id === id ? { ...x, status: 'rejected' } : x))
+ )
+ if (selectedPost?.id === id) {
+ setSelectedPost(prev => ({ ...prev, status: 'rejected' }))
}
- }, [isResizing]);
+ }
+
+ const handleSelect = post => setSelectedPost(post)
+
+ const handleMouseMove = useCallback(
+ e => {
+ if (!isResizing) return
+ const newWidth = e.clientX
+ const minWidth = 200
+ const maxWidth = window.innerWidth - 300
+ if (newWidth >= minWidth && newWidth <= maxWidth) {
+ setLeftPanelWidth(newWidth)
+ }
+ },
+ [isResizing]
+ )
const handleMouseUp = useCallback(() => {
- setIsResizing(false);
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- }, [handleMouseMove]);
+ setIsResizing(false)
+ document.removeEventListener('mousemove', handleMouseMove)
+ document.removeEventListener('mouseup', handleMouseUp)
+ document.body.style.cursor = ''
+ document.body.style.userSelect = ''
+ }, [handleMouseMove])
- const handleMouseDown = useCallback((e) => {
- e.preventDefault();
- setIsResizing(true);
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = 'col-resize';
- document.body.style.userSelect = 'none';
- }, [handleMouseMove, handleMouseUp]);
+ const handleMouseDown = useCallback(
+ e => {
+ e.preventDefault()
+ setIsResizing(true)
+ document.addEventListener('mousemove', handleMouseMove)
+ document.addEventListener('mouseup', handleMouseUp)
+ document.body.style.cursor = 'col-resize'
+ document.body.style.userSelect = 'none'
+ },
+ [handleMouseMove, handleMouseUp]
+ )
- // 新增:组件卸载时清理事件监听器
useEffect(() => {
return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- };
- }, [handleMouseMove, handleMouseUp]);
+ document.removeEventListener('mousemove', handleMouseMove)
+ document.removeEventListener('mouseup', handleMouseUp)
+ document.body.style.cursor = ''
+ document.body.style.userSelect = ''
+ }
+ }, [handleMouseMove, handleMouseUp])
- if (loading) return <Spin spinning tip="加载中…" style={{ width: '100%', marginTop: 100 }} />;
- if (!hasPermission) return <div style={{ textAlign: 'center', marginTop: 100 }}>权限不足</div>;
+ if (loading)
+ return (
+ <Spin
+ spinning
+ tip="加载中…"
+ style={{ width: '100%', marginTop: 100 }}
+ />
+ )
+ if (!hasPermission)
+ return (
+ <div style={{ textAlign: 'center', marginTop: 100 }}>
+ 权限不足
+ </div>
+ )
- const { Content } = Layout;
- const { TabPane } = Tabs;
- const { Title, Text } = Typography;
+ const { Content } = Layout
+ const { TabPane } = Tabs
+ const { Title, Text } = Typography
return (
<div style={{ height: '100vh', display: 'flex' }}>
- {/* 左侧面板 */}
- <div
- style={{
+ <div
+ style={{
width: leftPanelWidth,
- background: '#fff',
+ background: '#fff',
padding: 16,
borderRight: '1px solid #f0f0f0',
overflow: 'hidden'
}}
>
<div style={{ marginBottom: 24 }}>
- <Title level={3}>小红书</Title>
+ <Title level={3}>小红书 管理</Title>
<Input.Search
placeholder="搜索帖子标题..."
value={searchTerm}
@@ -156,26 +175,38 @@
enterButton
/>
</div>
- <Tabs activeKey={activeTab} onChange={key => { setActiveTab(key); setSelectedPost(null); }}>
+ <Tabs
+ activeKey={activeTab}
+ onChange={key => {
+ setActiveTab(key)
+ setSelectedPost(null)
+ }}
+ >
<TabPane tab="全部" key="all" />
<TabPane tab="待审核" key="pending" />
<TabPane tab="已通过" key="published" />
<TabPane tab="已驳回" key="rejected" />
</Tabs>
- <div style={{ height: 'calc(100vh - 200px)', overflow: 'auto' }}>
+ <div
+ style={{
+ height: 'calc(100vh - 200px)',
+ overflow: 'auto'
+ }}
+ >
<List
dataSource={filteredPosts}
pagination={{
pageSize: 5,
showSizeChanger: true,
- pageSizeOptions: ['5','10','20'],
+ pageSizeOptions: ['5', '10', '20'],
onChange: () => setSelectedPost(null)
}}
renderItem={p => (
<List.Item
key={p.id}
style={{
- background: selectedPost?.id === p.id ? '#e6f7ff' : '',
+ background:
+ selectedPost?.id === p.id ? '#e6f7ff' : '',
cursor: 'pointer',
marginBottom: 8
}}
@@ -187,32 +218,38 @@
<img
src={p.thumbnail}
alt=""
- style={{ width: 64, height: 64, objectFit: 'cover' }}
+ style={{
+ width: 64,
+ height: 64,
+ objectFit: 'cover'
+ }}
/>
)
}
title={p.title}
- description={`${p.createdAt} · ${p.author} · ${p.likes || 0}赞`}
+ description={`${p.createdAt} · ${p.author} · ${
+ p.likes || 0
+ }赞`}
/>
- <Tag color={statusColors[p.status]}>{p.status}</Tag>
+ <Tag color={statusColors[p.status]}>
+ {p.status}
+ </Tag>
</List.Item>
)}
/>
</div>
</div>
- {/* 拖拽分割条 */}
<div
style={{
width: 5,
cursor: 'col-resize',
background: isResizing ? '#1890ff' : '#f0f0f0',
- transition: isResizing ? 'none' : 'background-color 0.2s',
position: 'relative',
flexShrink: 0
}}
onMouseDown={handleMouseDown}
- onSelectStart={(e) => e.preventDefault()}
+ onSelectStart={e => e.preventDefault()}
>
<div
style={{
@@ -228,41 +265,111 @@
/>
</div>
- {/* 右侧内容区域 */}
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
- <Content style={{ padding: 24, background: '#fff', overflow: 'auto' }}>
+ <div
+ style={{
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column'
+ }}
+ >
+ <Content
+ style={{
+ padding: 24,
+ background: '#fff',
+ overflow: 'auto'
+ }}
+ >
{selectedPost ? (
<Card
- cover={selectedPost.image && <img alt="cover" src={selectedPost.image} />}
+ cover={
+ selectedPost.image && (
+ <img
+ alt="cover"
+ src={selectedPost.image}
+ />
+ )
+ }
title={selectedPost.title}
extra={
<div>
{selectedPost.status === 'pending' && (
<>
- <Button type="primary" onClick={() => handleApprove(selectedPost.id)}>通过</Button>
- <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
+ <Button
+ type="primary"
+ onClick={() =>
+ handleApprove(selectedPost.id)
+ }
+ >
+ 通过
+ </Button>
+ <Button
+ danger
+ onClick={() =>
+ handleReject(selectedPost.id)
+ }
+ >
+ 驳回
+ </Button>
</>
)}
{selectedPost.status === 'published' && (
- <Button danger onClick={() => handleReject(selectedPost.id)}>驳回</Button>
+ <Button
+ danger
+ onClick={() =>
+ handleReject(selectedPost.id)
+ }
+ >
+ 驳回
+ </Button>
)}
{selectedPost.status === 'rejected' && (
<>
- <Button onClick={() => {
- setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'pending' } : x));
- setSelectedPost(prev => ({ ...prev, status: 'pending' }));
- }}>恢复待审</Button>
- <Button onClick={() => {
- setPosts(ps => ps.map(x => x.id === selectedPost.id ? { ...x, status: 'published' } : x));
- setSelectedPost(prev => ({ ...prev, status: 'published' }));
- }}>恢复已发</Button>
+ <Button
+ onClick={() => {
+ setPosts(ps =>
+ ps.map(x =>
+ x.id === selectedPost.id
+ ? { ...x, status: 'pending' }
+ : x
+ )
+ )
+ setSelectedPost(prev => ({
+ ...prev,
+ status: 'pending'
+ }))
+ }}
+ >
+ 恢复待审
+ </Button>
+ <Button
+ onClick={() => {
+ setPosts(ps =>
+ ps.map(x =>
+ x.id === selectedPost.id
+ ? {
+ ...x,
+ status: 'published'
+ }
+ : x
+ )
+ )
+ setSelectedPost(prev => ({
+ ...prev,
+ status: 'published'
+ }))
+ }}
+ >
+ 恢复已发
+ </Button>
</>
)}
</div>
}
>
<Text type="secondary">
- {`${selectedPost.createdAt} · ${selectedPost.author} · ${selectedPost.likes || 0}赞`}
+ {`${selectedPost.createdAt} · ${selectedPost.author} · ${
+ selectedPost.likes || 0
+ }赞`}
</Text>
<Divider />
<p>{selectedPost.content}</p>
@@ -274,10 +381,12 @@
</ul>
</Card>
) : (
- <Text type="secondary">请选择左侧列表中的帖子查看详情</Text>
+ <Text type="secondary">
+ 请选择左侧列表中的帖子查看详情
+ </Text>
)}
</Content>
</div>
</div>
- );
+)
}
diff --git a/Merge/front/src/components/CreatePost.jsx b/Merge/front/src/components/CreatePost.jsx
index 9817ac0..1d2f306 100644
--- a/Merge/front/src/components/CreatePost.jsx
+++ b/Merge/front/src/components/CreatePost.jsx
@@ -1,5 +1,3 @@
-// src/components/CreatePost.jsx
-
import React, { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import UploadPage from './UploadPage'
@@ -8,6 +6,7 @@
updatePost,
fetchPost as fetchPostDetail
} from '../api/posts_wzy'
+import { getUserInfo } from '../utils/auth'
import '../style/CreatePost.css'
export default function CreatePost() {
@@ -36,6 +35,10 @@
{ id: 3, name: '我染上了拼豆' },
]
+ // 获取当前登录用户id
+ const user = getUserInfo()
+ const currentUserId = user?.id
+
// 编辑模式:拉取原帖数据填入
useEffect(() => {
if (!isEdit) return
@@ -68,6 +71,10 @@
setError('标题和正文必填')
return
}
+ if (!currentUserId) {
+ setError('未获取到用户ID,请重新登录')
+ return
+ }
setError(null)
try {
if (isEdit) {
@@ -81,7 +88,7 @@
alert('更新成功!')
} else {
await createPost({
- user_id: 1,
+ user_id: currentUserId,
topic_id: topicId || undefined,
title: title.trim(),
content: content.trim(),
diff --git a/Merge/front/src/components/Header.jsx b/Merge/front/src/components/Header.jsx
index 3b21c98..96ae6ac 100644
--- a/Merge/front/src/components/Header.jsx
+++ b/Merge/front/src/components/Header.jsx
@@ -1,13 +1,21 @@
+// src/components/Header.jsx
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { User } from 'lucide-react'
-import '../App.css' // 或者单独的 Header.css
+import { getUserInfo } from '../utils/auth'
+import '../App.css'
export default function Header() {
const navigate = useNavigate()
+ const user = getUserInfo() || {}
+ const userId = user.id
+ // 假设后端返回的 user 对象里有个 nickname 字段,否则 fallback 到 “小红薯”
+ const displayName = user.nickname || user.username || '小红薯'
const handleUserClick = () => {
- navigate('/user/1') // 或者使用实际的用户ID
+ if (userId) {
+ navigate(`/user/${userId}`)
+ }
}
return (
@@ -16,16 +24,19 @@
<div className="logo">小红书</div>
<h1 className="header-title">创作服务平台</h1>
</div>
- <div
+ <div
className="header-right"
onClick={handleUserClick}
- style={{ cursor: 'pointer' }}
+ style={{ cursor: userId ? 'pointer' : 'default' }}
>
<div className="user-info">
<User size={16} />
- <span>小红薯1</span>
+ <span>
+ {displayName}
+ {userId ? userId : ''}
+ </span>
</div>
</div>
</header>
)
-}
\ No newline at end of file
+}
diff --git a/Merge/front/src/components/NotebookPage.jsx b/Merge/front/src/components/NotebookPage.jsx
index 25264ec..4214213 100644
--- a/Merge/front/src/components/NotebookPage.jsx
+++ b/Merge/front/src/components/NotebookPage.jsx
@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { fetchPosts, deletePost } from '../api/posts_wzy'
+import { getUserInfo } from '../utils/auth' // ← 导入 getUserInfo
import '../style/NotebookPage.css'
export default function NotebookPage() {
@@ -11,13 +12,20 @@
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
- // TODO: 替换成真实用户 ID
- const currentUserId = 2
+ // 从 auth 获取当前用户信息
+ const userInfo = getUserInfo()
+ const currentUserId = userInfo?.id
useEffect(() => {
+ if (!currentUserId) {
+ setError('未获取到用户信息,无法加载帖子。')
+ setLoading(false)
+ return
+ }
+
async function load() {
try {
- // GET /posts?user_id=1
+ // GET /posts?user_id=currentUserId
const list = await fetchPosts(currentUserId)
setPosts(list)
} catch (e) {
@@ -27,7 +35,7 @@
}
}
load()
- }, [])
+ }, [currentUserId])
async function handleDelete(id) {
if (!window.confirm('确定要删除该帖子吗?')) return
@@ -41,7 +49,6 @@
}
function handleEdit(id) {
- // 假设你在路由里挂载了 /posts/edit/:postId
navigate(`/posts/edit/${id}`)
}
diff --git a/Merge/front/src/components/RequireAuth.jsx b/Merge/front/src/components/RequireAuth.jsx
new file mode 100644
index 0000000..e217e4f
--- /dev/null
+++ b/Merge/front/src/components/RequireAuth.jsx
@@ -0,0 +1,13 @@
+// src/components/RequireAuth.jsx
+import React from 'react'
+import { Navigate, useLocation } from 'react-router-dom'
+import { isLoggedIn } from '../utils/auth'
+
+export function RequireAuth({ children }) {
+ const location = useLocation()
+ if (!isLoggedIn()) {
+ // 未登录跳到 /login,并保存当前尝试访问的地址
+ return <Navigate to="/login" replace state={{ from: location }} />
+ }
+ return children
+}
diff --git a/Merge/front/src/components/RequireRole.jsx b/Merge/front/src/components/RequireRole.jsx
new file mode 100644
index 0000000..75a4c28
--- /dev/null
+++ b/Merge/front/src/components/RequireRole.jsx
@@ -0,0 +1,17 @@
+// src/components/RequireRole.jsx
+import React from 'react'
+import { Navigate } from 'react-router-dom'
+import { getUserInfo, isLoggedIn } from '../utils/auth'
+
+export function RequireRole({ role, children }) {
+ if (!isLoggedIn()) {
+ // 未登录
+ return <Navigate to="/login" replace />
+ }
+ const user = getUserInfo()
+ if (user.role !== role) {
+ // 角色不匹配,回首页
+ return <Navigate to="/" replace />
+ }
+ return children
+}
diff --git a/Merge/front/src/components/Sidebar.jsx b/Merge/front/src/components/Sidebar.jsx
index 26118b2..92bc8f1 100644
--- a/Merge/front/src/components/Sidebar.jsx
+++ b/Merge/front/src/components/Sidebar.jsx
@@ -24,10 +24,10 @@
{ id: 'fans', label: '粉丝数据', path: '/dashboard/fans' },
]
},
- { id: 'activity', label: '活动中心', icon: Activity, path: '/activity' },
- { id: 'notes', label: '笔记灵感', icon: BookOpen, path: '/notes' },
- { id: 'creator', label: '创作学院', icon: Users, path: '/creator' },
- { id: 'journal', label: '创作日刊', icon: BookOpen, path: '/journal' },
+ // { id: 'activity', label: '活动中心', icon: Activity, path: '/activity' },
+ // { id: 'notes', label: '笔记灵感', icon: BookOpen, path: '/notes' },
+ // { id: 'creator', label: '创作学院', icon: Users, path: '/creator' },
+ // { id: 'journal', label: '创作日刊', icon: BookOpen, path: '/journal' },
]
export default function Sidebar() {
diff --git a/Merge/front/src/components/SuperAdmin.js b/Merge/front/src/components/SuperAdmin.js
index f24e5d4..0fa6722 100644
--- a/Merge/front/src/components/SuperAdmin.js
+++ b/Merge/front/src/components/SuperAdmin.js
@@ -1,33 +1,48 @@
-import React, { useState, useEffect } from 'react';
-import { NavLink, Outlet } from 'react-router-dom';
-import { Spin } from 'antd';
-import { fetchUserList } from '../api/posts_trm';
-import '../style/SuperAdmin.css';
+// src/components/SuperAdmin.jsx
+import React, { useState, useEffect } from 'react'
+import { NavLink, Outlet, useParams } from 'react-router-dom'
+import { Spin } from 'antd'
+import { fetchUserList } from '../api/posts_trm'
+import '../style/SuperAdmin.css'
export default function SuperAdmin() {
- const SUPERADMIN_USER_ID = 3;
- const [loading, setLoading] = useState(true);
- const [hasPermission, setHasPermission] = useState(true);
+ const { userId } = useParams() // ← 从路由拿到
+ const [loading, setLoading] = useState(true)
+ const [hasPermission, setHasPermission] = useState(true)
useEffect(() => {
async function check() {
try {
- await fetchUserList(SUPERADMIN_USER_ID);
+ await fetchUserList(userId) // ← 传入 userId
} catch (e) {
if (e.message === 'Unauthorized') {
- setHasPermission(false);
+ setHasPermission(false)
} else {
- console.error(e);
+ console.error(e)
}
} finally {
- setLoading(false);
+ setLoading(false)
}
}
- check();
- }, []);
+ check()
+ }, [userId])
- if (loading) return <Spin spinning tip="加载中…" style={{ width: '100%', marginTop: 100 }} />;
- if (!hasPermission) return <div style={{ textAlign: 'center', marginTop: 100 }}>权限不足</div>;
+ if (loading) {
+ return (
+ <Spin
+ spinning
+ tip="加载中…"
+ style={{ width: '100%', marginTop: 100 }}
+ />
+ )
+ }
+ if (!hasPermission) {
+ return (
+ <div style={{ textAlign: 'center', marginTop: 100 }}>
+ 权限不足
+ </div>
+ )
+ }
return (
<div className="super-admin-container">
@@ -36,18 +51,22 @@
<nav>
<ul>
<li>
- <NavLink
- to="users"
- end
- className={({ isActive }) => isActive ? 'active' : ''}
+ <NavLink
+ to="users"
+ end
+ className={({ isActive }) =>
+ isActive ? 'active' : ''
+ }
>
用户管理
</NavLink>
</li>
<li>
- <NavLink
- to="dashboard"
- className={({ isActive }) => isActive ? 'active' : ''}
+ <NavLink
+ to="dashboard"
+ className={({ isActive }) =>
+ isActive ? 'active' : ''
+ }
>
平台运行监控
</NavLink>
@@ -60,5 +79,5 @@
<Outlet />
</main>
</div>
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/Merge/front/src/components/UserManagement.js b/Merge/front/src/components/UserManagement.js
index a48f8cf..bed6b8f 100644
--- a/Merge/front/src/components/UserManagement.js
+++ b/Merge/front/src/components/UserManagement.js
@@ -1,51 +1,77 @@
-import React, { useState, useEffect } from 'react';
-import '../style/Admin.css';
-import { Select, message, Table } from 'antd';
-import { fetchUserList, giveUser, giveAdmin, giveSuperAdmin } from '../api/posts_trm';
+// src/components/UserManagement.jsx
-const { Option } = Select;
-const ROLE_LIST = ['用户', '管理员', '超级管理员'];
+import React, { useState, useEffect } from 'react'
+import { useParams } from 'react-router-dom'
+import { Table, Select, message } from 'antd'
+import {
+ fetchUserList,
+ giveUser,
+ giveAdmin,
+ giveSuperAdmin
+} from '../api/posts_trm'
+import '../style/Admin.css'
-function UserManagement({ superAdminId }) {
- const [users, setUsers] = useState([]);
+const { Option } = Select
+const ROLE_LIST = ['用户', '管理员', '超级管理员']
+
+export default function UserManagement() {
+ // 直接从 URL 参数里拿到 superAdminId
+ const { userId: superAdminId } = useParams()
+
+ const [users, setUsers] = useState([])
useEffect(() => {
async function load() {
try {
- const data = superAdminId
- ? await fetchUserList(superAdminId)
- : await fetch('/api/users').then(res => res.json());
- setUsers(data);
+ // 调用接口获取用户列表
+ const data = await fetchUserList(superAdminId)
+ setUsers(data)
} catch (e) {
- console.error(e);
+ console.error(e)
+ message.error('获取用户列表失败:' + e.message)
}
}
- load();
- }, [superAdminId]);
- // handle role changes
+ // 只有当 superAdminId 有值时才发请求
+ if (superAdminId) {
+ load()
+ }
+ }, [superAdminId])
+
+ // 处理角色变更
const handleRoleChange = async (userId, newRole) => {
try {
- if (newRole === '用户') await giveUser(superAdminId, userId);
- else if (newRole === '管理员') await giveAdmin(superAdminId, userId);
- else if (newRole === '超级管理员') await giveSuperAdmin(superAdminId, userId);
- setUsers(us => us.map(u => u.id === userId ? { ...u, role: newRole } : u));
- message.success('修改成功');
+ if (newRole === '用户') {
+ await giveUser(superAdminId, userId)
+ } else if (newRole === '管理员') {
+ await giveAdmin(superAdminId, userId)
+ } else if (newRole === '超级管理员') {
+ await giveSuperAdmin(superAdminId, userId)
+ }
+ // 本地更新状态
+ setUsers(us =>
+ us.map(u => (u.id === userId ? { ...u, role: newRole } : u))
+ )
+ message.success('修改成功')
} catch (e) {
- console.error(e);
- message.error('修改失败');
+ console.error(e)
+ message.error('修改失败:' + e.message)
}
- };
+ }
- // define table columns
+ // 表格列定义
const columns = [
{ title: '用户名', dataIndex: 'username', key: 'username' },
- { title: '角色', dataIndex: 'role', key: 'role' },
+ { title: '角色', dataIndex: 'role', key: 'role' },
{
title: '操作',
key: 'action',
render: (_, record) => {
- const orderedRoles = [record.role, ...ROLE_LIST.filter(r => r !== record.role)];
+ // 当前角色排第一
+ const orderedRoles = [
+ record.role,
+ ...ROLE_LIST.filter(r => r !== record.role)
+ ]
return (
<Select
value={record.role}
@@ -53,13 +79,15 @@
onChange={value => handleRoleChange(record.id, value)}
>
{orderedRoles.map(r => (
- <Option key={r} value={r}>{r}</Option>
+ <Option key={r} value={r}>
+ {r}
+ </Option>
))}
</Select>
- );
- },
- },
- ];
+ )
+ }
+ }
+ ]
return (
<div className="admin-container">
@@ -70,7 +98,5 @@
pagination={false}
/>
</div>
- );
+ )
}
-
-export default UserManagement;
diff --git a/Merge/front/src/pages/LoginPage/LoginPage.js b/Merge/front/src/pages/LoginPage/LoginPage.js
index c315b7d..bdd75d0 100644
--- a/Merge/front/src/pages/LoginPage/LoginPage.js
+++ b/Merge/front/src/pages/LoginPage/LoginPage.js
@@ -1,380 +1,240 @@
-import React, { useState, useEffect } from 'react';
-import { Link } from 'react-router-dom';
-import { Input, Checkbox, Modal, Alert } from 'antd';
-import { MailOutlined, LockOutlined, ExclamationCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
-import {
- getRememberedLoginInfo,
- saveRememberedLoginInfo,
- saveAuthInfo,
- isLoggedIn
-} from '../../utils/auth';
-import { hashPassword } from '../../utils/crypto';
-import './LoginPage.css';
+// src/pages/LoginPage/LoginPage.jsx
-const baseURL = 'http://10.126.59.25:8082';
+import React, { useState, useEffect } from 'react'
+import { useNavigate, Link } from 'react-router-dom'
+import { Input, Checkbox, Modal, Alert } from 'antd'
+import {
+ MailOutlined,
+ LockOutlined,
+ ExclamationCircleOutlined,
+ CheckCircleOutlined
+} from '@ant-design/icons'
+import {
+ getRememberedLoginInfo,
+ saveRememberedLoginInfo,
+ saveAuthInfo,
+ isLoggedIn,
+ clearAuthInfo // ← 新增
+} from '../../utils/auth'
+import { hashPassword } from '../../utils/crypto'
+import './LoginPage.css'
-const LoginPage = () => {
+const baseURL = 'http://10.126.59.25:8082'
+
+export default function LoginPage() {
+ const navigate = useNavigate()
+
+ // —— 登录页加载时先清除旧的认证信息 ——
+ useEffect(() => {
+ clearAuthInfo(/* clearRemembered= */ false)
+ }, [])
+
const [formData, setFormData] = useState({
email: '',
password: ''
- });
-
- const [rememberMe, setRememberMe] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [errors, setErrors] = useState({
- email: '',
- password: ''
- });
+ })
+ const [rememberMe, setRememberMe] = useState(false)
+ const [isLoading, setIsLoading] = useState(false)
+ const [errors, setErrors] = useState({ email: '', password: '' })
const [errorModal, setErrorModal] = useState({
visible: false,
title: '',
content: ''
- });
+ })
const [successAlert, setSuccessAlert] = useState({
visible: false,
message: ''
- });
+ })
// 显示错误弹窗
const showErrorModal = (title, content) => {
- setErrorModal({
- visible: true,
- title: title,
- content: content
- });
- };
-
+ setErrorModal({ visible: true, title, content })
+ }
// 关闭错误弹窗
const closeErrorModal = () => {
- setErrorModal({
- visible: false,
- title: '',
- content: ''
- });
- };
-
+ setErrorModal({ visible: false, title: '', content: '' })
+ }
// 显示成功提示
const showSuccessAlert = (message) => {
- setSuccessAlert({
- visible: true,
- message: message
- });
-
- // 3秒后自动隐藏
+ setSuccessAlert({ visible: true, message })
setTimeout(() => {
- setSuccessAlert({
- visible: false,
- message: ''
- });
- }, 3000);
- };
+ setSuccessAlert({ visible: false, message: '' })
+ }, 3000)
+ }
- // 页面加载时检查是否有记住的登录信息
+ // 初始化:检查登录 & 填充“记住我”
useEffect(() => {
- // 检查是否已经登录
if (isLoggedIn()) {
- // 如果已经有token,可以选择直接跳转到主页面
- // window.location.href = '/test-dashboard';
- console.log('用户已登录');
+ console.log('用户已登录')
+ // 如果想自动跳转: navigate('/home', { replace: true })
}
-
- // 获取记住的登录信息
- const rememberedInfo = getRememberedLoginInfo();
- if (rememberedInfo.rememberMe && rememberedInfo.email) {
- setFormData({
- email: rememberedInfo.email,
- password: rememberedInfo.password
- });
- setRememberMe(true);
+ const { email, password, rememberMe } = getRememberedLoginInfo()
+ if (rememberMe && email) {
+ setFormData({ email, password })
+ setRememberMe(true)
}
- }, []);
+ }, [])
const handleEmailChange = (e) => {
- const value = e.target.value;
- setFormData(prev => ({
- ...prev,
- email: value
- }));
-
- // 清除邮箱错误提示
- if (errors.email) {
- setErrors(prev => ({
- ...prev,
- email: ''
- }));
- }
- };
-
+ setFormData(f => ({ ...f, email: e.target.value }))
+ if (errors.email) setErrors(e => ({ ...e, email: '' }))
+ }
const handlePasswordChange = (e) => {
- const value = e.target.value;
- setFormData(prev => ({
- ...prev,
- password: value
- }));
-
- // 清除密码错误提示
- if (errors.password) {
- setErrors(prev => ({
- ...prev,
- password: ''
- }));
- }
- };
-
+ setFormData(f => ({ ...f, password: e.target.value }))
+ if (errors.password) setErrors(e => ({ ...e, password: '' }))
+ }
const handleRememberMeChange = (e) => {
- const checked = e.target.checked;
- setRememberMe(checked);
-
- // 如果取消记住我,清除已保存的登录信息
+ const checked = e.target.checked
+ setRememberMe(checked)
if (!checked) {
- saveRememberedLoginInfo('', '', false);
+ saveRememberedLoginInfo('', '', false)
}
- };
+ }
const validateForm = () => {
- const newErrors = {
- email: '',
- password: ''
- };
-
- let hasError = false;
-
- // 验证邮箱
- if (!formData.email || typeof formData.email !== 'string' || !formData.email.trim()) {
- newErrors.email = '请输入邮箱地址';
- hasError = true;
- } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
- newErrors.email = '请输入有效的邮箱地址';
- hasError = true;
+ const newErr = { email: '', password: '' }
+ let hasError = false
+ if (!formData.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErr.email = '请输入有效的邮箱地址'
+ hasError = true
}
-
- // 验证密码
- if (!formData.password || typeof formData.password !== 'string' || !formData.password.trim()) {
- newErrors.password = '请输入密码';
- hasError = true;
- } else if (formData.password.length < 6) {
- newErrors.password = '密码长度至少6位';
- hasError = true;
+ if (!formData.password.trim() || formData.password.length < 6) {
+ newErr.password = '密码长度至少6位'
+ hasError = true
}
-
- setErrors(newErrors);
- return !hasError;
- };
+ setErrors(newErr)
+ return !hasError
+ }
const handleSubmit = async (e) => {
- e.preventDefault();
-
- // 验证表单
- if (!validateForm()) {
- return;
- }
-
- setIsLoading(true);
-
+ e.preventDefault()
+ if (!validateForm()) return
+
+ setIsLoading(true)
try {
- // 发送登录请求到后端
- const response = await fetch(baseURL + '/login', {
+ const res = await fetch(baseURL + '/login', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- email: formData.email, // 后端支持邮箱登录
- password: hashPassword(formData.password) // 前端加密密码
+ email: formData.email,
+ password: hashPassword(formData.password)
})
- });
-
- const result = await response.json();
-
+ })
+ const result = await res.json()
if (result.success) {
- // 显示成功提示
- showSuccessAlert('登录成功!正在跳转...');
-
- // 保存认证信息
- saveAuthInfo(result.token, result.user, rememberMe);
-
- // 保存或清除记住的登录信息
- saveRememberedLoginInfo(formData.email, formData.password, rememberMe);
-
- // 延迟跳转,让用户看到成功提示
+ showSuccessAlert('登录成功!正在跳转...')
+ saveAuthInfo(result.token, result.user, rememberMe)
+ saveRememberedLoginInfo(formData.email, formData.password, rememberMe)
setTimeout(() => {
- window.location.href = '/test-dashboard';
- }, 1500);
+ const uid = result.user.id
+ switch (result.user.role) {
+ case 'admin':
+ navigate(`/admin/${uid}`, { replace: true })
+ break
+ case 'superadmin':
+ navigate(`/superadmin/${uid}/users`, { replace: true })
+ break
+ default:
+ navigate('/home', { replace: true })
+ }
+ }, 1500)
} else {
- // 登录失败,显示错误信息
- let errorTitle = '登录失败';
- let errorContent = result.message || '登录失败,请检查您的邮箱和密码';
-
- // 根据错误类型提供更详细的信息
+ let title = '登录失败'
+ let content = result.message || '登录失败,请检查您的邮箱和密码'
if (result.message) {
- if (result.message.includes('邮箱') || result.message.includes('email')) {
- errorTitle = '邮箱验证失败';
- errorContent = '您输入的邮箱地址不存在或格式不正确,请检查后重试。';
- } else if (result.message.includes('密码') || result.message.includes('password')) {
- errorTitle = '密码验证失败';
- errorContent = '您输入的密码不正确,请检查后重试。如果忘记密码,请点击"忘记密码"进行重置。';
- } else if (result.message.includes('用户不存在')) {
- errorTitle = '用户不存在';
- errorContent = '该邮箱尚未注册,请先注册账户或检查邮箱地址是否正确。';
- } else if (result.message.includes('账户被锁定') || result.message.includes('locked')) {
- errorTitle = '账户被锁定';
- errorContent = '您的账户因安全原因被暂时锁定,请联系客服或稍后重试。';
+ if (/邮箱|email/.test(result.message)) {
+ title = '邮箱验证失败'
+ content = '请输入正确的邮箱地址'
+ } else if (/密码|password/.test(result.message)) {
+ title = '密码验证失败'
+ content = '密码不正确,请重试'
}
}
-
- showErrorModal(errorTitle, errorContent);
+ showErrorModal(title, content)
}
- } catch (error) {
- console.error('登录请求失败:', error);
-
- // 根据错误类型显示不同的错误信息
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
- showErrorModal('网络连接失败', '无法连接到服务器,请检查您的网络连接后重试。如果问题持续存在,请联系客服。');
- } else if (error.name === 'AbortError') {
- showErrorModal('请求超时', '请求超时,请检查网络连接后重试。');
- } else {
- showErrorModal('登录失败', '网络连接失败,请检查网络或稍后重试。如果问题持续存在,请联系客服。');
- }
+ } catch (err) {
+ console.error(err)
+ showErrorModal('网络异常', '无法连接到服务器,请稍后重试')
} finally {
- setIsLoading(false);
+ setIsLoading(false)
}
- };
+ }
return (
<div className="login-container">
- <div className="login-background"></div>
-
+ <div className="login-background" />
{isLoading && (
<div className="loading-overlay">
<div className="loading-content">
- <div className="loading-spinner-large"></div>
+ <div className="loading-spinner-large" />
<p className="loading-text">正在登录...</p>
</div>
</div>
)}
-
<div className="login-content">
<div className="login-card">
- {/* 成功提示 */}
{successAlert.visible && (
- <div style={{ marginBottom: '16px' }}>
- <Alert
- message={successAlert.message}
- type="success"
- icon={<CheckCircleOutlined />}
- showIcon
- closable
- onClose={() => setSuccessAlert({ visible: false, message: '' })}
- style={{
- borderRadius: '8px',
- border: '1px solid #b7eb8f',
- backgroundColor: '#f6ffed'
- }}
- />
- </div>
+ <Alert
+ message={successAlert.message}
+ type="success"
+ icon={<CheckCircleOutlined />}
+ closable
+ style={{ marginBottom: 16, borderRadius: 8 }}
+ />
)}
-
<div className="login-header">
- <h1 className="login-title">欢迎来到小红书</h1>
- <p className="login-subtitle">标记我的生活</p>
+ <h1>欢迎来到小红书</h1>
+ <p>标记我的生活</p>
</div>
-
<form className="login-form" onSubmit={handleSubmit}>
<div className="form-group">
<Input
type="email"
- id="email"
- name="email"
- className={`form-input ${errors.email ? 'input-error' : ''}`}
- placeholder="请输入您的邮箱"
+ placeholder="邮箱"
value={formData.email}
onChange={handleEmailChange}
prefix={<MailOutlined />}
- size="large"
- title=""
status={errors.email ? 'error' : ''}
/>
- {errors.email && (
- <div className="error-message">
- {errors.email}
- </div>
- )}
+ {errors.email && <div className="error-message">{errors.email}</div>}
</div>
-
<div className="form-group">
<Input.Password
- id="password"
- name="password"
- className={`form-input ${errors.password ? 'input-error' : ''}`}
- placeholder="请输入您的密码"
+ placeholder="密码"
value={formData.password}
onChange={handlePasswordChange}
prefix={<LockOutlined />}
- size="large"
- title=""
status={errors.password ? 'error' : ''}
/>
- {errors.password && (
- <div className="error-message">
- {errors.password}
- </div>
- )}
+ {errors.password && <div className="error-message">{errors.password}</div>}
</div>
-
<div className="form-options">
- <Checkbox
- checked={rememberMe}
- onChange={handleRememberMeChange}
- >
+ <Checkbox checked={rememberMe} onChange={handleRememberMeChange}>
记住我
</Checkbox>
- <Link to="/forgot-password" className="forgot-password">忘记密码?</Link>
+ <Link to="/forgot-password">忘记密码?</Link>
</div>
-
<button
type="submit"
className={`login-button ${isLoading ? 'loading' : ''}`}
disabled={isLoading}
>
- {isLoading ? (
- <>
- <div className="loading-spinner"></div>
- 登录中...
- </>
- ) : (
- '登录'
- )}
+ {isLoading ? '登录中...' : '登录'}
</button>
</form>
-
<div className="signup-link">
- <p>还没有账户? <Link to="/register">立即注册</Link></p>
+ <p>还没有账户?<Link to="/register">立即注册</Link></p>
</div>
</div>
</div>
-
- {/* 错误弹窗 */}
<Modal
- title={
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
- <ExclamationCircleOutlined style={{ color: '#ff4d4f', fontSize: '18px' }} />
- {errorModal.title}
- </div>
- }
+ title={<><ExclamationCircleOutlined style={{ color: '#ff4d4f' }} /> {errorModal.title}</>}
open={errorModal.visible}
onOk={closeErrorModal}
- onCancel={closeErrorModal}
- okText="我知道了"
cancelButtonProps={{ style: { display: 'none' } }}
- centered
- className="error-modal"
>
- <div style={{ padding: '16px 0', fontSize: '14px', lineHeight: '1.6' }}>
- {errorModal.content}
- </div>
+ <p>{errorModal.content}</p>
</Modal>
</div>
- );
-};
-
-export default LoginPage;
+ )
+}
diff --git a/Merge/front/src/router/App.js b/Merge/front/src/router/App.js
index d91b3b7..d7f5f09 100644
--- a/Merge/front/src/router/App.js
+++ b/Merge/front/src/router/App.js
@@ -1,72 +1,86 @@
-import React from 'react';
+// src/router/index.jsx
+import React from 'react'
+import { Routes, Route, Navigate } from 'react-router-dom'
+
+import LoginPage from '../pages/LoginPage/LoginPage'
+import RegisterPage from '../pages/RegisterPage/RegisterPage'
+import ForgotPasswordPage from '../pages/ForgotPasswordPage/ForgotPasswordPage'
+import TestDashboard from '../pages/TestDashboard/TestDashboard'
+
+import HomeFeed from '../components/HomeFeed'
+import CreatePost from '../components/CreatePost'
+import NotebookPage from '../components/NotebookPage'
+import PlaceholderPage from '../components/PlaceholderPage'
+import UserProfile from '../components/UserProfile'
+
+import AdminPage from '../components/Admin'
+import SuperAdmin from '../components/SuperAdmin'
+import UserManagement from '../components/UserManagement'
+import LogsDashboard from '../components/LogsDashboard'
+import TransactionLogs from '../components/TransactionLogs'
+import PerformanceLogs from '../components/PerformanceLogs'
+
import {
- Routes,
- Route,
- Navigate,
-} from 'react-router-dom';
-import AdminPage from '../components/Admin';
-import UserManagement from '../components/UserManagement';
-import LogsDashboard from '../components/LogsDashboard';
-import SuperAdmin from '../components/SuperAdmin';
-
-import CreatePost from '../components/CreatePost' // src/components/CreatePost.jsx
-import HomeFeed from '../components/HomeFeed' // src/components/HomeFeed.jsx
-import PlaceholderPage from '../components/PlaceholderPage'// src/components/PlaceholderPage.jsx
-import UploadPage from '../components/UploadPage' // src/components/UploadPage.jsx
-
-import UserProfile from '../components/UserProfile'; // src/components/UserProfileRoute.jsx
-
-import LoginPage from '../pages/LoginPage/LoginPage';
-import RegisterPage from '../pages/RegisterPage/RegisterPage';
-import ForgotPasswordPage from '../pages/ForgotPasswordPage/ForgotPasswordPage';
-import TestDashboard from '../pages/TestDashboard/TestDashboard';
-
-import TransactionLogs from '../components/TransactionLogs';
-import PerformanceLogs from '../components/PerformanceLogs';
-import NotebookPage from '../components/NotebookPage'
+ RequireAuth,
+ RequireRole,
+ RequireOwnProfile,
+ RequireAdminOwn,
+ RequireSuperAdminOwn
+} from './Guards'
export default function AppRoutes() {
return (
<Routes>
- <Route path="/posts/new" element={<CreatePost />} />
-
- <Route path="/home" element={<HomeFeed />} />
-
- <Route path="/notebooks" element={<NotebookPage />} />
- <Route path="/activity" element={<PlaceholderPage pageId="activity" />} />
- <Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
- <Route path="/creator" element={<PlaceholderPage pageId="creator" />} />
- <Route path="/journal" element={<PlaceholderPage pageId="journal" />} />
- <Route path="/user/:userId" element={<UserProfile />} />
- <Route path="/dashboard/*" element={<UploadPage />} />
-
- {/* 根路径重定向到 dashboard */}
- {/* <Route path="/" element={<Navigate to="/dashboard/overview" replace />} /> */}
-
- <Route path="/" element={<LoginPage />} />
- <Route path="/login" element={<LoginPage />} />
- <Route path="/register" element={<RegisterPage />} />
+ {/* 1. 公开路由 */}
+ <Route path="/login" element={<LoginPage />} />
+ <Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
- <Route path="/test-dashboard" element={<TestDashboard />} />
- {/* 普通管理员,无 header */}
- <Route path="admin" element={<AdminPage />} />
+ {/* 2. 受保护路由 */}
+ <Route element={<RequireAuth />}>
+ {/* 2.1 任何登录用户都能看自己的主页 */}
+ <Route element={<RequireOwnProfile />}>
+ <Route path="/user/:userId" element={<UserProfile />} />
+ </Route>
- {/* 超级管理员,只用 SuperAdminLayout */}
- <Route path="superadmin" element={<SuperAdmin />}>
- <Route index element={<Navigate to="users" replace />} />
- <Route path="users" element={<UserManagement superAdminId={3} />} />
+ {/* 2.2 普通用户 */}
+ <Route element={<RequireRole allowedRoles={['user']} />}>
+ <Route path="/home" element={<HomeFeed />} />
+ <Route path="/posts/new" element={<CreatePost />} />
+ <Route path="/posts/edit/:postId" element={<CreatePost />} />
+ <Route path="/notebooks" element={<NotebookPage />} />
+ <Route path="/dashboard/*" element={<PlaceholderPage />} />
+ <Route path="/activity" element={<PlaceholderPage pageId="activity" />} />
+ <Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
+ <Route path="/creator" element={<PlaceholderPage pageId="creator" />} />
+ <Route path="/journal" element={<PlaceholderPage pageId="journal" />} />
+ <Route path="/" element={<Navigate to="/home" replace />} />
+ </Route>
- {/* dashboard as layout */}
- <Route path="dashboard" element={<LogsDashboard />}>
- <Route index element={<Navigate to="transactions" replace />} />
- <Route path="transactions" element={<TransactionLogs userId={1} />} />
- <Route path="performance" element={<PerformanceLogs userId={1} />} />
+ {/* 2.3 Admin 自己的页面 */}
+ <Route element={<RequireAdminOwn />}>
+ <Route path="/admin/:userId" element={<AdminPage />} />
+ </Route>
+ <Route element={<RequireRole allowedRoles={['admin']} />}>
+ <Route path="/test-dashboard" element={<TestDashboard />} />
+ </Route>
+
+ {/* 2.4 SuperAdmin 自己的区域 */}
+ <Route element={<RequireSuperAdminOwn />}>
+ <Route path="/superadmin/:userId/*" element={<SuperAdmin />}>
+ <Route index element={<Navigate to="users" replace />} />
+ <Route path="users" element={<UserManagement />} />
+ <Route path="dashboard" element={<LogsDashboard />}>
+ <Route index element={<Navigate to="transactions" replace />} />
+ <Route path="transactions" element={<TransactionLogs />} />
+ <Route path="performance" element={<PerformanceLogs />} />
+ </Route>
+ </Route>
</Route>
</Route>
- {/* 最后一个兜底,放在最末尾 */}
- <Route path="*" element={<PlaceholderPage pageId="home" />} />
+ {/* 3. 兜底:未匹配一律回登录 */}
+ <Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/Merge/front/src/router/Guards.jsx b/Merge/front/src/router/Guards.jsx
new file mode 100644
index 0000000..fe45e30
--- /dev/null
+++ b/Merge/front/src/router/Guards.jsx
@@ -0,0 +1,53 @@
+// src/router/Guards.jsx
+import React from 'react'
+import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom'
+import { getUserInfo } from '../utils/auth'
+
+/** 需登录 */
+export function RequireAuth() {
+ const user = getUserInfo()
+ const location = useLocation()
+ if (!user) {
+ return <Navigate to="/login" state={{ from: location }} replace />
+ }
+ return <Outlet />
+}
+
+/** 需特定角色 */
+export function RequireRole({ allowedRoles }) {
+ const user = getUserInfo()
+ if (!user || !allowedRoles.includes(user.role)) {
+ return <Navigate to="/login" replace />
+ }
+ return <Outlet />
+}
+
+/** 只能访问自己的用户详情 */
+export function RequireOwnProfile() {
+ const user = getUserInfo()
+ const { userId } = useParams()
+ if (!user || user.id.toString() !== userId) {
+ return <Navigate to="/home" replace />
+ }
+ return <Outlet />
+}
+
+/** 只能访问自己的 Admin 页面 */
+export function RequireAdminOwn() {
+ const user = getUserInfo()
+ const { userId } = useParams()
+ if (!user || user.role !== 'admin' || user.id.toString() !== userId) {
+ return <Navigate to="/login" replace />
+ }
+ return <Outlet />
+}
+
+/** 只能访问自己的 SuperAdmin 区 */
+export function RequireSuperAdminOwn() {
+ const user = getUserInfo()
+ const { userId } = useParams()
+ if (!user || user.role !== 'superadmin' || user.id.toString() !== userId) {
+ return <Navigate to="/login" replace />
+ }
+ return <Outlet />
+}
diff --git a/WZY/xhs_front/src/router/index.jsx b/WZY/xhs_front/src/router/index.jsx
index fd959b9..225a714 100644
--- a/WZY/xhs_front/src/router/index.jsx
+++ b/WZY/xhs_front/src/router/index.jsx
@@ -5,7 +5,6 @@
import CreatePost from '../components/CreatePost'
import HomeFeed from '../components/HomeFeed'
import NotebookPage from '../components/NotebookPage'
-import UploadPage from '../components/UploadPage'
import PlaceholderPage from '../components/PlaceholderPage'
export default function AppRouter() {