From 1fc799a5b07c8ca52ab9139e514e298f44f59484 Mon Sep 17 00:00:00 2001 From: shangfangjian Date: Mon, 21 Sep 2026 01:21:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E6=B5=8B=E5=8E=86=E5=8F=B2:=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E6=AF=94=E5=88=86=E8=87=AA=E5=8A=A8=E5=A1=AB=E5=85=85?= =?UTF-8?q?=20+=20=E6=98=BE=E7=A4=BA=E6=AF=94=E8=B5=9B=E6=97=A5/=E9=98=9F?= =?UTF-8?q?=E4=BC=8D/=E4=B8=BB=E5=AE=A2=E5=BE=BD=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实际比分自动从比赛赛果填充,无需手动输入 - 比赛列显示:比赛日(M/D) + 主客队徽标 + 队伍中文名 - 状态分三类:待赛果(灰)/可结算(蓝)/已结算(绿) - 一键结算按钮:仅当比赛有赛果且未结算时显示 - 结算后1X2自动判断命中(绿✓/红✗) - 顶部新增统计概览:已结算数/命中数/1X2准确率 Co-Authored-By: new-provider/LongCat-2.0 <> ) --- .../src/admin/pages/PredictionHistory.tsx | 246 +++++++++--------- 1 file changed, 120 insertions(+), 126 deletions(-) diff --git a/frontend/src/admin/pages/PredictionHistory.tsx b/frontend/src/admin/pages/PredictionHistory.tsx index bcb64b4..fd64a89 100644 --- a/frontend/src/admin/pages/PredictionHistory.tsx +++ b/frontend/src/admin/pages/PredictionHistory.tsx @@ -2,23 +2,23 @@ * 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' +import TeamSideTag from '../../components/TeamSideTag' const OUTCOME_LABEL: Record = { '1': '主胜', X: '平局', '2': '客胜' } -function fmtTime(s?: string | null): string { +function fmtDate(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' }) + return isNaN(d.getTime()) ? s : `${d.getMonth() + 1}/${d.getDate()}` } export default function PredictionHistoryPage() { @@ -28,13 +28,7 @@ export default function PredictionHistoryPage() { const [error, setError] = useState(null) const [filter, setFilter] = useState<'all' | 'settled' | 'unsettled'>('all') const [expandedId, setExpandedId] = useState(null) - - // 标注实际比分表单 - const [editingId, setEditingId] = useState(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 [settlingId, setSettlingId] = useState(null) const load = useCallback(async () => { setLoading(true) @@ -42,10 +36,9 @@ export default function PredictionHistoryPage() { 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 matchesData = await fetchMatches({ limit: 500 }) const matchMap = new Map() matchesData.items.forEach(m => matchMap.set(m.id, m)) setMatches(matchMap) @@ -59,68 +52,70 @@ export default function PredictionHistoryPage() { useEffect(() => { load() }, [load]) + const handleSettle = async (p: Prediction) => { + const match = matches.get(p.match_id) + 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 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 + 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} 条待标注` : ''}`} + description={`共 ${predictions.length} 条记录${unsettledCount > 0 ? `, ${unsettledCount} 条待结算` : ''}`} /> - {saveMsg && ( - setSaveMsg(null)} /> + {/* 统计概览 */} + {settledWithScore.length > 0 && ( +
+ + +

{settledWithScore.length}

+

已结算

+
+
+ + +

{hitCount}

+

命中

+
+
+ + +

{accuracy}%

+

1X2 准确率

+
+
+
)} {/* 筛选 */}
{([ { v: 'all', label: '全部' }, - { v: 'unsettled', label: '待标注' }, + { v: 'unsettled', label: '待结算' }, { v: 'settled', label: '已结算' }, ] as const).map(opt => (