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
|
||||
/**
|
||||
* 单条评论组件
|
||||
* 功能:显示用户头像、昵称、时间、评论内容
|
||||
* 功能:显示用户头像、昵称、时间、评论内容、删除按钮
|
||||
*/
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user