整理记录状态改为排队中 / 整理中 / 已整理 / 部分整理 / 整理失败(列表摘要、组汇总、队列面板同口径); 下载计划「等你批准」「准备提交」;资源评估「可下载」「信息不全」「已跳过」;任务状态带「中」; 识别等待提示改匹配「TMDB 结果在后台查询」。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
import type { ApiErrorCode } from "@umt/contracts";
|
|
import type { z } from "zod";
|
|
import { ApiRequestError, apiFetch, type FetcherArg } from "~/lib/api.ts";
|
|
|
|
export { ApiRequestError };
|
|
|
|
/** 请求并用 contracts 的 zod schema 校验 data;失败抛 ApiRequestError(给 react-query 的 error 态) */
|
|
export async function apiParsed<S extends z.ZodType>(schema: S, path: string, arg?: FetcherArg): Promise<z.infer<S>> {
|
|
const r = await apiFetch<unknown>(path, arg);
|
|
if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message);
|
|
const parsed = schema.safeParse(r.data);
|
|
if (!parsed.success) {
|
|
const issue = parsed.error.issues[0];
|
|
throw new ApiRequestError(
|
|
"internal" satisfies ApiErrorCode,
|
|
`响应格式不符合接口定义:${issue ? `${issue.path.join(".")} ${issue.message}` : "未知字段"}`,
|
|
);
|
|
}
|
|
return parsed.data;
|
|
}
|
|
|
|
/** 变更请求:返回 data,失败抛错(给 useMutation,由 onError 统一 toast) */
|
|
export async function apiMutate<T = unknown>(path: string, arg: FetcherArg = {}): Promise<T> {
|
|
const r = await apiFetch<T>(path, { method: "POST", ...arg });
|
|
if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message);
|
|
return r.data;
|
|
}
|
|
|
|
export const errorCode = (err: unknown): string | null => (err instanceof ApiRequestError ? err.code : null);
|
|
|
|
/** 端点是否不存在(后端尚未提供) */
|
|
export const isMissingEndpoint = (err: unknown) => errorCode(err) === "not_found";
|
|
|
|
/** 查询字符串:去掉空值 */
|
|
export function cleanParams(p: Record<string, string | number | boolean | undefined | null>) {
|
|
const out: Record<string, string | number | boolean> = {};
|
|
for (const [k, v] of Object.entries(p)) if (v !== undefined && v !== null && v !== "") out[k] = v;
|
|
return out;
|
|
}
|
|
|
|
/** 小规模配置集合:跟随游标取齐,保留列表返回形状供选择器与本地分页共用。 */
|
|
export async function fetchAllCursorPages<P extends { items: unknown[]; nextCursor?: string | null }>(
|
|
fetchPage: (cursor?: string) => Promise<P>,
|
|
): Promise<P> {
|
|
const first = await fetchPage();
|
|
const items = [...first.items];
|
|
const seen = new Set<string>();
|
|
let cursor = first.nextCursor;
|
|
while (cursor) {
|
|
if (seen.has(cursor)) throw new ApiRequestError("internal", "分页游标重复。重试");
|
|
seen.add(cursor);
|
|
const page = await fetchPage(cursor);
|
|
items.push(...page.items);
|
|
cursor = page.nextCursor;
|
|
}
|
|
return { ...first, items, nextCursor: null };
|
|
}
|