feat(api): 重构 API 调用并优化用户认证流程
- 新增 auth、forum 和 user API 文件夹,重新组织 API 调用结构
- 重构 AuthContext,使用新 API 进行用户认证和信息管理
- 更新 AdminPanel、ForumPage 等组件,使用新的 API 调用
- 删除旧的 authApi.js 文件,清理冗余代码
- 优化用户登录、注册和登出流程,改进错误处理和用户提示
Change-Id: If664193e1bf30036c197f164edc5b10df75f1331
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