/** * Admin 后台 - 预测历史页面(报刊风) * * 功能: * - 查看预测记录列表(支持状态筛选) * - 实际比分自动从比赛赛果填充 * - 赛后一键结算,供准确率评估 */ import { useCallback, useEffect, useState } from 'react' import { fetchPredictions, settlePrediction } from '../dal' import type { Prediction } from '../types' import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components' import TeamSideTag from '../../components/TeamSideTag' const OUTCOME_LABEL: Record = { '1': '主胜', X: '平局', '2': '客胜' } function fmtDate(s?: string | null): string { if (!s) return '—' const d = new Date(s) return isNaN(d.getTime()) ? s : `${d.getMonth() + 1}/${d.getDate()}` } export default function PredictionHistoryPage() { const [predictions, setPredictions] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [filter, setFilter] = useState<'all' | 'settled' | 'unsettled'>('all') const [expandedId, setExpandedId] = useState(null) const [settlingId, setSettlingId] = useState(null) const load = useCallback(async () => { setLoading(true) setError(null) try { const list = await fetchPredictions(200) setPredictions(list) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) const handleSettle = async (p: Prediction) => { const match = (p as any).match if (!match || match.home_goals == null || match.away_goals == null) return setSettlingId(p.id) try { await settlePrediction(p.id, match.home_goals, match.away_goals) await load() } catch (err) { console.error('结算失败:', err) } finally { setSettlingId(null) } } const filtered = predictions.filter(p => { if (filter === 'settled') return p.settled if (filter === 'unsettled') return !p.settled return true }) const unsettledCount = predictions.filter(p => !p.settled).length const settledWithScore = predictions.filter(p => p.settled && p.actual_home_goals != null && p.actual_away_goals != null) const hitCount = settledWithScore.filter(p => { const actual = p.actual_home_goals! > p.actual_away_goals! ? '1' : p.actual_home_goals! < p.actual_away_goals! ? '2' : 'X' return actual === p.pred_1x2 }).length const accuracy = settledWithScore.length > 0 ? Math.round(hitCount / settledWithScore.length * 100) : 0 return (
0 ? `, ${unsettledCount} 条待结算` : ''}`} /> {/* 统计概览 */} {settledWithScore.length > 0 && (

{settledWithScore.length}

已结算

{hitCount}

命中

{accuracy}%

1X2 准确率

)} {/* 筛选 */}
{([ { v: 'all', label: '全部' }, { v: 'unsettled', label: '待结算' }, { v: 'settled', label: '已结算' }, ] as const).map(opt => ( ))}
{loading && (
)} {error && ( setError(null)} /> )} {!loading && !error && filtered.length === 0 && (

暂无预测记录

在主站比赛列表点击「预测」按钮生成预测记录

)} {/* 预测列表 */} {!loading && !error && filtered.length > 0 && (
{filtered.map(p => { const match = (p as any).match const matchDate = match?.match_date const homeName = match?.home_team_zh || match?.home_team || '?' const awayName = match?.away_team_zh || match?.away_team || '?' const actualHome = match?.home_goals const actualAway = match?.away_goals const hasActual = actualHome != null && actualAway != null const isExpanded = expandedId === p.id const predHit = p.settled && hasActual ? (actualHome! > actualAway! ? '1' : actualHome! < actualAway! ? '2' : 'X') === p.pred_1x2 : null return ( ) })}
比赛日 比赛 预测 实际 1X2 状态 操作
{fmtDate(matchDate)}
{homeName}
{awayName}
{p.pred_home_goals != null && p.pred_away_goals != null ? `${p.pred_home_goals} : ${p.pred_away_goals}` : '—'} {hasActual ? ( {actualHome} : {actualAway} ) : ( 暂无赛果 )} {p.pred_1x2 ? ( {OUTCOME_LABEL[p.pred_1x2] ?? p.pred_1x2} {predHit === true && ' ✓'} {predHit === false && ' ✗'} ) : '—'} {p.settled ? ( 已结算 ) : hasActual ? ( 可结算 ) : ( 待赛果 )}
{!p.settled && hasActual && ( )}
)} {/* 展开详情 */} {expandedId && (() => { const p = predictions.find(pr => pr.id === expandedId) if (!p) return null const match = (p as any).match const reports = p.agent_outputs ?? [] return (
{fmtDate(match?.match_date)} | {match?.home_team_zh || match?.home_team || '?'} vs {match?.away_team_zh || match?.away_team || '?'}

预测比分

{p.pred_home_goals != null && p.pred_away_goals != null ? `${p.pred_home_goals} : ${p.pred_away_goals}` : '—'}

实际比分

{match?.home_goals != null && match?.away_goals != null ? `${match.home_goals} : ${match.away_goals}` : '—'}

置信度

{p.subjective_confidence != null ? `${(p.subjective_confidence * 100).toFixed(0)}%` : '—'}

{p.reasoning && (

终裁意见

{p.reasoning}

)} {reports.length > 0 && (

专家意见

{reports.map((r) => (

{r.agent}

{r.analysis || '—'}

))}
)}
) })()}
) }