2026-09-07 14:54:20 +08:00
|
|
|
|
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> = {
|
2026-09-08 09:22:53 +08:00
|
|
|
|
catalog_sync: "同步数据集目录",
|
|
|
|
|
|
field_sync: "同步数据字段",
|
2026-09-07 14:54:20 +08:00
|
|
|
|
full_sync: "全量同步 Alpha",
|
2026-09-08 10:40:49 +08:00
|
|
|
|
daily_sync: "按天同步 Alpha",
|
|
|
|
|
|
self_correlation: "本地自相关检测",
|
2026-09-07 14:54:20 +08:00
|
|
|
|
alpha_refresh: "导入 / 刷新 Alpha",
|
|
|
|
|
|
pnl_refresh: "获取 PnL",
|
|
|
|
|
|
connect: "连接 WorldQuant",
|
|
|
|
|
|
verify: "继续人工验证",
|
|
|
|
|
|
profile: "刷新个人资料",
|
|
|
|
|
|
};
|
2026-09-08 10:40:49 +08:00
|
|
|
|
export const correlationLabels = {
|
|
|
|
|
|
high: "相关性偏高",
|
|
|
|
|
|
low: "低于阈值",
|
|
|
|
|
|
partial: "样本不完整",
|
|
|
|
|
|
insufficient_data: "数据不足",
|
|
|
|
|
|
};
|
2026-09-07 14:54:20 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|