健康摘要与联赛卡片联动

- 问题项新增「定位」按钮,点击滚动到对应联赛卡片
- 被定位的联赛卡片高亮显示(ring + scale),3秒后自动消失
- 健康摘要 → 联赛详情形成闭环操作

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
This commit is contained in:
shangfangjian
2026-09-21 00:44:19 +08:00
parent 7025338226
commit 83cc24e54b
+34 -9
View File
@@ -7,7 +7,7 @@
* 3. 覆盖是否新鲜(最近一场/最近一次采集) * 3. 覆盖是否新鲜(最近一场/最近一次采集)
*/ */
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback, useRef } from 'react'
import { fetchDataCompleteness } from '../dal' import { fetchDataCompleteness } from '../dal'
import type { DataCompletenessResponse } from '../dal' import type { DataCompletenessResponse } from '../dal'
import { import {
@@ -32,20 +32,20 @@ function pctColor(pct: number): string {
} }
/** 根据问题描述生成可操作的修复链接 */ /** 根据问题描述生成可操作的修复链接 */
function getIssueAction(issue: string): { href: string; label: string } | null { function getIssueAction(issue: string): { href: string; label: string; code: string } | null {
// 提取联赛代码(大写字母+数字,如 E0, SP1) // 提取联赛代码(大写字母+数字,如 E0, SP1)
const codeMatch = issue.match(/\b([A-Z]{1,2}\d?)\b/) const codeMatch = issue.match(/\b([A-Z]{1,2}\d?)\b/)
const code = codeMatch ? codeMatch[1] : '' const code = codeMatch ? codeMatch[1] : ''
if (!code) return null if (!code) return null
if (issue.includes('无已完赛比赛')) { if (issue.includes('无已完赛比赛')) {
return { href: `/admin/collection?task=events&league=${code}`, label: '去采集' } return { href: `/admin/collection?task=events&league=${code}`, label: '去采集', code }
} }
if (issue.includes('无统计回填') || issue.includes('统计覆盖率')) { if (issue.includes('无统计回填') || issue.includes('统计覆盖率')) {
return { href: `/admin/collection?task=stats&league=${code}`, label: '去回填' } return { href: `/admin/collection?task=stats&league=${code}`, label: '去回填', code }
} }
if (issue.includes('无积分榜')) { if (issue.includes('无积分榜')) {
return { href: `/admin/collection?task=standings&league=${code}`, label: '去采集' } return { href: `/admin/collection?task=standings&league=${code}`, label: '去采集', code }
} }
return null return null
} }
@@ -54,6 +54,8 @@ export default function DataCompletenessPage() {
const [data, setData] = useState<DataCompletenessResponse | null>(null) const [data, setData] = useState<DataCompletenessResponse | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [highlightedLeague, setHighlightedLeague] = useState<string | null>(null)
const leagueRefs = useRef<Record<string, HTMLDivElement | null>>({})
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true) setLoading(true)
@@ -70,6 +72,14 @@ export default function DataCompletenessPage() {
useEffect(() => { load() }, [load]) useEffect(() => { load() }, [load])
// 点击问题项 → 滚动到对应联赛卡片并高亮
const scrollToLeague = useCallback((code: string) => {
setHighlightedLeague(code)
leagueRefs.current[code]?.scrollIntoView({ behavior: 'smooth', block: 'center' })
// 3秒后移除高亮
setTimeout(() => setHighlightedLeague(null), 3000)
}, [])
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<SectionHeader <SectionHeader
@@ -128,9 +138,17 @@ export default function DataCompletenessPage() {
title={issue.includes('良好') ? '数据良好' : '需要关注'} title={issue.includes('良好') ? '数据良好' : '需要关注'}
message={issue} message={issue}
action={action ? ( action={action ? (
<a href={action.href} className="btn btn-sm whitespace-nowrap"> <div className="flex items-center gap-2">
{action.label} <button
</a> onClick={() => scrollToLeague(action.code)}
className="btn btn-sm whitespace-nowrap"
>
</button>
<a href={action.href} className="btn btn-sm whitespace-nowrap">
{action.label}
</a>
</div>
) : undefined} ) : undefined}
/> />
) )
@@ -144,8 +162,14 @@ export default function DataCompletenessPage() {
{data.leagues.map(league => { {data.leagues.map(league => {
const finished = league.matches.finished const finished = league.matches.finished
const statsPct = finished > 0 ? Math.round((league.stats.rows / finished) * 100) : 0 const statsPct = finished > 0 ? Math.round((league.stats.rows / finished) * 100) : 0
const isHighlighted = highlightedLeague === league.code
return ( return (
<Card key={league.code}> <div
key={league.code}
ref={el => { leagueRefs.current[league.code] = el }}
className={`transition-all duration-500 ${isHighlighted ? 'ring-2 ring-press scale-[1.01]' : ''}`}
>
<Card>
<CardHeader <CardHeader
title={league.name} title={league.name}
description={[ description={[
@@ -196,6 +220,7 @@ export default function DataCompletenessPage() {
)} )}
</CardBody> </CardBody>
</Card> </Card>
</div>
) )
})} })}
</div> </div>