blob: da50850664eb59336071fc4bfc9bee0b137a201a [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,
6 likePost
7} from '../api/helpPost';
8import {
9 likePostComment,
10 getCommentReplies,
11 addCommentReply
12} from '../api/helpComment';
13import './HelpDetail.css';
14
15const HelpDetail = () => {
16 const { id } = useParams();
17 const navigate = useNavigate();
18 const location = useLocation();
19 const fileInputRef = useRef(null);
20 const [post, setPost] = useState(null);
21 const [comments, setComments] = useState([]);
22 const [loading, setLoading] = useState(true);
23 const [error, setError] = useState(null);
24 const [newComment, setNewComment] = useState('');
25 const [newReply, setNewReply] = useState({});
26 const [images, setImages] = useState([]);
27 const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的
28 const [loadingReplies, setLoadingReplies] = useState({});
29 const [setReplyingTo] = useState(null);
30 const [replyContent, setReplyContent] = useState('');
31
32 const [activeReplyId, setActiveReplyId] = useState(null);
33 const [replyModal, setReplyModal] = useState({
34 visible: false,
35 replyingTo: null,
36 replyingToUsername: '',
37 isReply: false
38 });
39
40 // 确保openReplyModal接收username参数
41 const openReplyModal = (commentId, username) => {
42 setReplyModal({
43 visible: true,
44 replyingTo: commentId,
45 replyingToUsername: username, // 确保这里接收username
46 isReply: false
47 });
48 };
49
50 // 关闭回复弹窗
51 const closeReplyModal = () => {
52 setReplyModal({
53 visible: false,
54 replyingTo: null,
55 replyingToUsername: '',
56 isReply: false
57 });
58 setReplyContent('');
59 };
60
61 const Comment = ({ comment, onLike, onReply, isReply = false }) => {
62 return (
63 <div className={`comment-container ${isReply ? "is-reply" : ""}`}>
64 <div className="comment-item">
65 <div className="comment-avatar">
66 {(comment.authorId || "?").charAt(0)} {/* 修复点 */}
67 </div>
68 <div className="comment-content">
69 <div className="comment-header">
70 <span className="comment-user">{comment.authorId || "匿名用户"}</span>
71 {comment.replyTo && (
72 <span className="reply-to">回复 @{comment.replyTo}</span>
73 )}
74 <span className="comment-time">
75 {new Date(comment.createTime).toLocaleString()}
76 </span>
77 </div>
78 <p className="comment-text">{comment.content}</p>
79 <div className="comment-actions">
80 <button onClick={() => onLike(comment.id)}>
81 👍 ({comment.likeCount || 0})
82 </button>
83 <button onClick={() => onReply(comment.id, comment.authorId)}>
84 回复
85 </button>
86 </div>
87 </div>
88 </div>
89 </div>
90 );
91 };
92
93 // 递归渲染评论组件
94 const renderComment = (comment, depth = 0) => {
95 return (
96 <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}>
97 <Comment
98 comment={comment}
99 onLike={handleLikeComment}
100 onReply={openReplyModal}
101 isReply={depth > 0}
102 />
103
104 {/* 递归渲染所有回复 */}
105 {comment.replies && comment.replies.map(reply =>
106 renderComment(reply, depth + 1)
107 )}
108 </div>
109 );
110 };
111
112
113 const fetchPostDetail = async () => {
114 try {
115 setLoading(true);
116 const response = await getPostDetail(id);
117 console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响
118 setPost(response.data.data.post);
119 setComments(response.data.data.comments);
120 } catch (err) {
121 setError(err.response?.data?.message || '获取帖子详情失败');
122 } finally {
123 setLoading(false);
124 }
125 };
126
127 useEffect(() => {
128 fetchPostDetail();
129 }, [id]);
130
131 // 点赞帖子
132 const handleLikePost = async () => {
133 try {
134 await likePost(id);
135 setPost(prev => ({
136 ...prev,
137 likeCount: prev.likeCount + 1
138 }));
139 } catch (err) {
140 setError('点赞失败: ' + (err.response?.data?.message || err.message));
141 }
142 };
143
144 const handleCommentSubmit = async (e) => {
145 e.preventDefault();
146 if (!newComment.trim()) return;
147
148 try {
149 const username = localStorage.getItem('username');
150 const response = await addPostComment(id, {
151 content: newComment,
152 authorId: username
153 });
154
155 // 修改这里的响应处理逻辑
156 if (response.data && response.data.code === 200) {
157 await fetchPostDetail();
158
159 setNewComment('');
160 } else {
161 setError(response.data.message || '评论失败');
162 }
163 } catch (err) {
164 setError('评论失败: ' + (err.response?.data?.message || err.message));
165 }
166 };
167
168
169 const handleLikeComment = async (commentId) => {
170 try {
171 await likePostComment(commentId);
172
173 // 递归更新评论点赞数
174 const updateComments = (comments) => {
175 return comments.map(comment => {
176 // 当前评论匹配
177 if (comment.id === commentId) {
178 return { ...comment, likeCount: comment.likeCount + 1 };
179 }
180
181 // 递归处理回复
182 if (comment.replies && comment.replies.length > 0) {
183 return {
184 ...comment,
185 replies: updateComments(comment.replies)
186 };
187 }
188
189 return comment;
190 });
191 };
192
193 setComments(prev => updateComments(prev));
194 } catch (err) {
195 setError('点赞失败: ' + (err.response?.data?.message || err.message));
196 }
197 };
198
199
200 // 修改startReply函数
201 const startReply = (commentId) => {
202 if (activeReplyId === commentId) {
203 // 如果点击的是已经激活的回复按钮,则关闭
204 setActiveReplyId(null);
205 setReplyingTo(null);
206 } else {
207 // 否则打开新的回复框
208 setActiveReplyId(commentId);
209 setReplyingTo(commentId);
210 }
211 };
212
213 const handleReplySubmit = async (e) => {
214 e.preventDefault();
215 if (!replyContent.trim()) return;
216
217 try {
218 const username = localStorage.getItem('username');
219 const response = await addCommentReply(replyModal.replyingTo, {
220 content: replyContent,
221 authorId: username
222 });
223
224 console.log('回复响应:', response.data); // 调试
225
226 if (response.data && response.data.code === 200) {
227 await fetchPostDetail();
228
229 closeReplyModal();
230 }
231 } catch (err) {
232 console.error('回复错误:', err);
233 setError('回复失败: ' + (err.response?.data?.message || err.message));
234 }
235 };
236
237
238 // 返回按钮
239 const handleBack = () => {
240 const fromTab = location.state?.fromTab || 'share';
241 navigate(`/dashboard/help`);
242 };
243
244
245
246 const handleMarkSolved = () => {
247 // TODO: 实现标记为已解决的功能
248 setPost(prev => ({
249 ...prev,
250 isSolved: !prev.isSolved
251 }));
252 };
253
254 const handleImageUpload = (e) => {
255 const files = Array.from(e.target.files);
256 const newImages = files.map(file => URL.createObjectURL(file));
257 setImages(prev => [...prev, ...newImages]);
258 };
259
260 const handleRemoveImage = (index) => {
261 setImages(prev => prev.filter((_, i) => i !== index));
262 };
263
264
265
266 if (loading) return <div className="loading">加载中...</div>;
267 if (error) return <div className="error">{error}</div>;
268 if (!post) return <div className="error">帖子不存在</div>;
269
270 return (
271 <div className="help-detail-container">
272 <button className="back-button" onClick={handleBack}>
273 &larr; 返回求助区
274 </button>
275
276 <div className={`help-post ${post.isSolved ? 'solved' : ''}`}>
277 <div className="post-header">
278 <img
279 src={post.authorAvatar || 'https://via.placeholder.com/40'}
280 alt={post.authorId}
281 className="post-avatar"
282 />
283 <div className="post-meta">
284 <div className="post-author">{post.authorId}</div>
285 <div className="post-date">
286 {new Date(post.createTime).toLocaleString()}
287 </div>
288 </div>
289 {post.isSolved && <span className="solved-badge">已解决</span>}
290 </div>
291
292 <h1 className="post-title">{post.title}</h1>
293
294 <div className="post-content">
295 {post.content.split('\n').map((para, i) => (
296 <p key={i}>{para}</p>
297 ))}
298 </div>
299
300 <div className="post-actions">
301 <button
302 className={`like-button ${post.isLiked ? 'liked' : ''}`}
303 onClick={handleLikePost}
304 >
305 👍 点赞 ({post.likeCount})
306 </button>
307 <button
308 className={`solve-button ${post.isSolved ? 'solved' : ''}`}
309 onClick={handleMarkSolved}
310 >
311 {post.isSolved ? '✓ 已解决' : '标记为已解决'}
312 </button>
313 </div>
314 </div>
315
316 <div className="comments-section">
317 <h2>评论 ({post.replyCount})</h2>
318
319 <form onSubmit={handleCommentSubmit} className="comment-form">
320 <textarea
321 value={newComment}
322 onChange={(e) => setNewComment(e.target.value)}
323 placeholder="写下你的评论..."
324 rows="3"
325 required
326 />
327 <button type="submit">发表评论</button>
328 </form>
329
330 <div className="comment-list">
331 {comments.map(comment => renderComment(comment))}
332 </div>
333
334 {replyModal.visible && (
335 <div className="reply-modal-overlay">
336 <div className="reply-modal">
337 <div className="modal-header">
338 <h3>回复 @{replyModal.replyingToUsername}</h3>
339 <button onClick={closeReplyModal} className="close-modal">&times;</button>
340 </div>
341 <form onSubmit={handleReplySubmit}>
342 <textarea
343 value={replyContent}
344 onChange={(e) => setReplyContent(e.target.value)}
345 placeholder={`回复 @${replyModal.replyingToUsername}...`}
346 rows="5"
347 autoFocus
348 required
349 />
350 <div className="modal-actions">
351 <button type="button" onClick={closeReplyModal} className="cancel-btn">
352 取消
353 </button>
354 <button type="submit" className="submit-btn">
355 发送回复
356 </button>
357 </div>
358 </form>
359 </div>
360 </div>
361 )}
362
363 </div>
364 </div>
365 );
366};
367
368export default HelpDetail;