diff --git a/apps/web/src/hooks/useApiRequest.ts b/apps/web/src/hooks/useApiRequest.ts index 8bceed5..623d025 100644 --- a/apps/web/src/hooks/useApiRequest.ts +++ b/apps/web/src/hooks/useApiRequest.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { message } from 'antd'; function extractErrorMessage(err: unknown): string { @@ -10,12 +10,20 @@ function extractErrorMessage(err: unknown): string { return ''; } -export function useApiRequest() { +interface UseApiRequestReturn { + execute: (fn: () => Promise, successMsg?: string, errorMsg?: string) => Promise; + loading: boolean; +} + +export function useApiRequest(): UseApiRequestReturn { + const [loading, setLoading] = useState(false); + const execute = useCallback(async ( fn: () => Promise, successMsg?: string, errorMsg = '操作失败', ): Promise => { + setLoading(true); try { const result = await fn(); if (successMsg) message.success(successMsg); @@ -24,8 +32,10 @@ export function useApiRequest() { const msg = extractErrorMessage(err); message.error(msg || errorMsg); return null; + } finally { + setLoading(false); } }, []); - return { execute }; + return { execute, loading }; } diff --git a/apps/web/src/hooks/usePaginatedData.ts b/apps/web/src/hooks/usePaginatedData.ts index 06c93cd..818d866 100644 --- a/apps/web/src/hooks/usePaginatedData.ts +++ b/apps/web/src/hooks/usePaginatedData.ts @@ -11,21 +11,36 @@ interface PaginatedState { /** * 通用分页数据 Hook,封装 data / total / page / loading / fetch 逻辑。 * - * 支持两种签名: - * 1. 三参数 (page, pageSize, search) — 带搜索的列表页 - * 2. 两参数 (page, pageSize) — 纯分页,不含搜索 - * - * @param fetchFn - 数据获取函数 - * @param pageSize - 每页条数,默认 20 - * @param autoFetch - 是否在 mount / fetchFn 变化时自动请求第一页,默认 true + * 支持三种签名: + * 1. 泛型筛选 (page, pageSize, filters: F) — 带结构化筛选的列表页 + * 2. 三参数 (page, pageSize, search: string) — 带搜索的列表页 + * 3. 两参数 (page, pageSize) — 纯分页,不含搜索 */ + +// 重载签名 +export function usePaginatedData( + fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>, + options: { pageSize?: number; defaultFilters: F; autoFetch?: boolean }, +): PaginatedResult; + 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, + pageSize?: number, + autoFetch?: boolean, +): PaginatedResult; + +export function usePaginatedData( + fetchFn: (...args: any[]) => Promise<{ data: T[]; total: number }>, + pageSizeOrOptions?: number | { pageSize?: number; defaultFilters: F; autoFetch?: boolean }, autoFetch = true, -) { +): PaginatedResult { + const isOptions = typeof pageSizeOrOptions === 'object' && pageSizeOrOptions !== null; + const pageSize = isOptions ? (pageSizeOrOptions as any).pageSize ?? 20 : (pageSizeOrOptions as number) ?? 20; + const shouldAutoFetch = isOptions ? (pageSizeOrOptions as any).autoFetch ?? true : autoFetch; + const defaultFilters = isOptions ? (pageSizeOrOptions as any).defaultFilters : ('' as unknown as F); + const [state, setState] = useState>({ data: [], total: 0, @@ -33,29 +48,26 @@ export function usePaginatedData( loading: false, }); const [searchText, setSearchText] = useState(''); + const [filters, setFilters] = useState(defaultFilters); - // 用 ref 保存最新 fetchFn,避免 refresh 因闭包引用过期 fetchFn 而频繁重建 const fetchFnRef = useRef(fetchFn); fetchFnRef.current = fetchFn; - // 用 ref 保存最新 searchText,同理 const searchTextRef = useRef(searchText); searchTextRef.current = searchText; + const filtersRef = useRef(filters); + filtersRef.current = filters; + 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 }>)( + const result = await (fetchFnRef.current as any)( targetPage, pageSize, - searchTextRef.current, + filtersRef.current ?? searchTextRef.current, ); setState({ data: result.data, total: result.total, page: targetPage, loading: false }); } catch { @@ -66,12 +78,19 @@ export function usePaginatedData( [pageSize, state.page], ); - // mount 或 fetchFn 变化时自动请求 useEffect(() => { - if (autoFetch) { + if (shouldAutoFetch) { refresh(1); } - }, [autoFetch, refresh]); + }, [shouldAutoFetch, refresh]); - return { ...state, searchText, setSearchText, refresh }; + return { ...state, searchText, setSearchText, filters, setFilters, refresh }; +} + +interface PaginatedResult extends PaginatedState { + searchText: string; + setSearchText: (text: string) => void; + filters: F; + setFilters: (filters: F | ((prev: F) => F)) => void; + refresh: (page?: number) => Promise; }