blob: d5ecdf70851da9e512a4632f9cb7fbf1bdab6d04 [file] [log] [blame]
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">&times;</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;