| import React, { useState, useEffect, useRef } from 'react'; | |
| import { useParams, useNavigate, useLocation } from 'react-router-dom'; | |
| import { | |
| getPostDetail, | |
| addPostComment, | |
| likePost, | |
| deletePost | |
| } from '../api/helpPost'; | |
| import { | |
| likePostComment, | |
| getCommentReplies, | |
| addCommentReply, | |
| deleteComment | |
| } from '../api/helpComment'; | |
| import './HelpDetail.css'; | |
| const HelpDetail = () => { | |
| const { id } = useParams(); | |
| const navigate = useNavigate(); | |
| const location = useLocation(); | |
| const fileInputRef = useRef(null); | |
| const [post, setPost] = useState(null); | |
| const [comments, setComments] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState(null); | |
| const [newComment, setNewComment] = useState(''); | |
| const [replyContent, setReplyContent] = useState(''); | |
| const [replyImage, setReplyImage] = useState([]); | |
| const [commentImage, setCommentImage] = useState([]); | |
| const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的 | |
| const [loadingReplies, setLoadingReplies] = useState({}); | |
| const [setReplyingTo] = useState(null); | |
| const [activeReplyId, setActiveReplyId] = useState(null); | |
| const [replyModal, setReplyModal] = useState({ | |
| visible: false, | |
| replyingTo: null, | |
| replyingToUsername: '', | |
| isReply: false | |
| }); | |
| // 确保openReplyModal接收username参数 | |
| const openReplyModal = (commentId, username) => { | |
| setReplyModal({ | |
| visible: true, | |
| replyingTo: commentId, | |
| replyingToUsername: username, // 确保这里接收username | |
| isReply: false | |
| }); | |
| }; | |
| // 关闭回复弹窗 | |
| const closeReplyModal = () => { | |
| setReplyModal({ | |
| visible: false, | |
| replyingTo: null, | |
| replyingToUsername: '', | |
| isReply: false | |
| }); | |
| setReplyContent(''); | |
| }; | |
| const Comment = ({ comment, onLike, onReply, onDelete, isReply = false }) => { | |
| return ( | |
| <div className={`comment-container ${isReply ? "is-reply" : ""}`}> | |
| <div className="comment-item"> | |
| <div className="comment-avatar"> | |
| {(comment.authorId || "?").charAt(0)} {/* 修复点 */} | |
| </div> | |
| <div className="comment-content"> | |
| <div className="comment-header"> | |
| <span className="comment-user">{comment.authorId || "匿名用户"}</span> | |
| {comment.replyTo && ( | |
| <span className="reply-to">回复 @{comment.replyTo}</span> | |
| )} | |
| <span className="comment-time"> | |
| {new Date(comment.createTime).toLocaleString()} | |
| </span> | |
| </div> | |
| <p className="comment-text">{comment.content}</p> | |
| {/* 添加评论图片展示 */} | |
| {comment.imageUrl && ( | |
| <div className="comment-image-container"> | |
| <img | |
| src={`http://localhost:8088${comment.imageUrl}`} | |
| alt="评论图片" | |
| className="comment-image" | |
| onClick={() => window.open(comment.imageUrl, '_blank')} | |
| /> | |
| </div> | |
| )} | |
| <div className="comment-actions"> | |
| <button onClick={() => onLike(comment.id)}> | |
| 👍 ({comment.likeCount || 0}) | |
| </button> | |
| <button onClick={() => onReply(comment.id, comment.authorId)}> | |
| 回复 | |
| </button> | |
| {comment.authorId === localStorage.getItem('username') && ( | |
| <button | |
| className="delete-comment-btn" | |
| onClick={() => onDelete(comment.id)} | |
| > | |
| 删除 | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // 递归渲染评论组件 | |
| const renderComment = (comment, depth = 0) => { | |
| return ( | |
| <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}> | |
| <Comment | |
| comment={comment} | |
| onLike={handleLikeComment} | |
| onReply={openReplyModal} | |
| isReply={depth > 0} | |
| onDelete={handleDeleteComment} | |
| /> | |
| {/* 递归渲染所有回复 */} | |
| {comment.replies && comment.replies.map(reply => | |
| renderComment(reply, depth + 1) | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const fetchPostDetail = async () => { | |
| try { | |
| setLoading(true); | |
| const response = await getPostDetail(id); | |
| console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响 | |
| setPost(response.data.data.post); | |
| setComments(response.data.data.comments); | |
| } catch (err) { | |
| setError(err.response?.data?.message || '获取帖子详情失败'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchPostDetail(); | |
| }, [id]); | |
| // 点赞帖子 | |
| const handleLikePost = async () => { | |
| try { | |
| await likePost(id); | |
| setPost(prev => ({ | |
| ...prev, | |
| likeCount: prev.likeCount + 1 | |
| })); | |
| } catch (err) { | |
| setError('点赞失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| }; | |
| // 添加删除处理函数 | |
| const handleDeletePost = async (postId) => { | |
| if (window.confirm('确定要删除这个帖子吗?所有评论也将被删除!')) { | |
| try { | |
| const username = localStorage.getItem('username'); | |
| await deletePost(postId, username); | |
| navigate('/dashboard/help'); // 删除成功后返回求助区 | |
| } catch (err) { | |
| setError('删除失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| } | |
| }; | |
| const handleCommentSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!newComment.trim()) return; | |
| try { | |
| const username = localStorage.getItem('username'); | |
| const response = await addPostComment(id, { | |
| content: newComment, | |
| authorId: username | |
| }); | |
| // 修改这里的响应处理逻辑 | |
| if (response.data && response.data.code === 200) { | |
| await fetchPostDetail(); | |
| setNewComment(''); | |
| } else { | |
| setError(response.data.message || '评论失败'); | |
| } | |
| } catch (err) { | |
| setError('评论失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| }; | |
| const handleLikeComment = async (commentId) => { | |
| try { | |
| await likePostComment(commentId); | |
| // 递归更新评论点赞数 | |
| const updateComments = (comments) => { | |
| return comments.map(comment => { | |
| // 当前评论匹配 | |
| if (comment.id === commentId) { | |
| return { ...comment, likeCount: comment.likeCount + 1 }; | |
| } | |
| // 递归处理回复 | |
| if (comment.replies && comment.replies.length > 0) { | |
| return { | |
| ...comment, | |
| replies: updateComments(comment.replies) | |
| }; | |
| } | |
| return comment; | |
| }); | |
| }; | |
| setComments(prev => updateComments(prev)); | |
| } catch (err) { | |
| setError('点赞失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| }; | |
| const handleDeleteComment = async (commentId) => { | |
| if (window.confirm('确定要删除这条评论吗?')) { | |
| try { | |
| const username = localStorage.getItem('username'); | |
| await deleteComment(commentId, username); | |
| await fetchPostDetail(); // 刷新评论列表 | |
| } catch (err) { | |
| setError('删除失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| } | |
| }; | |
| // 修改startReply函数 | |
| const startReply = (commentId) => { | |
| if (activeReplyId === commentId) { | |
| // 如果点击的是已经激活的回复按钮,则关闭 | |
| setActiveReplyId(null); | |
| setReplyingTo(null); | |
| } else { | |
| // 否则打开新的回复框 | |
| setActiveReplyId(commentId); | |
| setReplyingTo(commentId); | |
| } | |
| }; | |
| const handleReplySubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!replyContent.trim()) return; | |
| try { | |
| const username = localStorage.getItem('username'); | |
| const response = await addCommentReply(replyModal.replyingTo, { | |
| authorId: username, | |
| content: replyContent, | |
| image: replyImage | |
| }); | |
| console.log('回复响应:', response.data); // 调试 | |
| if (response.data && response.data.code === 200) { | |
| await fetchPostDetail(); | |
| setReplyContent(''); | |
| closeReplyModal(); | |
| } | |
| } catch (err) { | |
| console.error('回复错误:', err); | |
| setError('回复失败: ' + (err.response?.data?.message || err.message)); | |
| } | |
| }; | |
| // 返回按钮 | |
| const handleBack = () => { | |
| const fromTab = location.state?.fromTab || 'share'; | |
| navigate(`/dashboard/help`); | |
| }; | |
| const handleMarkSolved = () => { | |
| // TODO: 实现标记为已解决的功能 | |
| setPost(prev => ({ | |
| ...prev, | |
| isSolved: !prev.isSolved | |
| })); | |
| }; | |
| // const handleImageUpload = (e) => { | |
| // const files = Array.from(e.target.files); | |
| // const newImages = files.map(file => URL.createObjectURL(file)); | |
| // setImages(prev => [...prev, ...newImages]); | |
| // }; | |
| // const handleRemoveImage = (index) => { | |
| // setImages(prev => prev.filter((_, i) => i !== index)); | |
| // }; | |
| if (loading) return <div className="loading">加载中...</div>; | |
| if (error) return <div className="error">{error}</div>; | |
| if (!post) return <div className="error">帖子不存在</div>; | |
| return ( | |
| <div className="help-detail-container"> | |
| <button className="back-button" onClick={handleBack}> | |
| ← 返回求助区 | |
| </button> | |
| <div className={`help-post ${post.isSolved ? 'solved' : ''}`}> | |
| <div className="post-header"> | |
| <img | |
| src={post.authorAvatar || 'https://via.placeholder.com/40'} | |
| alt={post.authorId} | |
| className="post-avatar" | |
| /> | |
| <div className="post-meta"> | |
| <div className="post-author">{post.authorId}</div> | |
| <div className="post-date"> | |
| {new Date(post.createTime).toLocaleString()} | |
| </div> | |
| </div> | |
| {post.isSolved && <span ClassName="solved-badge">已解决</span>} | |
| <div classname="delete-post"> | |
| {post.authorId === localStorage.getItem('username') && ( | |
| <button | |
| className="delete-button" | |
| onClick={() => handleDeletePost(post.id)} | |
| > | |
| 删除帖子 | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <h1 className="post-title">{post.title}</h1> | |
| <div className="post-content"> | |
| {post.content.split('\n').map((para, i) => ( | |
| <p key={i}>{para}</p> | |
| ))} | |
| {/* 添加帖子图片展示 */} | |
| {post.imageUrl && ( | |
| <div className="post-image-container"> | |
| <img | |
| src={`http://localhost:8088${post.imageUrl}`} | |
| alt="帖子图片" | |
| className="post-image" | |
| // onError={(e) => { | |
| // e.target.onerror = null; | |
| // e.target.src = 'https://via.placeholder.com/400x300?text=图片加载失败'; | |
| // console.error('图片加载失败:', post.imageUrl); | |
| // }} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="post-actions"> | |
| <button | |
| className={`like-button ${post.isLiked ? 'liked' : ''}`} | |
| onClick={handleLikePost} | |
| > | |
| 👍 点赞 ({post.likeCount}) | |
| </button> | |
| <button | |
| className={`solve-button ${post.isSolved ? 'solved' : ''}`} | |
| onClick={handleMarkSolved} | |
| > | |
| {post.isSolved ? '✓ 已解决' : '标记为已解决'} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="comments-section"> | |
| <h2>评论 ({post.replyCount})</h2> | |
| <form onSubmit={handleCommentSubmit} className="comment-form"> | |
| <textarea | |
| value={newComment} | |
| onChange={(e) => setNewComment(e.target.value)} | |
| placeholder="写下你的评论..." | |
| rows="3" | |
| required | |
| /> | |
| <button type="submit">发表评论</button> | |
| {/* 图片上传部分 */} | |
| <div className="form-group"> | |
| <div className="upload-image-btn"> | |
| <input | |
| type="file" | |
| accept="image/*" | |
| onChange={(e) => setCommentImage(e.target.files[0])} | |
| data-testid="comment-image-input" | |
| /> | |
| </div> | |
| </div> | |
| </form> | |
| <div className="comment-list"> | |
| {comments.map(comment => renderComment(comment))} | |
| </div> | |
| {replyModal.visible && ( | |
| <div className="reply-modal-overlay"> | |
| <div className="reply-modal"> | |
| <div className="modal-header"> | |
| <h3>回复 @{replyModal.replyingToUsername}</h3> | |
| <button onClick={closeReplyModal} className="close-modal">×</button> | |
| </div> | |
| <form onSubmit={handleReplySubmit}> | |
| <textarea | |
| value={replyContent} | |
| onChange={(e) => setReplyContent(e.target.value)} | |
| placeholder={`回复 @${replyModal.replyingToUsername}...`} | |
| rows="5" | |
| autoFocus | |
| required | |
| /> | |
| {/* 图片上传部分 */} | |
| <div className="form-group"> | |
| <div className="upload-image-btn"> | |
| <input | |
| type="file" | |
| accept="image/*" | |
| onChange={(e) => setReplyImage(e.target.files[0])} | |
| /> | |
| </div> | |
| </div> | |
| <div className="modal-actions"> | |
| <button type="button" onClick={closeReplyModal} className="cancel-btn"> | |
| 取消 | |
| </button> | |
| <button type="submit" className="submit-btn"> | |
| 发送回复 | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default HelpDetail; |