import { useState, useCallback, useRef, useEffect } from 'react'; import { message } from 'antd'; interface PaginatedState { data: T[]; total: number; page: number; loading: boolean; } /** * 通用分页数据 Hook,封装 data / total / page / loading / fetch 逻辑。 * * 支持两种签名: * 1. 三参数 (page, pageSize, search) — 带搜索的列表页 * 2. 两参数 (page, pageSize) — 纯分页,不含搜索 * * @param fetchFn - 数据获取函数 * @param pageSize - 每页条数,默认 20 * @param autoFetch - 是否在 mount / fetchFn 变化时自动请求第一页,默认 true */ export function usePaginatedData( fetchFn: | ((page: number, pageSize: number, search: string) => Promise<{ data: T[]; total: number }>) | ((page: number, pageSize: number) => Promise<{ data: T[]; total: number }>), pageSize = 20, autoFetch = true, ) { const [state, setState] = useState>({ data: [], total: 0, page: 1, loading: false, }); const [searchText, setSearchText] = useState(''); // 用 ref 保存最新 fetchFn,避免 refresh 因闭包引用过期 fetchFn 而频繁重建 const fetchFnRef = useRef(fetchFn); fetchFnRef.current = fetchFn; // 用 ref 保存最新 searchText,同理 const searchTextRef = useRef(searchText); searchTextRef.current = searchText; const refresh = useCallback( async (p?: number) => { const targetPage = p ?? state.page; setState((s) => ({ ...s, loading: true })); try { // 统一按三参数调用;若 fetchFn 只接受两参数,第三个参数会被忽略 const result = await (fetchFnRef.current as ( page: number, pageSize: number, search: string, ) => Promise<{ data: T[]; total: number }>)( targetPage, pageSize, searchTextRef.current, ); setState({ data: result.data, total: result.total, page: targetPage, loading: false }); } catch { message.error('加载数据失败'); setState((s) => ({ ...s, loading: false })); } }, [pageSize, state.page], ); // mount 或 fetchFn 变化时自动请求 useEffect(() => { if (autoFetch) { refresh(1); } }, [autoFetch, refresh]); return { ...state, searchText, setSearchText, refresh }; }