import React, { useState, useEffect, useRef } from 'react'; | |
import { useParams, useNavigate, useLocation } from 'react-router-dom'; | |
import { | |
getRequestPostDetail, | |
addRequestPostComment, | |
likeRequestPost, | |
deleteRequestPost | |
} from '../api/requestPost'; | |
import { | |
likeRequestPostComment, | |
getCommentReplies, | |
addRequestCommentReply, | |
deleteRequestComment | |
} from '../api/requestComment'; | |
import './RequestDetail.css'; | |
const RequestDetail = () => { | |
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 getRequestPostDetail(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 likeRequestPost(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 deleteRequestPost(postId, username); | |
navigate('/dashboard/request'); // 删除成功后返回求助区 | |
} 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 formData = new FormData(); | |
formData.append('content', newComment); | |
formData.append('authorId', username); | |
if (commentImage) { | |
formData.append('image', commentImage); | |
} | |
const response = await addRequestPostComment(id, formData); | |
// 修改这里的响应处理逻辑 | |
if (response.data && response.data.code === 200) { | |
await fetchPostDetail(); | |
setNewComment(''); | |
setCommentImage(null); // 清空评论图片 | |
} else { | |
setError(response.data.message || '评论失败'); | |
} | |
} catch (err) { | |
setError('评论失败: ' + (err.response?.data?.message || err.message)); | |
} | |
}; | |
const handleLikeComment = async (commentId) => { | |
try { | |
await likeRequestPostComment(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 deleteRequestComment(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 addRequestCommentReply(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/request`); | |
}; | |
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="request-detail-container"> | |
<button className="back-button" onClick={handleBack}> | |
← 返回求助区 | |
</button> | |
<div className={`request-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 RequestDetail; |