| 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; |