fix: 修复 Admin 后台多个 bug

- api.ts: 修复 URL 拼接 bug(缺少 API_BASE) 和 204 判断逻辑
- dal.ts: 修复 fetchDashboard 类型错乱,修正 API 端点
- types.ts: 对齐后端 Pydantic 模型
- 所有页面: Badge variant→status, CardHeader children→action
- 移除不存在的后端端点调用
This commit is contained in:
shangfangjian
2026-09-17 02:11:50 +08:00
parent d3284c48c3
commit 6680da7d61
9 changed files with 572 additions and 1642 deletions
+99 -116
View File
@@ -2,114 +2,94 @@
* Admin 后台 - 数据访问层
*
* 封装所有 API 端点调用,返回类型安全的数据。
* 页面组件直接调用这些函数,无需关心网络细节
* 所有端点对齐 FastAPI 后端实际实现
*/
import { api, API_BASE } from './api'
import type {
DashboardStats,
ConfigEntry,
ConfigUpdate,
CollectionRequest,
CollectionResponse,
CollectionTask,
PredictRequest,
PredictionHistoryItem,
SettleResponse,
EvalSummary,
BacktestRequest,
BacktestResult,
BacktestSummary,
League,
Paginated,
ErrorLog,
Match,
Prediction,
EvalSummary,
} from './types'
// ── 仪表盘 ──────────────────────────────────────────────────────
/**
* 从多个端点聚合仪表盘数据。
* 后端暂无专用仪表盘端点,这里组合 health + 各列表端点。
*/
export async function fetchDashboard(): Promise<DashboardStats> {
// 后端可能没有专门的仪表盘聚合端点,这里用 health + 各端点组合
const health = await fetchHealth()
// 并行获取各端点数据
const [leagues, matches, predictions, health] = await Promise.allSettled([
api.get<League[]>(`${API_BASE}/leagues`),
api.get<Match[]>(`${API_BASE}/matches?limit=1`),
api.get<Prediction[]>(`${API_BASE}/predictions?limit=1`),
api.get<{ status: string }>('/health'),
])
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 ?? [],
leagues: leagues.status === 'fulfilled' ? leagues.value : [],
total_matches: matches.status === 'fulfilled' ? (matches.value as any)?.total ?? 0 : 0,
total_predictions: predictions.status === 'fulfilled' ? (predictions.value as any)?.total ?? 0 : 0,
health: health.status === 'fulfilled' ? (health.value as any).status : 'unknown',
db_tables: [], // 后端暂无表统计端点
last_collection: [], // 后端暂无采集历史端点
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 triggerCollection(req: CollectionRequest): Promise<any> {
const sourceMap: Record<string, { path: string; body: any }> = {
bzzoiro: {
path: `${API_BASE}/ingest/bzzoiro`,
body: {
leagues: req.leagues,
date_from: req.date_from,
date_to: req.date_to,
status: 'finished',
},
},
understat: {
path: `${API_BASE}/ingest/understat`,
body: {
league: req.league,
season: req.season ? parseInt(req.season) : new Date().getFullYear(),
},
},
injuries: {
path: `${API_BASE}/ingest/injuries`,
body: {
date: req.date_from || new Date().toISOString().slice(0, 10),
},
},
}
const cfg = sourceMap[req.source]
if (!cfg) throw new Error(`未知数据源: ${req.source}`)
return api.post(cfg.path, cfg.body)
}
// ── 预测管理 ────────────────────────────────────────────────────
export async function triggerPrediction(req: PredictRequest): Promise<unknown> {
return api.post(`${API_BASE}/predict`, req)
export async function triggerPrediction(req: { match_id: number; mode?: string }): Promise<any> {
return api.post(`${API_BASE}/predict`, {
match_id: req.match_id,
mode: req.mode || 'multi',
})
}
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 fetchPredictions(limit = 50): Promise<any[]> {
const res = await api.get<any>(`${API_BASE}/predictions?limit=${limit}`)
return Array.isArray(res) ? res : (res as any)?.items ?? []
}
// ── 评估 & 结算 ─────────────────────────────────────────────────
export async function triggerSettle(): Promise<SettleResponse> {
return api.post<SettleResponse>(`${API_BASE}/eval/settle`)
}
// ── 评估 & 回测 ─────────────────────────────────────────────────
export async function fetchEvalSummary(): Promise<EvalSummary | null> {
try {
@@ -119,26 +99,8 @@ export async function fetchEvalSummary(): Promise<EvalSummary | 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 triggerBacktest(req: BacktestRequest): Promise<BacktestSummary> {
return api.post<BacktestSummary>(`${API_BASE}/backtest`, req)
}
// ── 辅助数据 ────────────────────────────────────────────────────
@@ -146,23 +108,44 @@ export async function fetchBacktestHistory(): Promise<BacktestResult[]> {
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 []
}
}
export async function fetchMatches(params: {
league?: string
status?: string
limit?: number
cursor?: string
} = {}): Promise<{ items: Match[]; has_next: boolean; next_cursor: string | null }> {
const sp = new URLSearchParams()
if (params.league) sp.set('league', params.league)
if (params.status) sp.set('status', params.status)
if (params.limit) sp.set('limit', String(params.limit))
if (params.cursor) sp.set('cursor', params.cursor)
try {
return await api.get<any>(`${API_BASE}/matches?${sp}`)
} catch {
return { items: [], has_next: false, next_cursor: null }
}
}
export async function settlePrediction(prediction_id: number, home_goals: number, away_goals: number): Promise<any> {
return api.post(`${API_BASE}/eval/settle`, {
prediction_id,
home_goals,
away_goals,
})
}
// ── 健康检查 ────────────────────────────────────────────────────
export async function fetchHealth(): Promise<any> {
try {
return await api.get<any>('/health')
} catch {
return { status: 'unknown' }
}
}