ybt | bac75f2 | 2025-06-08 22:31:15 +0800 | [diff] [blame^] | 1 | import React, { useState, useEffect } from 'react'; |
| 2 | import { useParams, useNavigate } from 'react-router-dom'; |
| 3 | import { |
| 4 | Card, |
| 5 | Avatar, |
| 6 | Typography, |
| 7 | Divider, |
| 8 | List, |
| 9 | Form, |
| 10 | Input, |
| 11 | Button, |
| 12 | message, |
| 13 | Spin, |
| 14 | Space, |
| 15 | Tag, |
| 16 | Empty |
| 17 | } from 'antd'; |
| 18 | import { ArrowLeftOutlined, MessageOutlined, UserOutlined, CommentOutlined } from '@ant-design/icons'; |
| 19 | import { getComments, addComment } from '@/api/forum'; |
| 20 | import { useAuth } from '@/features/auth/contexts/AuthContext'; |
| 21 | |
| 22 | const { Title, Paragraph, Text } = Typography; |
| 23 | const { TextArea } = Input; |
| 24 | |
| 25 | const PostDetailPage = () => { |
| 26 | const { postId } = useParams(); |
| 27 | const navigate = useNavigate(); |
| 28 | const { user, isAuthenticated } = useAuth(); |
| 29 | const [loading, setLoading] = useState(true); |
| 30 | const [commenting, setCommenting] = useState(false); |
| 31 | const [postContent, setPostContent] = useState(''); |
| 32 | const [comments, setComments] = useState([]); |
| 33 | const [form] = Form.useForm(); |
| 34 | const [replyForms] = Form.useForm(); // 用于回复的表单 |
| 35 | const [replyingTo, setReplyingTo] = useState(null); // 当前正在回复的评论ID |
| 36 | const [replying, setReplying] = useState(false); // 回复中状态 |
| 37 | |
| 38 | // 获取帖子详情和评论 |
| 39 | useEffect(() => { |
| 40 | if (isAuthenticated && user?.username && postId) { |
| 41 | fetchPostAndComments(); |
| 42 | } |
| 43 | }, [isAuthenticated, user, postId]); |
| 44 | |
| 45 | // 监听ESC键取消回复 |
| 46 | useEffect(() => { |
| 47 | const handleKeyDown = (event) => { |
| 48 | if (event.key === 'Escape' && replyingTo) { |
| 49 | cancelReply(); |
| 50 | } |
| 51 | }; |
| 52 | |
| 53 | document.addEventListener('keydown', handleKeyDown); |
| 54 | return () => { |
| 55 | document.removeEventListener('keydown', handleKeyDown); |
| 56 | }; |
| 57 | }, [replyingTo]); |
| 58 | |
| 59 | const fetchPostAndComments = async () => { |
| 60 | try { |
| 61 | setLoading(true); |
| 62 | |
| 63 | const params = { |
| 64 | postId: postId, |
| 65 | username: user.username |
| 66 | } |
| 67 | const response = await getComments(params); |
| 68 | |
| 69 | if (response && response.data) { |
| 70 | setPostContent(response.data.content || ''); |
| 71 | // 直接按发布时间排序,最新的在前面 |
| 72 | const allComments = response.data.comments || []; |
| 73 | const sortedComments = allComments.sort((a, b) => |
| 74 | new Date(b.publishDate) - new Date(a.publishDate) |
| 75 | ); |
| 76 | setComments(sortedComments); |
| 77 | } else { |
| 78 | message.error('获取帖子详情失败'); |
| 79 | } |
| 80 | } catch (error) { |
| 81 | console.error('获取帖子详情失败:', error); |
| 82 | message.error(error.message || '获取帖子详情失败'); |
| 83 | } finally { |
| 84 | setLoading(false); |
| 85 | } |
| 86 | }; |
| 87 | |
| 88 | // 提交主评论 |
| 89 | const handleSubmitComment = async () => { |
| 90 | try { |
| 91 | const values = await form.validateFields(); |
| 92 | setCommenting(true); |
| 93 | |
| 94 | const params = { |
| 95 | content: values.comment, |
| 96 | username: user.username, |
| 97 | postId: postId |
| 98 | }; |
| 99 | |
| 100 | console.log('提交评论数据:', params); |
| 101 | const response = await addComment(params); |
| 102 | |
| 103 | if (response && response.message === 'Comment added successfully') { |
| 104 | message.success('评论发布成功'); |
| 105 | form.resetFields(); |
| 106 | fetchPostAndComments(); |
| 107 | } else { |
| 108 | message.error('评论发布失败'); |
| 109 | } |
| 110 | } catch (error) { |
| 111 | console.error('发布评论失败:', error); |
| 112 | message.error(error.message || '发布评论失败'); |
| 113 | } finally { |
| 114 | setCommenting(false); |
| 115 | } |
| 116 | }; |
| 117 | |
| 118 | // 提交回复评论 |
| 119 | const handleSubmitReply = async (reviewerId) => { |
| 120 | try { |
| 121 | const values = await replyForms.validateFields(); |
| 122 | setReplying(true); |
| 123 | |
| 124 | const replyData = { |
| 125 | content: values.reply, |
| 126 | username: user.username, |
| 127 | postId: postId, |
| 128 | reviewer: reviewerId |
| 129 | }; |
| 130 | |
| 131 | console.log('提交回复数据:', replyData); |
| 132 | const response = await addComment(replyData); |
| 133 | |
| 134 | if (response && response.message === 'Comment added successfully') { |
| 135 | message.success('回复发布成功'); |
| 136 | replyForms.resetFields(); |
| 137 | setReplyingTo(null); |
| 138 | fetchPostAndComments(); |
| 139 | } else { |
| 140 | message.error('回复发布失败'); |
| 141 | } |
| 142 | } catch (error) { |
| 143 | console.error('发布回复失败:', error); |
| 144 | message.error(error.message || '发布回复失败'); |
| 145 | } finally { |
| 146 | setReplying(false); |
| 147 | } |
| 148 | }; |
| 149 | |
| 150 | // 开始回复评论 |
| 151 | const startReply = (commentId) => { |
| 152 | setReplyingTo(commentId); |
| 153 | replyForms.resetFields(); |
| 154 | }; |
| 155 | |
| 156 | // 取消回复 |
| 157 | const cancelReply = () => { |
| 158 | setReplyingTo(null); |
| 159 | replyForms.resetFields(); |
| 160 | }; |
| 161 | |
| 162 | // 格式化日期 |
| 163 | const formatDate = (dateString) => { |
| 164 | try { |
| 165 | return new Date(dateString).toLocaleString('zh-CN', { |
| 166 | year: 'numeric', |
| 167 | month: '2-digit', |
| 168 | day: '2-digit', |
| 169 | hour: '2-digit', |
| 170 | minute: '2-digit' |
| 171 | }); |
| 172 | } catch { |
| 173 | return dateString; |
| 174 | } |
| 175 | }; |
| 176 | |
| 177 | // 查找被回复的评论 |
| 178 | const getReviewedComment = (reviewerId) => { |
| 179 | return comments.find(comment => comment.commentId === reviewerId); |
| 180 | }; |
| 181 | |
| 182 | // 如果用户未认证 |
| 183 | if (!isAuthenticated) { |
| 184 | return ( |
| 185 | <div className="text-center py-8"> |
| 186 | <Title level={3}>请先登录</Title> |
| 187 | <Paragraph>您需要登录后才能查看帖子详情</Paragraph> |
| 188 | </div> |
| 189 | ); |
| 190 | } |
| 191 | |
| 192 | return ( |
| 193 | <div className="max-w-4xl mx-auto space-y-6"> |
| 194 | {/* 返回按钮 */} |
| 195 | <Button |
| 196 | icon={<ArrowLeftOutlined />} |
| 197 | onClick={() => navigate('/forum')} |
| 198 | className="mb-4" |
| 199 | > |
| 200 | 返回论坛 |
| 201 | </Button> |
| 202 | |
| 203 | {loading ? ( |
| 204 | <div className="flex justify-center py-8"> |
| 205 | <Spin size="large" tip="加载中..." /> |
| 206 | </div> |
| 207 | ) : ( |
| 208 | <> |
| 209 | {/* 帖子内容 */} |
| 210 | <Card title={ |
| 211 | <Space> |
| 212 | <MessageOutlined /> |
| 213 | <span>帖子详情</span> |
| 214 | </Space> |
| 215 | }> |
| 216 | <div className="mb-4"> |
| 217 | <Title level={4}>帖子内容</Title> |
| 218 | <Paragraph style={{ fontSize: '16px', lineHeight: 1.6 }}> |
| 219 | {postContent || '暂无内容'} |
| 220 | </Paragraph> |
| 221 | </div> |
| 222 | <Divider /> |
| 223 | <div className="flex justify-between items-center text-sm text-gray-500"> |
| 224 | <span>帖子ID: {postId}</span> |
| 225 | <Space> |
| 226 | <Tag color="blue"> |
| 227 | <MessageOutlined /> {comments.length} 条评论 |
| 228 | </Tag> |
| 229 | </Space> |
| 230 | </div> |
| 231 | </Card> |
| 232 | |
| 233 | {/* 评论区 */} |
| 234 | <Card title={ |
| 235 | <Space> |
| 236 | <MessageOutlined /> |
| 237 | <span>评论区 ({comments.length})</span> |
| 238 | </Space> |
| 239 | }> |
| 240 | {/* 发表评论 */} |
| 241 | <div className="mb-6"> |
| 242 | <Title level={5}>发表评论</Title> |
| 243 | <Form form={form} layout="vertical"> |
| 244 | <Form.Item |
| 245 | name="comment" |
| 246 | rules={[{ required: true, message: '请输入评论内容' }]} |
| 247 | > |
| 248 | <TextArea |
| 249 | rows={4} |
| 250 | placeholder="请输入您的评论..." |
| 251 | maxLength={500} |
| 252 | showCount |
| 253 | /> |
| 254 | </Form.Item> |
| 255 | <Form.Item> |
| 256 | <Button |
| 257 | type="primary" |
| 258 | onClick={handleSubmitComment} |
| 259 | loading={commenting} |
| 260 | > |
| 261 | 发布评论 |
| 262 | </Button> |
| 263 | </Form.Item> |
| 264 | </Form> |
| 265 | </div> |
| 266 | |
| 267 | <Divider /> |
| 268 | |
| 269 | {/* 评论列表 */} |
| 270 | {comments.length > 0 ? ( |
| 271 | <List |
| 272 | itemLayout="vertical" |
| 273 | dataSource={comments} |
| 274 | renderItem={(comment) => ( |
| 275 | <List.Item |
| 276 | key={comment.commentId} |
| 277 | className={`border-l-2 pl-4 hover:bg-gray-50 transition-colors ${ |
| 278 | comment.reviewerId ? 'border-l-orange-200 bg-orange-50' : 'border-l-blue-100' |
| 279 | }`} |
| 280 | > |
| 281 | <List.Item.Meta |
| 282 | avatar={ |
| 283 | <Avatar |
| 284 | src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${comment.writer || 'anonymous'}`} |
| 285 | icon={<UserOutlined />} |
| 286 | /> |
| 287 | } |
| 288 | title={ |
| 289 | <div className="flex flex-wrap items-center gap-2"> |
| 290 | <Text strong>{comment.writer || '匿名用户'}</Text> |
| 291 | <Text type="secondary" className="text-sm"> |
| 292 | {formatDate(comment.publishDate)} |
| 293 | </Text> |
| 294 | <Tag size="small" color="blue"> |
| 295 | #{comment.commentId} |
| 296 | </Tag> |
| 297 | {comment.reviewerId && ( |
| 298 | <Tag size="small" color="orange" className="ml-2"> |
| 299 | 回复 #{comment.reviewerId} |
| 300 | </Tag> |
| 301 | )} |
| 302 | </div> |
| 303 | } |
| 304 | description={ |
| 305 | <div> |
| 306 | {/* 显示被回复的评论 */} |
| 307 | {comment.reviewerId && ( |
| 308 | <div className="mb-3 p-3 bg-gray-100 rounded border-l-4 border-l-orange-400"> |
| 309 | <Text type="secondary" className="text-xs"> |
| 310 | 回复 #{comment.reviewerId}: |
| 311 | </Text> |
| 312 | {(() => { |
| 313 | const reviewedComment = getReviewedComment(comment.reviewerId); |
| 314 | return reviewedComment ? ( |
| 315 | <div className="mt-1"> |
| 316 | <Text type="secondary" className="text-sm"> |
| 317 | {reviewedComment.writer || '匿名用户'}: |
| 318 | </Text> |
| 319 | <Text className="text-sm ml-1"> |
| 320 | {reviewedComment.content.length > 50 |
| 321 | ? reviewedComment.content.substring(0, 50) + '...' |
| 322 | : reviewedComment.content} |
| 323 | </Text> |
| 324 | </div> |
| 325 | ) : ( |
| 326 | <Text type="secondary" className="text-sm"> |
| 327 | 原评论已被删除 |
| 328 | </Text> |
| 329 | ); |
| 330 | })()} |
| 331 | </div> |
| 332 | )} |
| 333 | |
| 334 | <Paragraph className="mt-2 mb-3"> |
| 335 | {comment.content} |
| 336 | </Paragraph> |
| 337 | |
| 338 | {/* 回复按钮 */} |
| 339 | <div className="mb-3"> |
| 340 | <Button |
| 341 | type="link" |
| 342 | icon={<CommentOutlined />} |
| 343 | onClick={() => startReply(comment.commentId)} |
| 344 | size="small" |
| 345 | disabled={replyingTo === comment.commentId} |
| 346 | className="p-0" |
| 347 | > |
| 348 | {replyingTo === comment.commentId ? '回复中...' : '回复'} |
| 349 | </Button> |
| 350 | </div> |
| 351 | |
| 352 | {/* 回复表单 */} |
| 353 | {replyingTo === comment.commentId && ( |
| 354 | <div className="mt-4 p-4 bg-gray-50 rounded-lg"> |
| 355 | <Form form={replyForms} layout="vertical"> |
| 356 | <Form.Item |
| 357 | name="reply" |
| 358 | rules={[{ required: true, message: '请输入回复内容' }]} |
| 359 | > |
| 360 | <TextArea |
| 361 | rows={3} |
| 362 | placeholder={`回复 ${comment.writer || '匿名用户'}...`} |
| 363 | maxLength={500} |
| 364 | showCount |
| 365 | /> |
| 366 | </Form.Item> |
| 367 | <Form.Item className="mb-0"> |
| 368 | <Space> |
| 369 | <Button |
| 370 | type="primary" |
| 371 | size="small" |
| 372 | onClick={() => handleSubmitReply(comment.commentId)} |
| 373 | loading={replying} |
| 374 | > |
| 375 | 发布回复 |
| 376 | </Button> |
| 377 | <Button |
| 378 | size="small" |
| 379 | onClick={cancelReply} |
| 380 | > |
| 381 | 取消 |
| 382 | </Button> |
| 383 | </Space> |
| 384 | </Form.Item> |
| 385 | </Form> |
| 386 | </div> |
| 387 | )} |
| 388 | </div> |
| 389 | } |
| 390 | /> |
| 391 | </List.Item> |
| 392 | )} |
| 393 | /> |
| 394 | ) : ( |
| 395 | <Empty |
| 396 | description="暂无评论,快来发表第一条评论吧!" |
| 397 | image={Empty.PRESENTED_IMAGE_SIMPLE} |
| 398 | /> |
| 399 | )} |
| 400 | </Card> |
| 401 | </> |
| 402 | )} |
| 403 | </div> |
| 404 | ); |
| 405 | }; |
| 406 | |
| 407 | export default PostDetailPage; |