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:
zdl
2025-12-15 13:56:02 +08:00
parent e493ae5ad1
commit a89489ba46
4 changed files with 197 additions and 46 deletions

View File

@@ -1,29 +1,63 @@
// src/components/EventCommentSection/CommentItem.js
/**
* 单条评论组件
* 功能:显示用户头像、昵称、时间、评论内容
* 功能:显示用户头像、昵称、时间、评论内容、删除按钮
*/
import React from 'react';
import React, { useState } from 'react';
import {
Box,
HStack,
VStack,
Avatar,
Text,
IconButton,
useColorModeValue,
Tooltip,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
Button,
useDisclosure,
} from '@chakra-ui/react';
import { DeleteIcon } from '@chakra-ui/icons';
import dayjs from 'dayjs';
import '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 usernameColor = useColorModeValue('gray.800', 'gray.100');
const timeColor = useColorModeValue('gray.500', 'gray.400');
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 now = dayjs();
@@ -46,43 +80,99 @@ const CommentItem = ({ comment }) => {
};
return (
<Box
p={3}
bg={itemBg}
borderRadius="md"
transition="all 0.2s"
_hover={{
transform: 'translateY(-2px)',
boxShadow: 'sm',
}}
>
<HStack align="start" spacing={3}>
{/* 用户头像 */}
<Avatar
size="sm"
name={comment.author?.username || 'Anonymous'}
src={comment.author?.avatar}
/>
<>
<Box
p={3}
bg={itemBg}
borderRadius="md"
transition="all 0.2s"
_hover={{
transform: 'translateY(-2px)',
boxShadow: 'sm',
}}
position="relative"
>
<HStack align="start" spacing={3}>
{/* 用户头像 */}
<Avatar
size="sm"
name={comment.author?.username || 'Anonymous'}
src={comment.author?.avatar}
/>
{/* 评论内容区 */}
<VStack align="stretch" flex={1} spacing={1}>
{/* 用户名和时间 */}
<HStack spacing={2}>
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
{comment.author?.username || 'Anonymous'}
</Text>
<Text fontSize="xs" color={timeColor}>
{formatTime(comment.created_at)}
</Text>
</HStack>
{/* 评论内容区 */}
<VStack align="stretch" flex={1} spacing={1}>
{/* 用户名和时间 */}
<HStack spacing={2} justify="space-between">
<HStack spacing={2}>
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
{comment.author?.username || 'Anonymous'}
</Text>
<Text fontSize="xs" color={timeColor}>
{formatTime(comment.created_at)}
</Text>
</HStack>
{/* 评论内容 */}
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
{comment.content}
</Text>
</VStack>
</HStack>
</Box>
{/* 删除按钮 - 只对自己的评论显示 */}
{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">
{comment.content}
</Text>
</VStack>
</HStack>
</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>
</>
);
};

View File

@@ -16,7 +16,7 @@ import {
import { ChatIcon } from '@chakra-ui/icons';
import CommentItem from './CommentItem';
const CommentList = ({ comments, loading }) => {
const CommentList = ({ comments, loading, currentUserId, onDelete }) => {
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
@@ -58,7 +58,12 @@ const CommentList = ({ comments, loading }) => {
return (
<VStack align="stretch" spacing={3}>
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
<CommentItem
key={comment.id}
comment={comment}
currentUserId={currentUserId}
onDelete={onDelete}
/>
))}
</VStack>
);

View File

@@ -144,8 +144,9 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
content_type: 'text',
author: {
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(),
likes_count: 0,
@@ -187,6 +188,51 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
}
}, [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 (
<Box>
{/* 标题栏 */}
@@ -203,7 +249,12 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
{/* 评论列表 */}
<Box mb={4}>
<CommentList comments={comments} loading={loading} />
<CommentList
comments={comments}
loading={loading}
currentUserId={user?.id}
onDelete={handleDeleteComment}
/>
</Box>
{/* 加载更多按钮(仅当有更多评论时显示) */}

View File

@@ -5,6 +5,7 @@ import { http, HttpResponse } from 'msw';
import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events';
import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account';
import { generatePopularConcepts } from './concept';
import { getCurrentUser } from '../data/users';
// 模拟网络延迟
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);
try {
// 获取当前登录用户信息
const currentUser = getCurrentUser();
// 模拟创建新评论
const newComment = {
id: `comment_${eventId}_${Date.now()}`,
content: body.content,
content_type: body.content_type || 'text',
author: {
id: 'current_user',
username: '当前用户',
avatar: null,
id: currentUser?.id || 'current_user',
// 与导航区保持一致:优先显示昵称
username: currentUser?.nickname || currentUser?.username || currentUser?.email || '当前用户',
avatar: currentUser?.avatar_url || null,
},
created_at: new Date().toISOString(),
likes_count: 0,