预测历史:比赛信息内嵌到预测API + 实际比分自动填充

后端:
- PredictionOut 新增 match 字段(内嵌比赛信息)
- list_predictions 返回完整比赛数据(日期/队名/赛果/主客徽标)
- 新增 _match_dict() 辅助函数序列化比赛对象

前端:
- 移除独立的 fetchMatches 调用,直接使用 p.match
- 实际比分自动从比赛赛果填充
- 显示:比赛日(M/D) + 主客队徽标 + 队伍中文名
- 赛后一键结算(使用比赛实际赛果)

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
This commit is contained in:
shangfangjian
2026-09-21 01:54:30 +08:00
parent a9e85a5d62
commit 0a6af0657a
3 changed files with 28 additions and 14 deletions
+6 -14
View File
@@ -8,8 +8,8 @@
*/
import { useCallback, useEffect, useState } from 'react'
import { fetchPredictions, settlePrediction, fetchMatches } from '../dal'
import type { Prediction, Match } from '../types'
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'
@@ -23,7 +23,6 @@ function fmtDate(s?: string | null): string {
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')
@@ -34,15 +33,8 @@ export default function PredictionHistoryPage() {
setLoading(true)
setError(null)
try {
const list = await fetchPredictions(100)
const list = await fetchPredictions(200)
setPredictions(list)
const matchIds = [...new Set(list.map(p => p.match_id))]
if (matchIds.length > 0) {
const matchesData = await fetchMatches({ limit: 500 })
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 {
@@ -53,7 +45,7 @@ export default function PredictionHistoryPage() {
useEffect(() => { load() }, [load])
const handleSettle = async (p: Prediction) => {
const match = matches.get(p.match_id)
const match = (p as any).match
if (!match || match.home_goals == null || match.away_goals == null) return
setSettlingId(p.id)
try {
@@ -162,7 +154,7 @@ export default function PredictionHistoryPage() {
</thead>
<tbody>
{filtered.map(p => {
const match = matches.get(p.match_id)
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 || '?'
@@ -254,7 +246,7 @@ export default function PredictionHistoryPage() {
{expandedId && (() => {
const p = predictions.find(pr => pr.id === expandedId)
if (!p) return null
const match = matches.get(p.match_id)
const match = (p as any).match
const reports = p.agent_outputs ?? []
return (
<Card>
+20
View File
@@ -167,11 +167,31 @@ async def list_predictions(
actual_home_goals=p.actual_home_goals,
actual_away_goals=p.actual_away_goals,
settled=p.settled,
match=_match_dict(p.match) if p.match else None,
)
for p in rows
]
def _match_dict(m) -> dict | None:
if m is None:
return None
return {
"id": m.id,
"league_code": m.league.code if m.league else None,
"season": m.season,
"home_team": m.home_team.name if m.home_team else "?",
"away_team": m.away_team.name if m.away_team else "?",
"home_team_zh": m.home_team.name_zh if m.home_team else None,
"away_team_zh": m.away_team.name_zh if m.away_team else None,
"match_date": m.match_date.isoformat() if m.match_date else None,
"match_status": m.match_status,
"home_goals": m.home_goals,
"away_goals": m.away_goals,
"match_stage": m.match_stage,
}
@router.get("/predictions/{prediction_id}", response_model=PredictionOut, dependencies=[Depends(require_admin)])
async def get_prediction(prediction_id: int, db: AsyncSession = Depends(get_db_read)):
p = await db.get(Prediction, prediction_id)
+2
View File
@@ -102,6 +102,8 @@ class PredictionOut(BaseModel):
actual_home_goals: int | None
actual_away_goals: int | None
settled: bool
# 比赛信息(可选,列表接口不返回以减少 payload)
match: dict | None = None
class IngestBzzoiroRequest(BaseModel):