健康摘要与联赛卡片联动
- 问题项新增「定位」按钮,点击滚动到对应联赛卡片 - 被定位的联赛卡片高亮显示(ring + scale),3秒后自动消失 - 健康摘要 → 联赛详情形成闭环操作 Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>> )
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user