blob: 7e7b7c8aadeedd7d4c525dc0c8daa34d141c5f56 [file] [log] [blame]
Akane121765b61a72025-05-17 13:52:25 +08001import React, { useState, useEffect, useRef } from 'react';
2import { useParams, useNavigate, useLocation } from 'react-router-dom';
3import {
4 getPostDetail,
5 addPostComment,
Akane1217d1e9f712025-05-29 14:36:56 +08006 likePost,
7 deletePost
Akane121765b61a72025-05-17 13:52:25 +08008} from '../api/helpPost';
9import {
10 likePostComment,
11 getCommentReplies,
Akane1217d1e9f712025-05-29 14:36:56 +080012 addCommentReply,
13 deleteComment
Akane121765b61a72025-05-17 13:52:25 +080014} from '../api/helpComment';
15import './HelpDetail.css';
16
17const HelpDetail = () => {
18 const { id } = useParams();
19 const navigate = useNavigate();
20 const location = useLocation();
21 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);
26 const [newComment, setNewComment] = useState('');
Akane1217d1e9f712025-05-29 14:36:56 +080027 const [replyContent, setReplyContent] = useState('');
28 const [replyImage, setReplyImage] = useState([]);
29 const [commentImage, setCommentImage] = useState([]);
Akane121765b61a72025-05-17 13:52:25 +080030 const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的
31 const [loadingReplies, setLoadingReplies] = useState({});
32 const [setReplyingTo] = useState(null);
Akane1217d1e9f712025-05-29 14:36:56 +080033
Akane121765b61a72025-05-17 13:52:25 +080034
35 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
Akane1217d1e9f712025-05-29 14:36:56 +080064 const Comment = ({ comment, onLike, onReply, onDelete, isReply = false }) => {
Akane121765b61a72025-05-17 13:52:25 +080065 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>
Akane1217d1e9f712025-05-29 14:36:56 +080082 {/* 添加评论图片展示 */}
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 )}
Akane121765b61a72025-05-17 13:52:25 +080093 <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>
Akane1217d1e9f712025-05-29 14:36:56 +0800100 {comment.authorId === localStorage.getItem('username') && (
101 <button
102 className="delete-comment-btn"
103 onClick={() => onDelete(comment.id)}
104 >
105 删除
106 </button>
107 )}
Akane121765b61a72025-05-17 13:52:25 +0800108 </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}
Akane1217d1e9f712025-05-29 14:36:56 +0800124 onDelete={handleDeleteComment}
Akane121765b61a72025-05-17 13:52:25 +0800125 />
126
127 {/* 递归渲染所有回复 */}
128 {comment.replies && comment.replies.map(reply =>
129 renderComment(reply, depth + 1)
130 )}
131 </div>
132 );
133 };
134
135
136 const fetchPostDetail = async () => {
137 try {
138 setLoading(true);
139 const response = await getPostDetail(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 likePost(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 };
Akane1217d1e9f712025-05-29 14:36:56 +0800166
167 // 添加删除处理函数
168 const handleDeletePost = async (postId) => {
169 if (window.confirm('确定要删除这个帖子吗?所有评论也将被删除!')) {
170 try {
171 const username = localStorage.getItem('username');
172 await deletePost(postId, username);
173 navigate('/dashboard/help'); // 删除成功后返回求助区
174 } catch (err) {
175 setError('删除失败: ' + (err.response?.data?.message || err.message));
176 }
177 }
178 };
Akane121765b61a72025-05-17 13:52:25 +0800179
180 const handleCommentSubmit = async (e) => {
181 e.preventDefault();
182 if (!newComment.trim()) return;
183
184 try {
185 const username = localStorage.getItem('username');
186 const response = await addPostComment(id, {
187 content: newComment,
188 authorId: username
189 });
190
191 // 修改这里的响应处理逻辑
192 if (response.data && response.data.code === 200) {
193 await fetchPostDetail();
194
195 setNewComment('');
196 } else {
197 setError(response.data.message || '评论失败');
198 }
199 } catch (err) {
200 setError('评论失败: ' + (err.response?.data?.message || err.message));
201 }
202 };
203
204
205 const handleLikeComment = async (commentId) => {
206 try {
207 await likePostComment(commentId);
208
209 // 递归更新评论点赞数
210 const updateComments = (comments) => {
211 return comments.map(comment => {
212 // 当前评论匹配
213 if (comment.id === commentId) {
214 return { ...comment, likeCount: comment.likeCount + 1 };
215 }
216
217 // 递归处理回复
218 if (comment.replies && comment.replies.length > 0) {
219 return {
220 ...comment,
221 replies: updateComments(comment.replies)
222 };
223 }
224
225 return comment;
226 });
227 };
228
229 setComments(prev => updateComments(prev));
230 } catch (err) {
231 setError('点赞失败: ' + (err.response?.data?.message || err.message));
232 }
233 };
234
Akane1217d1e9f712025-05-29 14:36:56 +0800235 const handleDeleteComment = async (commentId) => {
236 if (window.confirm('确定要删除这条评论吗?')) {
237 try {
238 const username = localStorage.getItem('username');
239 await deleteComment(commentId, username);
240 await fetchPostDetail(); // 刷新评论列表
241 } catch (err) {
242 setError('删除失败: ' + (err.response?.data?.message || err.message));
243 }
244 }
245 };
246
Akane121765b61a72025-05-17 13:52:25 +0800247
248 // 修改startReply函数
249 const startReply = (commentId) => {
250 if (activeReplyId === commentId) {
251 // 如果点击的是已经激活的回复按钮,则关闭
252 setActiveReplyId(null);
253 setReplyingTo(null);
254 } else {
255 // 否则打开新的回复框
256 setActiveReplyId(commentId);
257 setReplyingTo(commentId);
258 }
259 };
260
261 const handleReplySubmit = async (e) => {
262 e.preventDefault();
263 if (!replyContent.trim()) return;
264
265 try {
266 const username = localStorage.getItem('username');
267 const response = await addCommentReply(replyModal.replyingTo, {
Akane1217d1e9f712025-05-29 14:36:56 +0800268 authorId: username,
Akane121765b61a72025-05-17 13:52:25 +0800269 content: replyContent,
Akane1217d1e9f712025-05-29 14:36:56 +0800270 image: replyImage
Akane121765b61a72025-05-17 13:52:25 +0800271 });
272
273 console.log('回复响应:', response.data); // 调试
274
275 if (response.data && response.data.code === 200) {
276 await fetchPostDetail();
Akane1217d1e9f712025-05-29 14:36:56 +0800277 setReplyContent('');
Akane121765b61a72025-05-17 13:52:25 +0800278 closeReplyModal();
279 }
280 } catch (err) {
281 console.error('回复错误:', err);
282 setError('回复失败: ' + (err.response?.data?.message || err.message));
283 }
284 };
285
286
287 // 返回按钮
288 const handleBack = () => {
289 const fromTab = location.state?.fromTab || 'share';
290 navigate(`/dashboard/help`);
291 };
292
293
294
295 const handleMarkSolved = () => {
296 // TODO: 实现标记为已解决的功能
297 setPost(prev => ({
298 ...prev,
299 isSolved: !prev.isSolved
300 }));
301 };
302
Akane1217d1e9f712025-05-29 14:36:56 +0800303 // const handleImageUpload = (e) => {
304 // const files = Array.from(e.target.files);
305 // const newImages = files.map(file => URL.createObjectURL(file));
306 // setImages(prev => [...prev, ...newImages]);
307 // };
Akane121765b61a72025-05-17 13:52:25 +0800308
Akane1217d1e9f712025-05-29 14:36:56 +0800309 // const handleRemoveImage = (index) => {
310 // setImages(prev => prev.filter((_, i) => i !== index));
311 // };
Akane121765b61a72025-05-17 13:52:25 +0800312
313
314
315 if (loading) return <div className="loading">加载中...</div>;
316 if (error) return <div className="error">{error}</div>;
317 if (!post) return <div className="error">帖子不存在</div>;
318
319 return (
320 <div className="help-detail-container">
321 <button className="back-button" onClick={handleBack}>
322 &larr; 返回求助区
323 </button>
324
325 <div className={`help-post ${post.isSolved ? 'solved' : ''}`}>
326 <div className="post-header">
327 <img
328 src={post.authorAvatar || 'https://via.placeholder.com/40'}
329 alt={post.authorId}
330 className="post-avatar"
331 />
332 <div className="post-meta">
333 <div className="post-author">{post.authorId}</div>
334 <div className="post-date">
335 {new Date(post.createTime).toLocaleString()}
Akane1217d1e9f712025-05-29 14:36:56 +0800336 </div>
Akane121765b61a72025-05-17 13:52:25 +0800337 </div>
Akane1217d1e9f712025-05-29 14:36:56 +0800338 {post.isSolved && <span ClassName="solved-badge">已解决</span>}
339 <div classname="delete-post">
340 {post.authorId === localStorage.getItem('username') && (
341 <button
342 className="delete-button"
343 onClick={() => handleDeletePost(post.id)}
344 >
345 删除帖子
346 </button>
347 )}
348 </div>
Akane121765b61a72025-05-17 13:52:25 +0800349 </div>
350
351 <h1 className="post-title">{post.title}</h1>
352
353 <div className="post-content">
354 {post.content.split('\n').map((para, i) => (
355 <p key={i}>{para}</p>
356 ))}
Akane1217d1e9f712025-05-29 14:36:56 +0800357 {/* 添加帖子图片展示 */}
358 {post.imageUrl && (
359 <div className="post-image-container">
360 <img
361 src={`http://localhost:8088${post.imageUrl}`}
362 alt="帖子图片"
363 className="post-image"
364 // onError={(e) => {
365 // e.target.onerror = null;
366 // e.target.src = 'https://via.placeholder.com/400x300?text=图片加载失败';
367 // console.error('图片加载失败:', post.imageUrl);
368 // }}
369 />
370 </div>
371 )}
Akane121765b61a72025-05-17 13:52:25 +0800372 </div>
373
374 <div className="post-actions">
375 <button
376 className={`like-button ${post.isLiked ? 'liked' : ''}`}
377 onClick={handleLikePost}
378 >
379 👍 点赞 ({post.likeCount})
380 </button>
381 <button
382 className={`solve-button ${post.isSolved ? 'solved' : ''}`}
383 onClick={handleMarkSolved}
384 >
385 {post.isSolved ? '✓ 已解决' : '标记为已解决'}
386 </button>
387 </div>
388 </div>
389
390 <div className="comments-section">
391 <h2>评论 ({post.replyCount})</h2>
392
393 <form onSubmit={handleCommentSubmit} className="comment-form">
394 <textarea
395 value={newComment}
396 onChange={(e) => setNewComment(e.target.value)}
397 placeholder="写下你的评论..."
398 rows="3"
399 required
400 />
401 <button type="submit">发表评论</button>
Akane1217d1e9f712025-05-29 14:36:56 +0800402
403 {/* 图片上传部分 */}
404 <div className="form-group">
405 <div className="upload-image-btn">
406 <input
407 type="file"
408 accept="image/*"
409 onChange={(e) => setCommentImage(e.target.files[0])}
410 data-testid="comment-image-input"
411 />
412 </div>
413 </div>
Akane121765b61a72025-05-17 13:52:25 +0800414 </form>
415
Akane1217d1e9f712025-05-29 14:36:56 +0800416
Akane121765b61a72025-05-17 13:52:25 +0800417 <div className="comment-list">
418 {comments.map(comment => renderComment(comment))}
419 </div>
420
421 {replyModal.visible && (
422 <div className="reply-modal-overlay">
423 <div className="reply-modal">
424 <div className="modal-header">
425 <h3>回复 @{replyModal.replyingToUsername}</h3>
426 <button onClick={closeReplyModal} className="close-modal">&times;</button>
427 </div>
428 <form onSubmit={handleReplySubmit}>
429 <textarea
430 value={replyContent}
431 onChange={(e) => setReplyContent(e.target.value)}
432 placeholder={`回复 @${replyModal.replyingToUsername}...`}
433 rows="5"
434 autoFocus
435 required
436 />
Akane1217d1e9f712025-05-29 14:36:56 +0800437
438 {/* 图片上传部分 */}
439 <div className="form-group">
440 <div className="upload-image-btn">
441 <input
442 type="file"
443 accept="image/*"
444 onChange={(e) => setReplyImage(e.target.files[0])}
445 />
446 </div>
447 </div>
448
Akane121765b61a72025-05-17 13:52:25 +0800449 <div className="modal-actions">
450 <button type="button" onClick={closeReplyModal} className="cancel-btn">
451 取消
452 </button>
453 <button type="submit" className="submit-btn">
454 发送回复
455 </button>
456 </div>
457 </form>
458 </div>
459 </div>
460 )}
461
462 </div>
463 </div>
464 );
465};
466
467export default HelpDetail;