120 lines
3.2 KiB
TypeScript
120 lines
3.2 KiB
TypeScript
export class ApiError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public status: number,
|
||
) {
|
||
super(message);
|
||
}
|
||
}
|
||
|
||
export async function api<T>(
|
||
path: string,
|
||
options: RequestInit = {},
|
||
): Promise<T> {
|
||
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<T>;
|
||
}
|
||
|
||
export const post = <T>(path: string, body?: unknown) =>
|
||
api<T>(path, {
|
||
method: "POST",
|
||
body: body === undefined ? undefined : JSON.stringify(body),
|
||
});
|
||
export const patch = <T>(path: string, body: unknown) =>
|
||
api<T>(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<string, string> = {
|
||
full_sync: "全量同步 Alpha",
|
||
daily_sync: "按天同步 Alpha",
|
||
self_correlation: "本地自相关检测",
|
||
alpha_refresh: "导入 / 刷新 Alpha",
|
||
pnl_refresh: "获取 PnL",
|
||
connect: "连接 WorldQuant",
|
||
verify: "继续人工验证",
|
||
profile: "刷新个人资料",
|
||
};
|
||
export const correlationLabels = {
|
||
high: "相关性偏高",
|
||
low: "低于阈值",
|
||
partial: "样本不完整",
|
||
insufficient_data: "数据不足",
|
||
};
|
||
export const jobStateLabels: Record<string, string> = {
|
||
queued: "排队中",
|
||
running: "执行中",
|
||
waiting_auth: "等待人工验证",
|
||
waiting_connection: "等待连接",
|
||
completed: "已完成",
|
||
completed_with_errors: "部分失败",
|
||
failed: "失败",
|
||
cancelled: "已取消",
|
||
};
|
||
|
||
export function queryString(
|
||
values: Record<string, string | number | boolean | undefined>,
|
||
): string {
|
||
return new URLSearchParams(
|
||
Object.entries(values)
|
||
.filter(([, v]) => v !== undefined && v !== "")
|
||
.map(([k, v]) => [k, String(v)]),
|
||
).toString();
|
||
}
|