geo/frontend/lib/hooks/use-api.ts

172 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 通用 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 数据变更 HookPOST / 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 };
}