feat(web): Q3 前端体验优化 — ErrorBoundary + 5 hooks + 共享类型 + i18n 基础
- ErrorBoundary 组件:全局错误捕获与优雅降级 - 提取 5 个自定义 hooks:useCountUp, useDarkMode, useDebouncedValue, usePaginatedData, useApiRequest - 从 11 个 API 文件提取 PaginatedResponse 共享类型到 api/types.ts - 统一 API 错误处理(api/errors.ts) - client.ts 迁移到 axios adapter 模式(替代废弃的 CancelToken) - 添加 react-i18next 国际化基础设施 + zh-CN 语言包
This commit is contained in:
36
apps/web/src/hooks/usePaginatedData.ts
Normal file
36
apps/web/src/hooks/usePaginatedData.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { message } from 'antd';
|
||||
|
||||
interface PaginatedState<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function usePaginatedData<T>(
|
||||
fetchFn: (page: number, pageSize: number, search: string) => Promise<{ data: T[]; total: number }>,
|
||||
pageSize = 20,
|
||||
) {
|
||||
const [state, setState] = useState<PaginatedState<T>>({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
loading: false,
|
||||
});
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const refresh = useCallback(async (p?: number) => {
|
||||
const targetPage = p ?? state.page;
|
||||
setState(s => ({ ...s, loading: true }));
|
||||
try {
|
||||
const result = await fetchFn(targetPage, pageSize, searchText);
|
||||
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
||||
} catch {
|
||||
message.error('加载数据失败');
|
||||
setState(s => ({ ...s, loading: false }));
|
||||
}
|
||||
}, [fetchFn, pageSize, searchText, state.page]);
|
||||
|
||||
return { ...state, searchText, setSearchText, refresh };
|
||||
}
|
||||
Reference in New Issue
Block a user