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 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; } export async function session() { const response = await fetch("/api/session", { credentials: "same-origin" }); if (!response.ok) throw new ApiError("无法连接看板服务", response.status); const payload = await response.json(); return payload?.data ?? payload; } export async function stats(params) { return api("/api/stats", params); } export function visit(path) { // 访问统计埋点:优先 sendBeacon(不阻塞导航),失败时用 keepalive fetch 兜底。 const body = JSON.stringify({ path }); try { if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") { const blob = new Blob([body], { type: "application/json" }); if (navigator.sendBeacon("/api/visit", blob)) return; } } catch { // 忽略,走 fetch 兜底 } fetch("/api/visit", { method: "POST", headers: { "Content-Type": "application/json" }, body, credentials: "same-origin", keepalive: true, }).catch(() => {}); }