fix(frontend): 按审计报告修复全部17项问题,数据层迁至 src/api/
P0(4): 假数据清除(avg_latency_ms:2400→null,无端点字段改null)、假进度条改诚实的不确定态、 公共页不再反向依赖 admin(api/public.ts)、PredictProgress 重写 P1(6): 23处 any 归零(对齐后端 Pydantic 契约新增 PredictionMatchRef/HealthProbe 等)、 a11y(aria-live 0→4,htmlFor 1→17,aria-describedby/invalid 补齐)、App.tsx 抽 SiteLayout、 路由级 lazy+代码分割(首屏 315KB→238KB)、index.html 补 SEO/favicon/OG、死代码清理(AdminIcon 抽出) P2(7): Login/index.css 裸色值令牌化、groupByDate useMemo、滚动监听统一、原生控件基元化、 useLeagues 静默失败补告警、路由级 ErrorBoundary 另修复审计未列问题: - Monitoring todos 过滤器 t!==false 放行 null 导致整页崩溃 → Boolean(t) 真值过滤 - Collection 渲染期 Date.now()(react-hooks/purity 捕获)→ 计时器 effect - bg-press-wash/60 透明度修饰符静默失效 → RGB 三元组 + 构建期令牌守卫(下个提交接入) 工程化:数据层 dal/api/types(1047行)git mv 至 src/api/,admin 留 @deprecated 兼容壳, 21 个引用方直指新路径;tsconfig 开启 noUnusedLocals/noUnusedParameters(清理9处存量)
This commit is contained in:
@@ -11,10 +11,11 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { triggerCollection, fetchLeagues, fetchIngestJob, fetchIngestJobs } from '../dal'
|
||||
import type { IngestJob, League } from '../types'
|
||||
import type { CollectionRequest } from '../types'
|
||||
import { triggerCollection, fetchLeagues, fetchIngestJob, fetchIngestJobs } from '../../api/dal'
|
||||
import type { IngestJob, League } from '../../api/types'
|
||||
import type { CollectionRequest } from '../../api/types'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components'
|
||||
import { Button, Input, Select } from '../../components/ui'
|
||||
|
||||
// 图标用与全站一致的几何字符(Dashboard 工作流卡同款),不混用 emoji
|
||||
const TASKS = [
|
||||
@@ -74,6 +75,22 @@ export default function CollectionPage() {
|
||||
|
||||
useEffect(() => () => stopPolling(), [stopPolling])
|
||||
|
||||
// ── 已运行时长 ──
|
||||
// 必须放在 state 里、由定时器推进,而不是在 render 里读 Date.now()。
|
||||
// 后者是「渲染期间的副作用」:同一份 props/state 会渲染出不同结果,
|
||||
// React 的并发特性(以及未来的编译器优化)都依赖渲染幂等。
|
||||
// 采集任务在跑时每秒推进一次,既给出真实的时长,也不制造额外重渲染。
|
||||
const [elapsedSec, setElapsedSec] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskStartedAt) { setElapsedSec(0); return }
|
||||
if (taskStatus !== 'running') return
|
||||
const id = setInterval(() => {
|
||||
setElapsedSec(Math.round((Date.now() - taskStartedAt) / 1000))
|
||||
}, 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [taskStartedAt, taskStatus])
|
||||
|
||||
// ── 最近任务历史(GET /admin/ingest/jobs,最新在前) ──
|
||||
// 声明须在 startJobPolling 之前(其终态回调会刷新历史)
|
||||
const [recentJobs, setRecentJobs] = useState<IngestJob[] | null>(null)
|
||||
@@ -174,7 +191,7 @@ export default function CollectionPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = taskStartedAt ? Math.round((Date.now() - taskStartedAt) / 1000) : 0
|
||||
const elapsed = elapsedSec
|
||||
const summary = jobInfo ? jobSummary(jobInfo) : null
|
||||
|
||||
return (
|
||||
@@ -214,51 +231,53 @@ export default function CollectionPage() {
|
||||
|
||||
{/* 联赛选择 */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">联赛</label>
|
||||
<select
|
||||
<label className="mb-1.5 block text-xs text-ink-500" htmlFor="col-league">联赛</label>
|
||||
<Select
|
||||
id="col-league"
|
||||
value={leagueCode}
|
||||
onChange={e => setLeagueCode(e.target.value)}
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">全部联赛</option>
|
||||
{leagues.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name_zh || l.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* events/all 任务专用: 比赛状态 + 日期 */}
|
||||
{isEventsTask && (
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">比赛状态</label>
|
||||
<select
|
||||
<label className="mb-1.5 block text-xs text-ink-500" htmlFor="col-status">比赛状态</label>
|
||||
<Select
|
||||
id="col-status"
|
||||
value={ingestStatus}
|
||||
onChange={e => setIngestStatus(e.target.value)}
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
>
|
||||
<option value="">全部(已完赛 + 未开赛)</option>
|
||||
<option value="finished">仅已完赛</option>
|
||||
<option value="scheduled">仅未开赛</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">起始日期</label>
|
||||
<input
|
||||
<label className="mb-1.5 block text-xs text-ink-500" htmlFor="col-date-from">起始日期</label>
|
||||
<Input id="col-date-from"
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={e => setDateFrom(e.target.value)}
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">结束日期</label>
|
||||
<input
|
||||
<label className="mb-1.5 block text-xs text-ink-500" htmlFor="col-date-to">结束日期</label>
|
||||
<Input id="col-date-to"
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={e => setDateTo(e.target.value)}
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,13 +287,13 @@ export default function CollectionPage() {
|
||||
{/* standings 任务专用: 赛季 */}
|
||||
{(task === 'standings') && (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">赛季(留空取当前赛季)</label>
|
||||
<input
|
||||
<label className="mb-1.5 block text-xs text-ink-500" htmlFor="col-season">赛季(留空取当前赛季)</label>
|
||||
<Input id="col-season"
|
||||
type="text"
|
||||
value={season}
|
||||
onChange={e => setSeason(e.target.value)}
|
||||
placeholder="如 2026-2027"
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -283,13 +302,13 @@ export default function CollectionPage() {
|
||||
{(task === 'stats') && (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs text-ink-500">单次最大回填比赛数(1-500)</label>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={500}
|
||||
value={limit}
|
||||
onChange={e => setLimit(parseInt(e.target.value) || 100)}
|
||||
className="field w-full"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-2xs text-ink-400">
|
||||
仅回填已有 source_event_id 且无统计的比赛(增量),上游限速约 1.2 秒/次。
|
||||
@@ -308,9 +327,9 @@ export default function CollectionPage() {
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<button type="submit" disabled={loading} className="btn btn-solid w-full">
|
||||
<Button variant="solid" className="w-full" type="submit" disabled={loading}>
|
||||
{loading ? (<><Spinner /> 采集中</>) : '触发采集'}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -366,7 +385,7 @@ export default function CollectionPage() {
|
||||
<CardHeader
|
||||
title="最近任务"
|
||||
description="后台采集任务执行历史(最新在前)"
|
||||
action={<button onClick={loadRecentJobs} className="btn btn-sm">刷新</button>}
|
||||
action={<Button size="sm" onClick={loadRecentJobs}>刷新</Button>}
|
||||
/>
|
||||
<CardBody className="px-0">
|
||||
{recentJobs === null ? (
|
||||
|
||||
Reference in New Issue
Block a user