- 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>
297 lines
8.1 KiB
TypeScript
297 lines
8.1 KiB
TypeScript
// 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;
|