feat(api): 重构 API 调用并优化用户认证流程
- 新增 auth、forum 和 user API 文件夹,重新组织 API 调用结构
- 重构 AuthContext,使用新 API 进行用户认证和信息管理
- 更新 AdminPanel、ForumPage 等组件,使用新的 API 调用
- 删除旧的 authApi.js 文件,清理冗余代码
- 优化用户登录、注册和登出流程,改进错误处理和用户提示
Change-Id: If664193e1bf30036c197f164edc5b10df75f1331
diff --git a/src/features/admin/pages/AdminPanel.jsx b/src/features/admin/pages/AdminPanel.jsx
index 93d8d87..3717b52 100644
--- a/src/features/admin/pages/AdminPanel.jsx
+++ b/src/features/admin/pages/AdminPanel.jsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Button, Space, Typography, message, Popconfirm, Spin } from 'antd';
import { UserOutlined, UploadOutlined, SettingOutlined } from '@ant-design/icons';
-import { getUserList, deleteUser } from '../../auth/services/authApi';
+import { getUserList, deleteUser } from '@/api/auth';
const { Title } = Typography;
diff --git a/src/features/auth/contexts/AuthContext.jsx b/src/features/auth/contexts/AuthContext.jsx
index 5f9b37e..b0afd8e 100644
--- a/src/features/auth/contexts/AuthContext.jsx
+++ b/src/features/auth/contexts/AuthContext.jsx
@@ -1,7 +1,13 @@
-import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
-import { loginUser, registerUser, getUserInfo, logoutUser } from '../services/authApi';
-import { message } from 'antd';
-import { useNavigate } from 'react-router-dom'; // 导入 useNavigate
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ useCallback,
+} from "react";
+import { userLogin, registerUser, logoutUser } from "@/api/auth";
+import { message } from "antd";
+import { useNavigate } from "react-router-dom"; // 导入 useNavigate
const AuthContext = createContext(null);
@@ -14,22 +20,27 @@
const loadAuthData = useCallback(() => {
setLoading(true);
try {
- const storedToken = localStorage.getItem('authToken'); // 假设您使用token
- const storedUser = localStorage.getItem('user');
+ const storedToken = localStorage.getItem("token");
+ const storedUser = localStorage.getItem("user");
+
+ console.log("Loading auth data - token:", !!storedToken, "user:", !!storedUser);
if (storedToken && storedUser) {
- setUser(JSON.parse(storedUser));
- setIsAuthenticated(true);
- // 调用API获取最新的用户信息
- getUserInfo().then(response => {
- if (response.data && response.data.user) {
- setUser(response.data.user);
- localStorage.setItem('user', JSON.stringify(response.data.user));
- }
- }).catch(error => {
- console.error("获取用户信息失败", error);
- });
+ try {
+ const userData = JSON.parse(storedUser);
+ // console.log("Parsed user data:", userData);
+ setUser(userData);
+ setIsAuthenticated(true);
+ } catch (parseError) {
+ console.error("Failed to parse stored user data:", parseError);
+ // 如果解析失败,清除损坏的数据
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ setIsAuthenticated(false);
+ setUser(null);
+ }
} else {
+ console.log("No stored auth data found");
setIsAuthenticated(false);
setUser(null);
}
@@ -46,23 +57,32 @@
loadAuthData();
}, [loadAuthData]);
- const login = async (credentials) => {
+ const login = async (params) => {
setLoading(true);
try {
- const response = await loginUser(credentials);
- const { token, user: userData } = response.data;
-
- localStorage.setItem('authToken', token);
- localStorage.setItem('user', JSON.stringify(userData));
- setUser(userData);
- setIsAuthenticated(true);
- message.success('登录成功');
- return userData;
+ const response = await userLogin(params);
+ // 检查响应是否成功
+ if (response && response.data) {
+ const token = response.data.token;
+ const userData = response.data;
+
+ console.log("Saving token:", token);
+ console.log("Saving user data:", userData);
+
+ localStorage.setItem("token", token);
+ localStorage.setItem("user", JSON.stringify(userData));
+
+ setUser(userData);
+ setIsAuthenticated(true);
+
+ message.success(response?.message || "登录成功");
+ return userData;
+ }
} catch (error) {
- console.error("Login failed", error);
+ console.log("login error", error);
setIsAuthenticated(false);
setUser(null);
- message.error(error.message || '登录失败,请检查用户名和密码');
+ message.error(error.message || "登录失败,请检查用户名和密码");
} finally {
setLoading(false);
}
@@ -72,17 +92,23 @@
setLoading(true);
try {
const response = await registerUser(userData);
- const { token, user: newUser } = response.data;
- localStorage.setItem('authToken', token);
- localStorage.setItem('user', JSON.stringify(newUser));
- setUser(newUser);
- setIsAuthenticated(true);
- message.success('注册成功');
- return newUser;
+ // 检查响应是否成功
+ if (response.data && response.data.success) {
+ const { token, user: newUser } = response.data.data;
+
+ localStorage.setItem("token", token);
+ localStorage.setItem("user", JSON.stringify(newUser));
+ setUser(newUser);
+ setIsAuthenticated(true);
+ message.success("注册成功");
+ return newUser;
+ } else {
+ throw new Error(response.data?.message || "注册失败");
+ }
} catch (error) {
console.error("Registration failed", error);
- message.error(error.message || '注册失败,请稍后再试');
+ message.error(error.message || "注册失败,请稍后再试");
} finally {
setLoading(false);
}
@@ -91,24 +117,27 @@
const logout = async () => {
try {
await logoutUser();
- localStorage.removeItem('authToken');
- localStorage.removeItem('user');
- localStorage.removeItem('permissions'); // 移除旧的权限存储
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ localStorage.removeItem("permissions"); // 移除旧的权限存储
setUser(null);
setIsAuthenticated(false);
- message.success('已成功退出登录');
- navigate('/login');
+ message.success("已成功退出登录");
+ navigate("/login");
return true;
} catch (error) {
console.error("登出失败", error);
- message.error('登出失败');
+ message.error("登出失败");
return false;
}
};
- const hasRole = useCallback((roleName) => {
- return user?.role === roleName;
- }, [user]);
+ const hasRole = useCallback(
+ (roleName) => {
+ return user?.role === roleName;
+ },
+ [user]
+ );
const value = {
user,
@@ -118,7 +147,7 @@
register,
logout,
hasRole,
- reloadAuthData: loadAuthData // 暴露一个重新加载数据的方法
+ reloadAuthData: loadAuthData, // 暴露一个重新加载数据的方法
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
@@ -126,8 +155,9 @@
export const useAuth = () => {
const context = useContext(AuthContext);
- if (context === undefined || context === null) { // 增加了对 null 的检查
- throw new Error('useAuth must be used within an AuthProvider');
+ if (context === undefined || context === null) {
+ // 增加了对 null 的检查
+ throw new Error("useAuth must be used within an AuthProvider");
}
return context;
-};
\ No newline at end of file
+};
diff --git a/src/features/auth/pages/LoginPage.jsx b/src/features/auth/pages/LoginPage.jsx
index 19434a4..3a8cdbc 100644
--- a/src/features/auth/pages/LoginPage.jsx
+++ b/src/features/auth/pages/LoginPage.jsx
@@ -14,7 +14,6 @@
} from "antd";
import { UserOutlined, LockOutlined } from "@ant-design/icons";
import { useAuth } from "../contexts/AuthContext"; // 使用新的 AuthContext
-// import { loginUser } from '../services/authApi'; // 如果不直接在 context 中调用 API
const { Title, Text } = Typography;
@@ -33,10 +32,18 @@
const onFinish = async (values) => {
setLoading(true);
try {
- await login({ username: values.username, password: values.password });
- // 登录成功后的导航由 AuthContext 内部或 ProtectedRoute 处理
- // AuthContext 已经包含成功提示,这里不再重复提示
- navigate("/"); // 或者根据用户角色导航到不同页面
+ const params = {
+ username: values.username,
+ password: values.password,
+ };
+ const userData = await login(params);
+
+ // 根据用户角色导航到不同页面
+ if (userData && userData.role === 'admin') {
+ navigate("/admin"); // 管理员导航到管理面板
+ } else {
+ navigate("/"); // 普通用户导航到首页
+ }
} catch (error) {
// 错误消息由 AuthContext 中的 login 方法或 request 拦截器处理
console.error("Login page error:", error);
diff --git a/src/features/auth/services/authApi.js b/src/features/auth/services/authApi.js
deleted file mode 100644
index dd40a80..0000000
--- a/src/features/auth/services/authApi.js
+++ /dev/null
@@ -1,165 +0,0 @@
-// src/features/auth/services/authApi.js
-import request from "../../../services/request";
-import { message } from "antd";
-
-// 使用API前缀
-const API_PREFIX = "/user";
-const ADMIN_PREFIX = "/admin";
-
-// 导出API函数
-export const loginUser = (credentials) => {
- return request.post(`${API_PREFIX}/login`, credentials).then((response) => {
- if (response.data && response.data.success) {
- // 保存token和用户信息到localStorage
- localStorage.setItem("token", response.data.data.token);
- localStorage.setItem("user", JSON.stringify(response.data.data.user));
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "登录失败"));
- }
- });
-};
-
-export const adminLogin = (credentials) => {
- return request.post(`${ADMIN_PREFIX}/login`, credentials).then((response) => {
- if (response.data && response.data.success) {
- // 保存token和用户信息到localStorage
- localStorage.setItem("token", response.data.data.token);
- localStorage.setItem("user", JSON.stringify(response.data.data.user));
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "管理员登录失败"));
- }
- });
-};
-
-export const registerUser = (userData) => {
- return request.post(`${API_PREFIX}/register`, userData).then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "注册失败"));
- }
- });
-};
-
-export const updateUsername = (username, newUsername) => {
- const token = localStorage.getItem("token");
- return request
- .post(`${API_PREFIX}/update/username`,
- { username, newUsername },
- { headers: { token } })
- .then((response) => {
- if (response.data && response.data.success) {
- // 更新本地存储的用户信息
- const user = JSON.parse(localStorage.getItem("user") || "{}");
- user.username = newUsername;
- localStorage.setItem("user", JSON.stringify(user));
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "修改用户名失败")
- );
- }
- });
-};
-
-export const updatePassword = (username, newPassword) => {
- const token = localStorage.getItem("token");
- return request
- .post(`${API_PREFIX}/update/password`,
- { username, newPassword },
- { headers: { token } })
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "修改密码失败")
- );
- }
- });
-};
-
-export const updateEmail = (username, newEmail) => {
- const token = localStorage.getItem("token");
- return request
- .post(`${API_PREFIX}/update/email`,
- { username, newEmail },
- { headers: { token } })
- .then((response) => {
- if (response.data && response.data.success) {
- // 更新本地存储的用户信息
- const user = JSON.parse(localStorage.getItem("user") || "{}");
- user.email = newEmail;
- localStorage.setItem("user", JSON.stringify(user));
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "修改邮箱失败")
- );
- }
- });
-};
-
-export const getUserInfo = (username) => {
- const token = localStorage.getItem("token");
- return request
- .get(`${API_PREFIX}/get/info?username=${username}`,
- { headers: { token } })
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "获取用户信息失败")
- );
- }
- });
-};
-
-export const getUserList = (username) => {
- const token = localStorage.getItem("token");
- return request
- .get(`/user/list?username=${username}`,
- { headers: { token } })
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "获取用户列表失败")
- );
- }
- });
-};
-
-export const deleteUser = (username, targetUsername) => {
- const token = localStorage.getItem("token");
- return request
- .delete(`/user/delete`,
- {
- headers: { token },
- data: { username, targetUsername }
- })
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(
- new Error(response.data.message || "删除用户失败")
- );
- }
- });
-};
-
-export const logoutUser = () => {
- // 清除本地存储
- localStorage.removeItem("token");
- localStorage.removeItem("user");
-
- return Promise.resolve({
- success: true,
- message: "注销成功"
- });
-};
diff --git a/src/features/forum/pages/ForumPage.jsx b/src/features/forum/pages/ForumPage.jsx
index 7e6e78f..54638fc 100644
--- a/src/features/forum/pages/ForumPage.jsx
+++ b/src/features/forum/pages/ForumPage.jsx
@@ -1,6 +1,20 @@
-import React, { useState, useEffect } from 'react';
-import { List, Avatar, Space, Tag, Typography, Button, message, Modal, Form, Input, Spin } from 'antd';
-import { getPosts, createPost } from '../services/forumApi';
+import React, { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import {
+ List,
+ Avatar,
+ Space,
+ Tag,
+ Typography,
+ Button,
+ message,
+ Modal,
+ Form,
+ Input,
+ Spin,
+} from "antd";
+import { getPosts, createPost } from "@/api/forum";
+import { useAuth } from "@/features/auth/contexts/AuthContext";
const { Title, Paragraph, Text } = Typography;
const { TextArea } = Input;
@@ -10,68 +24,125 @@
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [form] = Form.useForm();
-
- // 获取用户信息
- const user = JSON.parse(localStorage.getItem('user') || '{}');
-
+
+ // 使用 AuthProvider 获取用户信息
+ const { user, isAuthenticated } = useAuth();
+
// 加载帖子数据
useEffect(() => {
- fetchPosts();
- }, []);
-
+ // 只有在用户已认证且有用户信息时才获取帖子
+ if (isAuthenticated && user?.username) {
+ fetchPosts();
+ }
+ }, [isAuthenticated, user]);
+
// 获取帖子列表
const fetchPosts = async () => {
try {
setLoading(true);
+ console.log("正在获取帖子列表,用户名:", user.username);
const response = await getPosts({ username: user.username });
- if (response.success) {
- setPosts(response.data.posts || []);
+ console.log("获取帖子列表响应:", response);
+
+ if (response) {
+ const posts = response.data?.posts || [];
+ console.log("获取到的帖子数量:", posts.length);
+ console.log("帖子数据结构:", posts[0]); // 查看第一个帖子的数据结构
+ setPosts(posts);
+ } else {
+ console.error("获取帖子列表失败:", response.data);
+ message.error(response.data?.message || "获取帖子列表失败");
}
} catch (error) {
- message.error(error.message || '获取帖子列表失败');
+ message.error(error.message || "获取帖子列表失败");
} finally {
setLoading(false);
}
};
-
+
// 显示新建帖子对话框
const showModal = () => {
setIsModalOpen(true);
};
-
+
// 关闭对话框
const handleCancel = () => {
setIsModalOpen(false);
form.resetFields();
};
-
+
// 提交新帖子
const handleSubmit = async () => {
try {
- const values = await form.validateFields();
-
+ const params = await form.validateFields();
+
// 添加作者信息
- values.author = user.username;
-
- const response = await createPost(values);
- if (response.success) {
- message.success('帖子发布成功');
+ params.author = user.username;
+ console.log("提交的帖子数据:", params);
+
+ const response = await createPost(params);
+ if (response.message === "Post created successfully") {
+ message.success("帖子发布成功");
setIsModalOpen(false);
form.resetFields();
fetchPosts(); // 重新加载帖子列表
+ } else {
+ message.error(response.message || "发布帖子失败");
}
} catch (error) {
- message.error(error.message || '发布帖子失败');
+ console.error("发布帖子失败:", error);
+ message.error(error.message || "发布帖子失败");
}
};
+ // 如果用户未认证,显示提示信息
+ if (!isAuthenticated) {
+ return (
+ <div className="text-center py-8">
+ <Title level={3}>请先登录</Title>
+ <Paragraph>您需要登录后才能查看论坛内容</Paragraph>
+ </div>
+ );
+ }
+
return (
<div className="space-y-6">
<Title level={2}>社区论坛</Title>
<Paragraph className="text-slate-500">
欢迎来到我们的社区论坛,这里是会员交流分享的地方。
</Paragraph>
-
+ <div className="text-center mt-4">
+ <Button type="primary" onClick={showModal}>
+ 发布新主题
+ </Button>
+ </div>
+
+ {/* 新建帖子对话框 */}
+ <Modal
+ title="发布新主题"
+ open={isModalOpen}
+ onOk={handleSubmit}
+ onCancel={handleCancel}
+ okText="发布"
+ cancelText="取消"
+ >
+ <Form form={form} layout="vertical">
+ <Form.Item
+ name="title"
+ label="标题"
+ rules={[{ required: true, message: "请输入标题" }]}
+ >
+ <Input placeholder="请输入标题" />
+ </Form.Item>
+ <Form.Item
+ name="content"
+ label="内容"
+ rules={[{ required: true, message: "请输入帖子内容" }]}
+ >
+ <TextArea rows={6} placeholder="请输入帖子内容" />
+ </Form.Item>
+ </Form>
+ </Modal>
{loading ? (
<div className="flex justify-center py-8">
<Spin size="large" tip="加载中..." />
@@ -86,15 +157,24 @@
key={item.id}
extra={
<Space>
- <Tag color="green">浏览: {item.views || 0}</Tag>
- <Tag color="blue">点赞: {item.likes || 0}</Tag>
- <Text type="secondary">{item.createTime}</Text>
+ <Text type="secondary">{item.publishDate}</Text>
</Space>
}
>
<List.Item.Meta
- avatar={<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.author}`} />}
- title={<a href={`/post/${item.id}`}>{item.title}</a>}
+ avatar={
+ <Avatar
+ src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.author}`}
+ />
+ }
+ title={
+ <Link
+ to={`/post/${item.pid}`}
+ className="text-blue-600 hover:text-blue-800 hover:underline"
+ >
+ {item.title}
+ </Link>
+ }
description={<Text type="secondary">作者: {item.author}</Text>}
/>
<Paragraph ellipsis={{ rows: 2 }}>{item.content}</Paragraph>
@@ -102,42 +182,8 @@
)}
/>
)}
-
- <div className="text-center mt-4">
- <Button type="primary" onClick={showModal}>发布新主题</Button>
- </div>
-
- {/* 新建帖子对话框 */}
- <Modal
- title="发布新主题"
- open={isModalOpen}
- onOk={handleSubmit}
- onCancel={handleCancel}
- okText="发布"
- cancelText="取消"
- >
- <Form
- form={form}
- layout="vertical"
- >
- <Form.Item
- name="title"
- label="标题"
- rules={[{ required: true, message: '请输入标题' }]}
- >
- <Input placeholder="请输入标题" />
- </Form.Item>
- <Form.Item
- name="content"
- label="内容"
- rules={[{ required: true, message: '请输入帖子内容' }]}
- >
- <TextArea rows={6} placeholder="请输入帖子内容" />
- </Form.Item>
- </Form>
- </Modal>
</div>
);
};
-export default ForumPage;
\ No newline at end of file
+export default ForumPage;
diff --git a/src/features/forum/pages/PostDetailPage.jsx b/src/features/forum/pages/PostDetailPage.jsx
new file mode 100644
index 0000000..5e28820
--- /dev/null
+++ b/src/features/forum/pages/PostDetailPage.jsx
@@ -0,0 +1,407 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ Card,
+ Avatar,
+ Typography,
+ Divider,
+ List,
+ Form,
+ Input,
+ Button,
+ message,
+ Spin,
+ Space,
+ Tag,
+ Empty
+} from 'antd';
+import { ArrowLeftOutlined, MessageOutlined, UserOutlined, CommentOutlined } from '@ant-design/icons';
+import { getComments, addComment } from '@/api/forum';
+import { useAuth } from '@/features/auth/contexts/AuthContext';
+
+const { Title, Paragraph, Text } = Typography;
+const { TextArea } = Input;
+
+const PostDetailPage = () => {
+ const { postId } = useParams();
+ const navigate = useNavigate();
+ const { user, isAuthenticated } = useAuth();
+ const [loading, setLoading] = useState(true);
+ const [commenting, setCommenting] = useState(false);
+ const [postContent, setPostContent] = useState('');
+ const [comments, setComments] = useState([]);
+ const [form] = Form.useForm();
+ const [replyForms] = Form.useForm(); // 用于回复的表单
+ const [replyingTo, setReplyingTo] = useState(null); // 当前正在回复的评论ID
+ const [replying, setReplying] = useState(false); // 回复中状态
+
+ // 获取帖子详情和评论
+ useEffect(() => {
+ if (isAuthenticated && user?.username && postId) {
+ fetchPostAndComments();
+ }
+ }, [isAuthenticated, user, postId]);
+
+ // 监听ESC键取消回复
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (event.key === 'Escape' && replyingTo) {
+ cancelReply();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [replyingTo]);
+
+ const fetchPostAndComments = async () => {
+ try {
+ setLoading(true);
+
+ const params = {
+ postId: postId,
+ username: user.username
+ }
+ const response = await getComments(params);
+
+ if (response && response.data) {
+ setPostContent(response.data.content || '');
+ // 直接按发布时间排序,最新的在前面
+ const allComments = response.data.comments || [];
+ const sortedComments = allComments.sort((a, b) =>
+ new Date(b.publishDate) - new Date(a.publishDate)
+ );
+ setComments(sortedComments);
+ } else {
+ message.error('获取帖子详情失败');
+ }
+ } catch (error) {
+ console.error('获取帖子详情失败:', error);
+ message.error(error.message || '获取帖子详情失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 提交主评论
+ const handleSubmitComment = async () => {
+ try {
+ const values = await form.validateFields();
+ setCommenting(true);
+
+ const params = {
+ content: values.comment,
+ username: user.username,
+ postId: postId
+ };
+
+ console.log('提交评论数据:', params);
+ const response = await addComment(params);
+
+ if (response && response.message === 'Comment added successfully') {
+ message.success('评论发布成功');
+ form.resetFields();
+ fetchPostAndComments();
+ } else {
+ message.error('评论发布失败');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ message.error(error.message || '发布评论失败');
+ } finally {
+ setCommenting(false);
+ }
+ };
+
+ // 提交回复评论
+ const handleSubmitReply = async (reviewerId) => {
+ try {
+ const values = await replyForms.validateFields();
+ setReplying(true);
+
+ const replyData = {
+ content: values.reply,
+ username: user.username,
+ postId: postId,
+ reviewer: reviewerId
+ };
+
+ console.log('提交回复数据:', replyData);
+ const response = await addComment(replyData);
+
+ if (response && response.message === 'Comment added successfully') {
+ message.success('回复发布成功');
+ replyForms.resetFields();
+ setReplyingTo(null);
+ fetchPostAndComments();
+ } else {
+ message.error('回复发布失败');
+ }
+ } catch (error) {
+ console.error('发布回复失败:', error);
+ message.error(error.message || '发布回复失败');
+ } finally {
+ setReplying(false);
+ }
+ };
+
+ // 开始回复评论
+ const startReply = (commentId) => {
+ setReplyingTo(commentId);
+ replyForms.resetFields();
+ };
+
+ // 取消回复
+ const cancelReply = () => {
+ setReplyingTo(null);
+ replyForms.resetFields();
+ };
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ try {
+ return new Date(dateString).toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch {
+ return dateString;
+ }
+ };
+
+ // 查找被回复的评论
+ const getReviewedComment = (reviewerId) => {
+ return comments.find(comment => comment.commentId === reviewerId);
+ };
+
+ // 如果用户未认证
+ if (!isAuthenticated) {
+ return (
+ <div className="text-center py-8">
+ <Title level={3}>请先登录</Title>
+ <Paragraph>您需要登录后才能查看帖子详情</Paragraph>
+ </div>
+ );
+ }
+
+ return (
+ <div className="max-w-4xl mx-auto space-y-6">
+ {/* 返回按钮 */}
+ <Button
+ icon={<ArrowLeftOutlined />}
+ onClick={() => navigate('/forum')}
+ className="mb-4"
+ >
+ 返回论坛
+ </Button>
+
+ {loading ? (
+ <div className="flex justify-center py-8">
+ <Spin size="large" tip="加载中..." />
+ </div>
+ ) : (
+ <>
+ {/* 帖子内容 */}
+ <Card title={
+ <Space>
+ <MessageOutlined />
+ <span>帖子详情</span>
+ </Space>
+ }>
+ <div className="mb-4">
+ <Title level={4}>帖子内容</Title>
+ <Paragraph style={{ fontSize: '16px', lineHeight: 1.6 }}>
+ {postContent || '暂无内容'}
+ </Paragraph>
+ </div>
+ <Divider />
+ <div className="flex justify-between items-center text-sm text-gray-500">
+ <span>帖子ID: {postId}</span>
+ <Space>
+ <Tag color="blue">
+ <MessageOutlined /> {comments.length} 条评论
+ </Tag>
+ </Space>
+ </div>
+ </Card>
+
+ {/* 评论区 */}
+ <Card title={
+ <Space>
+ <MessageOutlined />
+ <span>评论区 ({comments.length})</span>
+ </Space>
+ }>
+ {/* 发表评论 */}
+ <div className="mb-6">
+ <Title level={5}>发表评论</Title>
+ <Form form={form} layout="vertical">
+ <Form.Item
+ name="comment"
+ rules={[{ required: true, message: '请输入评论内容' }]}
+ >
+ <TextArea
+ rows={4}
+ placeholder="请输入您的评论..."
+ maxLength={500}
+ showCount
+ />
+ </Form.Item>
+ <Form.Item>
+ <Button
+ type="primary"
+ onClick={handleSubmitComment}
+ loading={commenting}
+ >
+ 发布评论
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+
+ <Divider />
+
+ {/* 评论列表 */}
+ {comments.length > 0 ? (
+ <List
+ itemLayout="vertical"
+ dataSource={comments}
+ renderItem={(comment) => (
+ <List.Item
+ key={comment.commentId}
+ className={`border-l-2 pl-4 hover:bg-gray-50 transition-colors ${
+ comment.reviewerId ? 'border-l-orange-200 bg-orange-50' : 'border-l-blue-100'
+ }`}
+ >
+ <List.Item.Meta
+ avatar={
+ <Avatar
+ src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${comment.writer || 'anonymous'}`}
+ icon={<UserOutlined />}
+ />
+ }
+ title={
+ <div className="flex flex-wrap items-center gap-2">
+ <Text strong>{comment.writer || '匿名用户'}</Text>
+ <Text type="secondary" className="text-sm">
+ {formatDate(comment.publishDate)}
+ </Text>
+ <Tag size="small" color="blue">
+ #{comment.commentId}
+ </Tag>
+ {comment.reviewerId && (
+ <Tag size="small" color="orange" className="ml-2">
+ 回复 #{comment.reviewerId}
+ </Tag>
+ )}
+ </div>
+ }
+ description={
+ <div>
+ {/* 显示被回复的评论 */}
+ {comment.reviewerId && (
+ <div className="mb-3 p-3 bg-gray-100 rounded border-l-4 border-l-orange-400">
+ <Text type="secondary" className="text-xs">
+ 回复 #{comment.reviewerId}:
+ </Text>
+ {(() => {
+ const reviewedComment = getReviewedComment(comment.reviewerId);
+ return reviewedComment ? (
+ <div className="mt-1">
+ <Text type="secondary" className="text-sm">
+ {reviewedComment.writer || '匿名用户'}:
+ </Text>
+ <Text className="text-sm ml-1">
+ {reviewedComment.content.length > 50
+ ? reviewedComment.content.substring(0, 50) + '...'
+ : reviewedComment.content}
+ </Text>
+ </div>
+ ) : (
+ <Text type="secondary" className="text-sm">
+ 原评论已被删除
+ </Text>
+ );
+ })()}
+ </div>
+ )}
+
+ <Paragraph className="mt-2 mb-3">
+ {comment.content}
+ </Paragraph>
+
+ {/* 回复按钮 */}
+ <div className="mb-3">
+ <Button
+ type="link"
+ icon={<CommentOutlined />}
+ onClick={() => startReply(comment.commentId)}
+ size="small"
+ disabled={replyingTo === comment.commentId}
+ className="p-0"
+ >
+ {replyingTo === comment.commentId ? '回复中...' : '回复'}
+ </Button>
+ </div>
+
+ {/* 回复表单 */}
+ {replyingTo === comment.commentId && (
+ <div className="mt-4 p-4 bg-gray-50 rounded-lg">
+ <Form form={replyForms} layout="vertical">
+ <Form.Item
+ name="reply"
+ rules={[{ required: true, message: '请输入回复内容' }]}
+ >
+ <TextArea
+ rows={3}
+ placeholder={`回复 ${comment.writer || '匿名用户'}...`}
+ maxLength={500}
+ showCount
+ />
+ </Form.Item>
+ <Form.Item className="mb-0">
+ <Space>
+ <Button
+ type="primary"
+ size="small"
+ onClick={() => handleSubmitReply(comment.commentId)}
+ loading={replying}
+ >
+ 发布回复
+ </Button>
+ <Button
+ size="small"
+ onClick={cancelReply}
+ >
+ 取消
+ </Button>
+ </Space>
+ </Form.Item>
+ </Form>
+ </div>
+ )}
+ </div>
+ }
+ />
+ </List.Item>
+ )}
+ />
+ ) : (
+ <Empty
+ description="暂无评论,快来发表第一条评论吧!"
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
+ />
+ )}
+ </Card>
+ </>
+ )}
+ </div>
+ );
+};
+
+export default PostDetailPage;
\ No newline at end of file
diff --git a/src/features/forum/services/forumApi.js b/src/features/forum/services/forumApi.js
deleted file mode 100644
index 68feca1..0000000
--- a/src/features/forum/services/forumApi.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import request from "../../../services/request";
-
-// 帖子相关API
-export const createPost = (postData) => {
- const token = localStorage.getItem("token");
- return request
- .post(
- "/posts/create",
- postData,
- { headers: { token } }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "创建帖子失败"));
- }
- });
-};
-
-export const getPosts = (filters) => {
- const token = localStorage.getItem("token");
-
- // 构建查询参数
- let queryParams = new URLSearchParams();
- if (filters) {
- if (filters.username) queryParams.append("username", filters.username);
- if (filters.title) queryParams.append("title", filters.title);
- if (filters.author) queryParams.append("author", filters.author);
- if (filters.date) queryParams.append("date", filters.date);
- }
-
- return request
- .get(
- `/posts/list?${queryParams.toString()}`,
- { headers: { token } }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "获取帖子列表失败"));
- }
- });
-};
-
-export const deletePost = (username, pid) => {
- const token = localStorage.getItem("token");
- return request
- .delete(
- "/posts/delete",
- {
- headers: { token },
- data: { username, pid }
- }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "删除帖子失败"));
- }
- });
-};
-
-// 评论相关API
-export const addComment = (commentData) => {
- const token = localStorage.getItem("token");
- return request
- .post(
- "/comment/add",
- commentData,
- { headers: { token } }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "添加评论失败"));
- }
- });
-};
-
-export const getComments = (postId, username) => {
- const token = localStorage.getItem("token");
- return request
- .get(
- `/comment/get?postId=${postId}&username=${username}`,
- { headers: { token } }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "获取评论失败"));
- }
- });
-};
-
-export const deleteComment = (username, commentId) => {
- const token = localStorage.getItem("token");
- return request
- .delete(
- "/comment/delete",
- {
- headers: { token },
- data: { username, commentId }
- }
- )
- .then((response) => {
- if (response.data && response.data.success) {
- return response.data;
- } else {
- return Promise.reject(new Error(response.data.message || "删除评论失败"));
- }
- });
-};
\ No newline at end of file
diff --git a/src/features/profile/pages/ProfilePage.jsx b/src/features/profile/pages/ProfilePage.jsx
index 79fbfe4..bdbdb0a 100644
--- a/src/features/profile/pages/ProfilePage.jsx
+++ b/src/features/profile/pages/ProfilePage.jsx
@@ -1,19 +1,365 @@
-import React from 'react';
-import { Typography, Button, Empty } from 'antd';
+import React, { useState, useEffect } from 'react';
+import {
+ Typography,
+ Card,
+ Avatar,
+ Statistic,
+ Row,
+ Col,
+ Tag,
+ Progress,
+ Button,
+ Divider,
+ Space,
+ Tooltip,
+ message,
+ Modal,
+ Form,
+ Input,
+ Upload
+} from 'antd';
+import {
+ UserOutlined,
+ EditOutlined,
+ UploadOutlined,
+ DownloadOutlined,
+ TrophyOutlined,
+ HeartOutlined,
+ WarningOutlined,
+ CheckCircleOutlined,
+ SyncOutlined,
+ GiftOutlined,
+ SettingOutlined,
+ CameraOutlined
+} from '@ant-design/icons';
+import { useAuth } from '@/features/auth/contexts/AuthContext';
+import { getUserStats, updateUserProfile, uploadAvatar } from '@/api/user';
-const { Title } = Typography;
+const { Title, Text, Paragraph } = Typography;
-const ProfilePage = () => (
- <div className="space-y-6">
- <Title level={2}>个人资料</Title>
+const ProfilePage = () => {
+ const { user } = useAuth();
+ const [loading, setLoading] = useState(false);
+ const [editModalVisible, setEditModalVisible] = useState(false);
+ const [form] = Form.useForm();
+
+ // PT站统计数据
+ const [ptStats, setPtStats] = useState({
+ uploadSize: 157.89, // GB
+ downloadSize: 89.32, // GB
+ ratio: 1.77,
+ points: 12580,
+ userClass: 'Power User',
+ seedingCount: 12,
+ leechingCount: 2,
+ completedCount: 156,
+ invites: 3,
+ warnings: 0,
+ hitAndRuns: 0,
+ joinDate: '2023-05-15',
+ lastActive: '2024-12-28 15:30:00'
+ });
+
+ // 获取用户统计信息
+ useEffect(() => {
+ if (user?.username) {
+ fetchUserStats();
+ }
+ }, [user]);
+
+ const fetchUserStats = async () => {
+ try {
+ setLoading(true);
+ const response = await getUserStats(user.username);
+ if (response && response.data) {
+ setPtStats(prevStats => ({
+ ...prevStats,
+ ...response.data
+ }));
+ }
+ } catch (error) {
+ console.error('获取用户统计信息失败:', error);
+ // 使用默认数据,不显示错误信息
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 格式化文件大小
+ const formatSize = (sizeInGB) => {
+ if (sizeInGB >= 1024) {
+ return `${(sizeInGB / 1024).toFixed(2)} TB`;
+ }
+ return `${sizeInGB.toFixed(2)} GB`;
+ };
+
+ // 获取分享率颜色
+ const getRatioColor = (ratio) => {
+ if (ratio >= 2.0) return '#52c41a'; // 绿色
+ if (ratio >= 1.0) return '#1890ff'; // 蓝色
+ if (ratio >= 0.5) return '#faad14'; // 橙色
+ return '#f5222d'; // 红色
+ };
+
+ // 获取用户等级颜色
+ const getUserClassColor = (userClass) => {
+ const classColors = {
+ 'User': 'default',
+ 'Power User': 'blue',
+ 'Elite User': 'purple',
+ 'Crazy User': 'gold',
+ 'Insane User': 'red',
+ 'Veteran User': 'green',
+ 'Extreme User': 'volcano',
+ 'VIP': 'magenta'
+ };
+ return classColors[userClass] || 'default';
+ };
+
+ // 显示编辑对话框
+ const showEditModal = () => {
+ form.setFieldsValue({
+ username: user?.username,
+ email: user?.email
+ });
+ setEditModalVisible(true);
+ };
+
+ // 处理编辑提交
+ const handleEditSubmit = async () => {
+ try {
+ const values = await form.validateFields();
+ setLoading(true);
+
+ const response = await updateUserProfile({
+ username: user.username,
+ ...values
+ });
+
+ if (response && response.data) {
+ message.success('资料更新成功');
+ setEditModalVisible(false);
+ // 可以触发AuthContext的用户信息更新
+ } else {
+ message.error('更新失败,请重试');
+ }
+
+ } catch (error) {
+ console.error('更新失败:', error);
+ message.error(error.message || '更新失败,请重试');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 头像上传处理
+ const handleAvatarUpload = async (file) => {
+ const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+ if (!isJpgOrPng) {
+ message.error('只能上传 JPG/PNG 格式的图片!');
+ return false;
+ }
+ const isLt2M = file.size / 1024 / 1024 < 2;
+ if (!isLt2M) {
+ message.error('图片大小不能超过 2MB!');
+ return false;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('avatar', file);
+ formData.append('username', user.username);
+
+ const response = await uploadAvatar(formData);
+ if (response && response.data) {
+ message.success('头像上传成功');
+ // 可以触发AuthContext的用户信息更新或重新获取用户信息
+ } else {
+ message.error('头像上传失败');
+ }
+ } catch (error) {
+ console.error('头像上传失败:', error);
+ message.error('头像上传失败');
+ }
- <div className="text-center py-12">
- <Empty description="用户资料页面正在开发中" />
- <Button type="primary" className="mt-4">
- 编辑资料
- </Button>
+ return false; // 阻止默认上传行为
+ };
+
+ // 头像上传配置
+ const uploadProps = {
+ name: 'avatar',
+ showUploadList: false,
+ beforeUpload: handleAvatarUpload,
+ };
+
+ return (
+ <div className="space-y-6">
+ <div className="flex justify-between items-center">
+ <Title level={2}>个人资料</Title>
+ <Button
+ type="primary"
+ icon={<EditOutlined />}
+ onClick={showEditModal}
+ >
+ 编辑资料
+ </Button>
+ </div>
+
+ <Row gutter={[24, 24]}>
+ {/* 用户基本信息卡片 */}
+ <Col xs={24} lg={8}>
+ <Card>
+ <div className="text-center">
+ <div className="relative inline-block">
+ <Avatar
+ size={120}
+ src={user?.avatar}
+ icon={<UserOutlined />}
+ className="mb-4"
+ />
+ <Upload {...uploadProps}>
+ <Button
+ type="primary"
+ shape="circle"
+ icon={<CameraOutlined />}
+ size="small"
+ className="absolute bottom-0 right-0"
+ />
+ </Upload>
+ </div>
+
+ <Title level={3} className="mb-2">{user?.username || '用户'}</Title>
+
+ <Space direction="vertical" className="w-full">
+ <Tag
+ color={getUserClassColor(ptStats.userClass)}
+ className="text-lg px-3 py-1"
+ >
+ {ptStats.userClass}
+ </Tag>
+
+ <Text type="secondary">邮箱:{user?.email || '未设置'}</Text>
+ <Text type="secondary">注册时间:{ptStats.joinDate}</Text>
+ <Text type="secondary">最后活跃:{ptStats.lastActive}</Text>
+ </Space>
+ </div>
+ </Card>
+ </Col>
+
+ {/* PT站统计信息 */}
+ <Col xs={24} lg={16}>
+ <Card title={
+ <Space>
+ <TrophyOutlined />
+ <span>PT站统计</span>
+ </Space>
+ }>
+ <Row gutter={[16, 16]}>
+ {/* 上传下载统计 */}
+ <Col xs={12} sm={6}>
+ <Statistic
+ title="上传量"
+ value={formatSize(ptStats.uploadSize)}
+ prefix={<UploadOutlined style={{ color: '#52c41a' }} />}
+ valueStyle={{ color: '#52c41a' }}
+ />
+ </Col>
+ <Col xs={12} sm={6}>
+ <Statistic
+ title="下载量"
+ value={formatSize(ptStats.downloadSize)}
+ prefix={<DownloadOutlined style={{ color: '#1890ff' }} />}
+ valueStyle={{ color: '#1890ff' }}
+ />
+ </Col>
+ <Col xs={12} sm={6}>
+ <Statistic
+ title="分享率"
+ value={ptStats.ratio}
+ precision={2}
+ valueStyle={{ color: getRatioColor(ptStats.ratio) }}
+ />
+ </Col>
+ <Col xs={12} sm={6}>
+ <Statistic
+ title="积分"
+ value={ptStats.points}
+ prefix={<HeartOutlined style={{ color: '#eb2f96' }} />}
+ valueStyle={{ color: '#eb2f96' }}
+ />
+ </Col>
+ </Row>
+ </Card>
+ </Col>
+ </Row>
+
+ {/* 分享率进度条 */}
+ <Card title="分享率分析">
+ <Row gutter={[24, 16]}>
+ <Col xs={24} md={12}>
+ <div className="mb-4">
+ <Text strong>当前分享率:{ptStats.ratio}</Text>
+ <Progress
+ percent={Math.min(ptStats.ratio * 50, 100)} // 转换为百分比显示
+ strokeColor={getRatioColor(ptStats.ratio)}
+ format={() => ptStats.ratio}
+ />
+ </div>
+ <Space wrap>
+ <Tag color="green">≥2.0 优秀</Tag>
+ <Tag color="blue">≥1.0 良好</Tag>
+ <Tag color="orange">≥0.5 及格</Tag>
+ <Tag color="red"><0.5 需要改善</Tag>
+ </Space>
+ </Col>
+ <Col xs={24} md={12}>
+ <div className="space-y-2">
+ <Paragraph>
+ <Text strong>分享率说明:</Text>
+ </Paragraph>
+ <Paragraph type="secondary" className="text-sm">
+ • 分享率 = 上传量 ÷ 下载量<br/>
+ • 保持良好的分享率有助于维护账号状态<br/>
+ • 建议长期做种热门资源提升分享率<br/>
+ • 分享率过低可能导致账号受限
+ </Paragraph>
+ </div>
+ </Col>
+ </Row>
+ </Card>
+
+ {/* 编辑资料对话框 */}
+ <Modal
+ title="编辑个人资料"
+ open={editModalVisible}
+ onOk={handleEditSubmit}
+ onCancel={() => setEditModalVisible(false)}
+ confirmLoading={loading}
+ okText="保存"
+ cancelText="取消"
+ >
+ <Form form={form} layout="vertical">
+ <Form.Item
+ name="username"
+ label="用户名"
+ rules={[{ required: true, message: '请输入用户名' }]}
+ >
+ <Input placeholder="请输入用户名" />
+ </Form.Item>
+ <Form.Item
+ name="email"
+ label="邮箱"
+ rules={[
+ { required: true, message: '请输入邮箱' },
+ { type: 'email', message: '请输入有效的邮箱地址' }
+ ]}
+ >
+ <Input placeholder="请输入邮箱" />
+ </Form.Item>
+ </Form>
+ </Modal>
</div>
- </div>
-);
+ );
+};
export default ProfilePage;
\ No newline at end of file