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