feat: add initial frontend setup with styles, types, and testing framework
- Created a global CSS file for styling the frontend with responsive design. - Introduced TypeScript types for various entities including Research, Alpha, and Account. - Implemented Playwright tests for account management and data synchronization workflows. - Configured TypeScript with strict settings and included necessary libraries. - Set up Vite as the build tool with React plugin and API proxy configuration. - Added a Python script to initialize environment variables securely.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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",
|
||||
alpha_refresh: "导入 / 刷新 Alpha",
|
||||
pnl_refresh: "获取 PnL",
|
||||
connect: "连接 WorldQuant",
|
||||
verify: "继续人工验证",
|
||||
profile: "刷新个人资料",
|
||||
};
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user