diff --git a/.scratch/ui-consistency/issues/01-baseline-components.md b/.scratch/ui-consistency/issues/01-baseline-components.md
new file mode 100644
index 0000000..bb48169
--- /dev/null
+++ b/.scratch/ui-consistency/issues/01-baseline-components.md
@@ -0,0 +1,14 @@
+# 01 — 基准组件沉淀 + IntelligentEvals 切换
+
+**What to build:** 抽取两个共享件并让基准页成为第一个消费者。
+- `components/FormDrawer.tsx`:标准表单 Drawer(width 640 / footer 取消+主按钮 / destroyOnClose / submitting 态)
+- `hooks/usePolling.ts`:活跃态静默轮询(5s,组件卸载/条件关闭自动清理)
+
+**Blocked by:** 无
+
+**Acceptance criteria:**
+- [ ] FormDrawer 与 usePolling 创建完成,props/hook 签名简洁
+- [ ] IntelligentEvals 的创建 Drawer 改用 FormDrawer,行为不变
+- [ ] IntelligentEvals 详情页轮询与 EvalDetail 会话轮询改用 usePolling,行为不变
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:创建评估、详情轮询、P0 全流程体验无回归
diff --git a/.scratch/ui-consistency/issues/02-targets.md b/.scratch/ui-consistency/issues/02-targets.md
new file mode 100644
index 0000000..0297eda
--- /dev/null
+++ b/.scratch/ui-consistency/issues/02-targets.md
@@ -0,0 +1,13 @@
+# 02 — Targets 页迁移
+
+**What to build:** 评测对象页对齐交互标准:PageWrapper 骨架复核、创建/编辑表单统一 FormDrawer、删除 Popconfirm 复核、空态 Empty、分页 20 行阈值。
+
+**Blocked by:** 01
+
+**Acceptance criteria:**
+- [ ] 表单容器为 FormDrawer(创建与编辑统一)
+- [ ] 删除有 Popconfirm 确认,连通测试按钮行为不变
+- [ ] 列表空态 Empty、加载用 Table loading、20 行分页阈值
+- [ ] 无 `window.confirm`、无绕过 tokens 的硬编码色值(迁移可见部分)
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:增删改查 + 连通测试 golden path
diff --git a/.scratch/ui-consistency/issues/03-files.md b/.scratch/ui-consistency/issues/03-files.md
new file mode 100644
index 0000000..b3fe324
--- /dev/null
+++ b/.scratch/ui-consistency/issues/03-files.md
@@ -0,0 +1,14 @@
+# 03 — Files 页迁移(检查点前最后一票)
+
+**What to build:** 原始文件页对齐交互标准;**重点修正:删除分类/文件补 Popconfirm 确认**(现状无确认)。
+
+**Blocked by:** 01
+
+**Acceptance criteria:**
+- [ ] 删除分类、删除文件均有 Popconfirm 确认(行为修正,记录在提交信息)
+- [ ] 上传/下载交互保持不变,外壳对齐 PageWrapper 标准
+- [ ] 空态 Empty(无分类/无文件时)
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:分类树 CRUD + 上传下载 golden path
+
+**检查点**:本票完成后暂停,评审 FormDrawer/usePolling API 与三页走查结果,用户确认后再继续 04–08。
diff --git a/.scratch/ui-consistency/issues/04-model-configs.md b/.scratch/ui-consistency/issues/04-model-configs.md
new file mode 100644
index 0000000..483e6f6
--- /dev/null
+++ b/.scratch/ui-consistency/issues/04-model-configs.md
@@ -0,0 +1,12 @@
+# 04 — ModelConfigs 页迁移
+
+**What to build:** 模型配置页对齐交互标准:表单统一 FormDrawer、删除 Popconfirm、空态、分页阈值;能力/模态约束的校验逻辑保持不变。
+
+**Blocked by:** 03(检查点通过后)
+
+**Acceptance criteria:**
+- [ ] 创建/编辑表单统一 FormDrawer
+- [ ] 删除有 Popconfirm;启用/停用开关行为不变
+- [ ] 空态 Empty、20 行分页阈值
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:模型配置 CRUD + 能力约束校验 golden path
diff --git a/.scratch/ui-consistency/issues/05-scenarios.md b/.scratch/ui-consistency/issues/05-scenarios.md
new file mode 100644
index 0000000..4958a9a
--- /dev/null
+++ b/.scratch/ui-consistency/issues/05-scenarios.md
@@ -0,0 +1,13 @@
+# 05 — Scenarios 页迁移
+
+**What to build:** 评测场景页对齐交互标准。Monaco 编辑器与版本对比等功能保持不变,只统一外壳与基础模式。
+
+**Blocked by:** 04
+
+**Acceptance criteria:**
+- [ ] 创建/编辑入口统一(表单 Drawer 或保留 Monaco 全屏编辑——以现有主模式为准,但容器样式对齐标准)
+- [ ] 删除有 Popconfirm;模板相关交互不变
+- [ ] 空态 Empty、20 行分页阈值
+- [ ] 现状 Modal 与 Drawer 混用处收敛(保留 Monaco 场景所需的 Modal)
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:场景 CRUD + 编辑 + 校验 golden path
diff --git a/.scratch/ui-consistency/issues/06-runs.md b/.scratch/ui-consistency/issues/06-runs.md
new file mode 100644
index 0000000..7cfae6b
--- /dev/null
+++ b/.scratch/ui-consistency/issues/06-runs.md
@@ -0,0 +1,12 @@
+# 06 — Runs 页迁移
+
+**What to build:** 评测执行页对齐交互标准。**护栏**:「导航内容分离」hack(nav-only-tabs 隐藏 content-holder、内容 div flex 控高)必须保留,只统一外壳(PageWrapper/标题区/加载态/空态)。
+
+**Blocked by:** 05
+
+**Acceptance criteria:**
+- [ ] WebSocket 实时会话、取消、重跑等功能零回归
+- [ ] 导航内容分离 hack 保留且有注释说明原因
+- [ ] 空态、加载态对齐标准;危险操作(取消)Popconfirm
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:run 列表 + 详情 + 实时进度 + 取消 golden path
diff --git a/.scratch/ui-consistency/issues/07-reports.md b/.scratch/ui-consistency/issues/07-reports.md
new file mode 100644
index 0000000..dcb82ac
--- /dev/null
+++ b/.scratch/ui-consistency/issues/07-reports.md
@@ -0,0 +1,12 @@
+# 07 — Reports 页迁移
+
+**What to build:** 评测报告页对齐交互标准。图表与对比区保持不变,只统一外壳与基础模式。
+
+**Blocked by:** 06
+
+**Acceptance criteria:**
+- [ ] 报告选择/切换、图表渲染零回归
+- [ ] PageWrapper 外壳、空态、加载态对齐标准
+- [ ] 分页 20 行阈值
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:报告页各区块 golden path
diff --git a/.scratch/ui-consistency/issues/08-campaigns.md b/.scratch/ui-consistency/issues/08-campaigns.md
new file mode 100644
index 0000000..a2bcb81
--- /dev/null
+++ b/.scratch/ui-consistency/issues/08-campaigns.md
@@ -0,0 +1,13 @@
+# 08 — Campaigns 页迁移(最后硬骨头)
+
+**What to build:** 评估活动页(1094 行)对齐交互标准。时间线、探索区、分析区等独特交互不做模式化改造,只统一页面外壳与基础模式(表单 Drawer、删除确认、空态、反馈)。
+
+**Blocked by:** 07
+
+**Acceptance criteria:**
+- [ ] 创建/编辑活动表单统一 FormDrawer
+- [ ] 删除/取消活动 Popconfirm;探索派发等独特交互不变
+- [ ] 空态 Empty、20 行分页阈值
+- [ ] 文件体量显著下降(重复模式收敛到共享件),不强制目标行数
+- [ ] `tsc --noEmit` 通过
+- [ ] t480 走查:活动 CRUD + 时间线 + 探索区 + 报告 golden path
diff --git a/.scratch/ui-consistency/issues/09-final-regression.md b/.scratch/ui-consistency/issues/09-final-regression.md
new file mode 100644
index 0000000..9de5f53
--- /dev/null
+++ b/.scratch/ui-consistency/issues/09-final-regression.md
@@ -0,0 +1,12 @@
+# 09 — 全量回归与收口
+
+**What to build:** 9 页全部迁移后的最终验收与文档收口。
+
+**Blocked by:** 08
+
+**Acceptance criteria:**
+- [ ] 全部 9 页 t480 逐页走查一遍(golden path + keep-alive 切 tab 状态不丢)
+- [ ] 基准页(IntelligentEvals)重走 P0 全流程确认无回归
+- [ ] `tsc --noEmit` + 后端 pytest 全绿
+- [ ] AGENTS.md 前端章节更新(新增交互标准说明、清理过时描述)
+- [ ] 本 spec 与票据验收项勾选归档
diff --git a/.scratch/ui-consistency/spec.md b/.scratch/ui-consistency/spec.md
new file mode 100644
index 0000000..8f0db22
--- /dev/null
+++ b/.scratch/ui-consistency/spec.md
@@ -0,0 +1,45 @@
+# UI 一致性重构 — Spec(grill 共识沉淀)
+
+日期:2026-08-05 | 状态:已确认,开始实施
+
+## 背景
+
+管理后台 9 个业务页面约 4000 行,交互模式不一致:Runs/Reports 不用 PageWrapper、表单容器混用 Drawer/Modal、Files 删除无确认、空态/加载态/反馈方式各页不同。智能评估 v1.0 交付后,其页面模式成为事实上的最新标准。
+
+## 决策清单(grill 结论)
+
+| # | 决策 | 结论 |
+|---|------|------|
+| 1 | 重构目标 | 交互层为主、视觉层顺带、信息架构不动 |
+| 2 | 一致性基准 | 以智能评估页为标准,其他页面向它对齐 |
+| 3 | 实施策略 | 先沉淀共享组件,再逐页迁移;每页一票一提交一验证 |
+| 4 | 范围 | 9 个业务页面;排除 Login、OpenClaw(AI 助手嵌入应用) |
+| 5 | 行为边界 | 交互对齐优先:不一致处按标准修正,不增删业务功能、不动 API |
+| 6 | 迁移顺序 | IntelligentEvals → Targets → Files → ModelConfigs → Scenarios → Runs → Reports → Campaigns |
+| 7 | 交互标准 | 见下表 |
+| 8 | 验收 | 每票 tsc + t480 走查 + keep-alive 不回归;前 3 票后组件稳定检查点;基准页重走 P0 全流程;最终全量走查 |
+
+## 交互标准(唯一基准)
+
+| 模式 | 标准 |
+|---|---|
+| 页面骨架 | `PageWrapper inline fullHeight`,标题 + 描述 + extra 区(刷新按钮 + 主操作按钮) |
+| 列表 | Table + `rowKey="id"`;空态用 `Empty`(带文案);加载用 Table 自带 loading |
+| 分页 | 超过 20 行才分页,否则 `pagination={false}` |
+| 表单 | Drawer 宽 640、`layout="vertical"`、底部 footer(取消/主按钮)、`destroyOnClose`、提交前本地校验 |
+| 详情 | 页面内部视图切换(不加新路由),顶部「返回 + 标题 + 状态 Tag + 动作按钮」 |
+| 危险操作 | 统一 `Popconfirm`(含删除、取消等不可逆动作),禁用 `window.confirm` 和无确认删除 |
+| 反馈 | 错误统一由 axios 拦截器弹 `message.error`;成功由动作函数显式 `message.success` |
+| 轮询 | 活跃态(外部会变化的状态)5 秒静默轮询 |
+
+## 实施风险与护栏
+
+1. **Runs/Reports 的「导航内容分离」**是绕过 AntD CSS-in-JS 冲突的 hack(见 AGENTS.md 陷阱 3),迁移时必须保留其行为,只统一外壳。
+2. **Campaigns 的独特交互**(时间线、探索区、分析区)不做模式化改造,只统一页面外壳与基础模式。
+3. **keep-alive 架构不动**:所有页面仍按顶级路径挂载,详情仍是页面内部状态。
+4. **只抽取 ≥2 个页面会用的共享件**,防止过度抽象。第一版只抽 `FormDrawer`(表单 Drawer 标准件)与 `usePolling`(活跃态轮询 hook)。
+5. 每票据:一个页面、一次提交、一次 t480 部署走查。
+
+## 票据规划
+
+见 `.scratch/ui-consistency/issues/`。依赖链:01 → 02/03(检查点)→ 04–08 → 09。
diff --git a/docs/adr/0005-ui-consistency-baseline.md b/docs/adr/0005-ui-consistency-baseline.md
new file mode 100644
index 0000000..820f3ca
--- /dev/null
+++ b/docs/adr/0005-ui-consistency-baseline.md
@@ -0,0 +1,23 @@
+# ADR-0005: 管理后台 UI 一致性重构以智能评估页为基准
+
+## 状态
+
+已接受(2026-08-05)
+
+## 背景
+
+管理后台 9 个业务页面交互模式不一致(表单容器 Drawer/Modal 混用、空态/加载态/删除确认各页不同、Runs/Reports 绕过 PageWrapper)。智能评估 v1.0 的页面模式是最新打磨并被用户验证过的。
+
+## 决策
+
+- 以智能评估页模式为唯一基准,逐页迁移对齐(先沉淀共享件 FormDrawer/usePolling,再按 IntelligentEvals → Targets → Files → ModelConfigs → Scenarios → Runs → Reports → Campaigns 顺序)。
+- 重构边界:只统一交互层,不增删业务功能、不动 API、不动信息架构与 keep-alive 路由。
+- 交互标准:PageWrapper 骨架、Table+Empty、20 行分页阈值、Drawer 640 表单、内部视图切换详情、Popconfirm 危险操作、message 反馈、活跃态 5s 轮询。
+
+## 后果
+
+- 正面:用户操作肌肉记忆可跨页面复用;新页面有明确模式可循;共享件减少重复代码。
+- 负面:迁移期间每页需人工走查验收;Runs/Reports 的特殊 hack 需小心保留。
+- 不做:视觉设计系统重做、导航重构、自动化视觉回归。
+
+详细 spec 见 `.scratch/ui-consistency/spec.md`。
diff --git a/frontend/web/src/components/FormDrawer.tsx b/frontend/web/src/components/FormDrawer.tsx
new file mode 100644
index 0000000..f9eba8b
--- /dev/null
+++ b/frontend/web/src/components/FormDrawer.tsx
@@ -0,0 +1,36 @@
+import { Button, Drawer, Space } from 'antd'
+import type { ReactNode } from 'react'
+
+interface FormDrawerProps {
+ title: string
+ open: boolean
+ onClose: () => void
+ onSubmit: () => void
+ submitting?: boolean
+ okText?: string
+ width?: number
+ children: ReactNode
+}
+
+/** 标准表单抽屉:宽 640、底部取消/主按钮、destroyOnClose。 */
+export default function FormDrawer({
+ title, open, onClose, onSubmit, submitting = false, okText = '确定', width = 640, children,
+}: FormDrawerProps) {
+ return (
+
+
+
+
+ }
+ >
+ {children}
+
+ )
+}
diff --git a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx
index 7faade1..3e8dd66 100644
--- a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx
+++ b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import {
Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message,
} from 'antd'
@@ -6,6 +6,7 @@ import { ArrowLeftOutlined, FileTextOutlined, StopOutlined } from '@ant-design/i
import { intelligentEvalsApi, type IntelligentEval, type IntelligentEvalSession } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime, shortDateTime } from '../../utils/date'
+import { usePolling } from '../../hooks/usePolling'
import { EVAL_STATUS, SESSION_STATUS } from './status'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
@@ -75,17 +76,18 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
const showSessions = ev.status === 'executing' || ev.status === 'completed'
+ const loadSessions = useCallback(() => {
+ intelligentEvalsApi.listSessions(ev.id)
+ .then((res) => setSessions(res.data.sessions))
+ .catch(() => undefined)
+ }, [ev.id])
+
useEffect(() => {
if (!showSessions) { setSessions(null); return }
- let cancelled = false
- const load = () => intelligentEvalsApi.listSessions(ev.id)
- .then((res) => { if (!cancelled) setSessions(res.data.sessions) })
- .catch(() => undefined)
- void load()
- if (ev.status !== 'executing') return () => { cancelled = true }
- const timer = setInterval(load, 5000)
- return () => { cancelled = true; clearInterval(timer) }
- }, [ev.id, ev.status, showSessions])
+ loadSessions()
+ }, [showSessions, loadSessions])
+
+ usePolling(loadSessions, 5000, showSessions && ev.status === 'executing')
const runAction = async (fn: () => Promise, okMsg: string) => {
setBusy(true)
diff --git a/frontend/web/src/pages/IntelligentEvals.tsx b/frontend/web/src/pages/IntelligentEvals.tsx
index 24b33f9..5ee9787 100644
--- a/frontend/web/src/pages/IntelligentEvals.tsx
+++ b/frontend/web/src/pages/IntelligentEvals.tsx
@@ -1,13 +1,15 @@
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
import {
- Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
+ Button, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
+import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import EvalDetail from '../components/intelligent_eval/EvalDetail'
import EvalReport from '../components/intelligent_eval/EvalReport'
import { EVAL_STATUS } from '../components/intelligent_eval/status'
+import { usePolling } from '../hooks/usePolling'
import { useResource } from '../hooks/useResource'
import {
intelligentEvalsApi, targetsApi,
@@ -53,11 +55,7 @@ export default function IntelligentEvalsPage() {
const pollActive = view === 'detail' && selected != null
&& (selected.status === 'planning' || selected.status === 'executing' || selected.status === 'pending_approval')
- useEffect(() => {
- if (!pollActive) return
- const timer = setInterval(() => void reloadSelected(true), 5000)
- return () => clearInterval(timer)
- }, [pollActive, reloadSelected])
+ usePolling(() => void reloadSelected(true), 5000, pollActive)
const targetName = (id: string) =>
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
@@ -189,20 +187,13 @@ export default function IntelligentEvalsPage() {
/>
- setCreateOpen(false)}
- width={640}
- destroyOnClose
- footer={
-
-
-
-
- }
+ onSubmit={submitCreate}
+ submitting={submitting}
+ okText="创建并开始规划"
>
-
+
)
}