)}
{!loading && groupByDate(matches).map(([dateKey, group]) => (
- {/* 日期分组头 */}
-
- {formatDateHeader(dateKey)}
· {group.length} 场
+ {/* 日期分组头:sticky 但 z 低于弹窗 z-50 */}
+
+ {formatDateHeader(dateKey)}
+ {group.length} 场
{group.map(m => {
const st = STATUS_META[m.match_status] ?? { label: m.match_status, cls: 'text-ink-400' }
@@ -503,7 +502,7 @@ export default function Matches() {
{/* 行:可点击展开 */}
{ if (e.key === 'Enter') toggleExpand() }}
aria-expanded={expanded}
>
- {/* 小屏:日期+状态行;桌面:日期单独一列 */}
-
- {fmtDate(m.match_date)}
- {st.label}
-
+ {/* 桌面 grid: 日期 | 主队 | 比分 | 客队 | 状态 | 按钮 */}
+
+ {/* 日期 + 状态:小屏同行;桌面 date 单独一列 */}
+
+ {fmtTime(m.match_date)}
+ {st.label}
+
- {/* 对阵行:小屏主队(弹性)/比分/客队(弹性)三格;桌面 sm:contents 走 grid */}
-
-
+ {/* 主队(右对齐) */}
+
- {homeName}
+ {homeName}
-
+
+ {/* 比分 / VS:桌面上更突出 */}
+
{m.home_goals !== null && m.away_goals !== null ? (
-
- {m.home_goals}:{m.away_goals}
+
+ {m.home_goals}:{m.away_goals}
) : (
- VS
+ VS
)}
- {m.home_xg !== null && m.away_xg != null && (
-
- xG {m.home_xg.toFixed(1)}-{m.away_xg.toFixed(1)}
+ {m.home_xg !== null && m.away_xg !== null && (
+
+ xG {m.home_xg.toFixed(1)}–{m.away_xg.toFixed(1)}
)}
-
+
+ {/* 客队(左对齐) */}
+
- {awayName}
+ {awayName}
-
- {/* 预测按钮:小屏独占一行(桌面端 sm:contents 下隐藏) */}
- {!finished && (
-
e.stopPropagation()}>
-
-
- )}
+ {/* 状态标签:小屏隐藏(已有);桌面用徽标样式 */}
+
+
+ {st.label}
+
+
- {/* 桌面端按钮(小屏隐藏) */}
-
{st.label}
-
e.stopPropagation()}>
+ {/* 预测按钮:小屏独占一行 */}
{!finished && (
-
+
e.stopPropagation()}>
+
+
)}
-
-
{/* 关闭可点击行(clickable row) */}
- {/* 展开详情面板(只读数据 + 预测按钮 + 历史预测 + 专家报告入口) */}
+ {/* 预测按钮:桌面 */}
+
e.stopPropagation()}>
+ {!finished && (
+
+ )}
+
+
+
{/* 关闭可点击行 */}
+
+ {/* 展开详情面板 */}
{expanded && (
)}
@@ -589,9 +604,9 @@ export default function Matches() {
))}
{!loading && nextCursor && (
-
-
@@ -1128,7 +1131,7 @@ function MatchDetailPanel({
{detail?.recent_predictions?.length ? (
{detail.recent_predictions.map(p => (
-
+
))}
) : (
@@ -1149,7 +1152,7 @@ function RecentBlock({ title, rows, side }: { title: string; rows?: TeamRecentMa
{rows && rows.length > 0 ? (
{rows.map((r, i) => {
- const date = r.match_date ? r.match_date.slice(5, 10) : '—'
+ const date = r.match_date ? new Date(r.match_date).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) : '—'
const score = (r.home_goals != null && r.away_goals != null) ? `${r.home_goals}-${r.away_goals}` : 'vs'
const label = side === 'h2h'
? `${r.home_team ?? '?'} ${score} ${r.away_team ?? '?'}`
@@ -1170,7 +1173,7 @@ function RecentBlock({ title, rows, side }: { title: string; rows?: TeamRecentMa
}
/** 历史预测单行(含专家报告入口) */
-function PredictionHistoryRow({ p, mode }: { p: MatchRecentPrediction; mode: 'single' | 'multi' }) {
+function PredictionHistoryRow({ p }: { p: MatchRecentPrediction }) {
const badge = p.status === 'degraded'
? { label: 'degraded', cls: 'text-press' }
: p.settled
@@ -1181,7 +1184,7 @@ function PredictionHistoryRow({ p, mode }: { p: MatchRecentPrediction; mode: 'si
: '—'
const alt = (p.alt_pred_home_goals != null && p.alt_pred_away_goals != null)
? `${p.alt_pred_home_goals.toFixed(1)}-${p.alt_pred_away_goals.toFixed(1)}` : null
- const hasAgents = mode === 'multi' && p.agent_outputs && p.agent_outputs.length > 0
+ const hasAgents = p.agent_outputs && p.agent_outputs.length > 0
return (
@@ -1198,7 +1201,7 @@ function PredictionHistoryRow({ p, mode }: { p: MatchRecentPrediction; mode: 'si
- {p.model} · {p.mode} · {p.created_at?.slice(0, 16).replace('T', ' ') ?? '—'}
+ {p.model} · {p.mode} · {p.created_at ? new Date(p.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : '—'}
{hasAgents && {p.agent_outputs!.length} 路专家报告}
{p.reasoning && (
@@ -1220,7 +1223,7 @@ function readablePredictError(e: unknown): string {
if (/402|Payment Required|额度|余额/.test(m)) return 'LLM 额度不足(402),请检查 API Key 余额'
if (/400|已完赛/.test(m)) return '该比赛已完赛,不再支持预测'
if (/409|已结算/.test(m)) return '该预测已结算,不能重新预测'
- if (/timeout|超时|timed out/i.test(m)) return '请求超时,请稍后重试或改用单次模式'
+ if (/timeout|超时|timed out/i.test(m)) return '请求超时,请稍后重试'
return m
}
return String(e)
diff --git a/tests/ui_audit.py b/tests/ui_audit.py
new file mode 100644
index 0000000..ad21feb
--- /dev/null
+++ b/tests/ui_audit.py
@@ -0,0 +1,34 @@
+"""前端 UI 审查清单(逐页排查结果)。
+
+发现的问题按影响排序,修复按任务范围执行。
+"""
+
+# ── 高影响(必修) ──────────────────────────────────────────────
+
+ISSUES_HIGH = [
+ # 1. 比赛列表筛选栏:小屏4组筛选换行混乱,日期 input 与文字标签不对齐
+ # 2. 比赛行:主/客队名在小屏截断过重,比分列固定 w-14 太窄
+ # 3. 预测弹窗:小屏内容溢出视口(无 max-height + overflow-y)
+ # 4. 弹窗关闭按钮 h-7 w-7 触控目标太小(应 ≥44px)
+ # 5. sticky 日期分组头 z-10 与弹窗 z-50 冲突(弹窗打开时分组头覆盖其上)
+ # 6. 预测按钮小屏 min-h-[44px] 但桌面 btn-sm min-h-[36px] 仍偏小
+ # 7. "载入更多"按钮在小屏不够醒目,用户不知道还有更多数据
+]
+
+# ── 中等(应修) ──────────────────────────────────────────────
+
+ISSUES_MEDIUM = [
+ # 8. 空态不统一:有的用 EmptyState,有的用 ,有的只有 "暂无"
+ # 9. 主页 error banner 与 admin Alert 组件重复实现,样式不同
+ # 10. Dashboard 空态文案("请先到数据采集导入")没有链接到采集页
+ # 11. 侧边栏小屏汉堡菜单图标和文字重叠风险
+ # 12. 预测结果弹窗的 reasoning 文字没有限制最大高度,长文撑满视口
+]
+
+# ── 低(可选) ──────────────────────────────────────────────
+
+ISSUES_LOW = [
+ # 13. "管理后台"链接的 icon ⚠ 和 nav 图标风格不一致
+ # 14. footer 文字在小屏可能被弹窗遮挡(z-index 层级)
+ # 15. 回测结果导出 CSV 按钮在移动端显示位置不明确
+]