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:
2026-09-22 23:45:06 +08:00
parent 2cfc9ccc0f
commit f71f29b4cb
42 changed files with 2553 additions and 1629 deletions
+40 -44
View File
@@ -15,14 +15,15 @@ import {
testLLMConnection, fetchLLMUsageStats, fetchLLMModels,
fetchKeyRingStatus, resetKeyRingCooldown,
fetchSchedules, createSchedule, updateSchedule, deleteSchedule, runScheduleNow,
} from '../dal'
import type { ScheduleItem } from '../dal'
import { changePassword, fetchAuthState, UNAUTHORIZED_EVENT } from '../api'
import type { LLMUsageStats, DataSourceSetting } from '../types'
import type { KeyRingStatusResponse } from '../dal'
} from '../../api/dal'
import type { ScheduleItem } from '../../api/dal'
import { changePassword, fetchAuthState, UNAUTHORIZED_EVENT } from '../../api/api'
import type { LLMUsageStats, DataSourceSetting } from '../../api/types'
import type { KeyRingStatusResponse } from '../../api/dal'
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components'
import SettingRow from '../SettingRow'
import AgentLLMCard from '../AgentLLMCard'
import { Button, Input } from '../../components/ui'
const DATA_SOURCE_KEYS = ['BZZOIRO_KEY', 'BZZOIRO_BASE']
const LLM_KEYS = ['LLM_API_KEY', 'LLM_BASE_URL', 'LLM_MODEL']
@@ -277,7 +278,7 @@ export default function SettingsPage() {
<CardHeader
title="Bzzoiro API"
description="赛程 / 比分 / 积分榜 / 比赛统计的唯一数据源"
action={<button onClick={loadSettings} disabled={settingsLoading} className="btn btn-sm">{settingsLoading ? <><Spinner /> </> : '刷新'}</button>}
action={<Button size="sm" onClick={loadSettings} disabled={settingsLoading}>{settingsLoading ? <><Spinner /> </> : '刷新'}</Button>}
/>
<CardBody>
{settingsLoading ? (
@@ -307,7 +308,7 @@ export default function SettingsPage() {
<CardHeader
title="API Key 轮换环"
description={keyRing?.has_multiple ? `已配置 ${keyRing.total} 个 key,遇限流自动切换` : '当前仅 1 个 key,无法轮换'}
action={<button onClick={handleResetCooldown} disabled={ringLoading} className="btn-sm btn-outline"></button>}
action={<Button variant="outline" size="sm" onClick={handleResetCooldown} disabled={ringLoading}></Button>}
/>
<CardBody>
{keyRing && keyRing.total > 0 ? (
@@ -345,7 +346,7 @@ export default function SettingsPage() {
<CardHeader
title="连接配置"
description="OpenAI 兼容接口(DeepSeek / 智谱 / 通义等)"
action={<button onClick={loadSettings} disabled={settingsLoading} className="btn btn-sm">{settingsLoading ? <><Spinner /> </> : '刷新'}</button>}
action={<Button size="sm" onClick={loadSettings} disabled={settingsLoading}>{settingsLoading ? <><Spinner /> </> : '刷新'}</Button>}
/>
<CardBody>
{settingsLoading ? (
@@ -373,15 +374,15 @@ export default function SettingsPage() {
<Alert kind={testResult.success ? 'ok' : 'error'} title={testResult.success ? '连接正常' : '连接失败'} message={testResult.success ? undefined : testResult.message} />
</div>
)}
<button onClick={handleTest} disabled={testing} className="btn btn-sm mt-4 w-full">
<Button size="sm" className="mt-4 w-full" onClick={handleTest} disabled={testing}>
{testing ? <><Spinner /> </> : '测试 LLM 连接'}
</button>
</Button>
<p className="mt-2 text-center text-2xs text-ink-400"> LLM ,</p>
</CardBody>
</Card>
<Card>
<CardHeader title="使用统计" description="从最近预测记录聚合" action={<button onClick={loadLlmStats} disabled={llmLoading} className="btn btn-sm">{llmLoading ? <><Spinner /> </> : '刷新'}</button>} />
<CardHeader title="使用统计" description="从最近预测记录聚合" action={<Button size="sm" onClick={loadLlmStats} disabled={llmLoading}>{llmLoading ? <><Spinner /> </> : '刷新'}</Button>} />
<CardBody>
{llmLoading ? (
<div className="space-y-3"><SkeletonBlock className="h-16 w-full" /><SkeletonBlock className="h-16 w-full" /></div>
@@ -392,8 +393,16 @@ export default function SettingsPage() {
<div className="mt-1 text-2xs text-ink-400"></div>
</div>
<div className="border-t-2 border-ink-900 pt-3 text-center">
<div className="font-serif text-2xl font-bold tabular-nums text-ink-900">{llmStats.avg_latency_ms > 0 ? `${(llmStats.avg_latency_ms / 1000).toFixed(1)}s` : '—'}</div>
<div className="mt-1 text-2xs text-ink-400"></div>
{/* 延迟统计后端未接入 → 明确显示「未接入」而非留白,
免得被误读成「延迟为 0,性能极好」 */}
<div className="font-serif text-2xl font-bold tabular-nums text-ink-900">
{llmStats.avg_latency_ms != null
? `${(llmStats.avg_latency_ms / 1000).toFixed(1)}s`
: '—'}
</div>
<div className="mt-1 text-2xs text-ink-400">
{llmStats.avg_latency_ms != null ? '平均延迟' : '平均延迟(未接入)'}
</div>
</div>
<div className="border-t-2 border-press pt-3 text-center">
<div className="font-serif text-2xl font-bold tabular-nums text-press">{llmStats.success_rate.toFixed(0)}%</div>
@@ -429,24 +438,24 @@ export default function SettingsPage() {
<form onSubmit={handleChangePassword} className="space-y-3">
<div className="grid gap-3 sm:grid-cols-3">
<div>
<label className="mb-1 block text-2xs text-ink-500"></label>
<input type="password" value={currentPwd} onChange={e => setCurrentPwd(e.target.value)} autoComplete="current-password" className="field w-full" />
<label htmlFor="pwd-current" className="mb-1 block text-2xs text-ink-500"></label>
<Input id="pwd-current" type="password" value={currentPwd} onChange={e => setCurrentPwd(e.target.value)} autoComplete="current-password" className="w-full" />
</div>
<div>
<label className="mb-1 block text-2xs text-ink-500">( 8 )</label>
<input type="password" value={newPwd} onChange={e => setNewPwd(e.target.value)} autoComplete="new-password" className="field w-full" />
<label htmlFor="pwd-new" className="mb-1 block text-2xs text-ink-500">( 8 )</label>
<Input id="pwd-new" type="password" value={newPwd} onChange={e => setNewPwd(e.target.value)} autoComplete="new-password" className="w-full" />
</div>
<div>
<label className="mb-1 block text-2xs text-ink-500"></label>
<input type="password" value={confirmPwd} onChange={e => setConfirmPwd(e.target.value)} autoComplete="new-password" className="field w-full" />
<label htmlFor="pwd-confirm" className="mb-1 block text-2xs text-ink-500"></label>
<Input id="pwd-confirm" type="password" value={confirmPwd} onChange={e => setConfirmPwd(e.target.value)} autoComplete="new-password" className="w-full" />
</div>
</div>
{pwdNotice && <Alert kind={pwdNotice.ok ? 'ok' : 'error'} title={pwdNotice.text} />}
<div className="flex items-center justify-between gap-2">
<p className="text-2xs text-ink-400">退</p>
<button type="submit" disabled={pwdBusy || !currentPwd || !newPwd || !confirmPwd} className="btn btn-solid btn-sm flex-shrink-0">
<Button variant="solid" size="sm" className="flex-shrink-0" type="submit" disabled={pwdBusy || !currentPwd || !newPwd || !confirmPwd}>
{pwdBusy ? <><Spinner /> </> : '修改密码'}
</button>
</Button>
</div>
</form>
</CardBody>
@@ -462,15 +471,9 @@ export default function SettingsPage() {
title="采集调度"
description="配置 cron 表达式定时触发采集任务"
action={
<button
onClick={() => {
createSchedule({ id: `schedule-${Date.now()}`, task: 'events', cron: '0 8 * * *', leagues: undefined, enabled: false })
.then(() => fetchSchedules().then(setSchedules))
}}
className="btn btn-sm"
>
<Button size="sm" onClick={() => { createSchedule({ id: `schedule-${Date.now()}`, task: 'events', cron: '0 8 * * *', leagues: undefined, enabled: false }) .then(() => fetchSchedules().then(setSchedules)) }}>
+
</button>
</Button>
}
/>
<CardBody>
@@ -502,27 +505,20 @@ export default function SettingsPage() {
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleRunSchedule(s)}
disabled={scheduleBusyId === s.id}
className="btn btn-sm"
>
<Button size="sm" onClick={() => handleRunSchedule(s)} disabled={scheduleBusyId === s.id}>
{scheduleBusyId === s.id ? <Spinner /> : '立即执行'}
</button>
<button
</Button>
<Button
onClick={() => handleToggleSchedule(s)}
disabled={scheduleBusyId === s.id}
className={`btn btn-sm ${s.enabled ? '' : 'btn-solid'}`}
variant={s.enabled ? 'default' : 'solid'}
size="sm"
>
{scheduleBusyId === s.id ? <Spinner /> : (s.enabled ? '禁用' : '启用')}
</button>
<button
onClick={() => handleDeleteSchedule(s)}
disabled={scheduleBusyId === s.id}
className="btn btn-sm btn-danger"
>
</Button>
<Button variant="danger" size="sm" onClick={() => handleDeleteSchedule(s)} disabled={scheduleBusyId === s.id}>
{scheduleBusyId === s.id ? <Spinner /> : '删除'}
</button>
</Button>
</div>
</div>
))}