feat(web): useApiRequest 添加 loading + usePaginatedData 泛型筛选
- useApiRequest 新增 loading 状态,execute 自动管理 loading 生命周期 - usePaginatedData 支持泛型筛选参数 (filters: F),函数重载保持旧签名兼容 - 新增 filters/setFilters 状态,fetchFn 调用时传入当前 filters - 向后兼容:旧调用点无需修改
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
function extractErrorMessage(err: unknown): string {
|
function extractErrorMessage(err: unknown): string {
|
||||||
@@ -10,12 +10,20 @@ function extractErrorMessage(err: unknown): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApiRequest() {
|
interface UseApiRequestReturn {
|
||||||
|
execute: <T>(fn: () => Promise<T>, successMsg?: string, errorMsg?: string) => Promise<T | null>;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApiRequest(): UseApiRequestReturn {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const execute = useCallback(async <T>(
|
const execute = useCallback(async <T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
successMsg?: string,
|
successMsg?: string,
|
||||||
errorMsg = '操作失败',
|
errorMsg = '操作失败',
|
||||||
): Promise<T | null> => {
|
): Promise<T | null> => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
if (successMsg) message.success(successMsg);
|
if (successMsg) message.success(successMsg);
|
||||||
@@ -24,8 +32,10 @@ export function useApiRequest() {
|
|||||||
const msg = extractErrorMessage(err);
|
const msg = extractErrorMessage(err);
|
||||||
message.error(msg || errorMsg);
|
message.error(msg || errorMsg);
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { execute };
|
return { execute, loading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,36 @@ interface PaginatedState<T> {
|
|||||||
/**
|
/**
|
||||||
* 通用分页数据 Hook,封装 data / total / page / loading / fetch 逻辑。
|
* 通用分页数据 Hook,封装 data / total / page / loading / fetch 逻辑。
|
||||||
*
|
*
|
||||||
* 支持两种签名:
|
* 支持三种签名:
|
||||||
* 1. 三参数 (page, pageSize, search) — 带搜索的列表页
|
* 1. 泛型筛选 (page, pageSize, filters: F) — 带结构化筛选的列表页
|
||||||
* 2. 两参数 (page, pageSize) — 纯分页,不含搜索
|
* 2. 三参数 (page, pageSize, search: string) — 带搜索的列表页
|
||||||
*
|
* 3. 两参数 (page, pageSize) — 纯分页,不含搜索
|
||||||
* @param fetchFn - 数据获取函数
|
|
||||||
* @param pageSize - 每页条数,默认 20
|
|
||||||
* @param autoFetch - 是否在 mount / fetchFn 变化时自动请求第一页,默认 true
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 重载签名
|
||||||
|
export function usePaginatedData<T, F>(
|
||||||
|
fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>,
|
||||||
|
options: { pageSize?: number; defaultFilters: F; autoFetch?: boolean },
|
||||||
|
): PaginatedResult<T, F>;
|
||||||
|
|
||||||
export function usePaginatedData<T>(
|
export function usePaginatedData<T>(
|
||||||
fetchFn:
|
fetchFn:
|
||||||
| ((page: number, pageSize: number, search: string) => Promise<{ data: T[]; total: number }>)
|
| ((page: number, pageSize: number, search: string) => Promise<{ data: T[]; total: number }>)
|
||||||
| ((page: number, pageSize: number) => Promise<{ data: T[]; total: number }>),
|
| ((page: number, pageSize: number) => Promise<{ data: T[]; total: number }>),
|
||||||
pageSize = 20,
|
pageSize?: number,
|
||||||
|
autoFetch?: boolean,
|
||||||
|
): PaginatedResult<T, string>;
|
||||||
|
|
||||||
|
export function usePaginatedData<T, F = string>(
|
||||||
|
fetchFn: (...args: any[]) => Promise<{ data: T[]; total: number }>,
|
||||||
|
pageSizeOrOptions?: number | { pageSize?: number; defaultFilters: F; autoFetch?: boolean },
|
||||||
autoFetch = true,
|
autoFetch = true,
|
||||||
) {
|
): PaginatedResult<T, F> {
|
||||||
|
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<PaginatedState<T>>({
|
const [state, setState] = useState<PaginatedState<T>>({
|
||||||
data: [],
|
data: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -33,29 +48,26 @@ export function usePaginatedData<T>(
|
|||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [filters, setFilters] = useState<F>(defaultFilters);
|
||||||
|
|
||||||
// 用 ref 保存最新 fetchFn,避免 refresh 因闭包引用过期 fetchFn 而频繁重建
|
|
||||||
const fetchFnRef = useRef(fetchFn);
|
const fetchFnRef = useRef(fetchFn);
|
||||||
fetchFnRef.current = fetchFn;
|
fetchFnRef.current = fetchFn;
|
||||||
|
|
||||||
// 用 ref 保存最新 searchText,同理
|
|
||||||
const searchTextRef = useRef(searchText);
|
const searchTextRef = useRef(searchText);
|
||||||
searchTextRef.current = searchText;
|
searchTextRef.current = searchText;
|
||||||
|
|
||||||
|
const filtersRef = useRef(filters);
|
||||||
|
filtersRef.current = filters;
|
||||||
|
|
||||||
const refresh = useCallback(
|
const refresh = useCallback(
|
||||||
async (p?: number) => {
|
async (p?: number) => {
|
||||||
const targetPage = p ?? state.page;
|
const targetPage = p ?? state.page;
|
||||||
setState((s) => ({ ...s, loading: true }));
|
setState((s) => ({ ...s, loading: true }));
|
||||||
try {
|
try {
|
||||||
// 统一按三参数调用;若 fetchFn 只接受两参数,第三个参数会被忽略
|
const result = await (fetchFnRef.current as any)(
|
||||||
const result = await (fetchFnRef.current as (
|
|
||||||
page: number,
|
|
||||||
pageSize: number,
|
|
||||||
search: string,
|
|
||||||
) => Promise<{ data: T[]; total: number }>)(
|
|
||||||
targetPage,
|
targetPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
searchTextRef.current,
|
filtersRef.current ?? searchTextRef.current,
|
||||||
);
|
);
|
||||||
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
setState({ data: result.data, total: result.total, page: targetPage, loading: false });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -66,12 +78,19 @@ export function usePaginatedData<T>(
|
|||||||
[pageSize, state.page],
|
[pageSize, state.page],
|
||||||
);
|
);
|
||||||
|
|
||||||
// mount 或 fetchFn 变化时自动请求
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFetch) {
|
if (shouldAutoFetch) {
|
||||||
refresh(1);
|
refresh(1);
|
||||||
}
|
}
|
||||||
}, [autoFetch, refresh]);
|
}, [shouldAutoFetch, refresh]);
|
||||||
|
|
||||||
return { ...state, searchText, setSearchText, refresh };
|
return { ...state, searchText, setSearchText, filters, setFilters, refresh };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedResult<T, F> extends PaginatedState<T> {
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: (text: string) => void;
|
||||||
|
filters: F;
|
||||||
|
setFilters: (filters: F | ((prev: F) => F)) => void;
|
||||||
|
refresh: (page?: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user