86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import { message } from "@/utils/notify";
|
||
import type { ApiEnvelope, ApiErrorBody } from "./types";
|
||
|
||
export class ApiError extends Error {
|
||
readonly code: string;
|
||
readonly httpStatus: number;
|
||
readonly data?: unknown;
|
||
|
||
constructor(code: string, msg: string, httpStatus = 400, data?: unknown) {
|
||
super(msg);
|
||
this.name = "ApiError";
|
||
this.code = code;
|
||
this.httpStatus = httpStatus;
|
||
this.data = data;
|
||
}
|
||
}
|
||
|
||
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||
|
||
export interface RequestOptions {
|
||
method?: HttpMethod;
|
||
body?: unknown;
|
||
headers?: Record<string, string>;
|
||
/** 为 true 时不弹出错误提示(由调用方处理) */
|
||
silent?: boolean;
|
||
rawBody?: BodyInit | null;
|
||
contentType?: string | null;
|
||
}
|
||
|
||
/**
|
||
* 管理接口请求封装:自动带 credentials 与 X-Nixmsg-Request: 1,错误直接 message 显示。
|
||
* W4 接真实后端时页面不必改,只需让 admin 模块走本函数。
|
||
*/
|
||
export async function requestAdmin<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||
const method = opts.method ?? (opts.body != null || opts.rawBody != null ? "POST" : "GET");
|
||
const headers: Record<string, string> = {
|
||
"X-Nixmsg-Request": "1",
|
||
...opts.headers,
|
||
};
|
||
|
||
let body: BodyInit | undefined | null = opts.rawBody;
|
||
if (opts.rawBody === undefined && opts.body !== undefined) {
|
||
headers["Content-Type"] = opts.contentType ?? "application/json";
|
||
body = JSON.stringify(opts.body);
|
||
} else if (opts.contentType) {
|
||
headers["Content-Type"] = opts.contentType;
|
||
}
|
||
|
||
const res = await fetch(path, {
|
||
method,
|
||
credentials: "include",
|
||
headers,
|
||
body: body ?? undefined,
|
||
});
|
||
|
||
let envelope: ApiEnvelope<T>;
|
||
try {
|
||
envelope = (await res.json()) as ApiEnvelope<T>;
|
||
} catch {
|
||
const err = new ApiError("internal", `响应不是 JSON(HTTP ${res.status})`, res.status);
|
||
if (!opts.silent) {
|
||
message.error(err.message);
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
if (typeof envelope !== "object" || envelope === null) {
|
||
const err = new ApiError("internal", "响应格式无效", res.status);
|
||
if (!opts.silent) {
|
||
message.error(err.message);
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
if (envelope.ok) {
|
||
return envelope.data;
|
||
}
|
||
|
||
const errBody: ApiErrorBody = envelope.error ?? { code: "internal", message: "未知错误" };
|
||
const err = new ApiError(errBody.code, errBody.message, res.status, "data" in envelope ? envelope.data : undefined);
|
||
if (!opts.silent) {
|
||
message.error(err.message);
|
||
}
|
||
throw err;
|
||
}
|