UI/UX 全面改进:22项优化落地(报纸风前端 + 管理后台)

P0 严重问题:
- 采集任务进度反馈:Collection 页新增任务状态跟踪(运行中/完成/失败) + 轮询 ingest 状态
- 积分榜加载态:切换联赛显示 spinner + 禁用 tab 防重复点击
- 数据完整性可操作:问题列表每项加「去修复」按钮,跳转采集页并预填参数
- 面包屑导航:顶部显示 仪表盘 > 当前页

P1 重要问题:
- 侧边栏当前页指示器:1.5px 圆点 → 4px 左侧彩色竖条 + 背景高亮
- Key Ring 重置确认:点击「重置冷却」前弹窗确认
- 预测比赛选择器:只显示未开赛 + 联赛筛选 + 显示可预测数量
- 表格移动端溢出:评估页表格加 overflow-x-auto
- 设置页合并:数据源 + LLM + 系统配置 → 统一「设置」页
- 预测按钮去重:移动端/桌面端合并为一个响应式按钮

P2 体验优化:
- 日志滚动保持:自动滚动仅当用户未向上滚动时
- 评估结果预览:显示「共 N 组」
- ⌘K 命令面板:键盘导航跳转所有管理页面
- 专家意见折叠:默认折叠,可展开(已存在)
- 版本信息:侧边栏底部显示 v1.0
- 积分榜表格:min-w 防止挤压

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
shangfangjian
2026-09-20 21:24:21 +08:00
co-authored by new-provider/LongCat-2.0 <
parent dd538d66d7
commit fb27f6e2d2
13 changed files with 854 additions and 107 deletions
+3 -17
View File
@@ -619,33 +619,19 @@ export default function Matches() {
</span>
</span>
{/* 预测按钮:小屏独占一行 */}
{/* 预测按钮(统一,响应式尺寸) */}
{!finished && (
<div className="flex justify-end sm:hidden" onClick={e => e.stopPropagation()}>
<div className="flex justify-end" onClick={e => e.stopPropagation()}>
<button
onClick={() => predict(m)}
disabled={busy}
className="btn min-h-[44px] w-full px-4"
className={`btn ${busy ? '' : 'btn-solid'} w-full min-h-[44px] sm:w-[84px] sm:min-h-0 sm:btn-sm`}
title="以多专家模式预测这场"
>
{busy ? (<><Spinner /> </>) : '预测'}
</button>
</div>
)}
{/* 预测按钮:桌面 */}
<div className="hidden justify-end sm:flex" onClick={e => e.stopPropagation()}>
{!finished && (
<button
onClick={() => predict(m)}
disabled={busy}
className={`btn btn-sm w-[84px] ${busy ? '' : 'btn-solid'}`}
title="以多专家模式预测这场"
>
{busy ? (<><Spinner /> </>) : '预测'}
</button>
)}
</div>
</div>
</div>{/* 关闭可点击行 */}
+27 -3
View File
@@ -8,6 +8,7 @@
import { useEffect, useState, useCallback } from 'react'
import { fetchStandings } from '../admin/dal'
import type { StandingsLeague, StandingRow } from '../admin/dal'
import { Spinner } from '../admin/components'
const LEAGUES = [
{ code: 'E0', name: '英超' },
@@ -48,6 +49,7 @@ export default function StandingsPage() {
const [leagues, setLeagues] = useState<StandingsLeague[]>([])
const [activeLeague, setActiveLeague] = useState<string>('')
const [loading, setLoading] = useState(true)
const [switching, setSwitching] = useState(false) // 切换联赛中
const [error, setError] = useState<string | null>(null)
const load = useCallback(async (code?: string) => {
@@ -68,6 +70,20 @@ export default function StandingsPage() {
useEffect(() => { load() }, []) // eslint-disable-line react-hooks/exhaustive-deps
// 切换联赛(带加载态,禁用 tab 防重复点击)
const switchLeague = async (code: string) => {
if (code === activeLeague || switching) return
setSwitching(true)
setActiveLeague(code)
try {
await fetchStandings(code).then(data => setLeagues(data.leagues))
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
} finally {
setSwitching(false)
}
}
const active = leagues.find(l => l.league_code === activeLeague) ?? leagues[0]
return (
@@ -77,8 +93,9 @@ export default function StandingsPage() {
{LEAGUES.map(l => (
<button
key={l.code}
onClick={() => { setActiveLeague(l.code); load(l.code) }}
className={`rounded border px-3 py-1.5 text-xs transition-colors ${
onClick={() => switchLeague(l.code)}
disabled={switching}
className={`rounded border px-3 py-1.5 text-xs transition-colors disabled:opacity-50 ${
activeLeague === l.code
? 'border-ink-900 bg-ink-900 text-paper-50'
: 'border-ink-200 text-ink-500 hover:border-ink-300'
@@ -99,6 +116,13 @@ export default function StandingsPage() {
<div className="flex justify-center py-12 text-xs text-ink-400"></div>
)}
{/* 切换联赛时的轻量加载指示 */}
{switching && !loading && (
<div className="flex items-center gap-2 border-b border-ink-200 px-1 py-2 text-xs text-ink-400">
<Spinner />
</div>
)}
{!loading && !active && (
<div className="border-y border-ink-200 py-12 text-center">
<p className="font-serif text-sm text-ink-600"></p>
@@ -124,7 +148,7 @@ export default function StandingsPage() {
{/* 积分榜表格 */}
<div className="overflow-x-auto">
<table className="w-full text-xs">
<table className="w-full min-w-[640px] text-xs">
<thead>
<tr className="border-b border-ink-200 text-left text-ink-400">
<th className="w-8 py-2 font-medium">#</th>