feat: 全屏个股详情预览 + Tailwind 改版 + 账号鉴权
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type {
|
||||
BacktestRequest,
|
||||
BacktestResponse,
|
||||
CurrentUser,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
PreviewResponse,
|
||||
ScreenerRunRequest,
|
||||
ScreenerRunResponse,
|
||||
ScreenerSyncRequest,
|
||||
@@ -12,71 +16,91 @@ import type {
|
||||
// dev 用 Vite 代理(/api -> :8000);生产构建设 VITE_API_BASE 指向后端地址。
|
||||
const BASE = import.meta.env.VITE_API_BASE ?? '';
|
||||
|
||||
export async function postBacktest(req: BacktestRequest): Promise<BacktestResponse> {
|
||||
const res = await fetch(`${BASE}/api/backtest`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`回测请求失败 (HTTP ${res.status}): ${await res.text()}`);
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
async function readError(res: Response, fallback: string): Promise<string> {
|
||||
const body = await res.text();
|
||||
if (!body) return fallback;
|
||||
try {
|
||||
const data = JSON.parse(body) as { detail?: string };
|
||||
return data.detail || fallback;
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
});
|
||||
if (res.status === 401 && !path.startsWith('/api/auth/login')) {
|
||||
window.dispatchEvent(new CustomEvent('stock:unauthorized'));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function login(req: LoginRequest): Promise<LoginResponse> {
|
||||
const res = await apiFetch('/api/auth/login', { method: 'POST', body: JSON.stringify(req) });
|
||||
if (!res.ok) throw new ApiError(await readError(res, '登录失败'), res.status);
|
||||
return (await res.json()) as LoginResponse;
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<CurrentUser> {
|
||||
const res = await apiFetch('/api/auth/me');
|
||||
if (!res.ok) throw new ApiError(await readError(res, '登录状态无效'), res.status);
|
||||
return (await res.json()) as CurrentUser;
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
const res = await apiFetch('/api/auth/logout', { method: 'POST' });
|
||||
if (!res.ok && res.status !== 401) throw new ApiError(await readError(res, '退出登录失败'), res.status);
|
||||
}
|
||||
|
||||
export async function postBacktest(req: BacktestRequest): Promise<BacktestResponse> {
|
||||
const res = await apiFetch('/api/backtest', { method: 'POST', body: JSON.stringify(req) });
|
||||
if (!res.ok) throw new ApiError(`回测请求失败 (HTTP ${res.status}): ${await res.text()}`, res.status);
|
||||
return (await res.json()) as BacktestResponse;
|
||||
}
|
||||
|
||||
export async function syncData(req: SyncRequest): Promise<SyncResponse> {
|
||||
const res = await fetch(`${BASE}/api/data/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`数据拉取失败 (HTTP ${res.status}): ${await res.text()}`);
|
||||
}
|
||||
const res = await apiFetch('/api/data/sync', { method: 'POST', body: JSON.stringify(req) });
|
||||
if (!res.ok) throw new ApiError(`数据拉取失败 (HTTP ${res.status}): ${await res.text()}`, res.status);
|
||||
return (await res.json()) as SyncResponse;
|
||||
}
|
||||
|
||||
// ---------- 智能选股 ----------
|
||||
export async function runScreener(req: ScreenerRunRequest): Promise<ScreenerRunResponse> {
|
||||
const res = await fetch(`${BASE}/api/screener/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) {
|
||||
// 后端 detail 字段携带可读中文原因(503 未配 key / 409 未同步 / 502 LLM 错)
|
||||
let detail = '';
|
||||
try {
|
||||
detail = (await res.json())?.detail ?? '';
|
||||
} catch {
|
||||
detail = await res.text();
|
||||
}
|
||||
throw new Error(detail || `选股失败 (HTTP ${res.status})`);
|
||||
}
|
||||
const res = await apiFetch('/api/screener/run', { method: 'POST', body: JSON.stringify(req) });
|
||||
if (!res.ok) throw new ApiError(await readError(res, `选股失败 (HTTP ${res.status})`), res.status);
|
||||
return (await res.json()) as ScreenerRunResponse;
|
||||
}
|
||||
|
||||
export async function startScreenerSync(req: ScreenerSyncRequest = {}): Promise<ScreenerSyncStatus> {
|
||||
const res = await fetch(`${BASE}/api/screener/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let detail = '';
|
||||
try {
|
||||
detail = (await res.json())?.detail ?? '';
|
||||
} catch {
|
||||
detail = await res.text();
|
||||
}
|
||||
throw new Error(detail || `启动同步失败 (HTTP ${res.status})`);
|
||||
}
|
||||
const res = await apiFetch('/api/screener/sync', { method: 'POST', body: JSON.stringify(req) });
|
||||
if (!res.ok) throw new ApiError(await readError(res, `启动同步失败 (HTTP ${res.status})`), res.status);
|
||||
return (await res.json()) as ScreenerSyncStatus;
|
||||
}
|
||||
|
||||
export async function getScreenerSyncStatus(): Promise<ScreenerSyncStatus> {
|
||||
const res = await fetch(`${BASE}/api/screener/sync/status`);
|
||||
if (!res.ok) throw new Error(`获取同步状态失败 (HTTP ${res.status})`);
|
||||
const res = await apiFetch('/api/screener/sync/status');
|
||||
if (!res.ok) throw new ApiError(`获取同步状态失败 (HTTP ${res.status})`, res.status);
|
||||
return (await res.json()) as ScreenerSyncStatus;
|
||||
}
|
||||
|
||||
export async function getStockPreview(tsCode: string, limit = 260): Promise<PreviewResponse> {
|
||||
const res = await apiFetch(`/api/screener/preview/${encodeURIComponent(tsCode)}?limit=${limit}`);
|
||||
if (!res.ok) throw new ApiError(await readError(res, `获取个股详情失败 (HTTP ${res.status})`), res.status);
|
||||
return (await res.json()) as PreviewResponse;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user