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:
@@ -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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
VStack,
|
|
||||||
Heading,
|
Heading,
|
||||||
Badge,
|
Badge,
|
||||||
HStack,
|
HStack,
|
||||||
Divider,
|
Divider,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useToast,
|
useToast,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { eventService } from '../../services/eventService';
|
import { eventService } from '../../services/eventService';
|
||||||
import { logger } from '../../utils/logger';
|
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 CommentList from './CommentList';
|
||||||
import CommentInput from './CommentInput';
|
import CommentInput from './CommentInput';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件评论区组件
|
* 组件 Props
|
||||||
* @param {Object} props
|
|
||||||
* @param {number} props.eventId - 事件 ID
|
|
||||||
*/
|
*/
|
||||||
const EventCommentSection = ({ eventId }) => {
|
interface EventCommentSectionProps {
|
||||||
|
/** 事件 ID */
|
||||||
|
eventId: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件评论区组件
|
||||||
|
*/
|
||||||
|
const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
const headingColor = useColorModeValue('gray.700', 'gray.200');
|
const headingColor = useColorModeValue('gray.700', 'gray.200');
|
||||||
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
||||||
|
|
||||||
// 状态管理
|
// 评论输入状态
|
||||||
const [comments, setComments] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [commentText, setCommentText] = useState('');
|
const [commentText, setCommentText] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [totalCount, setTotalCount] = useState(0); // 总评论数(从后端获取)
|
|
||||||
|
|
||||||
// 加载评论列表
|
/**
|
||||||
const loadComments = useCallback(async () => {
|
* 加载评论数据的函数
|
||||||
if (!eventId) return;
|
* @param page 页码
|
||||||
|
* @param append 是否追加到已有数据
|
||||||
setLoading(true);
|
* @returns 分页响应数据
|
||||||
try {
|
*/
|
||||||
// 加载第1页,每页5条评论
|
const loadCommentsFunction = useCallback(
|
||||||
const result = await eventService.getPosts(eventId, 'latest', 1, 5);
|
async (page: number, append: boolean): Promise<PaginationLoadResult<Comment>> => {
|
||||||
if (result.success) {
|
try {
|
||||||
setComments(result.data || []);
|
const result = await eventService.getPosts(
|
||||||
// 保存总评论数(从 pagination.total 读取)
|
|
||||||
setTotalCount(result.pagination?.total || result.data?.length || 0);
|
|
||||||
logger.info('EventCommentSection', '评论加载成功', {
|
|
||||||
eventId,
|
eventId,
|
||||||
count: result.data?.length || 0,
|
'latest',
|
||||||
total: result.pagination?.total || 0,
|
page,
|
||||||
});
|
5 // 每页 5 条评论
|
||||||
}
|
);
|
||||||
} catch (error) {
|
|
||||||
logger.error('EventCommentSection', 'loadComments', error, { eventId });
|
|
||||||
toast({
|
|
||||||
title: '加载评论失败',
|
|
||||||
description: error.message || '请稍后重试',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [eventId, toast]);
|
|
||||||
|
|
||||||
// 发表评论
|
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 () => {
|
const handleSubmitComment = useCallback(async () => {
|
||||||
if (!commentText.trim()) {
|
if (!commentText.trim()) {
|
||||||
toast({
|
toast({
|
||||||
@@ -86,14 +129,16 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const result = await eventService.createPost(eventId, {
|
const params: CreateCommentParams = {
|
||||||
content: commentText.trim(),
|
content: commentText.trim(),
|
||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const result = await eventService.createPost(eventId, params);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 乐观更新:立即将新评论添加到本地 state,避免重新加载导致的闪烁
|
// 乐观更新:立即将新评论添加到本地 state,避免重新加载导致的闪烁
|
||||||
const newComment = {
|
const newComment: Comment = {
|
||||||
id: result.data?.id || `comment_optimistic_${Date.now()}`,
|
id: result.data?.id || `comment_optimistic_${Date.now()}`,
|
||||||
content: commentText.trim(),
|
content: commentText.trim(),
|
||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
@@ -108,9 +153,9 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 将新评论追加到列表末尾(最新评论在底部)
|
// 将新评论追加到列表末尾(最新评论在底部)
|
||||||
setComments([...comments, newComment]);
|
setComments((prevComments) => [...prevComments, newComment]);
|
||||||
// 总评论数 +1
|
// 总评论数 +1
|
||||||
setTotalCount(totalCount + 1);
|
setTotalCount((prevTotal) => prevTotal + 1);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: '评论发布成功',
|
title: '评论发布成功',
|
||||||
@@ -119,7 +164,6 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
setCommentText(''); // 清空输入框
|
setCommentText(''); // 清空输入框
|
||||||
// ✅ 不再调用 loadComments(),避免 loading 状态导致高度闪烁
|
|
||||||
|
|
||||||
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
|
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
|
||||||
eventId,
|
eventId,
|
||||||
@@ -129,7 +173,7 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || '评论发布失败');
|
throw new Error(result.message || '评论发布失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
|
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
|
||||||
toast({
|
toast({
|
||||||
title: '评论发布失败',
|
title: '评论发布失败',
|
||||||
@@ -141,23 +185,12 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [eventId, commentText, toast, comments, user, totalCount]);
|
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
|
||||||
|
|
||||||
// 初始加载评论
|
|
||||||
useEffect(() => {
|
|
||||||
loadComments();
|
|
||||||
}, [loadComments]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
<HStack
|
<HStack spacing={3} mb={4} p={3} bg={sectionBg} borderRadius="md">
|
||||||
spacing={3}
|
|
||||||
mb={4}
|
|
||||||
p={3}
|
|
||||||
bg={sectionBg}
|
|
||||||
borderRadius="md"
|
|
||||||
>
|
|
||||||
<Heading size="sm" color={headingColor}>
|
<Heading size="sm" color={headingColor}>
|
||||||
讨论区
|
讨论区
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -173,13 +206,31 @@ const EventCommentSection = ({ eventId }) => {
|
|||||||
<CommentList comments={comments} loading={loading} />
|
<CommentList comments={comments} loading={loading} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
||||||
|
{hasMore && (
|
||||||
|
<Center mb={4}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="blue"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadMore}
|
||||||
|
isLoading={loadingMore}
|
||||||
|
loadingText="加载中..."
|
||||||
|
>
|
||||||
|
加载更多评论
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 评论输入框(仅登录用户显示) */}
|
{/* 评论输入框(仅登录用户显示) */}
|
||||||
{user && (
|
{user && (
|
||||||
<Box>
|
<Box>
|
||||||
<Divider borderColor={dividerColor} mb={4} />
|
<Divider borderColor={dividerColor} mb={4} />
|
||||||
<CommentInput
|
<CommentInput
|
||||||
value={commentText}
|
value={commentText}
|
||||||
onChange={(e) => setCommentText(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
setCommentText(e.target.value)
|
||||||
|
}
|
||||||
onSubmit={handleSubmitComment}
|
onSubmit={handleSubmitComment}
|
||||||
isSubmitting={submitting}
|
isSubmitting={submitting}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
137
src/hooks/usePagination.ts
Normal file
137
src/hooks/usePagination.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ const commentsStore = new Map();
|
|||||||
/**
|
/**
|
||||||
* 初始化某个事件的 mock 评论列表
|
* 初始化某个事件的 mock 评论列表
|
||||||
* @param {string} eventId - 事件 ID
|
* @param {string} eventId - 事件 ID
|
||||||
* @returns {Array} 初始的 8 条 mock 评论
|
* @returns {Array} 初始的 15 条 mock 评论
|
||||||
*/
|
*/
|
||||||
const initializeMockComments = (eventId) => {
|
const initializeMockComments = (eventId) => {
|
||||||
const comments = [];
|
const comments = [];
|
||||||
@@ -29,6 +29,13 @@ const initializeMockComments = (eventId) => {
|
|||||||
{ username: '价值投资者', avatar: null },
|
{ username: '价值投资者', avatar: null },
|
||||||
{ username: '技术分析师', avatar: null },
|
{ username: '技术分析师', avatar: null },
|
||||||
{ username: '基本面研究员', avatar: null },
|
{ username: '基本面研究员', avatar: null },
|
||||||
|
{ username: '量化交易员', avatar: null },
|
||||||
|
{ username: '市场观察者', avatar: null },
|
||||||
|
{ username: '行业分析师', avatar: null },
|
||||||
|
{ username: '财经评论员', avatar: null },
|
||||||
|
{ username: '趋势跟踪者', avatar: null },
|
||||||
|
{ username: '价值发现者', avatar: null },
|
||||||
|
{ username: '理性投资人', avatar: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
const commentTemplates = [
|
const commentTemplates = [
|
||||||
@@ -40,9 +47,16 @@ const initializeMockComments = (eventId) => {
|
|||||||
'相关产业链的龙头企业值得重点关注',
|
'相关产业链的龙头企业值得重点关注',
|
||||||
'这类事件一般都是短期刺激,长期影响有限',
|
'这类事件一般都是短期刺激,长期影响有限',
|
||||||
'建议大家理性对待,不要盲目追高',
|
'建议大家理性对待,不要盲目追高',
|
||||||
|
'这个消息已经在预期之中,股价可能提前反应了',
|
||||||
|
'关键要看后续的执行力度和落地速度',
|
||||||
|
'建议关注产业链上下游的投资机会',
|
||||||
|
'短期可能会有波动,但长期逻辑依然成立',
|
||||||
|
'市场情绪很高涨,需要警惕追高风险',
|
||||||
|
'从历史数据来看,类似事件后续表现都不错',
|
||||||
|
'这是一个结构性机会,需要精选个股',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 15; i++) {
|
||||||
const hoursAgo = Math.floor(Math.random() * 48) + 1; // 1-48 小时前
|
const hoursAgo = Math.floor(Math.random() * 48) + 1; // 1-48 小时前
|
||||||
const createdAt = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
|
const createdAt = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
|
||||||
const user = users[i % users.length];
|
const user = users[i % users.length];
|
||||||
|
|||||||
42
src/types/comment.ts
Normal file
42
src/types/comment.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 评论相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论作者信息
|
||||||
|
*/
|
||||||
|
export interface CommentAuthor {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论数据结构
|
||||||
|
*/
|
||||||
|
export interface Comment {
|
||||||
|
/** 评论 ID */
|
||||||
|
id: string;
|
||||||
|
/** 评论内容 */
|
||||||
|
content: string;
|
||||||
|
/** 内容类型 */
|
||||||
|
content_type: 'text' | 'image' | 'video';
|
||||||
|
/** 作者信息 */
|
||||||
|
author: CommentAuthor;
|
||||||
|
/** 创建时间(ISO 8601 格式) */
|
||||||
|
created_at: string;
|
||||||
|
/** 点赞数 */
|
||||||
|
likes_count: number;
|
||||||
|
/** 当前用户是否已点赞 */
|
||||||
|
is_liked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建评论请求参数
|
||||||
|
*/
|
||||||
|
export interface CreateCommentParams {
|
||||||
|
/** 评论内容 */
|
||||||
|
content: string;
|
||||||
|
/** 内容类型 */
|
||||||
|
content_type?: 'text' | 'image' | 'video';
|
||||||
|
}
|
||||||
@@ -38,3 +38,18 @@ export type {
|
|||||||
UserAccount,
|
UserAccount,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
} from './user';
|
} from './user';
|
||||||
|
|
||||||
|
// 分页相关类型
|
||||||
|
export type {
|
||||||
|
LoadFunction,
|
||||||
|
PaginationLoadResult,
|
||||||
|
UsePaginationOptions,
|
||||||
|
UsePaginationResult,
|
||||||
|
} from './pagination';
|
||||||
|
|
||||||
|
// 评论相关类型
|
||||||
|
export type {
|
||||||
|
Comment,
|
||||||
|
CommentAuthor,
|
||||||
|
CreateCommentParams,
|
||||||
|
} from './comment';
|
||||||
|
|||||||
72
src/types/pagination.ts
Normal file
72
src/types/pagination.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 分页相关类型定义
|
||||||
|
*
|
||||||
|
* 用于 usePagination Hook 和其他分页功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页加载函数类型
|
||||||
|
* @template T 数据项类型
|
||||||
|
* @param page 页码(从 1 开始)
|
||||||
|
* @param append 是否追加到已有数据(true: 追加,false: 替换)
|
||||||
|
* @returns Promise,解析为分页响应数据
|
||||||
|
*/
|
||||||
|
export type LoadFunction<T> = (
|
||||||
|
page: number,
|
||||||
|
append: boolean
|
||||||
|
) => Promise<PaginationLoadResult<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页加载结果
|
||||||
|
* @template T 数据项类型
|
||||||
|
*/
|
||||||
|
export interface PaginationLoadResult<T> {
|
||||||
|
/** 数据列表 */
|
||||||
|
data: T[];
|
||||||
|
/** 分页信息 */
|
||||||
|
pagination?: {
|
||||||
|
/** 总数据量 */
|
||||||
|
total?: number;
|
||||||
|
/** 当前页码 */
|
||||||
|
page?: number;
|
||||||
|
/** 每页数量 */
|
||||||
|
per_page?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* usePagination Hook 配置选项
|
||||||
|
*/
|
||||||
|
export interface UsePaginationOptions {
|
||||||
|
/** 每页数据量,默认 10 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 是否自动加载第一页,默认 true */
|
||||||
|
autoLoad?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* usePagination Hook 返回值
|
||||||
|
* @template T 数据项类型
|
||||||
|
*/
|
||||||
|
export interface UsePaginationResult<T> {
|
||||||
|
/** 当前数据列表 */
|
||||||
|
data: T[];
|
||||||
|
/** 是否正在加载第一页 */
|
||||||
|
loading: boolean;
|
||||||
|
/** 是否正在加载更多 */
|
||||||
|
loadingMore: boolean;
|
||||||
|
/** 当前页码 */
|
||||||
|
currentPage: number;
|
||||||
|
/** 是否还有更多数据 */
|
||||||
|
hasMore: boolean;
|
||||||
|
/** 总数据量 */
|
||||||
|
totalCount: number;
|
||||||
|
/** 加载更多数据 */
|
||||||
|
loadMore: () => Promise<void>;
|
||||||
|
/** 重置到第一页 */
|
||||||
|
reset: () => void;
|
||||||
|
/** 手动设置数据(用于乐观更新) */
|
||||||
|
setData: React.Dispatch<React.SetStateAction<T[]>>;
|
||||||
|
/** 手动设置总数(用于乐观更新) */
|
||||||
|
setTotalCount: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user