// 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 = ({ 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> => { 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(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 ( {/* 标题栏 */} 讨论区 {totalCount} 条评论 {/* 评论列表 */} {/* 加载更多按钮(仅当有更多评论时显示) */} {hasMore && (
)} {/* 评论输入框(仅登录用户显示) */} {user && ( ) => setCommentText(e.target.value) } onSubmit={handleSubmitComment} isSubmitting={submitting} maxLength={500} placeholder="说点什么..." /> )}
); }; export default EventCommentSection;