export class ApiError extends Error { constructor(message, status = 0, code = "error") { super(message); this.name = "ApiError"; this.status = status; this.code = code; } } function buildUrl(path, params) { const url = new URL(path, window.location.origin); if (params) { for (const [key, value] of Object.entries(params)) { if (value === null || value === undefined || value === "") continue; url.searchParams.set(key, String(value)); } } return url; } export async function api(path, params) { const response = await fetch(buildUrl(path, params), { headers: { Accept: "application/json" }, credentials: "same-origin", }); if (response.status === 401) { window.location.href = "/login"; throw new ApiError("登录已失效,请重新登录", 401, "unauthorized"); } let payload; try { payload = await response.json(); } catch { throw new ApiError(`接口返回异常(HTTP ${response.status})`, response.status); } if (!response.ok || !payload.ok) { const error = payload.error || {}; throw new ApiError( error.message || `请求失败(HTTP ${response.status})`, response.status, error.code || "error", ); } return payload.data; } export function apiUrl(path, params) { return buildUrl(path, params).toString(); } export async function health() { const response = await fetch("/api/health", { credentials: "same-origin" }); if (!response.ok) throw new ApiError("无法连接看板服务", response.status); const payload = await response.json(); return payload?.data ?? payload; }