feat: 后台管理 Admin 仪表盘
新增完整的后台管理系统 (/admin): - Dashboard: 系统概览、最近采集状态、预测统计 - Collection: 数据采集触发(bzzoiro/understat/injuries) - Predictions: 预测历史查看、触发新预测 - Backtest: 回测配置与结果查看 - Monitoring: 系统健康、错误日志、死色队列 - Config: API Key 与数据源配置 技术栈: React Router + Tailwind 暗色主题 + TypeScript 文件: 11 个新文件, +21KB JS / +5KB CSS
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Admin 后台管理系统 - 统一 API 客户端
|
||||
*
|
||||
* 封装 fetch 调用,提供:
|
||||
* - 统一错误处理
|
||||
* - 请求/响应日志
|
||||
* - 超时控制
|
||||
* - 类型安全的响应解析
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/v1'
|
||||
const TIMEOUT_MS = 30_000
|
||||
|
||||
/** 通用 API 错误类型 */
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public data?: unknown,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
/** 基础 fetch 封装,带超时和错误处理 */
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const url = path.startsWith('http') ? path : `${path}`
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
let detail: unknown
|
||||
try {
|
||||
detail = await res.json()
|
||||
} catch {
|
||||
detail = await res.text()
|
||||
}
|
||||
throw new ApiError(
|
||||
detail && typeof detail === 'object' && 'detail' in detail
|
||||
? String((detail as { detail: unknown }).detail)
|
||||
: `HTTP ${res.status}: ${res.statusText}`,
|
||||
res.status,
|
||||
detail,
|
||||
)
|
||||
}
|
||||
|
||||
// 204 No Content
|
||||
if (res.status === 200 && res.headers.get('content-length') === '0') {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) throw err
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
throw new ApiError('请求超时,请稍后重试', 0)
|
||||
}
|
||||
throw new ApiError(
|
||||
err instanceof Error ? err.message : '网络错误,请检查连接',
|
||||
0,
|
||||
)
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 通用 CRUD 快捷方法 ──────────────────────────────────────────
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
put: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
export { API_BASE }
|
||||
Reference in New Issue
Block a user