feat: 实现评论分页功能并迁移到 TypeScript

- 创建通用分页 Hook (usePagination.ts) 支持任意数据类型
- 将 EventCommentSection 迁移到 TypeScript (.tsx)
- 添加"加载更多"按钮,支持增量加载评论
- 创建分页和评论相关类型定义 (pagination.ts, comment.ts)
- 增加 Mock 评论数据从 5 条到 15 条,便于测试分页

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-14 17:27:12 +08:00
parent 9761ef9016
commit 9fd618c087
6 changed files with 397 additions and 66 deletions

View File

@@ -1,78 +1,121 @@
// src/components/EventCommentSection/EventCommentSection.js
// src/components/EventCommentSection/EventCommentSection.tsx
/**
*
* TypeScript
* +
* 使 usePagination Hook
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box,
VStack,
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';
/**
*
* @param {Object} props
* @param {number} props.eventId - ID
* Props
*/
const EventCommentSection = ({ eventId }) => {
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 [comments, setComments] = useState([]);
const [loading, setLoading] = useState(false);
// 评论输入状态
const [commentText, setCommentText] = useState('');
const [submitting, setSubmitting] = useState(false);
const [totalCount, setTotalCount] = useState(0); // 总评论数(从后端获取)
// 加载评论列表
const loadComments = useCallback(async () => {
if (!eventId) return;
setLoading(true);
try {
// 加载第1页每页5条评论
const result = await eventService.getPosts(eventId, 'latest', 1, 5);
if (result.success) {
setComments(result.data || []);
// 保存总评论数(从 pagination.total 读取)
setTotalCount(result.pagination?.total || result.data?.length || 0);
logger.info('EventCommentSection', '评论加载成功', {
/**
*
* @param page
* @param append
* @returns
*/
const loadCommentsFunction = useCallback(
async (page: number, append: boolean): Promise<PaginationLoadResult<Comment>> => {
try {
const result = await eventService.getPosts(
eventId,
count: result.data?.length || 0,
total: result.pagination?.total || 0,
});
}
} catch (error) {
logger.error('EventCommentSection', 'loadComments', error, { eventId });
toast({
title: '加载评论失败',
description: error.message || '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
}, [eventId, toast]);
'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({
@@ -86,14 +129,16 @@ const EventCommentSection = ({ eventId }) => {
setSubmitting(true);
try {
const result = await eventService.createPost(eventId, {
const params: CreateCommentParams = {
content: commentText.trim(),
content_type: 'text',
});
};
const result = await eventService.createPost(eventId, params);
if (result.success) {
// 乐观更新:立即将新评论添加到本地 state避免重新加载导致的闪烁
const newComment = {
const newComment: Comment = {
id: result.data?.id || `comment_optimistic_${Date.now()}`,
content: commentText.trim(),
content_type: 'text',
@@ -108,9 +153,9 @@ const EventCommentSection = ({ eventId }) => {
};
// 将新评论追加到列表末尾(最新评论在底部)
setComments([...comments, newComment]);
setComments((prevComments) => [...prevComments, newComment]);
// 总评论数 +1
setTotalCount(totalCount + 1);
setTotalCount((prevTotal) => prevTotal + 1);
toast({
title: '评论发布成功',
@@ -119,7 +164,6 @@ const EventCommentSection = ({ eventId }) => {
isClosable: true,
});
setCommentText(''); // 清空输入框
// ✅ 不再调用 loadComments(),避免 loading 状态导致高度闪烁
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
eventId,
@@ -129,7 +173,7 @@ const EventCommentSection = ({ eventId }) => {
} else {
throw new Error(result.message || '评论发布失败');
}
} catch (error) {
} catch (error: any) {
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
toast({
title: '评论发布失败',
@@ -141,23 +185,12 @@ const EventCommentSection = ({ eventId }) => {
} finally {
setSubmitting(false);
}
}, [eventId, commentText, toast, comments, user, totalCount]);
// 初始加载评论
useEffect(() => {
loadComments();
}, [loadComments]);
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
return (
<Box>
{/* 标题栏 */}
<HStack
spacing={3}
mb={4}
p={3}
bg={sectionBg}
borderRadius="md"
>
<HStack spacing={3} mb={4} p={3} bg={sectionBg} borderRadius="md">
<Heading size="sm" color={headingColor}>
</Heading>
@@ -173,13 +206,31 @@ const EventCommentSection = ({ eventId }) => {
<CommentList comments={comments} loading={loading} />
</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) => setCommentText(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setCommentText(e.target.value)
}
onSubmit={handleSubmitComment}
isSubmitting={submitting}
maxLength={500}