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

137
src/hooks/usePagination.ts Normal file
View File

@@ -0,0 +1,137 @@
/**
* usePagination - 通用分页 Hook
*
* 封装分页逻辑,支持初始加载、加载更多、重置等功能
*
* @example
* const {
* data: comments,
* loading,
* loadingMore,
* hasMore,
* totalCount,
* loadMore,
* setData,
* setTotalCount,
* } = usePagination<Comment>(loadCommentsFunction, { pageSize: 5 });
*/
import { useState, useCallback, useEffect } from 'react';
import type {
LoadFunction,
PaginationLoadResult,
UsePaginationOptions,
UsePaginationResult,
} from '@/types/pagination';
/**
* usePagination Hook
* @template T 数据项类型
* @param loadFunction 加载函数,接收 (page, append) 参数
* @param options 配置选项
* @returns 分页状态和操作方法
*/
export function usePagination<T>(
loadFunction: LoadFunction<T>,
options: UsePaginationOptions = {}
): UsePaginationResult<T> {
const { pageSize = 10, autoLoad = true } = options;
// 状态管理
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [hasMore, setHasMore] = useState(false);
/**
* 加载数据
* @param page 页码
* @param append 是否追加true: 追加false: 替换)
*/
const loadData = useCallback(
async (page: number, append: boolean = false) => {
// 设置加载状态
if (append) {
setLoadingMore(true);
} else {
setLoading(true);
}
try {
const result: PaginationLoadResult<T> = await loadFunction(page, append);
// 更新数据
if (append) {
setData((prevData) => [...prevData, ...(result.data || [])]);
} else {
setData(result.data || []);
}
// 更新分页信息
const total = result.pagination?.total || result.data?.length || 0;
setTotalCount(total);
// 计算是否还有更多数据
const currentTotal = append
? data.length + (result.data?.length || 0)
: result.data?.length || 0;
setHasMore(currentTotal < total);
} catch (error) {
console.error('[usePagination] 加载数据失败:', error);
throw error;
} finally {
if (append) {
setLoadingMore(false);
} else {
setLoading(false);
}
}
},
[loadFunction, data.length]
);
/**
* 加载更多数据
*/
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore) return;
const nextPage = currentPage + 1;
await loadData(nextPage, true);
setCurrentPage(nextPage);
}, [currentPage, loadData, loadingMore, hasMore]);
/**
* 重置到第一页
*/
const reset = useCallback(() => {
setCurrentPage(1);
setData([]);
setTotalCount(0);
setHasMore(false);
loadData(1, false);
}, [loadData]);
// 自动加载第一页
useEffect(() => {
if (autoLoad) {
loadData(1, false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoLoad]);
return {
data,
loading,
loadingMore,
currentPage,
hasMore,
totalCount,
loadMore,
reset,
setData,
setTotalCount,
};
}