预测管理改为预测历史页面

- 删除 Predictions.tsx,新建 PredictionHistory.tsx
- 不再支持触发预测(从主站比赛列表操作)
- 支持查看所有预测记录(全部/待标注/已结算筛选)
- 支持标注实际比分:行内编辑 + 保存,自动判断1X2是否命中
- 展开查看详情:预测比分/置信度/终裁意见/专家意见
- 侧边栏更名为「预测历史」,图标改为 logs

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
This commit is contained in:
shangfangjian
2026-09-21 01:17:14 +08:00
parent 857a21910b
commit f08fef8a22
4 changed files with 334 additions and 382 deletions
+3 -3
View File
@@ -92,7 +92,7 @@ const NAV_PAGES: Array<{ to: string; label: string; group: string }> = [
{ to: '/admin', label: '仪表盘', group: '概览' },
{ to: '/admin/collection', label: '数据采集', group: '数据流水线' },
{ to: '/admin/data-completeness', label: '数据完整性', group: '数据流水线' },
{ to: '/admin/predictions', label: '预测管理', group: '数据流水线' },
{ to: '/admin/predictions', label: '预测历史', group: '数据流水线' },
{ to: '/admin/backtest', label: '回测', group: '数据流水线' },
{ to: '/admin/monitoring', label: '监控', group: '评估与监控' },
{ to: '/admin/eval', label: '评估', group: '评估与监控' },
@@ -105,7 +105,7 @@ const ROUTE_LABELS: Record<string, string> = {
'/admin': '仪表盘',
'/admin/collection': '数据采集',
'/admin/data-completeness': '数据完整性',
'/admin/predictions': '预测管理',
'/admin/predictions': '预测历史',
'/admin/backtest': '回测',
'/admin/monitoring': '监控',
'/admin/settings': '设置',
@@ -119,7 +119,7 @@ const NAV_SECTIONS: { title: string; items: Array<{ to: string; label: string; i
items: [
{ to: '/admin/collection', label: '数据采集', icon: 'collection' },
{ to: '/admin/data-completeness', label: '数据完整性', icon: 'chart' },
{ to: '/admin/predictions', label: '预测管理', icon: 'target' },
{ to: '/admin/predictions', label: '预测历史', icon: 'logs' },
{ to: '/admin/backtest', label: '回测', icon: 'repeat' },
],
},
@@ -0,0 +1,330 @@
/**
* Admin 后台 - 预测历史页面(报刊风)
*
* 功能:
* - 查看预测记录列表(支持联赛/状态筛选)
* - 赛后标注实际比分,供准确率评估
* - 展开查看终裁理由与专家摘要
*/
import { useCallback, useEffect, useState } from 'react'
import { fetchPredictions, settlePrediction, fetchMatches } from '../dal'
import type { Prediction, Match } from '../types'
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components'
import { useState as useStateRef } from 'react'
const OUTCOME_LABEL: Record<string, string> = { '1': '主胜', X: '平局', '2': '客胜' }
function fmtTime(s?: string | null): string {
if (!s) return '—'
const d = new Date(s)
return isNaN(d.getTime()) ? s : d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
export default function PredictionHistoryPage() {
const [predictions, setPredictions] = useState<Prediction[]>([])
const [matches, setMatches] = useState<Map<number, Match>>(new Map())
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [filter, setFilter] = useState<'all' | 'settled' | 'unsettled'>('all')
const [expandedId, setExpandedId] = useState<number | null>(null)
// 标注实际比分表单
const [editingId, setEditingId] = useState<number | null>(null)
const [homeGoals, setHomeGoals] = useState('')
const [awayGoals, setAwayGoals] = useState('')
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState<{ kind: 'ok' | 'error'; text: string } | null>(null)
const load = useCallback(async () => {
setLoading(true)
setError(null)
try {
const list = await fetchPredictions(100)
setPredictions(list)
// 获取相关比赛信息
const matchIds = [...new Set(list.map(p => p.match_id))]
if (matchIds.length > 0) {
const matchesData = await fetchMatches({ limit: 200 })
const matchMap = new Map<number, Match>()
matchesData.items.forEach(m => matchMap.set(m.id, m))
setMatches(matchMap)
}
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { load() }, [load])
const filtered = predictions.filter(p => {
if (filter === 'settled') return p.settled
if (filter === 'unsettled') return !p.settled
return true
})
const startEdit = (p: Prediction) => {
setEditingId(p.id)
setHomeGoals(p.actual_home_goals?.toString() ?? '')
setAwayGoals(p.actual_away_goals?.toString() ?? '')
setSaveMsg(null)
}
const cancelEdit = () => {
setEditingId(null)
setHomeGoals('')
setAwayGoals('')
setSaveMsg(null)
}
const saveScore = async () => {
if (!editingId) return
const hg = parseInt(homeGoals)
const ag = parseInt(awayGoals)
if (isNaN(hg) || isNaN(ag)) {
setSaveMsg({ kind: 'error', text: '请输入有效比分' })
return
}
setSaving(true)
setSaveMsg(null)
try {
await settlePrediction(editingId, hg, ag)
setSaveMsg({ kind: 'ok', text: '已标注实际比分' })
await load()
setEditingId(null)
setHomeGoals('')
setAwayGoals('')
} catch (err) {
setSaveMsg({ kind: 'error', text: err instanceof Error ? err.message : '保存失败' })
} finally {
setSaving(false)
}
}
const unsettledCount = predictions.filter(p => !p.settled).length
return (
<div className="space-y-6">
<SectionHeader
title="预测历史"
description={`${predictions.length} 条预测记录${unsettledCount > 0 ? `, ${unsettledCount} 条待标注` : ''}`}
/>
{saveMsg && (
<Alert kind={saveMsg.kind} title={saveMsg.text} onClose={() => setSaveMsg(null)} />
)}
{/* 筛选 */}
<div className="flex gap-2">
{([
{ v: 'all', label: '全部' },
{ v: 'unsettled', label: '待标注' },
{ v: 'settled', label: '已结算' },
] as const).map(opt => (
<button
key={opt.v}
onClick={() => setFilter(opt.v)}
className={`btn btn-sm ${filter === opt.v ? 'btn-solid' : ''}`}
>
{opt.label}
</button>
))}
</div>
{loading && (
<div className="flex justify-center py-12"><Spinner /></div>
)}
{error && (
<Alert kind="error" title="加载失败" message={error} onClose={() => setError(null)} />
)}
{!loading && !error && filtered.length === 0 && (
<div className="empty-state">
<p className="empty-state-title"></p>
<p className="empty-state-sub"></p>
</div>
)}
{/* 预测列表 */}
{!loading && !error && filtered.length > 0 && (
<Card>
<CardBody className="px-0 sm:px-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="border-b border-ink-200 text-left text-ink-500">
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium">1X2</th>
<th className="px-4 py-2 font-medium"></th>
<th className="px-4 py-2 font-medium"></th>
</tr>
</thead>
<tbody>
{filtered.map(p => {
const isExpanded = expandedId === p.id
const isEditing = editingId === p.id
const matchInfo = matches.get(p.match_id)
const homeName = matchInfo?.home_team_zh || matchInfo?.home_team || `比赛 #${p.match_id}`
const awayName = matchInfo?.away_team_zh || matchInfo?.away_team || '?'
return (
<tr key={p.id} className="border-b border-ink-100 hover:bg-paper-100">
<td className="px-4 py-3">
<div className="text-xs text-ink-800">{homeName} vs {awayName}</div>
<div className="text-2xs text-ink-400">{fmtTime(p.created_at)}</div>
</td>
<td className="px-4 py-3 font-serif font-bold text-ink-900">
{p.pred_home_goals != null && p.pred_away_goals != null
? `${p.pred_home_goals} : ${p.pred_away_goals}`
: '—'}
</td>
<td className="px-4 py-3">
{isEditing ? (
<div className="flex items-center gap-1">
<input
type="number"
min={0}
max={20}
value={homeGoals}
onChange={e => setHomeGoals(e.target.value)}
className="field w-12 px-1 py-0.5 text-center text-xs"
/>
<span className="text-xs text-ink-400">:</span>
<input
type="number"
min={0}
max={20}
value={awayGoals}
onChange={e => setAwayGoals(e.target.value)}
className="field w-12 px-1 py-0.5 text-center text-xs"
/>
</div>
) : (
<span className={`font-serif ${p.settled ? 'font-bold text-ink-900' : 'text-ink-400'}`}>
{p.actual_home_goals != null && p.actual_away_goals != null
? `${p.actual_home_goals} : ${p.actual_away_goals}`
: '—'}
</span>
)}
</td>
<td className="px-4 py-3">
{p.pred_1x2 ? (
<span className={`inline-block border px-1.5 py-0.5 text-2xs ${
p.settled && p.actual_home_goals != null && p.actual_away_goals != null
? (() => {
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 ? 'border-emerald-300 text-emerald-700' : 'border-press text-press'
})()
: 'border-ink-200 text-ink-600'
}`}>
{OUTCOME_LABEL[p.pred_1x2] ?? p.pred_1x2}
</span>
) : '—'}
</td>
<td className="px-4 py-3">
{p.settled ? (
<Badge status="success"></Badge>
) : (
<Badge status="warning"></Badge>
)}
</td>
<td className="px-4 py-3">
<div className="flex gap-2">
{isEditing ? (
<>
<button onClick={saveScore} disabled={saving} className="btn btn-sm btn-solid">
{saving ? <Spinner /> : '保存'}
</button>
<button onClick={cancelEdit} className="btn btn-sm"></button>
</>
) : (
<>
{!p.settled && (
<button onClick={() => startEdit(p)} className="btn btn-sm">
</button>
)}
<button
onClick={() => setExpandedId(isExpanded ? null : p.id)}
className="btn btn-sm"
>
{isExpanded ? '收起' : '详情'}
</button>
</>
)}
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</CardBody>
</Card>
)}
{/* 展开详情 */}
{expandedId && (() => {
const p = predictions.find(pr => pr.id === expandedId)
if (!p) return null
const reports = p.agent_outputs ?? []
return (
<Card>
<CardHeader title="预测详情" />
<CardBody className="space-y-4">
<div className="grid gap-4 sm:grid-cols-3">
<div className="border-t-2 border-ink-900 pt-3">
<p className="text-2xs text-ink-400"></p>
<p className="mt-1 font-serif text-xl font-bold text-ink-900">
{p.pred_home_goals != null && p.pred_away_goals != null
? `${p.pred_home_goals} : ${p.pred_away_goals}` : '—'}
</p>
</div>
<div className="border-t-2 border-ink-900 pt-3">
<p className="text-2xs text-ink-400"></p>
<p className="mt-1 font-serif text-xl font-bold text-ink-900">
{p.subjective_confidence != null ? `${(p.subjective_confidence * 100).toFixed(0)}%` : '—'}
</p>
</div>
<div className="border-t-2 border-ink-900 pt-3">
<p className="text-2xs text-ink-400">1X2</p>
<p className="mt-1 font-serif text-xl font-bold text-ink-900">
{OUTCOME_LABEL[p.pred_1x2 ?? ''] ?? '—'}
</p>
</div>
</div>
{p.reasoning && (
<div>
<h4 className="section-head mb-2"></h4>
<blockquote className="border-l-2 border-press pl-4">
<p className="whitespace-pre-wrap font-serif text-sm leading-loose text-ink-700">{p.reasoning}</p>
</blockquote>
</div>
)}
{reports.length > 0 && (
<div>
<h4 className="section-head mb-2"></h4>
<div className="space-y-2">
{reports.map((r, i) => (
<div key={r.agent} className="border-b border-ink-100 pb-2 last:border-b-0">
<p className="text-xs font-medium text-ink-700">{r.agent}</p>
<p className="mt-0.5 text-xs text-ink-500">{r.analysis || '—'}</p>
</div>
))}
</div>
</div>
)}
</CardBody>
</Card>
)
})()}
</div>
)
}
-378
View File
@@ -1,378 +0,0 @@
/**
* Admin 后台 - 预测管理页面(报刊风)
*
* 功能:
* - 触发预测(选择比赛 + 模式)
* - 结算:录入实际比分,写入评估(接 /eval/settle)
* - 预测记录列表:可展开查看终裁理由与专家摘要
*
* 响应式布局: 移动端单列,桌面端双列
*/
import { useCallback, useEffect, useMemo, useState } from 'react'
import { triggerPrediction, fetchPredictions, fetchMatches, settlePrediction } from '../dal'
import type { Match, Prediction } from '../types'
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components'
import { teamSidePrefix } from '../../components/TeamSideTag'
const AGENT_LABELS: Record<string, string> = {
h2h: '历史交锋分析专家',
form: '近期状态分析专家',
stats: '攻防数据分析专家',
home_away: '主客因素分析专家',
standings: '联赛排名分析专家',
}
const OUTCOME_LABEL: Record<string, string> = { '1': '主胜', X: '平局', '2': '客胜' }
function fmtTime(s?: string | null): string {
if (!s) return '—'
const d = new Date(s)
return isNaN(d.getTime())
? s
: d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
}
export default function PredictionsPage() {
const [matches, setMatches] = useState<Match[]>([])
const [predictions, setPredictions] = useState<Prediction[]>([])
const [matchId, setMatchId] = useState('')
const [mode] = useState<'multi'>('multi')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [successMsg, setSuccessMsg] = useState<string | null>(null)
const [leagueFilter, setLeagueFilter] = useState('') // 联赛筛选
// 结算表单
const [settleId, setSettleId] = useState('')
const [homeGoals, setHomeGoals] = useState('')
const [awayGoals, setAwayGoals] = useState('')
const [settling, setSettling] = useState(false)
const [settleMsg, setSettleMsg] = useState<{ kind: 'error' | 'ok'; text: string } | null>(null)
const refreshPredictions = useCallback(async () => {
const list = await fetchPredictions(50)
setPredictions(list)
}, [])
useEffect(() => {
refreshPredictions()
// 只加载未开赛的比赛(用于预测)
fetchMatches({ status: 'scheduled', limit: 200 }).then(d => setMatches(d.items))
}, [refreshPredictions])
/** match_id → 中文名对阵 */
const matchName = useMemo(() => {
const map = new Map<number, string>()
for (const m of matches) {
const home = m.home_team_zh || m.home_team
const away = m.away_team_zh || m.away_team
map.set(m.id, `${teamSidePrefix('home')}${home} vs ${teamSidePrefix('away')}${away}`)
}
return map
}, [matches])
const nameOf = (id: number) => matchName.get(id) ?? `比赛 #${id}`
// 联赛列表(从比赛中提取) + 筛选后的比赛
const leagues = useMemo(() => {
const set = new Set(matches.map(m => m.league_code).filter(Boolean))
return [...set].sort()
}, [matches])
const filteredMatches = leagueFilter
? matches.filter(m => m.league_code === leagueFilter)
: matches
async function handlePredict(e: React.FormEvent) {
e.preventDefault()
if (!matchId) return
setLoading(true)
setError(null)
setSuccessMsg(null)
try {
await triggerPrediction({ match_id: parseInt(matchId), mode })
setSuccessMsg('预测任务已完成,记录已更新')
await refreshPredictions()
} catch (err: unknown) {
setError(err instanceof Error ? err.message : '预测失败')
} finally {
setLoading(false)
}
}
const unsettled = predictions.filter(p => !p.settled)
async function handleSettle(e: React.FormEvent) {
e.preventDefault()
const pid = parseInt(settleId)
const hg = parseInt(homeGoals)
const ag = parseInt(awayGoals)
if (!pid || isNaN(hg) || isNaN(ag)) return
setSettling(true)
setSettleMsg(null)
try {
await settlePrediction(pid, hg, ag)
setSettleMsg({ kind: 'ok', text: '结算完成,准确率统计已更新' })
setSettleId('')
setHomeGoals('')
setAwayGoals('')
await refreshPredictions()
} catch (err: unknown) {
setSettleMsg({
kind: 'error',
text: err instanceof Error ? err.message : '结算失败',
})
} finally {
setSettling(false)
}
}
const settleTarget = predictions.find(p => p.id === parseInt(settleId))
return (
<div className="space-y-6">
<SectionHeader
title="预测管理"
description="触发 LLM 预测;赛后录入实际比分完成结算,供准确率统计使用。"
/>
<div className="grid gap-6 lg:grid-cols-2">
{/* 新建预测 */}
<Card>
<CardHeader title="新建预测" />
<CardBody>
<form onSubmit={handlePredict} className="space-y-4">
<div className="space-y-3">
{leagues.length > 1 && (
<div>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<select
value={leagueFilter}
onChange={e => { setLeagueFilter(e.target.value); setMatchId('') }}
className="field w-full"
>
<option value=""></option>
{leagues.map(code => (
<option key={code} value={code}>{code}</option>
))}
</select>
</div>
)}
<div>
<label className="mb-1.5 block text-xs text-ink-500"> <span className="text-ink-400">({filteredMatches.length} )</span></label>
<select
value={matchId}
onChange={e => setMatchId(e.target.value)}
className="field w-full"
>
<option value=""></option>
{filteredMatches.map(m => (
<option key={m.id} value={m.id}>
{teamSidePrefix('home')}{(m.home_team_zh || m.home_team)} vs {teamSidePrefix('away')}{(m.away_team_zh || m.away_team)}
({m.match_date ? new Date(m.match_date).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) : '—'})
</option>
))}
</select>
</div>
</div>
<div>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<input
type="text"
value="多专家 (5 路 + 终裁)"
readOnly
className="field w-full bg-paper-100 text-ink-500"
/>
</div>
{error && <Alert kind="error" title="预测失败" message={error} onClose={() => setError(null)} />}
{successMsg && (
<Alert kind="ok" title={successMsg} onClose={() => setSuccessMsg(null)} />
)}
<button
type="submit"
disabled={loading || !matchId}
className="btn btn-solid w-full"
>
{loading ? (<><Spinner /> , 20-60 </>) : '触发预测'}
</button>
</form>
</CardBody>
</Card>
{/* 结算 */}
<Card>
<CardHeader
title="预测结算"
description="录入实际比分,系统据此统计 1X2 准确率与比分 RMSE"
/>
<CardBody>
{unsettled.length === 0 ? (
<p className="py-6 text-center text-xs text-ink-400">
</p>
) : (
<form onSubmit={handleSettle} className="space-y-4">
<div>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<select
value={settleId}
onChange={e => setSettleId(e.target.value)}
className="field w-full"
>
<option value="">({unsettled.length} )</option>
{unsettled.map(p => (
<option key={p.id} value={p.id}>
#{p.id} {nameOf(p.match_id)} · {p.pred_home_goals ?? '-'}:{p.pred_away_goals ?? '-'}
</option>
))}
</select>
</div>
{settleTarget && (
<p className="border-l-2 border-ink-300 pl-3 text-2xs text-ink-500">
:{settleTarget.pred_home_goals ?? '-'} : {settleTarget.pred_away_goals ?? '-'}
({OUTCOME_LABEL[settleTarget.pred_1x2 ?? ''] ?? '?'})
<span className="ml-2">{fmtTime(settleTarget.created_at)}</span>
</p>
)}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<input
type="number"
min={0}
max={30}
value={homeGoals}
onChange={e => setHomeGoals(e.target.value)}
className="field w-full"
/>
</div>
<div>
<label className="mb-1.5 block text-xs text-ink-500"></label>
<input
type="number"
min={0}
max={30}
value={awayGoals}
onChange={e => setAwayGoals(e.target.value)}
className="field w-full"
/>
</div>
</div>
{settleMsg && (
<Alert
kind={settleMsg.kind}
title={settleMsg.kind === 'ok' ? '结算完成' : '结算失败'}
message={settleMsg.kind === 'error' ? settleMsg.text : undefined}
onClose={() => setSettleMsg(null)}
/>
)}
<button
type="submit"
disabled={settling || !settleId || homeGoals === '' || awayGoals === ''}
className="btn btn-solid w-full"
>
{settling ? (<><Spinner /> </>) : '提交结算'}
</button>
</form>
)}
</CardBody>
</Card>
</div>
{/* 最近预测 */}
<Card>
<CardHeader title="预测记录" description="点击行可展开终裁理由与专家摘要" />
<CardBody className="px-0 sm:px-0">
{predictions.length === 0 ? (
<p className="py-10 text-center text-xs text-ink-400">
,
</p>
) : (
<div>
{predictions.map(p => {
const okAgents = (p.agent_outputs ?? []).filter(a => a.status === 'ok')
return (
<details key={p.id} className="group border-b border-ink-200 last:border-b-0">
<summary className="flex cursor-pointer list-none flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-3 transition-colors hover:bg-paper-100 sm:px-5">
<span className="text-2xs tabular-nums text-ink-400">#{p.id}</span>
<span className="text-sm font-medium text-ink-900">{nameOf(p.match_id)}</span>
<span className="font-serif text-sm font-bold tabular-nums text-ink-900">
{p.pred_home_goals ?? '-'}<span className="mx-0.5 font-normal text-ink-300">:</span>{p.pred_away_goals ?? '-'}
</span>
<span className="text-2xs text-ink-500">
{OUTCOME_LABEL[p.pred_1x2 ?? ''] ?? '—'}
{p.subjective_confidence !== null && p.subjective_confidence !== undefined &&
` · ${Math.round(p.subjective_confidence * 100)}%`}
</span>
<span className="ml-auto flex items-baseline gap-3">
{p.settled ? (
<Badge status="success"></Badge>
) : (
<Badge status="pending"></Badge>
)}
<span className="text-2xs tabular-nums text-ink-400">{fmtTime(p.created_at)}</span>
<svg
viewBox="0 0 20 20"
className="h-3 w-3 self-center text-ink-300 transition-transform group-open:rotate-90"
fill="currentColor"
aria-hidden="true"
>
<path d="M7.3 5.3a1 1 0 011.4 0l4 4a1 1 0 010 1.4l-4 4a1 1 0 01-1.4-1.4L10.6 10 7.3 6.7a1 1 0 010-1.4z" />
</svg>
</span>
</summary>
<div className="space-y-3 px-4 pb-4 pl-8 sm:px-6 sm:pl-9">
<p className="text-2xs text-ink-500">
`多专家 · ${okAgents.length}/${p.agent_outputs?.length ?? 0} 路有效`
{p.model && <span className="ml-2 font-mono">{p.model}</span>}
</p>
{p.reasoning && (
<blockquote className="border-l-2 border-press pl-4">
<p className="whitespace-pre-wrap font-serif text-sm leading-loose text-ink-700">
{p.reasoning}
</p>
</blockquote>
)}
{p.agent_outputs && p.agent_outputs.length > 0 && (
<ul className="space-y-1">
{p.agent_outputs.map((a, i) => (
<li key={i} className="flex items-baseline gap-2.5 text-xs">
<span className={`inline-block h-1.5 w-1.5 flex-shrink-0 self-center ${a.status === 'ok' ? 'bg-ink-900' : 'bg-ink-300'}`} aria-hidden="true" />
<span className="text-ink-800">{AGENT_LABELS[a.agent] ?? a.agent}</span>
{a.probable_score && (
<span className="font-serif font-bold tabular-nums text-ink-800">{a.probable_score}</span>
)}
<span className="text-2xs text-ink-400">
{a.status === 'ok' ? '' : a.status === 'no_data' ? '无数据' : '失败'}
</span>
</li>
))}
</ul>
)}
{p.settled && (
<p className="border-t border-ink-100 pt-2.5 text-2xs text-ink-500">
{p.actual_home_goals ?? '-'} : {p.actual_away_goals ?? '-'}
</p>
)}
</div>
</details>
)
})}
</div>
)}
</CardBody>
</Card>
</div>
)
}
+1 -1
View File
@@ -10,7 +10,7 @@ import AdminLayout from './AdminLayout'
import Dashboard from './pages/Dashboard'
import CollectionPage from './pages/Collection'
import DataCompletenessPage from './pages/DataCompleteness'
import PredictionsPage from './pages/Predictions'
import PredictionsPage from './pages/PredictionHistory'
import BacktestPage from './pages/Backtest'
import MonitoringPage from './pages/Monitoring'
import SettingsPage from './pages/Settings'