export class ApiError extends Error { constructor( message: string, public status: number, ) { super(message); } } export async function api( path: string, options: RequestInit = {}, ): Promise { const response = await fetch(`/api/v1${path}`, { ...options, credentials: "same-origin", headers: { "Content-Type": "application/json", "X-WQ-Request": "1", ...options.headers, }, }); if (!response.ok) { const body = await response.json().catch(() => ({})); if (response.status === 401 && path !== "/auth/login") window.dispatchEvent(new Event("session-expired")); throw new ApiError( typeof body.detail === "string" ? body.detail : `请求失败(${response.status})`, response.status, ); } return response.json() as Promise; } export const post = (path: string, body?: unknown) => api(path, { method: "POST", body: body === undefined ? undefined : JSON.stringify(body), }); export const patch = (path: string, body: unknown) => api(path, { method: "PATCH", body: JSON.stringify(body) }); export function formatNumber(value: unknown, digits = 2): string { return typeof value === "number" && Number.isFinite(value) ? value.toFixed(digits) : "—"; } export function displayValue(value: unknown): string { if (value === null || value === undefined || value === "") return "未提供"; if (typeof value === "object") return JSON.stringify(value); return String(value); } export function formatTime( value: string | null | undefined, timezone = "Asia/Shanghai", ): string { if (!value) return "尚未同步"; const date = new Date(value); if (!Number.isFinite(date.valueOf())) return "时间未知"; return new Intl.DateTimeFormat("zh-CN", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }).format(date); } export const stateLabels = { inbox: "待研究", candidate: "候选", optimizing: "优化中", archived: "归档", }; export const stateOptions = Object.entries(stateLabels).map( ([value, label]) => ({ value, label }), ); export const jobLabels: Record = { catalog_full_sync: "全量同步数据目录与字段", catalog_sync: "同步数据集目录", field_sync: "同步数据字段", full_sync: "全量同步 Alpha", daily_sync: "按天同步 Alpha", self_correlation: "本地自相关检测", submission_check: "写回 Description 并检查提交", alpha_refresh: "导入 / 刷新 Alpha", pnl_refresh: "获取 PnL", pnl_backfill: "检查已提交 Alpha 的 PnL", connect: "连接 WorldQuant", verify: "继续人工验证", profile: "刷新个人资料", }; export const correlationLabels = { high: "相关性偏高", low: "低于阈值", partial: "样本不完整", insufficient_data: "数据不足", }; export const jobStateLabels: Record = { queued: "排队中", running: "执行中", waiting_auth: "等待人工验证", waiting_connection: "等待连接", completed: "已完成", completed_with_errors: "部分失败", failed: "失败", cancelled: "已取消", }; export function queryString( values: Record, ): string { return new URLSearchParams( Object.entries(values) .filter(([, v]) => v !== undefined && v !== "") .map(([k, v]) => [k, String(v)]), ).toString(); }