/** * Admin 后台 - 数据完整性分析页 * * 回答三个问题: * 1. 数据是否齐全(各联赛比赛/统计/积分榜量级) * 2. 字段是否齐全(每张统计表各字段非空率) * 3. 覆盖是否新鲜(最近一场/最近一次采集) */ import { useEffect, useState, useCallback, useRef } from 'react' import { fetchDataCompleteness } from '../dal' import type { DataCompletenessResponse } from '../dal' import { Card, CardBody, CardHeader, SectionHeader, Alert, ProgressBar, Spinner, EmptyState, } from '../components' const FIELD_LABELS: Record = { xg: 'xG 预期进球', shots: '射门', possession: '控球率', corners: '角球', fouls: '犯规', big_chances: '绝佳机会', cards: '红黄牌', } function pctColor(pct: number): string { if (pct >= 80) return 'bg-emerald-500' if (pct >= 50) return 'bg-amber-500' return 'bg-rose-500' } /** 根据问题描述生成可操作的修复链接 */ function getIssueAction(issue: string): { href: string; label: string; code: string } | null { // 提取联赛代码(大写字母+数字,如 E0, SP1) const codeMatch = issue.match(/\b([A-Z]{1,2}\d?)\b/) const code = codeMatch ? codeMatch[1] : '' if (!code) return null if (issue.includes('无已完赛比赛')) { return { href: `/admin/collection?task=events&league=${code}`, label: '去采集', code } } if (issue.includes('无统计回填') || issue.includes('统计覆盖率')) { return { href: `/admin/collection?task=stats&league=${code}`, label: '去回填', code } } if (issue.includes('无积分榜')) { return { href: `/admin/collection?task=standings&league=${code}`, label: '去采集', code } } return null } export default function DataCompletenessPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [highlightedLeague, setHighlightedLeague] = useState(null) const leagueRefs = useRef>({}) const load = useCallback(async () => { setLoading(true) setError(null) try { const d = await fetchDataCompleteness() setData(d) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) // 点击问题项 → 滚动到对应联赛卡片并高亮 const scrollToLeague = useCallback((code: string) => { setHighlightedLeague(code) // 使用 requestAnimationFrame 确保 DOM 已更新 requestAnimationFrame(() => { const el = leagueRefs.current[code] if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }) // 5秒后移除高亮 setTimeout(() => setHighlightedLeague(null), 5000) }, []) return (
{loading ? <> 刷新中 : '刷新'} } /> {error && setError(null)} />} {loading && !data && (
)} {data && ( <> {/* 全局概览 */}

{data.totals.finished_matches}

已完赛比赛(总计)

{data.totals.stats_rows}

统计行数

{data.totals.stats_coverage_pct}%

统计覆盖率(有统计 / 已完赛)

{/* 健康问题(可操作) */}
{data.issues.map((issue, i) => { const action = getIssueAction(issue) return ( {action.label}
) : undefined} /> ) })}
{/* 各联赛详情 */}
{data.leagues.map(league => { const finished = league.matches.finished const statsPct = finished > 0 ? Math.round((league.stats.rows / finished) * 100) : 0 const isHighlighted = highlightedLeague === league.code return (
{ leagueRefs.current[league.code] = el }} className={`transition-all duration-500 ${isHighlighted ? 'ring-2 ring-press bg-press-wash/40 scale-[1.01]' : ''}`} > 0 ? `积分榜 ${league.standings.rows} 队` : '', ].filter(Boolean).join(' · ')} /> {/* 统计覆盖率进度条 */}
统计回填覆盖率 {league.stats.rows} / {finished} ({statsPct}%)
{/* 字段覆盖率矩阵 */} {league.stats.rows > 0 ? (

字段覆盖率(有值行数 / 总行数)

{Object.entries(league.stats.fields).map(([key, info]) => (
{FIELD_LABELS[key] ?? key} {info.pct}%

{info.count} / {league.stats.rows} 行

))}
) : finished > 0 ? ( ) : ( )}
) })}

生成时间: {new Date(data.generated_at).toLocaleString('zh-CN', { hour12: false })}

)}
) }