/** * 主站 - 联赛积分榜页 * * 展示各联赛最新积分榜(位置/积分/净胜/xG差/近期走势/分区), * 数据来自 bzzoiro /leagues/{id}/standings/ 管线采集。 */ 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 ZONE_META: Record = { // 欧战资格 'Champions League': { label: '欧冠区', cls: 'bg-emerald-100 text-emerald-700' }, 'Champions League Qualification': { label: '欧冠资格', cls: 'bg-emerald-100 text-emerald-700' }, 'Europa League': { label: '欧联区', cls: 'bg-amber-100 text-amber-700' }, 'Conference League': { label: '欧协杯', cls: 'bg-sky-100 text-sky-700' }, 'Conference League Qualification': { label: '欧协杯', cls: 'bg-sky-100 text-sky-700' }, 'Europa Conference League': { label: '欧协杯', cls: 'bg-sky-100 text-sky-700' }, 'Europa Conference League Qualification': { label: '欧协杯', cls: 'bg-sky-100 text-sky-700' }, // 升级 'Championship': { label: '升级区', cls: 'bg-emerald-100 text-emerald-700' }, 'Promotion': { label: '升级区', cls: 'bg-emerald-100 text-emerald-700' }, 'Promotion Group': { label: '升级组', cls: 'bg-emerald-100 text-emerald-700' }, // 降级 'Relegation': { label: '降级区', cls: 'bg-rose-100 text-rose-700' }, 'Relegation Playoffs': { label: '降级附加赛', cls: 'bg-orange-100 text-orange-700' }, 'Relegation Group': { label: '降级组', cls: 'bg-rose-100 text-rose-700' }, // 附加赛 'Playoffs': { label: '附加赛', cls: 'bg-amber-100 text-amber-700' }, 'Championship Playoffs': { label: '升级附加赛', cls: 'bg-amber-100 text-amber-700' }, 'Qualification Playoffs': { label: '资格附加赛', cls: 'bg-sky-100 text-sky-700' }, 'Qualification': { label: '资格赛', cls: 'bg-sky-100 text-sky-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-emerald-500', D: 'bg-ink-300', L: 'bg-rose-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 [showBackTop, setShowBackTop] = useState(false) // 回到顶部按钮显示态 // 监听滚动,超过 300px 显示回到顶部按钮 useEffect(() => { const handleScroll = () => setShowBackTop(window.scrollY > 300) window.addEventListener('scroll', handleScroll, { passive: true }) return () => window.removeEventListener('scroll', handleScroll) }, []) const scrollToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' }) 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 (
{/* 联赛切换 */}
{leagues.map(l => { // 标记该联赛是否有积分榜数据:有数据可正常切换,无数据也可选中但显示空态 const hasData = standings.some(s => s.league_code === l.code) const isEmpty = activeLeague === l.code && !hasData return ( ) })}
{error && (
{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-emerald-600' : r.goal_diff < 0 ? 'text-rose-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)}
)} {/* 回到顶部按钮 */}
) }