blob: 5e28820123658dd9870635fd7f5e98a2eb40b52c [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,
16 Empty
17} from 'antd';
18import { ArrowLeftOutlined, MessageOutlined, UserOutlined, CommentOutlined } from '@ant-design/icons';
19import { getComments, addComment } from '@/api/forum';
20import { useAuth } from '@/features/auth/contexts/AuthContext';
21
22const { Title, Paragraph, Text } = Typography;
23const { TextArea } = Input;
24
25const 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
407export default PostDetailPage;