172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
/**
|
||
* 通用 SWR Hooks
|
||
* 封装 useSWR + fetchWithAuth,统一处理加载/错误/重试逻辑
|
||
*/
|
||
|
||
import useSWR, { type SWRConfiguration, type KeyedMutator } from "swr";
|
||
import { useCallback, useState } from "react";
|
||
import { fetchWithAuth } from "@/lib/api/client";
|
||
|
||
// ── 类型定义 ────────────────────────────────────────────────────────────────────
|
||
|
||
export interface UseApiReturn<T> {
|
||
data: T | undefined;
|
||
isLoading: boolean;
|
||
error: Error | undefined;
|
||
mutate: KeyedMutator<T>;
|
||
/** 手动重新获取数据 */
|
||
refresh: () => void;
|
||
}
|
||
|
||
export interface UsePaginatedApiReturn<T> extends UseApiReturn<T[]> {
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
setPage: (page: number) => void;
|
||
}
|
||
|
||
export interface UseApiMutationReturn<T, B> {
|
||
trigger: (body?: B) => Promise<T | null>;
|
||
isMutating: boolean;
|
||
error: Error | undefined;
|
||
reset: () => void;
|
||
}
|
||
|
||
// ── SWR 全局 Fetcher ─────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 基于 fetchWithAuth 的 SWR fetcher
|
||
* key 格式:url string 或 [url, token]
|
||
*/
|
||
export async function swrFetcher<T>(url: string): Promise<T> {
|
||
return fetchWithAuth(url) as Promise<T>;
|
||
}
|
||
|
||
// ── 主 Hook ──────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 基础数据获取 Hook
|
||
* @param url API 路径(null 时暂停请求)
|
||
* @param options SWR 配置项
|
||
*/
|
||
export function useApi<T>(
|
||
url: string | null,
|
||
options?: SWRConfiguration<T>
|
||
): UseApiReturn<T> {
|
||
const { data, error, isLoading, mutate } = useSWR<T>(
|
||
url,
|
||
swrFetcher<T>,
|
||
{
|
||
revalidateOnFocus: false,
|
||
shouldRetryOnError: true,
|
||
errorRetryCount: 2,
|
||
errorRetryInterval: 3000,
|
||
...options,
|
||
}
|
||
);
|
||
|
||
const refresh = useCallback(() => {
|
||
mutate();
|
||
}, [mutate]);
|
||
|
||
return {
|
||
data,
|
||
isLoading,
|
||
error: error as Error | undefined,
|
||
mutate,
|
||
refresh,
|
||
};
|
||
}
|
||
|
||
// ── 分页 Hook ────────────────────────────────────────────────────────────────
|
||
|
||
export interface PaginatedResponse<T> {
|
||
items: T[];
|
||
total: number;
|
||
}
|
||
|
||
/**
|
||
* 支持分页的数据获取 Hook
|
||
* @param baseUrl 基础 API 路径(不含分页参数)
|
||
* @param params 分页参数
|
||
*/
|
||
export function usePaginatedApi<T>(
|
||
baseUrl: string | null,
|
||
params: { page: number; pageSize: number }
|
||
): UsePaginatedApiReturn<T> {
|
||
const [page, setPage] = useState(params.page);
|
||
const { pageSize } = params;
|
||
|
||
const url = baseUrl
|
||
? `${baseUrl}?limit=${pageSize}&offset=${(page - 1) * pageSize}`
|
||
: null;
|
||
|
||
const { data, error, isLoading, mutate } = useSWR<PaginatedResponse<T>>(
|
||
url,
|
||
swrFetcher<PaginatedResponse<T>>,
|
||
{
|
||
revalidateOnFocus: false,
|
||
shouldRetryOnError: true,
|
||
errorRetryCount: 2,
|
||
}
|
||
);
|
||
|
||
const refresh = useCallback(() => {
|
||
mutate();
|
||
}, [mutate]);
|
||
|
||
return {
|
||
data: data?.items,
|
||
total: data?.total ?? 0,
|
||
isLoading,
|
||
error: error as Error | undefined,
|
||
mutate: mutate as unknown as KeyedMutator<T[]>,
|
||
refresh,
|
||
page,
|
||
pageSize,
|
||
setPage,
|
||
};
|
||
}
|
||
|
||
// ── Mutation Hook ────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* API 数据变更 Hook(POST / PUT / DELETE)
|
||
* @param url API 路径
|
||
* @param method HTTP 方法(默认 POST)
|
||
*/
|
||
export function useApiMutation<T, B = unknown>(
|
||
url: string,
|
||
method: string = "POST"
|
||
): UseApiMutationReturn<T, B> {
|
||
const [isMutating, setIsMutating] = useState(false);
|
||
const [error, setError] = useState<Error | undefined>(undefined);
|
||
|
||
const trigger = useCallback(
|
||
async (body?: B): Promise<T | null> => {
|
||
try {
|
||
setIsMutating(true);
|
||
setError(undefined);
|
||
const result = await fetchWithAuth(url, {
|
||
method,
|
||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||
}) as T;
|
||
return result;
|
||
} catch (err) {
|
||
const e = err instanceof Error ? err : new Error(String(err));
|
||
setError(e);
|
||
return null;
|
||
} finally {
|
||
setIsMutating(false);
|
||
}
|
||
},
|
||
[url, method]
|
||
);
|
||
|
||
const reset = useCallback(() => {
|
||
setError(undefined);
|
||
}, []);
|
||
|
||
return { trigger, isMutating, error, reset };
|
||
}
|