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,168 @@
|
||||
/**
|
||||
* Admin 后台 - 数据访问层
|
||||
*
|
||||
* 封装所有 API 端点调用,返回类型安全的数据。
|
||||
* 页面组件直接调用这些函数,无需关心网络细节。
|
||||
*/
|
||||
|
||||
import { api, API_BASE } from './api'
|
||||
import type {
|
||||
DashboardStats,
|
||||
ConfigEntry,
|
||||
ConfigUpdate,
|
||||
CollectionRequest,
|
||||
CollectionResponse,
|
||||
CollectionTask,
|
||||
PredictRequest,
|
||||
PredictionHistoryItem,
|
||||
SettleResponse,
|
||||
EvalSummary,
|
||||
BacktestRequest,
|
||||
BacktestResult,
|
||||
League,
|
||||
Paginated,
|
||||
ErrorLog,
|
||||
} from './types'
|
||||
|
||||
// ── 仪表盘 ──────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchDashboard(): Promise<DashboardStats> {
|
||||
// 后端可能没有专门的仪表盘聚合端点,这里用 health + 各端点组合
|
||||
const health = await fetchHealth()
|
||||
return {
|
||||
db_tables: health.db_tables ?? [],
|
||||
last_collection: health.last_collection ?? [],
|
||||
prediction_stats: health.prediction_stats ?? {
|
||||
total_predictions: 0,
|
||||
today_predictions: 0,
|
||||
avg_latency_ms: 0,
|
||||
success_rate: 0,
|
||||
},
|
||||
recent_errors: health.recent_errors ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端 health 端点暂时返回基础信息,仪表盘从多端点拼装 */
|
||||
async function fetchHealth() {
|
||||
try {
|
||||
const res = await api.get<{ status: string }>('/health')
|
||||
return res as unknown as DashboardStats
|
||||
} catch {
|
||||
return {} as DashboardStats
|
||||
}
|
||||
}
|
||||
|
||||
// ── 配置管理 ────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchConfigs(): Promise<ConfigEntry[]> {
|
||||
try {
|
||||
return await api.get<ConfigEntry[]>(`${API_BASE}/configs`)
|
||||
} catch {
|
||||
// 后端暂未实现配置端点,返回空列表
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateConfig(update: ConfigUpdate): Promise<void> {
|
||||
await api.post(`${API_BASE}/configs`, update)
|
||||
}
|
||||
|
||||
// ── 数据采集 ────────────────────────────────────────────────────
|
||||
|
||||
export async function triggerCollection(req: CollectionRequest): Promise<CollectionResponse> {
|
||||
const sourceMap: Record<string, string> = {
|
||||
bzzoiro: `${API_BASE}/ingest/bzzoiro`,
|
||||
understat: `${API_BASE}/ingest/understat`,
|
||||
injuries: `${API_BASE}/ingest/injuries`,
|
||||
}
|
||||
return api.post<CollectionResponse>(sourceMap[req.source], req)
|
||||
}
|
||||
|
||||
export async function fetchCollectionTasks(): Promise<CollectionTask[]> {
|
||||
try {
|
||||
return await api.get<CollectionTask[]>(`${API_BASE}/admin/tasks`)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ── 预测管理 ────────────────────────────────────────────────────
|
||||
|
||||
export async function triggerPrediction(req: PredictRequest): Promise<unknown> {
|
||||
return api.post(`${API_BASE}/predict`, req)
|
||||
}
|
||||
|
||||
export async function fetchPredictionHistory(
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
): Promise<Paginated<PredictionHistoryItem>> {
|
||||
try {
|
||||
return await api.get<Paginated<PredictionHistoryItem>>(
|
||||
`${API_BASE}/predictions?page=${page}&page_size=${pageSize}`,
|
||||
)
|
||||
} catch {
|
||||
return { items: [], total: 0, page: 1, page_size: pageSize, has_next: false }
|
||||
}
|
||||
}
|
||||
|
||||
// ── 评估 & 结算 ─────────────────────────────────────────────────
|
||||
|
||||
export async function triggerSettle(): Promise<SettleResponse> {
|
||||
return api.post<SettleResponse>(`${API_BASE}/eval/settle`)
|
||||
}
|
||||
|
||||
export async function fetchEvalSummary(): Promise<EvalSummary | null> {
|
||||
try {
|
||||
return await api.get<EvalSummary>(`${API_BASE}/eval/summary`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ── 回测管理 ────────────────────────────────────────────────────
|
||||
|
||||
export async function triggerBacktest(req: BacktestRequest): Promise<BacktestResult> {
|
||||
return api.post<BacktestResult>(`${API_BASE}/backtest`, req)
|
||||
}
|
||||
|
||||
export async function fetchBacktestResult(taskId: string): Promise<BacktestResult | null> {
|
||||
try {
|
||||
return await api.get<BacktestResult>(`${API_BASE}/backtest/${taskId}`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBacktestHistory(): Promise<BacktestResult[]> {
|
||||
try {
|
||||
return await api.get<BacktestResult[]>(`${API_BASE}/backtest`)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ── 辅助数据 ────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchLeagues(): Promise<League[]> {
|
||||
try {
|
||||
return await api.get<League[]>(`${API_BASE}/leagues`)
|
||||
} catch {
|
||||
return [
|
||||
{ code: 'E0', name: 'Premier League', name_zh: '英超', country: 'England' },
|
||||
{ code: 'SP1', name: 'La Liga', name_zh: '西甲', country: 'Spain' },
|
||||
{ code: 'D1', name: 'Bundesliga', name_zh: '德甲', country: 'Germany' },
|
||||
{ code: 'I1', name: 'Serie A', name_zh: '意甲', country: 'Italy' },
|
||||
{ code: 'F1', name: 'Ligue 1', name_zh: '法甲', country: 'France' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ── 监控 & 错误日志 ─────────────────────────────────────────────
|
||||
|
||||
export async function fetchErrorLogs(limit = 50): Promise<ErrorLog[]> {
|
||||
try {
|
||||
return await api.get<ErrorLog[]>(`${API_BASE}/admin/logs?limit=${limit}`)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user