/** * 主站 - 联赛积分榜页 * * 展示各联赛最新积分榜(位置/积分/净胜/xG差/近期走势/分区), * 数据来自 bzzoiro /leagues/{id}/standings/ 管线采集。 */ import { useEffect, useState, useCallback } from 'react' import BackTop from '../components/BackTop' import { fetchStandings } from '../api/public' import type { StandingsLeague, StandingRow } from '../api/public' import { useLeagues } from './matches/hooks/useLeagues' import { Spinner, Tabs, Button } from '../components/ui' const ZONE_META: Record = { // 欧战资格 'Champions League': { label: '欧冠区', cls: 'bg-ok-100 text-ok-700' }, 'Champions League Qualification': { label: '欧冠资格', cls: 'bg-ok-100 text-ok-700' }, 'Europa League': { label: '欧联区', cls: 'bg-warn-100 text-warn-700' }, 'Conference League': { label: '欧协杯', cls: 'bg-euro-100 text-euro-700' }, 'Conference League Qualification': { label: '欧协杯', cls: 'bg-euro-100 text-euro-700' }, 'Europa Conference League': { label: '欧协杯', cls: 'bg-euro-100 text-euro-700' }, 'Europa Conference League Qualification': { label: '欧协杯', cls: 'bg-euro-100 text-euro-700' }, // 升级 'Championship': { label: '升级区', cls: 'bg-ok-100 text-ok-700' }, 'Promotion': { label: '升级区', cls: 'bg-ok-100 text-ok-700' }, 'Promotion Group': { label: '升级组', cls: 'bg-ok-100 text-ok-700' }, // 降级 'Relegation': { label: '降级区', cls: 'bg-bad-100 text-bad-700' }, 'Relegation Playoffs': { label: '降级附加赛', cls: 'bg-playoff-100 text-playoff-700' }, 'Relegation Group': { label: '降级组', cls: 'bg-bad-100 text-bad-700' }, // 附加赛 'Playoffs': { label: '附加赛', cls: 'bg-warn-100 text-warn-700' }, 'Championship Playoffs': { label: '升级附加赛', cls: 'bg-warn-100 text-warn-700' }, 'Qualification Playoffs': { label: '资格附加赛', cls: 'bg-euro-100 text-euro-700' }, 'Qualification': { label: '资格赛', cls: 'bg-euro-100 text-euro-700' }, } function zoneBadge(zone?: string | null) { if (!zone) return null const meta = ZONE_META[zone] ?? { label: zone, cls: 'bg-ink-100 text-ink-600' } return {meta.label} } /** 近期走势串(W/D/L) → 方点(与全站方角语言一致) */ function FormDots({ form }: { form?: string | null }) { if (!form) return const colorMap: Record = { W: 'bg-ok-500', D: 'bg-ink-300', L: 'bg-bad-500' } return ( {form.slice(0, 5).split('').map((c, i) => ( ))} ) } export default function StandingsPage() { // 统一数据源:复用 useLeagues hook(优先 API,失败回退本地常量) const leagues = useLeagues() const [standings, setStandings] = useState([]) const [activeLeague, setActiveLeague] = useState('') const [loading, setLoading] = useState(true) const [switching, setSwitching] = useState(false) // 切换联赛中 const [error, setError] = useState(null) const load = useCallback(async (code?: string) => { setLoading(true) setError(null) try { const data = await fetchStandings(code) setStandings(data.leagues) if (!activeLeague && data.leagues.length > 0) { setActiveLeague(data.leagues[0].league_code) } } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setLoading(false) } }, [activeLeague]) useEffect(() => { load() }, []) // eslint-disable-line react-hooks/exhaustive-deps // 切换联赛(带加载态,禁用 tab 防重复点击) const switchLeague = async (code: string) => { if (code === activeLeague || switching) return setSwitching(true) setActiveLeague(code) try { await fetchStandings(code).then(data => setStandings(data.leagues)) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setSwitching(false) } } const active = standings.find(l => l.league_code === activeLeague) ?? standings[0] return (
{/* 联赛切换:与首页/后台共用 .tab 语言(方角、宋体、印报红下划线) */} ({ value: l.code, label: l.name, // 无数据也可选中(会显示空态),仅做视觉弱化提示 empty: !standings.some(s => s.league_code === l.code), title: standings.some(s => s.league_code === l.code) ? undefined : '暂无积分榜数据', }))} /> {error && ( /* role=alert:错误是异步出现的,需立即被屏幕阅读器朗读 */

请求失败

{error}

)} {loading && (
加载中…
)} {/* 切换联赛时的轻量加载指示 */} {switching && !loading && (
切换联赛中…
)} {!loading && !active && (

暂无积分榜数据

请先在管理后台「数据采集」页运行「积分榜」任务。

)} {active && (

{active.league_name}

{active.season} 赛季 · {active.rows.length} 队 {active.retrieved_at && ` · 更新于 ${new Date(active.retrieved_at).toLocaleDateString('zh-CN')}`}

{/* 积分榜表格 */}
{active.rows.map((r: StandingRow) => ( ))}
# 球队 进/失 积分 xG± 走势
{r.position} {r.team} {r.played} {r.won} {r.drawn} {r.lost} {r.goals_for}/{r.goals_against} 0 ? 'text-ok-600' : r.goal_diff < 0 ? 'text-bad-600' : 'text-ink-500'}`}> {r.goal_diff > 0 ? `+${r.goal_diff}` : r.goal_diff} {r.points} {r.xg_for != null && r.xg_against != null ? `${(r.xg_for - r.xg_against).toFixed(1)}` : '—'}
{zoneBadge(r.zone)}
)} {/* 回到顶部(共享组件,方角纸片风) */}
) }