blob: ba4da2facd8e71db3a96735dec12455470b20ba7 [file] [log] [blame]
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,
Modal
} from 'antd';
import { ArrowLeftOutlined, MessageOutlined, UserOutlined, CommentOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { getComments, addComment, deleteComment } 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, hasRole } = useAuth();
// 判断是否为管理员
const isAdmin = hasRole('admin') || (user && user.uid && user.uid.includes('admin'));
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 handleDeleteComment = (comment) => {
Modal.confirm({
title: '确认删除评论',
icon: <ExclamationCircleOutlined />,
content: (
<div>
<p>您确定要删除这条评论吗?</p>
<p><strong>作者:</strong>{comment.writer}</p>
<p><strong>内容:</strong>{comment.content.length > 50 ? comment.content.substring(0, 50) + '...' : comment.content}</p>
<p className="text-red-500 mt-2">此操作不可撤销!</p>
</div>
),
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
const params = {
username: user.username,
commentId: comment.commentId
};
const response = await deleteComment(params);
if (response.message) {
message.success(response.message || '评论删除成功');
fetchPostAndComments(); // 重新加载评论
} else {
message.error(response.message || '删除评论失败');
}
} catch (error) {
console.error('删除评论失败:', error);
message.error(error.message || '删除评论失败');
}
},
});
};
// 格式化日期
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>
)}
{isAdmin && (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteComment(comment)}
title="删除评论(管理员)"
className="ml-2"
>
删除
</Button>
)}
</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;