diff --git a/frontend/src/pages/Standings.tsx b/frontend/src/pages/Standings.tsx index 7438ffc..90bc615 100644 --- a/frontend/src/pages/Standings.tsx +++ b/frontend/src/pages/Standings.tsx @@ -8,18 +8,9 @@ import { useEffect, useState, useCallback } from 'react' import { fetchStandings } from '../admin/dal' import type { StandingsLeague, StandingRow } from '../admin/dal' +import { useLeagues } from './matches/hooks/useLeagues' import { Spinner } from '../admin/components' -const LEAGUES = [ - { code: 'E0', name: '英超' }, - { code: 'SP1', name: '西甲' }, - { code: 'D1', name: '德甲' }, - { code: 'I1', name: '意甲' }, - { code: 'F1', name: '法甲' }, - { code: 'CL', name: '欧冠' }, - { code: 'EL', name: '欧联' }, -] - const ZONE_META: Record = { // 欧战资格 'Champions League': { label: '欧冠区', cls: 'bg-emerald-100 text-emerald-700' }, @@ -64,7 +55,9 @@ function FormDots({ form }: { form?: string | null }) { } export default function StandingsPage() { - const [leagues, setLeagues] = useState([]) + // 统一数据源:复用 useLeagues hook(优先 API,失败回退本地常量) + const leagues = useLeagues() + const [standings, setStandings] = useState([]) const [activeLeague, setActiveLeague] = useState('') const [loading, setLoading] = useState(true) const [switching, setSwitching] = useState(false) // 切换联赛中 @@ -85,7 +78,7 @@ export default function StandingsPage() { setError(null) try { const data = await fetchStandings(code) - setLeagues(data.leagues) + setStandings(data.leagues) if (!activeLeague && data.leagues.length > 0) { setActiveLeague(data.leagues[0].league_code) } @@ -104,7 +97,7 @@ export default function StandingsPage() { setSwitching(true) setActiveLeague(code) try { - await fetchStandings(code).then(data => setLeagues(data.leagues)) + await fetchStandings(code).then(data => setStandings(data.leagues)) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { @@ -112,26 +105,32 @@ export default function StandingsPage() { } } - const active = leagues.find(l => l.league_code === activeLeague) ?? leagues[0] + const active = standings.find(l => l.league_code === activeLeague) ?? standings[0] return (
{/* 联赛切换 */}
- {LEAGUES.map(l => ( - - ))} + {leagues.map(l => { + // 标记该联赛是否有积分榜数据:有数据可正常切换,无数据也可选中但显示空态 + const hasData = standings.some(s => s.league_code === l.code) + const isEmpty = activeLeague === l.code && !hasData + return ( + + ) + })}
{error && ( diff --git a/frontend/src/pages/matches/types.ts b/frontend/src/pages/matches/types.ts index 778c366..2fabb96 100644 --- a/frontend/src/pages/matches/types.ts +++ b/frontend/src/pages/matches/types.ts @@ -78,6 +78,8 @@ export const LEAGUES = [ { code: 'D1', name: '德甲' }, { code: 'I1', name: '意甲' }, { code: 'F1', name: '法甲' }, + { code: 'CL', name: '欧冠' }, + { code: 'EL', name: '欧联' }, ] /** 汉字编号,给专家意见排版用 */ diff --git a/src/api/routes/matches.py b/src/api/routes/matches.py index 1f2fda0..742b2f2 100644 --- a/src/api/routes/matches.py +++ b/src/api/routes/matches.py @@ -5,11 +5,11 @@ from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, or_, select -from sqlalchemy.orm import selectinload +from sqlalchemy.orm import load_only, selectinload from src.api.schemas import MatchListOut, MatchOut, PredictionOut from src.db.base import AsyncSession, get_db_read -from src.db.models import League, Match, Prediction, Standing +from src.db.models import League, Match, MatchStats, Prediction, Standing, Team router = APIRouter(prefix="/api/v1", tags=["data"]) @@ -49,8 +49,20 @@ async def list_matches( limit: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db_read), ): - """比赛列表(游标分页)。""" - q = select(Match).options(selectinload(Match.league), selectinload(Match.home_team), selectinload(Match.away_team)) + """比赛列表(游标分页)。 + + 加载策略(列表 vs 详情): + - 列表:仅 selectinload 序列化需要的 3 个关系 + stats,且用 load_only 限定列 + (League.code / Team.name,name_zh / MatchStats.home_xg,away_xg),避免传输全列; + 同时一次性加载 stats 消除 N+1(m.stats.home_xg 此前触发懒加载)。 + - 详情(/matches/{id}):保持完整 options(league/teams/stats 全列 + 最近预测)。 + """ + q = select(Match).options( + selectinload(Match.league).load_only(League.code), + selectinload(Match.home_team).load_only(Team.name, Team.name_zh), + selectinload(Match.away_team).load_only(Team.name, Team.name_zh), + selectinload(Match.stats).load_only(MatchStats.home_xg, MatchStats.away_xg), + ) if cursor: try: