summaryrefslogtreecommitdiff
path: root/frontend/src/lib/api.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/api.ts')
-rw-r--r--frontend/src/lib/api.ts70
1 files changed, 70 insertions, 0 deletions
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
new file mode 100644
index 0000000..923a6e8
--- /dev/null
+++ b/frontend/src/lib/api.ts
@@ -0,0 +1,70 @@
+export interface ApiErrorShape {
+ code: string;
+ message: string;
+ details?: unknown;
+}
+
+export interface ApiErrorBody {
+ error: ApiErrorShape;
+}
+
+export class ApiError extends Error {
+ status: number;
+ code: string;
+ details?: unknown;
+
+ constructor(status: number, code: string, message: string, details?: unknown) {
+ super(message);
+ this.name = 'ApiError';
+ this.status = status;
+ this.code = code;
+ this.details = details;
+ }
+}
+
+interface Options {
+ method?: string;
+ body?: unknown;
+ signal?: AbortSignal;
+}
+
+export async function apiFetch<T = unknown, R = T>(url: string, options: Options = {}): Promise<R> {
+ const headers: Record<string, string> = { accept: 'application/json' };
+ let bodyStr: string | undefined;
+ if (options.body !== undefined) {
+ headers['content-type'] = 'application/json';
+ bodyStr = JSON.stringify(options.body);
+ }
+ let res: Response;
+ try {
+ res = await fetch(url, {
+ method: options.method ?? 'GET',
+ headers,
+ body: bodyStr,
+ credentials: 'same-origin',
+ signal: options.signal
+ });
+ } catch (e) {
+ if (e instanceof DOMException && e.name === 'AbortError') throw e;
+ throw new ApiError(0, 'network', '网络请求失败,请确认服务已启动后重试');
+ }
+ if (res.status === 204) {
+ return undefined as R;
+ }
+ const contentType = res.headers.get('content-type') ?? '';
+ if (!contentType.includes('application/json')) {
+ if (!res.ok) {
+ throw new ApiError(res.status, 'server_error', `服务返回错误(HTTP ${res.status})`);
+ }
+ throw new ApiError(res.status, 'bad_response', '服务返回了非预期的非 JSON 响应(接口约定为 JSON)');
+ }
+ const parsed: unknown = await res.json().catch(() => null);
+ if (!res.ok) {
+ const errBody = parsed as ApiErrorBody | null;
+ if (errBody?.error) {
+ throw new ApiError(res.status, errBody.error.code, errBody.error.message, errBody.error.details);
+ }
+ throw new ApiError(res.status, 'server_error', `服务返回错误(HTTP ${res.status})`);
+ }
+ return parsed as R;
+}