Files
vf_react/src/components/EventCommentSection/EventCommentSection.tsx
zdl a89489ba46 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>
2025-12-15 14:10:44 +08:00

297 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/EventCommentSection/EventCommentSection.tsx
/**
* 事件评论区主组件TypeScript 版本)
* 功能:整合评论列表 + 评论输入框,管理评论数据
* 使用 usePagination Hook 实现分页功能
*/
import React, { useState, useCallback } from 'react';
import {
Box,
Heading,
Badge,
HStack,
Divider,
useColorModeValue,
useToast,
Button,
Center,
} from '@chakra-ui/react';
import { useAuth } from '../../contexts/AuthContext';
import { eventService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import { usePagination } from '../../hooks/usePagination';
import type { Comment, CreateCommentParams } from '@/types';
import type { PaginationLoadResult } from '@/types';
import CommentList from './CommentList';
import CommentInput from './CommentInput';
/**
* 组件 Props
*/
interface EventCommentSectionProps {
/** 事件 ID */
eventId: string | number;
}
/**
* 事件评论区组件
*/
const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) => {
const { user } = useAuth();
const toast = useToast();
const dividerColor = useColorModeValue('gray.200', 'gray.600');
const headingColor = useColorModeValue('gray.700', 'gray.200');
const sectionBg = useColorModeValue('gray.50', 'gray.750');
// 评论输入状态
const [commentText, setCommentText] = useState('');
const [submitting, setSubmitting] = useState(false);
/**
* 加载评论数据的函数
* @param page 页码
* @param append 是否追加到已有数据
* @returns 分页响应数据
*/
const loadCommentsFunction = useCallback(
async (page: number, append: boolean): Promise<PaginationLoadResult<Comment>> => {
try {
const result = await eventService.getPosts(
eventId,
'latest',
page,
5 // 每页 5 条评论
);
if (result.success) {
logger.info('EventCommentSection', '评论加载成功', {
eventId,
page,
count: result.data?.length || 0,
total: result.pagination?.total || 0,
append,
});
return {
data: result.data || [],
pagination: result.pagination,
};
} else {
throw new Error(result.message || '加载评论失败');
}
} catch (error: any) {
logger.error('EventCommentSection', 'loadCommentsFunction', error, {
eventId,
page,
});
toast({
title: '加载评论失败',
description: error.message || '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
throw error;
}
},
[eventId, toast]
);
// 使用 usePagination Hook
const {
data: comments,
loading,
loadingMore,
hasMore,
totalCount,
loadMore,
setData: setComments,
setTotalCount,
} = usePagination<Comment>(loadCommentsFunction, {
pageSize: 5,
autoLoad: true,
});
/**
* 发表评论
*/
const handleSubmitComment = useCallback(async () => {
if (!commentText.trim()) {
toast({
title: '请输入评论内容',
status: 'warning',
duration: 2000,
isClosable: true,
});
return;
}
setSubmitting(true);
try {
const params: CreateCommentParams = {
content: commentText.trim(),
content_type: 'text',
};
const result = await eventService.createPost(eventId, params);
if (result.success) {
// 乐观更新:立即将新评论添加到本地 state避免重新加载导致的闪烁
const newComment: Comment = {
id: result.data?.id || `comment_optimistic_${Date.now()}`,
content: commentText.trim(),
content_type: 'text',
author: {
id: user?.id || 'current_user',
// 与导航区保持一致:优先显示昵称
username: user?.nickname || user?.username || user?.email || '当前用户',
avatar: user?.avatar_url || null,
},
created_at: new Date().toISOString(),
likes_count: 0,
is_liked: false,
};
// 将新评论追加到列表末尾(最新评论在底部)
setComments((prevComments) => [...prevComments, newComment]);
// 总评论数 +1
setTotalCount((prevTotal) => prevTotal + 1);
toast({
title: '评论发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
setCommentText(''); // 清空输入框
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
eventId,
content: commentText.trim(),
commentId: newComment.id,
});
} else {
throw new Error(result.message || '评论发布失败');
}
} catch (error: any) {
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
toast({
title: '评论发布失败',
description: error.message || '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setSubmitting(false);
}
}, [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>
{/* 标题栏 */}
<HStack spacing={3} mb={4} p={3} bg={sectionBg} borderRadius="md">
<Heading size="sm" color={headingColor}>
</Heading>
<Badge colorScheme="blue" fontSize="sm" borderRadius="full" px={2}>
{totalCount}
</Badge>
</HStack>
<Divider borderColor={dividerColor} mb={4} />
{/* 评论列表 */}
<Box mb={4}>
<CommentList
comments={comments}
loading={loading}
currentUserId={user?.id}
onDelete={handleDeleteComment}
/>
</Box>
{/* 加载更多按钮(仅当有更多评论时显示) */}
{hasMore && (
<Center mb={4}>
<Button
variant="outline"
colorScheme="blue"
size="sm"
onClick={loadMore}
isLoading={loadingMore}
loadingText="加载中..."
>
</Button>
</Center>
)}
{/* 评论输入框(仅登录用户显示) */}
{user && (
<Box>
<Divider borderColor={dividerColor} mb={4} />
<CommentInput
value={commentText}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setCommentText(e.target.value)
}
onSubmit={handleSubmitComment}
isSubmitting={submitting}
maxLength={500}
placeholder="说点什么..."
/>
</Box>
)}
</Box>
);
};
export default EventCommentSection;