feat: 支持用户删除自己的评论
- CommentItem: 添加删除按钮(仅显示在自己的评论上) - CommentItem: 添加删除确认对话框,防止误删 - CommentList: 传递 currentUserId 和 onDelete 到 CommentItem - EventCommentSection: 添加 handleDeleteComment 处理函数 - mock handler: 使用真实登录用户信息创建评论 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,29 +1,63 @@
|
|||||||
// src/components/EventCommentSection/CommentItem.js
|
// src/components/EventCommentSection/CommentItem.js
|
||||||
/**
|
/**
|
||||||
* 单条评论组件
|
* 单条评论组件
|
||||||
* 功能:显示用户头像、昵称、时间、评论内容
|
* 功能:显示用户头像、昵称、时间、评论内容、删除按钮
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
HStack,
|
HStack,
|
||||||
VStack,
|
VStack,
|
||||||
Avatar,
|
Avatar,
|
||||||
Text,
|
Text,
|
||||||
|
IconButton,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
|
Tooltip,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { DeleteIcon } from '@chakra-ui/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
const CommentItem = ({ comment }) => {
|
const CommentItem = ({ comment, currentUserId, onDelete }) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const cancelRef = React.useRef();
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const itemBg = useColorModeValue('gray.50', 'gray.700');
|
const itemBg = useColorModeValue('gray.50', 'gray.700');
|
||||||
const usernameColor = useColorModeValue('gray.800', 'gray.100');
|
const usernameColor = useColorModeValue('gray.800', 'gray.100');
|
||||||
const timeColor = useColorModeValue('gray.500', 'gray.400');
|
const timeColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
const contentColor = useColorModeValue('gray.700', 'gray.300');
|
const contentColor = useColorModeValue('gray.700', 'gray.300');
|
||||||
|
|
||||||
|
// 判断当前用户是否可以删除此评论
|
||||||
|
const canDelete = currentUserId && (
|
||||||
|
comment.author?.id === currentUserId ||
|
||||||
|
comment.user_id === currentUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理删除
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!onDelete) return;
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await onDelete(comment.id);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
// 错误由父组件处理
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
@@ -46,6 +80,7 @@ const CommentItem = ({ comment }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
bg={itemBg}
|
bg={itemBg}
|
||||||
@@ -55,6 +90,7 @@ const CommentItem = ({ comment }) => {
|
|||||||
transform: 'translateY(-2px)',
|
transform: 'translateY(-2px)',
|
||||||
boxShadow: 'sm',
|
boxShadow: 'sm',
|
||||||
}}
|
}}
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
<HStack align="start" spacing={3}>
|
<HStack align="start" spacing={3}>
|
||||||
{/* 用户头像 */}
|
{/* 用户头像 */}
|
||||||
@@ -67,6 +103,7 @@ const CommentItem = ({ comment }) => {
|
|||||||
{/* 评论内容区 */}
|
{/* 评论内容区 */}
|
||||||
<VStack align="stretch" flex={1} spacing={1}>
|
<VStack align="stretch" flex={1} spacing={1}>
|
||||||
{/* 用户名和时间 */}
|
{/* 用户名和时间 */}
|
||||||
|
<HStack spacing={2} justify="space-between">
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
|
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
|
||||||
{comment.author?.username || 'Anonymous'}
|
{comment.author?.username || 'Anonymous'}
|
||||||
@@ -76,6 +113,23 @@ const CommentItem = ({ comment }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
{/* 删除按钮 - 只对自己的评论显示 */}
|
||||||
|
{canDelete && (
|
||||||
|
<Tooltip label="删除评论" placement="top">
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="red"
|
||||||
|
aria-label="删除评论"
|
||||||
|
onClick={onOpen}
|
||||||
|
opacity={0.6}
|
||||||
|
_hover={{ opacity: 1 }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
{/* 评论内容 */}
|
{/* 评论内容 */}
|
||||||
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
|
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
|
||||||
{comment.content}
|
{comment.content}
|
||||||
@@ -83,6 +137,42 @@ const CommentItem = ({ comment }) => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
leastDestructiveRef={cancelRef}
|
||||||
|
onClose={onClose}
|
||||||
|
isCentered
|
||||||
|
>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
删除评论
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
确定要删除这条评论吗?此操作不可撤销。
|
||||||
|
</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose} isDisabled={isDeleting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="red"
|
||||||
|
onClick={handleDelete}
|
||||||
|
ml={3}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
loadingText="删除中..."
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { ChatIcon } from '@chakra-ui/icons';
|
import { ChatIcon } from '@chakra-ui/icons';
|
||||||
import CommentItem from './CommentItem';
|
import CommentItem from './CommentItem';
|
||||||
|
|
||||||
const CommentList = ({ comments, loading }) => {
|
const CommentList = ({ comments, loading, currentUserId, onDelete }) => {
|
||||||
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
|
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
|
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
@@ -58,7 +58,12 @@ const CommentList = ({ comments, loading }) => {
|
|||||||
return (
|
return (
|
||||||
<VStack align="stretch" spacing={3}>
|
<VStack align="stretch" spacing={3}>
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<CommentItem key={comment.id} comment={comment} />
|
<CommentItem
|
||||||
|
key={comment.id}
|
||||||
|
comment={comment}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -144,8 +144,9 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
author: {
|
author: {
|
||||||
id: user?.id || 'current_user',
|
id: user?.id || 'current_user',
|
||||||
username: user?.username || '当前用户',
|
// 与导航区保持一致:优先显示昵称
|
||||||
avatar: user?.avatar || null,
|
username: user?.nickname || user?.username || user?.email || '当前用户',
|
||||||
|
avatar: user?.avatar_url || null,
|
||||||
},
|
},
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
likes_count: 0,
|
likes_count: 0,
|
||||||
@@ -187,6 +188,51 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
}
|
}
|
||||||
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
|
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
*/
|
||||||
|
const handleDeleteComment = useCallback(async (commentId: string | number) => {
|
||||||
|
try {
|
||||||
|
const result = await eventService.deletePost(commentId);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 从本地 state 中移除该评论
|
||||||
|
setComments((prevComments) =>
|
||||||
|
prevComments.filter((comment) => comment.id !== commentId)
|
||||||
|
);
|
||||||
|
// 总评论数 -1
|
||||||
|
setTotalCount((prevTotal) => Math.max(0, prevTotal - 1));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '评论已删除',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('EventCommentSection', '评论删除成功', {
|
||||||
|
eventId,
|
||||||
|
commentId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '删除评论失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('EventCommentSection', 'handleDeleteComment', error, {
|
||||||
|
eventId,
|
||||||
|
commentId,
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: '删除评论失败',
|
||||||
|
description: error.message || '请稍后重试',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
throw error; // 重新抛出让 CommentItem 知道删除失败
|
||||||
|
}
|
||||||
|
}, [eventId, toast, setComments, setTotalCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
@@ -203,7 +249,12 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
|
|
||||||
{/* 评论列表 */}
|
{/* 评论列表 */}
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<CommentList comments={comments} loading={loading} />
|
<CommentList
|
||||||
|
comments={comments}
|
||||||
|
loading={loading}
|
||||||
|
currentUserId={user?.id}
|
||||||
|
onDelete={handleDeleteComment}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { http, HttpResponse } from 'msw';
|
|||||||
import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events';
|
import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events';
|
||||||
import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account';
|
import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account';
|
||||||
import { generatePopularConcepts } from './concept';
|
import { generatePopularConcepts } from './concept';
|
||||||
|
import { getCurrentUser } from '../data/users';
|
||||||
|
|
||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
@@ -1498,15 +1499,19 @@ export const eventHandlers = [
|
|||||||
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
|
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
const currentUser = getCurrentUser();
|
||||||
|
|
||||||
// 模拟创建新评论
|
// 模拟创建新评论
|
||||||
const newComment = {
|
const newComment = {
|
||||||
id: `comment_${eventId}_${Date.now()}`,
|
id: `comment_${eventId}_${Date.now()}`,
|
||||||
content: body.content,
|
content: body.content,
|
||||||
content_type: body.content_type || 'text',
|
content_type: body.content_type || 'text',
|
||||||
author: {
|
author: {
|
||||||
id: 'current_user',
|
id: currentUser?.id || 'current_user',
|
||||||
username: '当前用户',
|
// 与导航区保持一致:优先显示昵称
|
||||||
avatar: null,
|
username: currentUser?.nickname || currentUser?.username || currentUser?.email || '当前用户',
|
||||||
|
avatar: currentUser?.avatar_url || null,
|
||||||
},
|
},
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
likes_count: 0,
|
likes_count: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user