合并
Change-Id: I19ca58c58a513cba20c162c9ed7cbab90e060bf6
diff --git a/src/api/helpComment.js b/src/api/helpComment.js
index e711587..c1d00c0 100644
--- a/src/api/helpComment.js
+++ b/src/api/helpComment.js
@@ -9,5 +9,20 @@
};
export const addCommentReply = (commentId, replyData) => {
- return api.post(`/help/comments/${commentId}/replies`, replyData);
+ const formData = new FormData();
+ formData.append('authorId', replyData.authorId);
+ formData.append('content', replyData.content);
+
+ // 如果有图片,添加到formData
+ if (replyData.image) {
+ formData.append('image', replyData.image);
+ }
+
+ return api.post(`/help/comments/${commentId}/replies`, formData);
+};
+
+export const deleteComment = (commentId, authorId) => {
+ return api.delete(`/help/comments/${commentId}`, {
+ params: { authorId }
+ });
};
\ No newline at end of file
diff --git a/src/api/helpComment.test.js b/src/api/helpComment.test.js
index 2b7be4c..a4a7d99 100644
--- a/src/api/helpComment.test.js
+++ b/src/api/helpComment.test.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { api } from './auth'; // 添加api导入
-import { likePostComment, getCommentReplies, addCommentReply } from './helpComment';
+import { likePostComment, getCommentReplies, addCommentReply, deleteComment } from './helpComment';
describe('求助帖评论API', () => {
let mockAxios;
@@ -34,11 +34,53 @@
});
describe('addCommentReply - 添加评论回复', () => {
- it('应该正确发送回复内容', async () => {
- const testData = { content: '测试回复', author: 'user1' };
- mockAxios.onPost('/help/comments/789/replies', testData).reply(200, { code: 200 });
+ it('应该正确发送回复内容(无图片)', async () => {
+ const commentId = '789';
+ const replyData = {
+ authorId: 'user1',
+ content: '测试回复'
+ };
+
+ mockAxios.onPost(`/help/comments/${commentId}/replies`).reply(config => {
+ const data = config.data;
+ expect(data.get('authorId')).toBe(replyData.authorId);
+ expect(data.get('content')).toBe(replyData.content);
+ expect(data.has('image')).toBe(false);
+ return [200, { code: 200 }];
+ });
- const response = await addCommentReply('789', testData);
+ const response = await addCommentReply(commentId, replyData);
+ expect(response.status).toBe(200);
+ });
+ it('应该正确处理带图片的回复', async () => {
+ const commentId = '789';
+ const replyData = {
+ authorId: 'user1',
+ content: '测试回复',
+ image: new File(['content'], 'reply.jpg')
+ };
+
+ mockAxios.onPost(`/help/comments/${commentId}/replies`).reply(config => {
+ const data = config.data;
+ expect(data.get('image')).toBeInstanceOf(File);
+ return [200, { code: 200 }];
+ });
+
+ const response = await addCommentReply(commentId, replyData);
+ expect(response.status).toBe(200);
+ });
+ });
+ describe('deleteComment - 删除评论', () => {
+ it('应该正确发送删除请求', async () => {
+ const commentId = '101112';
+ const authorId = 'user1';
+
+ mockAxios.onDelete(`/help/comments/${commentId}`).reply(config => {
+ expect(config.params).toEqual({ authorId });
+ return [200, { code: 200 }];
+ });
+
+ const response = await deleteComment(commentId, authorId);
expect(response.status).toBe(200);
});
});
diff --git a/src/api/helpPost.js b/src/api/helpPost.js
index 426e92b..4813aa3 100644
--- a/src/api/helpPost.js
+++ b/src/api/helpPost.js
@@ -1,12 +1,19 @@
// src/api/helpPost.js
import { api } from './auth'; // 复用已有的axios实例
-export const createPost = (title, content, authorId) => {
- return api.post('/help/posts', {
- title,
- content,
- authorId
- });
+export const createPost = (title, content, authorId, selectedImage) => {
+ // 创建 FormData 对象
+ const formData = new FormData();
+ formData.append('title', title);
+ formData.append('content', content);
+ formData.append('authorId', authorId);
+
+ // 如果有图片,添加到 FormData
+ if (selectedImage) {
+ formData.append('image', selectedImage);
+ }
+
+ return api.post('/help/posts', formData);
};
export const getPosts = (page = 1, size = 5) => {
@@ -19,10 +26,28 @@
return api.get(`/help/posts/${postId}`);
};
-export const likePost = (postId) => {
- return api.post(`/help/posts/${postId}/like`);
+export const likePost = (postId, data) => {
+ return api.post(`/help/posts/${postId}/like`, null, {
+ params: data
+ });
};
export const addPostComment = (postId, commentData) => {
- return api.post(`/help/posts/${postId}/comments`, commentData);
+ // 创建FormData对象来处理文件上传
+ const formData = new FormData();
+ formData.append('authorId', commentData.authorId);
+ formData.append('content', commentData.content);
+
+ // 如果有图片,添加到formData
+ if (commentData.commentImage) {
+ formData.append('image', commentData.commentImage);
+ }
+
+ return api.post(`/help/posts/${postId}/comments`, formData);
+};
+
+export const deletePost = (postId, authorId) => {
+ return api.delete(`/help/posts/${postId}`, {
+ params: { authorId }
+ });
};
\ No newline at end of file
diff --git a/src/api/helpPost.test.js b/src/api/helpPost.test.js
index e163090..b445b4d 100644
--- a/src/api/helpPost.test.js
+++ b/src/api/helpPost.test.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { api } from './auth'; // 添加api导入
-import { createPost, getPosts, getPostDetail, likePost, addPostComment } from './helpPost';
+import { createPost, getPosts, getPostDetail, likePost, addPostComment,deletePost } from './helpPost';
describe('求助帖API', () => {
let mockAxios;
@@ -14,18 +14,49 @@
});
describe('createPost - 创建求助帖', () => {
- it('应该正确发送帖子数据', async () => {
+ it('应该正确发送无图片帖子数据', async () => {
const postData = {
title: '测试标题',
content: '测试内容',
authorId: 'user123'
};
- mockAxios.onPost('/help/posts', postData).reply(201, { code: 201 });
+ // 使用函数匹配器来验证FormData内容
+ mockAxios.onPost('/help/posts').reply(config => {
+ const data = config.data;
+ expect(data.get('title')).toBe(postData.title);
+ expect(data.get('content')).toBe(postData.content);
+ expect(data.get('authorId')).toBe(postData.authorId);
+ expect(data.has('image')).toBe(false);
+ return [201, { code: 201 }];
+ });
const response = await createPost(postData.title, postData.content, postData.authorId);
expect(response.status).toBe(201);
});
});
+ it('应该正确处理带图片的帖子', async () => {
+ const postData = {
+ title: '测试标题',
+ content: '测试内容',
+ authorId: 'user123',
+ selectedImage: new File(['content'], 'test.jpg')
+ };
+
+ mockAxios.onPost('/help/posts').reply(config => {
+ const data = config.data;
+ expect(data.get('image')).toBeInstanceOf(File);
+ return [201, { code: 201 }];
+ });
+
+ const response = await createPost(
+ postData.title,
+ postData.content,
+ postData.authorId,
+ postData.selectedImage
+ );
+ expect(response.status).toBe(201);
+ });
+
describe('getPosts - 获取求助帖列表', () => {
it('应该支持分页参数', async () => {
@@ -49,11 +80,53 @@
});
describe('addPostComment - 添加帖子评论', () => {
- it('应该正确发送评论数据', async () => {
- const comment = { content: '测试评论', author: 'user1' };
- mockAxios.onPost('/help/posts/post456/comments', comment).reply(200, { code: 200 });
+ it('应该正确发送评论数据(无图片)', async () => {
+ const postId = 'post456';
+ const commentData = {
+ authorId: 'user1',
+ content: '测试评论'
+ };
- const response = await addPostComment('post456', comment);
+ mockAxios.onPost(`/help/posts/${postId}/comments`).reply(config => {
+ const data = config.data;
+ expect(data.get('authorId')).toBe(commentData.authorId);
+ expect(data.get('content')).toBe(commentData.content);
+ expect(data.has('image')).toBe(false);
+ return [200, { code: 200 }];
+ });
+
+ const response = await addPostComment('post456', commentData);
+ expect(response.status).toBe(200);
+ });
+ it('应该正确处理带图片的评论', async () => {
+ const postId = 'post456';
+ const commentData = {
+ authorId: 'user1',
+ content: '测试评论',
+ commentImage: new File(['content'], 'comment.jpg')
+ };
+
+ mockAxios.onPost(`/help/posts/${postId}/comments`).reply(config => {
+ const data = config.data;
+ expect(data.get('image')).toBeInstanceOf(File);
+ return [200, { code: 200 }];
+ });
+
+ const response = await addPostComment(postId, commentData);
+ expect(response.status).toBe(200);
+ });
+ });
+ describe('deletePost - 删除帖子', () => {
+ it('应该正确发送删除请求', async () => {
+ const postId = 'post789';
+ const authorId = 'user1';
+
+ mockAxios.onDelete(`/help/posts/${postId}`).reply(config => {
+ expect(config.params).toEqual({ authorId });
+ return [200, { code: 200 }];
+ });
+
+ const response = await deletePost(postId, authorId);
expect(response.status).toBe(200);
});
});
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index 97a01cc..836daa7 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -3,7 +3,7 @@
// import { getUserInfo } from '../api/auth';
import {createTorrent, getTorrents} from '../api/torrent';
import './Dashboard.css';
-import {createPost, getPosts} from '../api/helpPost';
+import {createPost, getPosts, getPostDetail} from '../api/helpPost';
const Dashboard = ({onLogout}) => {
@@ -34,6 +34,8 @@
const [helpError, setHelpError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
+ const [likedPosts,setLikedPosts] = useState({});
+
// 添加状态
const [torrentPosts, setTorrentPosts] = useState([]);
@@ -179,28 +181,29 @@
const handlePostSubmit = async (e) => {
e.preventDefault();
try {
- const username = localStorage.getItem('username');
- const response = await createPost(
- postTitle,
- postContent,
- username
- );
-
- if (response.data.code === 200) {
- // 刷新帖子列表
- await fetchHelpPosts();
- // 重置表单
- setShowPostModal(false);
- setPostTitle('');
- setPostContent('');
- setSelectedImage(null);
- } else {
- setHelpError(response.data.message || '发帖失败');
- }
+ const username = localStorage.getItem('username');
+ const response = await createPost(
+ postTitle,
+ postContent,
+ username,
+ selectedImage
+ );
+
+ if (response.data.code === 200) {
+ // 刷新帖子列表
+ await fetchHelpPosts(currentPage);
+ // 重置表单
+ setShowPostModal(false);
+ setPostTitle('');
+ setPostContent('');
+ setSelectedImage(null);
+ } else {
+ setHelpError(response.data.message || '发帖失败');
+ }
} catch (err) {
- setHelpError(err.message || '发帖失败');
+ setHelpError(err.message || '发帖失败');
}
- };
+ };
// 获取Torrent帖子列表
const fetchTorrentPosts = async (page = 1) => {
@@ -232,20 +235,40 @@
const fetchHelpPosts = async (page = 1) => {
setHelpLoading(true);
try {
- const response = await getPosts(page);
- if (response.data.code === 200) {
- setHelpPosts(response.data.data.records);
- setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
- setCurrentPage(page);
- } else {
- setHelpError(response.data.message || '获取求助帖失败');
- }
+ const response = await getPosts(page);
+ if (response.data.code === 200) {
+ const postsWithCounts = await Promise.all(
+ response.data.data.records.map(async (post) => {
+ try {
+ const detailResponse = await getPostDetail(post.id);
+ if (detailResponse.data.code === 200) {
+ return {
+ ...post,
+ replyCount: detailResponse.data.data.post.replyCount || 0,
+ isLiked: false // 根据需要添加其他字段
+ };
+ }
+ return post; // 如果获取详情失败,返回原始帖子数据
+ } catch (err) {
+ console.error(`获取帖子${post.id}详情失败:`, err);
+ return post;
+ }
+ })
+ );
+ setHelpPosts(postsWithCounts);
+ setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
+ setCurrentPage(page);
+ } else {
+ setHelpError(response.data.message || '获取求助帖失败');
+ }
} catch (err) {
- setHelpError(err.message || '获取求助帖失败');
+ setHelpError(err.message || '获取求助帖失败');
} finally {
- setHelpLoading(false);
+ setHelpLoading(false);
}
- };
+ };
+
+
useEffect(() => {
if (activeTab === 'help') {
fetchHelpPosts(currentPage);
diff --git a/src/components/HelpDetail.jsx b/src/components/HelpDetail.jsx
index da50850..7e7b7c8 100644
--- a/src/components/HelpDetail.jsx
+++ b/src/components/HelpDetail.jsx
@@ -3,12 +3,14 @@
import {
getPostDetail,
addPostComment,
- likePost
+ likePost,
+ deletePost
} from '../api/helpPost';
import {
likePostComment,
getCommentReplies,
- addCommentReply
+ addCommentReply,
+ deleteComment
} from '../api/helpComment';
import './HelpDetail.css';
@@ -22,12 +24,13 @@
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newComment, setNewComment] = useState('');
- const [newReply, setNewReply] = useState({});
- const [images, setImages] = 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 [replyContent, setReplyContent] = useState('');
+
const [activeReplyId, setActiveReplyId] = useState(null);
const [replyModal, setReplyModal] = useState({
@@ -58,7 +61,7 @@
setReplyContent('');
};
- const Comment = ({ comment, onLike, onReply, isReply = false }) => {
+ const Comment = ({ comment, onLike, onReply, onDelete, isReply = false }) => {
return (
<div className={`comment-container ${isReply ? "is-reply" : ""}`}>
<div className="comment-item">
@@ -76,6 +79,17 @@
</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})
@@ -83,6 +97,14 @@
<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>
@@ -99,6 +121,7 @@
onLike={handleLikeComment}
onReply={openReplyModal}
isReply={depth > 0}
+ onDelete={handleDeleteComment}
/>
{/* 递归渲染所有回复 */}
@@ -140,6 +163,19 @@
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();
@@ -196,6 +232,18 @@
}
};
+ 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) => {
@@ -217,15 +265,16 @@
try {
const username = localStorage.getItem('username');
const response = await addCommentReply(replyModal.replyingTo, {
+ authorId: username,
content: replyContent,
- authorId: username
+ image: replyImage
});
console.log('回复响应:', response.data); // 调试
if (response.data && response.data.code === 200) {
await fetchPostDetail();
-
+ setReplyContent('');
closeReplyModal();
}
} catch (err) {
@@ -251,15 +300,15 @@
}));
};
- const handleImageUpload = (e) => {
- const files = Array.from(e.target.files);
- const newImages = files.map(file => URL.createObjectURL(file));
- setImages(prev => [...prev, ...newImages]);
- };
+ // 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));
- };
+ // const handleRemoveImage = (index) => {
+ // setImages(prev => prev.filter((_, i) => i !== index));
+ // };
@@ -284,9 +333,19 @@
<div className="post-author">{post.authorId}</div>
<div className="post-date">
{new Date(post.createTime).toLocaleString()}
- </div>
+ </div>
</div>
- {post.isSolved && <span className="solved-badge">已解决</span>}
+ {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>
@@ -295,6 +354,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">
@@ -325,8 +399,21 @@
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>
@@ -347,6 +434,18 @@
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">
取消