完成上传下载连接,公告管理与详情页面,求种区页面,轮播图折扣显示,修改部分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}>

-        &larr; 返回求种区

+        &larr; 返回求助区

       </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">&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>

   );