完成上传下载连接,公告管理与详情页面,求种区页面,轮播图折扣显示,修改部分bug
Change-Id: I86fc294e32911cb3426a8b16f90aca371f975c11
diff --git a/src/components/RequestDetail.jsx b/src/components/RequestDetail.jsx
index 022b0cb..c138419 100644
--- a/src/components/RequestDetail.jsx
+++ b/src/components/RequestDetail.jsx
@@ -1,111 +1,355 @@
-import React, { useState } from 'react';
-import { useParams, useNavigate,useLocation } from 'react-router-dom';
+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 [post, setPost] = useState({
- id: 1,
- title: '求《药屋少女的呢喃》第二季全集',
- content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的。\n\n希望有热心大佬能分享,可以给积分奖励!',
- author: '动漫爱好者',
- authorAvatar: 'https://via.placeholder.com/40',
- date: '2023-10-15',
- likeCount: 24,
- isLiked: false,
- isFavorited: false
- });
-
- const [comments, setComments] = useState([
- {
- id: 1,
- type: 'text',
- author: '资源达人',
- authorAvatar: 'https://via.placeholder.com/40',
- content: '我有第1-5集,需要的话可以私聊',
- date: '2023-10-15 14:30',
- likeCount: 5
- },
- {
- id: 2,
- type: 'torrent',
- title: '药屋少女的呢喃第二季第8集',
- size: '1.2GB',
- author: '种子分享者',
- authorAvatar: 'https://via.placeholder.com/40',
- date: '2023-10-16 09:15',
- likeCount: 8
- }
- ]);
-
+ 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 handleLikePost = () => {
- setPost(prev => ({
- ...prev,
- likeCount: prev.isLiked ? prev.likeCount - 1 : prev.likeCount + 1,
- isLiked: !prev.isLiked
- }));
+ 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 handleFavoritePost = () => {
- setPost(prev => ({
- ...prev,
- isFavorited: !prev.isFavorited
- }));
- };
- const handleCommentSubmit = (e) => {
- e.preventDefault();
- if (!newComment.trim()) return;
+ 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);
- const newCommentObj = {
- id: comments.length + 1,
- type: 'text',
- author: '当前用户',
- authorAvatar: 'https://via.placeholder.com/40',
- content: newComment,
- date: new Date().toLocaleString(),
- likeCount: 0
+ // 修改这里的响应处理逻辑
+ 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));
+ }
};
- setComments([...comments, newCommentObj]);
- setNewComment('');
- };
-
- const handleDownloadTorrent = (commentId) => {
-
-
- console.log('下载种子', commentId);
- // 实际下载逻辑
+ 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 handleBack = () => {
- const fromTab = location.state?.fromTab; // 从跳转时传递的 state 中获取
- if (fromTab) {
- navigate(`/dashboard/${fromTab}`); // 明确返回对应标签页
- } else {
- navigate(-1); // 保底策略
- }
- }
+ // 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">
+ <div className={`request-post ${post.isSolved ? 'solved' : ''}`}>
<div className="post-header">
- <img src={post.authorAvatar} alt={post.author} className="post-avatar" />
+ <img
+ src={post.authorAvatar || 'https://via.placeholder.com/40'}
+ alt={post.authorId}
+ className="post-avatar"
+ />
<div className="post-meta">
- <div className="post-author">{post.author}</div>
- <div className="post-date">{post.date}</div>
+ <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>
@@ -115,6 +359,21 @@
{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">
@@ -125,74 +384,86 @@
👍 点赞 ({post.likeCount})
</button>
<button
- className={`favorite-button ${post.isFavorited ? 'favorited' : ''}`}
- onClick={handleFavoritePost}
+ className={`solve-button ${post.isSolved ? 'solved' : ''}`}
+ onClick={handleMarkSolved}
>
- {post.isFavorited ? '★ 已收藏' : '☆ 收藏'}
+ {post.isSolved ? '✓ 已解决' : '标记为已解决'}
</button>
</div>
</div>
- <div className="comments-section">
- <h2>回应 ({comments.length})</h2>
+ <div className="comments-section">
+ <h2>评论 ({post.replyCount})</h2>
<form onSubmit={handleCommentSubmit} className="comment-form">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
- placeholder="写下你的回应..."
+ placeholder="写下你的评论..."
rows="3"
required
/>
- <div className="form-actions">
- <button type="submit" className="submit-comment">发表文字回应</button>
- <button
- type="button"
- className="submit-torrent"
- onClick={() => console.log('打开种子上传对话框')}
- >
- 上传种子回应
- </button>
+ <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 => (
- <div key={comment.id} className={`comment-item ${comment.type}`}>
- <img
- src={comment.authorAvatar}
- alt={comment.author}
- className="comment-avatar"
- />
-
- <div className="comment-content">
- <div className="comment-header">
- <span className="comment-author">{comment.author}</span>
- <span className="comment-date">{comment.date}</span>
- </div>
-
- {comment.type === 'text' ? (
- <p className="comment-text">{comment.content}</p>
- ) : (
- <div className="torrent-comment">
- <span className="torrent-title">{comment.title}</span>
- <span className="torrent-size">{comment.size}</span>
- <button
- className="download-torrent"
- onClick={() => handleDownloadTorrent(comment.id)}
- >
- 立即下载
- </button>
- </div>
- )}
-
- <button className="comment-like">
- 👍 ({comment.likeCount})
- </button>
- </div>
- </div>
- ))}
+ {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>
);