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:
shangfangjian
2026-09-17 02:00:14 +08:00
parent 1219b4fd18
commit d3284c48c3
19 changed files with 2671 additions and 81 deletions
+168
View File
@@ -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 []
}
}