From 5c0ba37eda80d39e6ceca59bb1d5f4942f858995 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Thu, 17 Sep 2026 14:32:37 +0800 Subject: chore: establish Strategy Lab source baseline (development, not release) --- frontend/src/lib/api.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 frontend/src/lib/api.ts (limited to 'frontend/src/lib/api.ts') 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(url: string, options: Options = {}): Promise { + const headers: Record = { 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; +} -- cgit v1.2.3