blob: c138419109d1c08f3f2151f8642f31a061e44088 [file] [log] [blame]
DREWae420b22025-06-02 14:07:20 +08001import React, { useState, useEffect, useRef } from 'react';
2import { useParams, useNavigate, useLocation } from 'react-router-dom';
3import {
4 getRequestPostDetail,
5 addRequestPostComment,
6 likeRequestPost,
7 deleteRequestPost
8} from '../api/requestPost';
9import {
10 likeRequestPostComment,
11 getCommentReplies,
12 addRequestCommentReply,
13 deleteRequestComment
14} from '../api/requestComment';
Akane121765b61a72025-05-17 13:52:25 +080015import './RequestDetail.css';
16
17const RequestDetail = () => {
18 const { id } = useParams();
19 const navigate = useNavigate();
20 const location = useLocation();
DREWae420b22025-06-02 14:07:20 +080021 const fileInputRef = useRef(null);
22 const [post, setPost] = useState(null);
23 const [comments, setComments] = useState([]);
24 const [loading, setLoading] = useState(true);
25 const [error, setError] = useState(null);
Akane121765b61a72025-05-17 13:52:25 +080026 const [newComment, setNewComment] = useState('');
DREWae420b22025-06-02 14:07:20 +080027 const [replyContent, setReplyContent] = useState('');
28 const [replyImage, setReplyImage] = useState([]);
29 const [commentImage, setCommentImage] = useState([]);
30 const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的
31 const [loadingReplies, setLoadingReplies] = useState({});
32 const [setReplyingTo] = useState(null);
33
Akane121765b61a72025-05-17 13:52:25 +080034
DREWae420b22025-06-02 14:07:20 +080035 const [activeReplyId, setActiveReplyId] = useState(null);
36 const [replyModal, setReplyModal] = useState({
37 visible: false,
38 replyingTo: null,
39 replyingToUsername: '',
40 isReply: false
41 });
42
43 // 确保openReplyModal接收username参数
44 const openReplyModal = (commentId, username) => {
45 setReplyModal({
46 visible: true,
47 replyingTo: commentId,
48 replyingToUsername: username, // 确保这里接收username
49 isReply: false
50 });
51 };
52
53 // 关闭回复弹窗
54 const closeReplyModal = () => {
55 setReplyModal({
56 visible: false,
57 replyingTo: null,
58 replyingToUsername: '',
59 isReply: false
60 });
61 setReplyContent('');
62 };
63
64 const Comment = ({ comment, onLike, onReply, onDelete, isReply = false }) => {
65 return (
66 <div className={`comment-container ${isReply ? "is-reply" : ""}`}>
67 <div className="comment-item">
68 <div className="comment-avatar">
69 {(comment.authorId || "?").charAt(0)} {/* 修复点 */}
70 </div>
71 <div className="comment-content">
72 <div className="comment-header">
73 <span className="comment-user">{comment.authorId || "匿名用户"}</span>
74 {comment.replyTo && (
75 <span className="reply-to">回复 @{comment.replyTo}</span>
76 )}
77 <span className="comment-time">
78 {new Date(comment.createTime).toLocaleString()}
79 </span>
80 </div>
81 <p className="comment-text">{comment.content}</p>
82 {/* 添加评论图片展示 */}
83 {comment.imageUrl && (
84 <div className="comment-image-container">
85 <img
86 src={`http://localhost:8088${comment.imageUrl}`}
87 alt="评论图片"
88 className="comment-image"
89 onClick={() => window.open(comment.imageUrl, '_blank')}
90 />
91 </div>
92 )}
93 <div className="comment-actions">
94 <button onClick={() => onLike(comment.id)}>
95 👍 ({comment.likeCount || 0})
96 </button>
97 <button onClick={() => onReply(comment.id, comment.authorId)}>
98 回复
99 </button>
100 {comment.authorId === localStorage.getItem('username') && (
101 <button
102 className="delete-comment-btn"
103 onClick={() => onDelete(comment.id)}
104 >
105 删除
106 </button>
107 )}
108 </div>
109 </div>
110 </div>
111 </div>
112 );
113 };
114
115 // 递归渲染评论组件
116 const renderComment = (comment, depth = 0) => {
117 return (
118 <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}>
119 <Comment
120 comment={comment}
121 onLike={handleLikeComment}
122 onReply={openReplyModal}
123 isReply={depth > 0}
124 onDelete={handleDeleteComment}
125 />
126
127 {/* 递归渲染所有回复 */}
128 {comment.replies && comment.replies.map(reply =>
129 renderComment(reply, depth + 1)
130 )}
131 </div>
132 );
Akane121765b61a72025-05-17 13:52:25 +0800133 };
134
Akane121765b61a72025-05-17 13:52:25 +0800135
DREWae420b22025-06-02 14:07:20 +0800136 const fetchPostDetail = async () => {
137 try {
138 setLoading(true);
139 const response = await getRequestPostDetail(id);
140 console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响
141 setPost(response.data.data.post);
142 setComments(response.data.data.comments);
143 } catch (err) {
144 setError(err.response?.data?.message || '获取帖子详情失败');
145 } finally {
146 setLoading(false);
147 }
148 };
149
150 useEffect(() => {
151 fetchPostDetail();
152 }, [id]);
153
154 // 点赞帖子
155 const handleLikePost = async () => {
156 try {
157 await likeRequestPost(id);
158 setPost(prev => ({
159 ...prev,
160 likeCount: prev.likeCount + 1
161 }));
162 } catch (err) {
163 setError('点赞失败: ' + (err.response?.data?.message || err.message));
164 }
165 };
166
167 // 添加删除处理函数
168 const handleDeletePost = async (postId) => {
169 if (window.confirm('确定要删除这个帖子吗?所有评论也将被删除!')) {
170 try {
171 const username = localStorage.getItem('username');
172 await deleteRequestPost(postId, username);
173 navigate('/dashboard/request'); // 删除成功后返回求助区
174 } catch (err) {
175 setError('删除失败: ' + (err.response?.data?.message || err.message));
176 }
177 }
178 };
179
180 const handleCommentSubmit = async (e) => {
181 e.preventDefault();
182 if (!newComment.trim()) return;
183
184 try {
185 const username = localStorage.getItem('username');
186 const formData = new FormData();
187 formData.append('content', newComment);
188 formData.append('authorId', username);
189 if (commentImage) {
190 formData.append('image', commentImage);
191 }
192
193 const response = await addRequestPostComment(id, formData);
Akane121765b61a72025-05-17 13:52:25 +0800194
DREWae420b22025-06-02 14:07:20 +0800195 // 修改这里的响应处理逻辑
196 if (response.data && response.data.code === 200) {
197 await fetchPostDetail();
198
199 setNewComment('');
200 setCommentImage(null); // 清空评论图片
201 } else {
202 setError(response.data.message || '评论失败');
203 }
204 } catch (err) {
205 setError('评论失败: ' + (err.response?.data?.message || err.message));
206 }
Akane121765b61a72025-05-17 13:52:25 +0800207 };
208
22301080a93bebb2025-05-27 19:48:11 +0800209
DREWae420b22025-06-02 14:07:20 +0800210 const handleLikeComment = async (commentId) => {
211 try {
212 await likeRequestPostComment(commentId);
213
214 // 递归更新评论点赞数
215 const updateComments = (comments) => {
216 return comments.map(comment => {
217 // 当前评论匹配
218 if (comment.id === commentId) {
219 return { ...comment, likeCount: comment.likeCount + 1 };
220 }
221
222 // 递归处理回复
223 if (comment.replies && comment.replies.length > 0) {
224 return {
225 ...comment,
226 replies: updateComments(comment.replies)
227 };
228 }
229
230 return comment;
231 });
232 };
233
234 setComments(prev => updateComments(prev));
235 } catch (err) {
236 setError('点赞失败: ' + (err.response?.data?.message || err.message));
237 }
238 };
239
240 const handleDeleteComment = async (commentId) => {
241 if (window.confirm('确定要删除这条评论吗?')) {
242 try {
243 const username = localStorage.getItem('username');
244 await deleteRequestComment(commentId, username);
245 await fetchPostDetail(); // 刷新评论列表
246 } catch (err) {
247 setError('删除失败: ' + (err.response?.data?.message || err.message));
248 }
249 }
250 };
251
252
253 // 修改startReply函数
254 const startReply = (commentId) => {
255 if (activeReplyId === commentId) {
256 // 如果点击的是已经激活的回复按钮,则关闭
257 setActiveReplyId(null);
258 setReplyingTo(null);
259 } else {
260 // 否则打开新的回复框
261 setActiveReplyId(commentId);
262 setReplyingTo(commentId);
263 }
264 };
265
266 const handleReplySubmit = async (e) => {
267 e.preventDefault();
268 if (!replyContent.trim()) return;
269
270 try {
271 const username = localStorage.getItem('username');
272 const response = await addRequestCommentReply(replyModal.replyingTo, {
273 authorId: username,
274 content: replyContent,
275 image: replyImage
276 });
277
278 console.log('回复响应:', response.data); // 调试
279
280 if (response.data && response.data.code === 200) {
281 await fetchPostDetail();
282 setReplyContent('');
283 closeReplyModal();
284 }
285 } catch (err) {
286 console.error('回复错误:', err);
287 setError('回复失败: ' + (err.response?.data?.message || err.message));
288 }
289 };
290
291
292 // 返回按钮
293 const handleBack = () => {
294 const fromTab = location.state?.fromTab || 'share';
295 navigate(`/dashboard/request`);
296 };
297
298
299
300 const handleMarkSolved = () => {
301 // TODO: 实现标记为已解决的功能
302 setPost(prev => ({
303 ...prev,
304 isSolved: !prev.isSolved
305 }));
Akane121765b61a72025-05-17 13:52:25 +0800306 };
307
DREWae420b22025-06-02 14:07:20 +0800308 // const handleImageUpload = (e) => {
309 // const files = Array.from(e.target.files);
310 // const newImages = files.map(file => URL.createObjectURL(file));
311 // setImages(prev => [...prev, ...newImages]);
312 // };
313
314 // const handleRemoveImage = (index) => {
315 // setImages(prev => prev.filter((_, i) => i !== index));
316 // };
317
318
319
320 if (loading) return <div className="loading">加载中...</div>;
321 if (error) return <div className="error">{error}</div>;
322 if (!post) return <div className="error">帖子不存在</div>;
Akane121765b61a72025-05-17 13:52:25 +0800323
324 return (
325 <div className="request-detail-container">
326 <button className="back-button" onClick={handleBack}>
DREWae420b22025-06-02 14:07:20 +0800327 &larr; 返回求助区
Akane121765b61a72025-05-17 13:52:25 +0800328 </button>
329
DREWae420b22025-06-02 14:07:20 +0800330 <div className={`request-post ${post.isSolved ? 'solved' : ''}`}>
Akane121765b61a72025-05-17 13:52:25 +0800331 <div className="post-header">
DREWae420b22025-06-02 14:07:20 +0800332 <img
333 src={post.authorAvatar || 'https://via.placeholder.com/40'}
334 alt={post.authorId}
335 className="post-avatar"
336 />
Akane121765b61a72025-05-17 13:52:25 +0800337 <div className="post-meta">
DREWae420b22025-06-02 14:07:20 +0800338 <div className="post-author">{post.authorId}</div>
339 <div className="post-date">
340 {new Date(post.createTime).toLocaleString()}
341 </div>
342 </div>
343 {post.isSolved && <span ClassName="solved-badge">已解决</span>}
344 <div classname="delete-post">
345 {post.authorId === localStorage.getItem('username') && (
346 <button
347 className="delete-button"
348 onClick={() => handleDeletePost(post.id)}
349 >
350 删除帖子
351 </button>
352 )}
Akane121765b61a72025-05-17 13:52:25 +0800353 </div>
354 </div>
355
356 <h1 className="post-title">{post.title}</h1>
357
358 <div className="post-content">
359 {post.content.split('\n').map((para, i) => (
360 <p key={i}>{para}</p>
361 ))}
DREWae420b22025-06-02 14:07:20 +0800362 {/* 添加帖子图片展示 */}
363 {post.imageUrl && (
364 <div className="post-image-container">
365 <img
366 src={`http://localhost:8088${post.imageUrl}`}
367 alt="帖子图片"
368 className="post-image"
369 // onError={(e) => {
370 // e.target.onerror = null;
371 // e.target.src = 'https://via.placeholder.com/400x300?text=图片加载失败';
372 // console.error('图片加载失败:', post.imageUrl);
373 // }}
374 />
375 </div>
376 )}
Akane121765b61a72025-05-17 13:52:25 +0800377 </div>
378
379 <div className="post-actions">
380 <button
381 className={`like-button ${post.isLiked ? 'liked' : ''}`}
382 onClick={handleLikePost}
383 >
384 👍 点赞 ({post.likeCount})
385 </button>
386 <button
DREWae420b22025-06-02 14:07:20 +0800387 className={`solve-button ${post.isSolved ? 'solved' : ''}`}
388 onClick={handleMarkSolved}
Akane121765b61a72025-05-17 13:52:25 +0800389 >
DREWae420b22025-06-02 14:07:20 +0800390 {post.isSolved ? '✓ 已解决' : '标记为已解决'}
Akane121765b61a72025-05-17 13:52:25 +0800391 </button>
392 </div>
393 </div>
394
DREWae420b22025-06-02 14:07:20 +0800395 <div className="comments-section">
396 <h2>评论 ({post.replyCount})</h2>
Akane121765b61a72025-05-17 13:52:25 +0800397
398 <form onSubmit={handleCommentSubmit} className="comment-form">
399 <textarea
400 value={newComment}
401 onChange={(e) => setNewComment(e.target.value)}
DREWae420b22025-06-02 14:07:20 +0800402 placeholder="写下你的评论..."
Akane121765b61a72025-05-17 13:52:25 +0800403 rows="3"
404 required
405 />
DREWae420b22025-06-02 14:07:20 +0800406 <button type="submit">发表评论</button>
407
408 {/* 图片上传部分 */}
409 <div className="form-group">
410 <div className="upload-image-btn">
411 <input
412 type="file"
413 accept="image/*"
414 onChange={(e) => setCommentImage(e.target.files[0])}
415 data-testid="comment-image-input"
416 />
417 </div>
Akane121765b61a72025-05-17 13:52:25 +0800418 </div>
419 </form>
420
DREWae420b22025-06-02 14:07:20 +0800421
Akane121765b61a72025-05-17 13:52:25 +0800422 <div className="comment-list">
DREWae420b22025-06-02 14:07:20 +0800423 {comments.map(comment => renderComment(comment))}
Akane121765b61a72025-05-17 13:52:25 +0800424 </div>
DREWae420b22025-06-02 14:07:20 +0800425
426 {replyModal.visible && (
427 <div className="reply-modal-overlay">
428 <div className="reply-modal">
429 <div className="modal-header">
430 <h3>回复 @{replyModal.replyingToUsername}</h3>
431 <button onClick={closeReplyModal} className="close-modal">&times;</button>
432 </div>
433 <form onSubmit={handleReplySubmit}>
434 <textarea
435 value={replyContent}
436 onChange={(e) => setReplyContent(e.target.value)}
437 placeholder={`回复 @${replyModal.replyingToUsername}...`}
438 rows="5"
439 autoFocus
440 required
441 />
442
443 {/* 图片上传部分 */}
444 <div className="form-group">
445 <div className="upload-image-btn">
446 <input
447 type="file"
448 accept="image/*"
449 onChange={(e) => setReplyImage(e.target.files[0])}
450 />
451 </div>
452 </div>
453
454 <div className="modal-actions">
455 <button type="button" onClick={closeReplyModal} className="cancel-btn">
456 取消
457 </button>
458 <button type="submit" className="submit-btn">
459 发送回复
460 </button>
461 </div>
462 </form>
463 </div>
464 </div>
465 )}
466
Akane121765b61a72025-05-17 13:52:25 +0800467 </div>
468 </div>
469 );
470};
471
472export default RequestDetail;