帖子的评论的前端
Change-Id: Ida12e1874052498f22e40d0604e936f0d65d5fb6
diff --git a/src/api/__tests__/comment.test.js b/src/api/__tests__/comment.test.js
new file mode 100644
index 0000000..a55dba7
--- /dev/null
+++ b/src/api/__tests__/comment.test.js
@@ -0,0 +1,83 @@
+// src/api/__tests__/comment.test.js
+const axios = require('axios');
+jest.mock('axios');
+
+const {
+ createComment,
+ deleteComment,
+ updateComment,
+ getCommentsByPostId,
+ likeComment,
+ unlikeComment
+} = require('../comment');
+
+describe('Comment API Tests', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('createComment should send POST request', async () => {
+ const mockData = { userid: 1, postid: 2, postCommentcontent: 'Test comment' };
+ axios.post.mockResolvedValue({ data: mockData });
+
+ const response = await createComment(mockData);
+
+ expect(axios.post).toHaveBeenCalledWith('http://localhost:8080/comment/create', mockData);
+ expect(response.data).toEqual(mockData);
+ });
+
+ test('deleteComment should send DELETE request', async () => {
+ axios.delete.mockResolvedValue({ data: true });
+
+ const response = await deleteComment(123);
+
+ expect(axios.delete).toHaveBeenCalledWith('http://localhost:8080/comment/delete/123');
+ expect(response.data).toBe(true);
+ });
+
+ test('updateComment should send PUT request', async () => {
+ const updatedData = { commentid: 1, postCommentcontent: 'Updated comment' };
+ axios.put.mockResolvedValue({ data: true });
+
+ const response = await updateComment(updatedData);
+
+ expect(axios.put).toHaveBeenCalledWith('http://localhost:8080/comment/update', updatedData);
+ expect(response.data).toBe(true);
+ });
+
+ test('getCommentsByPostId should fetch comments by post ID', async () => {
+ const mockResponse = { data: [{ commentid: 1, postCommentcontent: 'Nice!' }] };
+ axios.get.mockResolvedValue(mockResponse);
+
+ const response = await getCommentsByPostId(5);
+
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/comment/post/5');
+ expect(response).toEqual(mockResponse.data);
+ });
+
+ test('getCommentsByPostId should return empty array on error', async () => {
+ axios.get.mockRejectedValue(new Error('Network Error'));
+
+ const response = await getCommentsByPostId(99);
+
+ expect(response).toEqual([]);
+ });
+
+ test('likeComment should send POST request', async () => {
+ axios.post.mockResolvedValue({ data: true });
+
+ const response = await likeComment(10);
+
+ expect(axios.post).toHaveBeenCalledWith('http://localhost:8080/comment/like/10');
+ expect(response.data).toBe(true);
+ });
+
+ test('unlikeComment should send POST request', async () => {
+ axios.post.mockResolvedValue({ data: true });
+
+ const response = await unlikeComment(10);
+
+ expect(axios.post).toHaveBeenCalledWith('http://localhost:8080/comment/unlike/10');
+ expect(response.data).toBe(true);
+ });
+});
diff --git a/src/api/comment.js b/src/api/comment.js
new file mode 100644
index 0000000..28b2b4e
--- /dev/null
+++ b/src/api/comment.js
@@ -0,0 +1,40 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/comment';
+
+// 创建评论
+export const createComment = (commentData) => {
+ return axios.post(`${BASE_URL}/create`, commentData);
+};
+
+// 删除评论
+export const deleteComment = (commentId) => {
+ return axios.delete(`${BASE_URL}/delete/${commentId}`);
+};
+
+// 更新评论
+export const updateComment = (commentData) => {
+ return axios.put(`${BASE_URL}/update`, commentData);
+};
+
+// 获取某个帖子的所有评论
+// comment.js
+export async function getCommentsByPostId(postid) {
+ try {
+ const response = await axios.get(`${BASE_URL}/post/${postid}`);
+ return Array.isArray(response.data) ? response.data : []; // 确保返回数据是数组
+ } catch (error) {
+ console.error('获取评论失败', error);
+ return [];
+ }
+}
+
+// 点赞评论
+export const likeComment = (commentId) => {
+ return axios.post(`${BASE_URL}/like/${commentId}`);
+};
+
+// 取消点赞评论
+export const unlikeComment = (commentId) => {
+ return axios.post(`${BASE_URL}/unlike/${commentId}`);
+};
diff --git a/src/components/Comment.css b/src/components/Comment.css
new file mode 100644
index 0000000..e5529d3
--- /dev/null
+++ b/src/components/Comment.css
@@ -0,0 +1,128 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+.comment-card {
+ max-width: 700px;
+ margin: 20px auto;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
+ background: #fff;
+ padding: 16px;
+}
+
+.comment-textarea {
+ border-radius: 6px;
+ resize: none;
+ font-size: 16px;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.mt-2 {
+ margin-top: 0.5rem;
+}
+
+.mt-6 {
+ margin-top: 1.5rem;
+}
+
+.comment-time {
+ font-size: 12px;
+ color: #888;
+ margin-left: 8px;
+ white-space: nowrap;
+}
+
+.comment-username {
+ font-weight: 600;
+ color: #1890ff;
+ user-select: none;
+}
+
+.comment-content {
+ font-size: 14px;
+ line-height: 1.5;
+ color: #333;
+ white-space: pre-wrap;
+}
+
+/* 头像带蓝色边框 */
+.avatar-with-border {
+ border: 2px solid #1890ff !important;
+ border-radius: 50% !important;
+}
+
+
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background-color: #fffaf5;
+}
+
+/* 全局动画效果 */
+.transition {
+ transition: all 0.3s ease;
+}
+
+/* 卡片阴影效果 */
+.shadow-lg {
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
+}
+
+.shadow-md {
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+}
+
+/* 圆角 */
+.rounded-xl {
+ border-radius: 1rem;
+}
+
+.rounded-lg {
+ border-radius: 0.75rem;
+}
+
+/* 渐变背景 */
+.bg-gradient-to-r {
+ background-size: 200% auto;
+ transition: background-position 0.5s ease;
+}
+
+.bg-gradient-to-r:hover {
+ background-position: right center;
+}
+
+/* 表单输入框动画 */
+input:focus,
+textarea:focus {
+ transform: translateY(-1px);
+}
+
+/* 卡片悬停效果 */
+.bg-white:hover {
+ transform: translateY(-3px);
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+ .text-xl {
+ font-size: 1.25rem;
+ }
+
+ .text-2xl {
+ font-size: 1.5rem;
+ }
+
+ .p-6 {
+ padding: 1.25rem;
+ }
+
+ .p-12 {
+ padding: 2rem;
+ }
+
+ .grid-cols-2 {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Comment.jsx b/src/components/Comment.jsx
new file mode 100644
index 0000000..704636e
--- /dev/null
+++ b/src/components/Comment.jsx
@@ -0,0 +1,290 @@
+import React, { useState, useEffect } from 'react';
+import {
+ getCommentsByPostId,
+ createComment,
+ deleteComment,
+ likeComment,
+ unlikeComment,
+} from '../api/comment';
+
+import {
+ Card,
+ Button,
+ Input,
+ List,
+ Avatar,
+ Popconfirm,
+ message,
+ Space,
+ Tooltip
+} from 'antd';
+
+import {
+ LikeOutlined,
+ LikeFilled,
+ DeleteOutlined
+} from '@ant-design/icons';
+
+import axios from 'axios';
+import './Comment.css';
+
+const { TextArea } = Input;
+
+const Comment = ({ postId, currentUser }) => {
+ const [comments, setComments] = useState([]);
+ const [newContent, setNewContent] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [userInfoMap, setUserInfoMap] = useState({});
+ const [isCommenting, setIsCommenting] = useState(false);
+
+ useEffect(() => {
+ loadComments();
+ }, [postId, currentUser]);
+
+ const loadComments = async () => {
+ try {
+ if (!postId) return;
+
+ const commentList = await getCommentsByPostId(postId);
+ setComments(commentList);
+
+ // 修复1: 统一使用 userid 而不是 id
+ const userIds = [
+ ...new Set(commentList.map(c => c.userid))
+ ];
+
+ // 如果当前用户存在,添加其 userid
+ if (currentUser && currentUser.userid) {
+ userIds.push(currentUser.userid);
+ }
+
+ // 批量获取用户信息
+ const userInfoPromises = userIds.map(async (id) => {
+ try {
+ const res = await axios.get(`http://localhost:8080/user/getDecoration?userid=${id}`);
+ if (res.data?.success) {
+ return { id, data: res.data.data };
+ }
+ return { id, data: { username: `用户${id}`, image: '' } };
+ } catch (error) {
+ return { id, data: { username: `用户${id}`, image: '' } };
+ }
+ });
+
+ const userInfoResults = await Promise.all(userInfoPromises);
+
+ // 构建用户信息映射
+ const newUserInfoMap = {};
+ userInfoResults.forEach(({ id, data }) => {
+ newUserInfoMap[id] = data;
+ });
+
+ setUserInfoMap(newUserInfoMap);
+
+ } catch (error) {
+ console.error('加载评论失败:', error);
+ message.error('加载评论失败');
+ }
+ };
+
+ const handleCreate = async () => {
+ // 检查用户是否登录
+ if (!currentUser) {
+ message.error('请先登录后再评论');
+ return;
+ }
+
+ // 修复2: 确保当前用户有 userid 属性
+ if (!currentUser.userid) {
+ message.error('用户信息异常,请重新登录');
+ return;
+ }
+
+ if (!newContent.trim()) {
+ message.warning('评论内容不能为空');
+ return;
+ }
+
+ setIsCommenting(true);
+ setLoading(true);
+
+ // 修复3: 使用 currentUser.userid 而非 currentUser.id
+ const commentData = {
+ postid: postId,
+ userid: Number(currentUser.userid),
+ postCommentcontent: newContent,
+ commenttime: new Date().toISOString()
+ };
+
+ try {
+ await createComment(commentData);
+ setNewContent('');
+ await loadComments(); // 重新加载评论,确保新评论显示
+ message.success('评论发布成功');
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ message.error('发布评论失败');
+ } finally {
+ setLoading(false);
+ setIsCommenting(false);
+ }
+ };
+
+ const handleDelete = async (commentid) => {
+ try {
+ await deleteComment(commentid);
+ message.success('删除成功');
+ loadComments();
+ } catch (error) {
+ console.error('删除评论失败:', error);
+ message.error('删除失败');
+ }
+ };
+
+ const handleLike = async (commentid) => {
+ try {
+ await likeComment(commentid);
+ loadComments();
+ } catch (error) {
+ console.error('点赞失败:', error);
+ message.error('操作失败');
+ }
+ };
+
+ const handleUnlike = async (commentid) => {
+ try {
+ await unlikeComment(commentid);
+ loadComments();
+ } catch (error) {
+ console.error('取消点赞失败:', error);
+ message.error('操作失败');
+ }
+ };
+
+ // 获取用户信息(包括当前登录用户)
+ const getUserInfo = (userId) => {
+ // 修复4: 使用 currentUser.userid 而非 currentUser.id
+ if (currentUser && currentUser.userid && userId === currentUser.userid) {
+ return {
+ username: currentUser.username || `用户${userId}`,
+ image: currentUser.image || '',
+ decoration: currentUser.decoration || ''
+ };
+ }
+
+ // 对于其他用户,从userInfoMap中获取
+ return userInfoMap[userId] || {
+ username: `用户${userId}`,
+ image: '',
+ decoration: ''
+ };
+ };
+
+ return (
+ <Card title="评论区" bordered={false} className="comment-card">
+ {/* 评论输入框 - 根据用户登录状态调整 */}
+ {currentUser ? (
+ <>
+ <TextArea
+ rows={3}
+ placeholder={`${currentUser.username},留下你的评论...`}
+ value={newContent}
+ onChange={(e) => setNewContent(e.target.value)}
+ className="comment-textarea"
+ disabled={loading}
+ />
+ <div className="text-right mt-2">
+ <Button
+ type="primary"
+ onClick={handleCreate}
+ loading={loading || isCommenting}
+ disabled={isCommenting}
+ >
+ {isCommenting ? '发布中...' : '发布评论'}
+ </Button>
+ </div>
+ </>
+ ) : (
+ <div className="login-prompt">
+ 请<a href="/login" className="login-link">登录</a>后发表评论
+ </div>
+ )}
+
+ {/* 评论列表 */}
+ <List
+ itemLayout="vertical"
+ dataSource={comments}
+ locale={{ emptyText: '暂无评论' }}
+ className="mt-6"
+ renderItem={(item) => {
+ const user = getUserInfo(item.userid);
+ // 修复5: 使用 userid 而非 id 比较当前用户
+ const isCurrentUser = currentUser && currentUser.userid === item.userid;
+
+ return (
+ <List.Item
+ key={item.commentid}
+ className={isCurrentUser ? "current-user-comment" : ""}
+ actions={[
+ <Tooltip title="点赞" key="like">
+ <Space>
+ <Button
+ icon={<LikeOutlined />}
+ size="small"
+ onClick={() => handleLike(item.commentid)}
+ />
+ {item.likes}
+ </Space>
+ </Tooltip>,
+ <Tooltip title="取消点赞" key="unlike">
+ <Button
+ icon={<LikeFilled style={{ color: '#fadb14' }} />}
+ size="small"
+ onClick={() => handleUnlike(item.commentid)}
+ />
+ </Tooltip>,
+ isCurrentUser && (
+ <Popconfirm
+ title="确定要删除这条评论吗?"
+ onConfirm={() => handleDelete(item.commentid)}
+ okText="删除"
+ cancelText="取消"
+ key="delete"
+ >
+ <Button icon={<DeleteOutlined />} size="small" danger />
+ </Popconfirm>
+ )
+ ]}
+ extra={
+ <div className="comment-time">
+ {new Date(item.commenttime).toLocaleString()}
+ </div>
+ }
+ >
+ <List.Item.Meta
+ avatar={
+ <Avatar
+ src={user.image || undefined}
+ alt={user.username}
+ className="comment-avatar"
+ >
+ {!user.image && user.username ? user.username.charAt(0).toUpperCase() : ''}
+ </Avatar>
+ }
+ title={
+ <span className="comment-username">
+ {user.username || `用户${item.userid}`}
+ {isCurrentUser && <span className="current-user-tag">(我)</span>}
+ </span>
+ }
+ description={<div className="comment-content">{item.postCommentcontent}</div>}
+ />
+ </List.Item>
+ );
+ }}
+ />
+ </Card>
+ );
+};
+
+export default Comment;
\ No newline at end of file