Browse Source

docs: 前端去 AI 味改造 v2 实现计划(10 任务分解)

对应规格 2026-06-20-medtech-ui-revamp-v2-design.md。
任务分解:theme token → components 类 → MainLayout → LoginView →
MyOkrView(表格化+抽屉)→ ScoringEditView(三列对照)→ MyResultView
(删圆环)→ verify 脚本 → 构建部署 → 验收。

自检已修正关键数据结构假设:
- MyOkrView 是扁平 keyResults 列表(非 objectives 嵌套)
- 复用现有 updateProgress(kr) 的 _newValue/_newStatus 机制
- ScoringEditView: krScores[i].score / dimScores[i].score 绑定
- computedTotal/computedGrade 复用现有变量(非 computedPreviewTotal)
wangkangyjy 1 month ago
parent
commit
0e3c9bd7ec
1 changed files with 1664 additions and 0 deletions
  1. 1664 0
      docs/superpowers/plans/2026-06-20-medtech-ui-revamp-v2.md

+ 1664 - 0
docs/superpowers/plans/2026-06-20-medtech-ui-revamp-v2.md

@@ -0,0 +1,1664 @@
+# 前端去 AI 味改造 v2 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 在 v1 已清理的 token 基础上,重构 5 个样板页(MainLayout / LoginView / MyOkrView / ScoringEditView / MyResultView)的信息架构,消除营销式 hero/胶囊/引导卡/圆环,达成 D 风格(AI 科研外壳 + 临床数据内核)。
+
+**架构:** theme.css 新增数据表格/工具栏/状态色点等增量 token(只增不改);components.css 新增 `.data-toolbar`/`.data-table`/`.compare-grid-3`/`.section-band`/`.metric-bar`/`.side-drawer`/`.status-dot`/`.underline-input` 通用类;5 个样板页逐页重写 `<template>`(视觉 token 复用,template 结构重构);verify 脚本同步更新断言。所有改动限定在前端,不动后端/API/数据库。
+
+**技术栈:** Vue 3 + Element Plus + Vite,CSS 变量 token 系统,`scripts/verify-medtech-theme.mjs` + `scripts/verify-scoring-contract.mjs` 守卫。
+
+**对应规格:** `docs/superpowers/specs/2026-06-20-medtech-ui-revamp-v2-design.md`
+
+**口径基线(v1 已确认,本次不改):** OKR 满分 60 + 绩效满分 40 = 100。
+
+---
+
+## 文件结构
+
+### 创建
+- 无新文件(所有改造在现有文件内进行)
+
+### 修改
+- `frontend/src/styles/theme.css` — 新增 v2 增量 token(第 1.2 节),不改动现有 token
+- `frontend/src/styles/components.css` — 新增 8 个通用组件类(第 1.3 节)
+- `frontend/src/components/MainLayout.vue` — 侧边栏去 emoji + 加深底 + 细化选中
+- `frontend/src/views/auth/LoginView.vue` — 全屏分栏 + 下划线输入
+- `frontend/src/views/okr/MyOkrView.vue` — 工具栏 + 表格化 O/KR + 侧边抽屉(改动最大)
+- `frontend/src/views/scoring/ScoringEditView.vue` — 三列对照(证据·自评·终评)+ 底部总览栏
+- `frontend/src/views/scoring/MyResultView.vue` — 删圆环,数据卡 + 构成条 + 历史趋势
+- `frontend/scripts/verify-medtech-theme.mjs` — 更新断言(移除旧类检查,新增 v2 类检查 + 禁用模式)
+
+### 不改动
+- `frontend/scripts/verify-scoring-contract.mjs`(口径不变)
+- 后端任何文件
+- 其余 ~10 个页面(本次不处理)
+
+---
+
+## 任务 1:theme.css 增量 token
+
+**文件:**
+- 修改:`frontend/src/styles/theme.css`(在 `:root` 块末尾追加,不改现有 token)
+
+- [ ] **步骤 1:在 theme.css 的 `:root { }` 末尾追加 v2 token**
+
+打开 `frontend/src/styles/theme.css`,找到 `:root {` 块,在其闭合 `}` 之前追加:
+
+```css
+  /* ===== v2 增量 token(信息架构重构,只增不改)===== */
+
+  /* 数据表格系统 */
+  --table-row-height: 40px;
+  --table-row-height-compact: 32px;
+  --table-header-bg: var(--color-surface-subtle, #f8fafc);
+  --table-row-hover: var(--color-surface-muted, #f3f6f9);
+  --table-row-group-bg: var(--color-surface-tint-brand, rgba(43, 31, 153, 0.03));
+  --table-border: var(--color-border-soft, #edf2f6);
+
+  /* 工具栏顶栏(替代 page-hero)*/
+  --toolbar-height: 48px;
+  --toolbar-bg: var(--color-surface-subtle, #f8fafc);
+  --toolbar-border: var(--color-border, #dfe7ee);
+
+  /* 状态色点(替代 effect="dark" round tag)*/
+  --dot-size: 6px;
+
+  /* 区段色带标题 */
+  --section-band-bg: var(--color-surface-tint-info, rgba(58, 212, 216, 0.08));
+  --section-band-color: var(--color-medical, #0d7a5f);
+  --section-band-font-size: 11px;
+  --section-band-letter-spacing: 0.3px;
+
+  /* 大写小字标签(表头/字段名)*/
+  --label-uppercase: uppercase;
+  --label-letter-spacing: 0.5px;
+  --label-font-size: 10px;
+
+  /* 侧边栏 v2 加深 */
+  --color-sidebar-bg: #1d166b;
+
+  /* 登录页品牌区渐变 */
+  --color-login-brand-from: #1d166b;
+  --color-login-brand-to: #120d4a;
+```
+
+- [ ] **步骤 2:运行 verify 脚本确认未破坏现有断言**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:PASS(现有 token 全部保留,新 token 不影响断言)。若 FAIL,检查是否误删了现有 token。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/styles/theme.css
+git commit -m "feat(frontend): 新增 v2 信息架构重构 token(表格/工具栏/状态点/区段带)"
+```
+
+---
+
+## 任务 2:components.css 新增通用组件类
+
+**文件:**
+- 修改:`frontend/src/styles/components.css`(文件末尾追加 8 个类,不改现有类)
+
+- [ ] **步骤 1:在 components.css 末尾追加 v2 通用类**
+
+```css
+/* ===== v2 信息架构通用组件 ===== */
+
+/* 工具栏顶栏(替代 page-hero)*/
+.data-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  height: var(--toolbar-height);
+  padding: 0 16px;
+  background: var(--toolbar-bg);
+  border-bottom: 1px solid var(--toolbar-border);
+  font-size: var(--font-size-base, 14px);
+}
+.data-toolbar__title {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--color-text, #18212f);
+  letter-spacing: 0.2px;
+}
+.data-toolbar__sep {
+  color: var(--toolbar-border);
+}
+.data-toolbar__meta {
+  color: var(--color-text-secondary, #5f6b7a);
+  font-size: 12px;
+}
+.data-toolbar__spacer { margin-left: auto; }
+
+/* 状态色点(替代彩色 tag)*/
+.status-dot {
+  display: inline-block;
+  width: var(--dot-size);
+  height: var(--dot-size);
+  border-radius: 50%;
+  vertical-align: middle;
+}
+.status-dot--active { background: var(--color-grade-success, #2e9d65); }
+.status-dot--warn { background: var(--color-grade-warn, #c98219); }
+.status-dot--danger { background: var(--color-grade-danger, #c94a4a); }
+.status-dot--info { background: var(--color-text-muted, #c0c6cf); }
+
+/* 状态标签(色点 + 文字)*/
+.status-label {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 11px;
+  font-weight: 500;
+}
+.status-label__dot {
+  width: var(--dot-size);
+  height: var(--dot-size);
+  border-radius: 50%;
+}
+
+/* 大写小字标签 */
+.uppercase-label {
+  text-transform: var(--label-uppercase);
+  letter-spacing: var(--label-letter-spacing);
+  font-size: var(--label-font-size);
+  font-weight: 600;
+  color: var(--color-text-muted, #8a97a8);
+}
+
+/* 区段色带标题 */
+.section-band {
+  padding: 7px 14px;
+  background: var(--section-band-bg);
+  color: var(--section-band-color);
+  font-size: var(--section-band-font-size);
+  font-weight: 700;
+  letter-spacing: var(--section-band-letter-spacing);
+  border-bottom: 1px solid var(--table-border);
+}
+
+/* 表格化数据容器 */
+.data-table {
+  width: 100%;
+  font-size: 12px;
+  background: var(--color-surface, #fff);
+}
+.data-table__header {
+  display: grid;
+  padding: 7px 16px;
+  background: var(--table-header-bg);
+  border-bottom: 1px solid var(--toolbar-border);
+  text-transform: var(--label-uppercase);
+  letter-spacing: var(--label-letter-spacing);
+  font-size: var(--label-font-size);
+  font-weight: 600;
+  color: var(--color-text-muted, #8a97a8);
+}
+.data-table__row {
+  display: grid;
+  align-items: center;
+  padding: 7px 16px;
+  border-bottom: 1px solid var(--table-border);
+  min-height: var(--table-row-height-compact);
+}
+.data-table__row--group {
+  background: var(--table-row-group-bg);
+  font-weight: 600;
+  min-height: var(--table-row-height);
+}
+.data-table__row--child {
+  padding-left: 44px;
+  color: var(--color-text-secondary, #5f6b7a);
+}
+.data-table__row--child::before {
+  content: '└';
+  position: absolute;
+  margin-left: -20px;
+  color: var(--color-text-muted, #c0c6cf);
+}
+.data-table__row:hover {
+  background: var(--table-row-hover);
+}
+.data-table__row--clickable {
+  cursor: pointer;
+}
+
+/* 三列对照网格(评分页)*/
+.compare-grid-3 {
+  display: grid;
+  grid-template-columns: 1.4fr 70px 1fr;
+  align-items: center;
+  padding: 7px 14px;
+  border-bottom: 1px solid var(--table-border);
+  min-height: var(--table-row-height-compact);
+}
+.compare-grid-3__self {
+  text-align: center;
+  font-weight: 700;
+  font-size: 13px;
+}
+.compare-grid-3__header {
+  display: grid;
+  grid-template-columns: 1.4fr 70px 1fr;
+  padding: 6px 14px;
+  background: var(--table-header-bg);
+  border-bottom: 1px solid var(--toolbar-border);
+  text-transform: var(--label-uppercase);
+  letter-spacing: var(--label-letter-spacing);
+  font-size: var(--label-font-size);
+  font-weight: 600;
+  color: var(--color-text-muted, #8a97a8);
+}
+
+/* 水平数据条(构成展示)*/
+.metric-bar {
+  margin-bottom: 10px;
+}
+.metric-bar__head {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 4px;
+  font-size: 12px;
+}
+.metric-bar__label { color: var(--color-text, #18212f); font-weight: 500; }
+.metric-bar__value { color: var(--color-text, #18212f); font-weight: 600; }
+.metric-bar__value--muted { color: var(--color-text-muted, #8a97a8); font-weight: 400; }
+.metric-bar__track {
+  height: 5px;
+  background: var(--table-border, #f3f6f9);
+  border-radius: 3px;
+  overflow: hidden;
+}
+.metric-bar__fill {
+  height: 100%;
+  border-radius: 3px;
+}
+
+/* 侧边抽屉容器(KR 详情/进度更新)*/
+.side-drawer__content {
+  padding: 0;
+}
+
+/* 底部固定总览栏(评分页)*/
+.review-summary-bar {
+  position: sticky;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 10px 14px;
+  background: var(--toolbar-bg);
+  border-top: 2px solid var(--color-brand, #2b1f99);
+  z-index: 10;
+}
+.review-summary-bar__score {
+  font-size: 20px;
+  font-weight: 700;
+  color: var(--color-brand-dark, #1d166b);
+}
+
+/* 下划线式输入框(登录页,覆盖 Element Plus)*/
+.underline-input :deep(.el-input__wrapper) {
+  background: transparent;
+  box-shadow: none;
+  border-radius: 0;
+  border-bottom: 1px solid var(--toolbar-border);
+  padding: 4px 0;
+}
+.underline-input :deep(.el-input__wrapper.is-focus),
+.underline-input :deep(.el-input__wrapper:hover) {
+  border-bottom-color: var(--color-accent, #3ad4d8);
+  box-shadow: none;
+}
+.underline-input :deep(.el-input__inner) {
+  border: none;
+}
+
+/* 历史趋势 mini bar chart */
+.history-trend {
+  display: flex;
+  align-items: flex-end;
+  gap: 16px;
+  height: 48px;
+}
+.history-trend__item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+}
+.history-trend__bar {
+  width: 24px;
+  border-radius: 2px;
+  background: var(--color-text-muted, #c0c6cf);
+}
+.history-trend__bar--current {
+  background: var(--color-brand, #2b1f99);
+}
+```
+
+- [ ] **步骤 2:运行 verify 脚本确认未破坏断言**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:PASS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/styles/components.css
+git commit -m "feat(frontend): 新增 v2 通用组件类(data-toolbar/data-table/compare-grid-3 等)"
+```
+
+---
+
+## 任务 3:MainLayout 侧边栏改造
+
+**文件:**
+- 修改:`frontend/src/components/MainLayout.vue`(侧边栏 template + style)
+- 修改:`frontend/scripts/verify-medtech-theme.mjs:75`(`--color-sidebar-bg` token 检查,可选)
+
+- [ ] **步骤 1:侧边栏背景色 token 切换**
+
+在 `frontend/src/components/MainLayout.vue` 的 `<style scoped>` 中,找到侧边栏背景色定义(当前硬编码 `#2b1f99` 或 `var(--color-brand)`),改为:
+
+```css
+.clinical-sidebar {
+  background: var(--color-sidebar-bg);
+}
+```
+
+- [ ] **步骤 2:菜单项去 emoji + 细化选中态**
+
+在 `<template>` 中,找到侧边栏菜单的 `<el-menu-item>` 列表。删除每个 item 文本中的 emoji(如 `📅 考核周期` → `考核周期`),改为在文字前加几何图标 span。示例(考核周期):
+
+```html
+<el-menu-item index="/period">
+  <span class="nav-ico">▦</span>
+  <template #title>考核周期</template>
+</el-menu-item>
+```
+
+几何图标对照表(统一用 Unicode 几何字符,不引入图标库):
+- 考核周期 → `▦`
+- 我的 OKR → `◎`
+- 团队 OKR → `⬡`
+- OKR 对齐 → `⇄`
+- 绩效打分 → `▱`
+- 绩效沟通 → `◠`
+- 绩效结果 → `⊞`
+- 组织管理 → `⬢`
+- 系统日志 → `≣`
+- 维度配置 → `⌗`
+- KPI 模板 → `⊟`
+- 通知 → `✉`(去掉,改 `⌧`)
+
+在 `<style scoped>` 追加:
+
+```css
+.nav-ico {
+  display: inline-block;
+  width: 18px;
+  margin-right: 8px;
+  color: rgba(255, 255, 255, 0.4);
+  font-size: 14px;
+  text-align: center;
+}
+.el-menu-item.is-active .nav-ico {
+  color: var(--color-accent);
+}
+.clinical-sidebar .el-menu-item.is-active {
+  border-left: 2px solid var(--color-accent) !important;
+  background: rgba(58, 212, 216, 0.08) !important;
+}
+.clinical-sidebar .el-menu-item {
+  border-left: 2px solid transparent;
+}
+```
+
+- [ ] **步骤 3:分组标题改大写小字**
+
+找到菜单分组标题(如"考评管理"、"系统管理"对应的 `<el-menu-item-group>` title 或独立 label),将文字改为大写小字样式。在 `<style scoped>` 追加:
+
+```css
+.clinical-sidebar .el-menu-item-group__title,
+.sidebar-group-label {
+  text-transform: uppercase;
+  letter-spacing: 0.8px;
+  font-size: 10px;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.35);
+  padding: 10px 16px 4px;
+}
+```
+
+- [ ] **步骤 4:品牌区副标题改大写小字**
+
+找到侧边栏顶部品牌区,将副标题"OKR 绩效管理"改为大写小字"OKR · 绩效":
+
+```html
+<div class="brand-sub">OKR · 绩效</div>
+```
+
+```css
+.brand-sub {
+  color: rgba(255, 255, 255, 0.45);
+  font-size: 9px;
+  letter-spacing: 0.8px;
+  text-transform: uppercase;
+}
+```
+
+- [ ] **步骤 5:顶栏角色 tag 改方角**
+
+找到顶栏的 `<el-tag>` 角色标签,把 `round` 属性去掉(如有),确认无 `round`。
+
+- [ ] **步骤 6:启动 dev server 人工目测**
+
+运行:`cd frontend && npm run dev`
+打开 http://localhost:5173,登录后检查:侧边栏无 emoji、选中态是 2px 左边框、分组标题是大写小字、底色更沉。
+
+- [ ] **步骤 7:运行 verify 脚本**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:PASS(`clinical-shell`/`clinical-sidebar` 断言仍在)
+
+- [ ] **步骤 8:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/components/MainLayout.vue
+git commit -m "feat(frontend): MainLayout 侧边栏去 emoji、细化选中态、加深背景"
+```
+
+---
+
+## 任务 4:LoginView 全屏分栏改造
+
+**文件:**
+- 修改:`frontend/src/views/auth/LoginView.vue`(template 重构为左右分栏 + 下划线输入)
+
+- [ ] **步骤 1:template 改为全屏左右分栏**
+
+将 `<template>` 根容器改为全屏 grid 两栏(左深色品牌区 1.2fr + 右白表单 1fr)。保留现有 `internal-login` 类名(verify 脚本依赖)。结构:
+
+```html
+<template>
+  <div class="internal-login">
+    <!-- 左:深色品牌区 -->
+    <div class="login-brand">
+      <div class="login-brand__top">
+        <div class="brand-logo">医</div>
+        <span class="brand-name">医梦AI</span>
+      </div>
+      <div class="login-brand__main">
+        <div class="brand-title">绩效运营工作台</div>
+        <div class="brand-desc">
+          目标对齐 · 进度可追溯<br>
+          结果可解释 · 医疗级数据严谨
+        </div>
+        <!-- 心电波形装饰,保留 -->
+        <svg class="brand-wave" viewBox="0 0 220 24" ...>...</svg>
+      </div>
+      <!-- 分子节点 SVG,右下角,opacity 0.08 -->
+      <svg class="brand-molecule" viewBox="0 0 180 150" ...>...</svg>
+    </div>
+
+    <!-- 右:白表单区 -->
+    <div class="login-form">
+      <div class="uppercase-label">Internal Access</div>
+      <h1 class="login-form__title">账号登录</h1>
+
+      <div class="form-field">
+        <label class="uppercase-label">用户名</label>
+        <el-input v-model="form.username" class="underline-input" placeholder="请输入用户名" />
+      </div>
+      <div class="form-field">
+        <label class="uppercase-label">密码</label>
+        <el-input v-model="form.password" type="password" class="underline-input" placeholder="请输入密码" show-password @keyup.enter="handleLogin" />
+      </div>
+
+      <el-button type="primary" :loading="loading" class="login-submit" @click="handleLogin">登 录</el-button>
+
+      <div class="form-tip uppercase-label">账号由 HR 管理员创建</div>
+    </div>
+  </div>
+</template>
+```
+
+- [ ] **步骤 2:style 改为全屏分栏 + 下划线输入**
+
+替换 `<style scoped>` 中布局相关样式(保留表单逻辑、`.underline-input` 由 components.css 全局提供):
+
+```css
+.internal-login {
+  display: grid;
+  grid-template-columns: 1.2fr 1fr;
+  min-height: 100vh;
+  width: 100%;
+  background: #fff;
+}
+
+/* 左侧深色品牌区 */
+.login-brand {
+  position: relative;
+  overflow: hidden;
+  background: linear-gradient(135deg, var(--color-login-brand-from), var(--color-login-brand-to));
+  padding: 28px 32px;
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+.login-brand__top {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  position: relative;
+  z-index: 1;
+}
+.brand-logo {
+  width: 28px;
+  height: 28px;
+  background: var(--color-accent);
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--color-login-brand-from);
+  font-weight: 700;
+  font-size: 12px;
+}
+.brand-name {
+  color: #fff;
+  font-weight: 600;
+  font-size: 12px;
+  letter-spacing: 0.3px;
+}
+.login-brand__main {
+  position: relative;
+  z-index: 1;
+}
+.brand-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #fff;
+  letter-spacing: 0;
+}
+.brand-desc {
+  color: rgba(255, 255, 255, 0.55);
+  font-size: 11px;
+  margin-top: 6px;
+  line-height: 1.7;
+}
+.brand-wave {
+  margin-top: 12px;
+  width: 100px;
+  height: 12px;
+  opacity: 0.6;
+}
+.brand-molecule {
+  position: absolute;
+  right: -10px;
+  bottom: -10px;
+  width: 140px;
+  height: 120px;
+  opacity: 0.08;
+}
+
+/* 右侧白表单区 */
+.login-form {
+  padding: 32px 40px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  max-width: 420px;
+  margin: 0 auto;
+  width: 100%;
+}
+.login-form__title {
+  font-size: 14px;
+  font-weight: 500;
+  color: var(--color-text, #18212f);
+  margin: 6px 0 24px;
+  letter-spacing: 0.3px;
+}
+.form-field {
+  margin-bottom: 20px;
+}
+.form-field .uppercase-label {
+  display: block;
+  margin-bottom: 4px;
+}
+.login-submit {
+  width: 100%;
+  border-radius: 4px;
+  letter-spacing: 0.5px;
+  font-weight: 500;
+}
+.form-tip {
+  text-align: center;
+  margin-top: 16px;
+  color: var(--color-text-muted, #8a97a8);
+  font-size: 9px;
+  letter-spacing: 1px;
+}
+
+@media (max-width: 768px) {
+  .internal-login { grid-template-columns: 1fr; }
+  .login-brand { display: none; }
+}
+```
+
+- [ ] **步骤 3:确认无 radial-gradient(verify 脚本禁止)**
+
+检查 template/style 中是否还有 `radial-gradient`,如有则删除。
+
+- [ ] **步骤 4:dev server 目测**
+
+运行:`cd frontend && npm run dev`
+打开 http://localhost:5173/login,检查:全屏左右分栏、左侧深色品牌区、右侧下划线输入框、focus 时下边框变青色。
+
+- [ ] **步骤 5:运行 verify 脚本**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:PASS(`internal-login` 仍在,无 `radial-gradient`)
+
+- [ ] **步骤 6:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/views/auth/LoginView.vue
+git commit -m "feat(frontend): LoginView 改全屏左右分栏 + 下划线式输入框"
+```
+
+---
+
+## 任务 5:MyOkrView 表格化改造(最复杂)
+
+**文件:**
+- 修改:`frontend/src/views/okr/MyOkrView.vue`(template 重构为 data-toolbar + data-table + 抽屉)
+
+**前置说明(关键数据结构):** 先读现有代码确认。当前数据结构是**扁平的**:
+- `myOkr.keyResults[]`(KR 直接挂在 myOkr 下,**不是** objectives[].keyResults[])
+- 每条 KR 有 `id/title/currentValue/targetValue/weight/status` 字段(已确认)
+- 上级 OKR 是 `superiorOkr.value.objectives[].keyResults[]`(这个是嵌套的,见第 96 行 `o.keyResults`)
+- 现有方法:`krPercent(kr)`、`krColor(kr.status)`、`krStatusLabel(kr.status)`、`updateProgress(kr)`(内部用 `kr._newValue`/`kr._newStatus`,第 650 行)
+- 保留所有业务逻辑。只重构展示结构。`okr-workbench` 类名保留(verify 依赖)。
+
+**重要**:因为我的 OKR 是扁平 KR 列表(无 O 分组),表格化时直接列出 KR 行即可,不需要 O 分组行。若需按 `parentObjectiveId` 分组,则先 group by 该字段。下面步骤 3 按扁平 KR 列表实现(更简单,符合现有数据)。
+
+- [ ] **步骤 1:新增侧边抽屉状态**
+
+在 `<script setup>` 的响应式状态区,新增抽屉状态:
+
+```js
+import { ref, computed } from 'vue'
+
+// 抽屉:当前选中的 KR(用于详情/进度更新)
+const drawerKr = ref(null)
+const drawerVisible = ref(false)
+
+function openKrDrawer(kr, objective) {
+  drawerKr.value = { ...kr, _objectiveTitle: objective.title }
+  drawerVisible.value = true
+}
+```
+
+- [ ] **步骤 2:template 顶部改为 data-toolbar**
+
+找到当前 `page-hero`/`hero-info`/`period-banner` 区域,替换为:
+
+```html
+<div class="okr-workbench">
+  <!-- 工具栏顶栏(替代 hero)-->
+  <div class="data-toolbar">
+    <span class="data-toolbar__title">我的 OKR</span>
+    <span class="data-toolbar__sep">|</span>
+    <span class="data-toolbar__meta">{{ period?.name || '—' }}</span>
+    <span class="status-label">
+      <span class="status-label__dot" :class="periodStatusDotClass"></span>
+      {{ periodStatusLabel }}
+    </span>
+    <span class="data-toolbar__spacer"></span>
+    <span class="data-toolbar__meta" v-if="daysLeft !== null">距周期结束 {{ daysLeft }} 天</span>
+    <el-button v-if="canCreateOkr" type="primary" size="small" @click="openEditDialog">制定 OKR</el-button>
+    <el-button v-else-if="canEditOkr" size="small" @click="openEditDialog">编辑 OKR</el-button>
+    <el-button v-if="period?.status === 'ASSESSING' && !selfScoreDone" size="small" @click="goSelfScore">去完成自评</el-button>
+  </div>
+
+  <!-- 下方表格区 -->
+  ...(步骤 3-5 填充)
+</div>
+```
+
+在 `<script setup>` 补充这些 computed(基于现有 period/myOkr 数据):
+
+```js
+const periodStatusLabel = computed(() => {
+  const map = { DRAFT: '草稿', OKR_ALIGN: '对齐中', EXECUTING: '执行中', ASSESSING: '考评中', ARCHIVED: '已归档' }
+  return map[period.value?.status] || '—'
+})
+const periodStatusDotClass = computed(() => {
+  const map = { EXECUTING: 'status-label__dot--active', OKR_ALIGN: 'status-label__dot--warn', ASSESSING: 'status-label__dot--warn', ARCHIVED: 'status-label__dot--info' }
+  // 注意:status-label__dot 的颜色需在 style 中按 class 定义
+  return map[period.value?.status] || ''
+})
+const daysLeft = computed(() => {
+  if (!period.value?.endDate) return null
+  const diff = Math.ceil((new Date(period.value.endDate) - new Date()) / 86400000)
+  return diff > 0 ? diff : 0
+})
+```
+
+补 `status-label__dot` 颜色在 components.css 或本页 style:
+
+```css
+.status-label__dot--active { background: var(--color-grade-success, #2e9d65); }
+.status-label__dot--warn { background: var(--color-grade-warn, #c98219); }
+.status-label__dot--info { background: var(--color-text-muted, #c0c6cf); }
+```
+
+- [ ] **步骤 3:表格头 + 我的 OKR 表格行(扁平 KR 列表)**
+
+`myOkr.keyResults` 是扁平列表(已确认),直接列出 KR 行,无需 O 分组。替换原 `okr-card` 区域:
+
+```html
+<div class="data-table" v-if="myOkr?.keyResults?.length">
+  <!-- 表头 -->
+  <div class="data-table__header" style="grid-template-columns: 28px 1fr 44px 70px 110px 56px;">
+    <span></span>
+    <span>关键结果</span>
+    <span>权重</span>
+    <span>进度</span>
+    <span>当前/目标</span>
+    <span>状态</span>
+  </div>
+
+  <!-- KR 行(扁平列表)-->
+  <div v-for="(kr, i) in myOkr.keyResults" :key="kr.id"
+       class="data-table__row data-table__row--clickable"
+       style="grid-template-columns: 28px 1fr 44px 70px 110px 56px;"
+       @click="openKrDrawer(kr)">
+    <span style="color: var(--color-accent); font-weight: 700;">KR{{ i + 1 }}</span>
+    <span style="color: var(--color-text);">{{ kr.title }}</span>
+    <span>{{ kr.weight }}</span>
+    <span style="color: var(--color-brand); font-weight: 600;">{{ krPercent(kr) }}%</span>
+    <span>{{ kr.currentValue }}/{{ kr.targetValue }}</span>
+    <span class="status-label" style="font-size: 11px;">
+      <span class="status-dot" :class="krStatusClass(kr)"></span>
+      {{ krStatusLabel(kr.status) }}
+    </span>
+  </div>
+</div>
+
+<!-- 空态(无 OKR)-->
+<div v-else-if="period?.status === 'OKR_ALIGN'" class="okr-empty">
+  <div class="uppercase-label" style="text-align: center; padding: 48px;">
+    尚未制定 OKR · 点击右上角「制定 OKR」开始
+  </div>
+</div>
+```
+
+补充 computed(复用现有 `krPercent`,仅新增状态色点映射):
+
+```js
+function krStatusClass(kr) {
+  if (kr.status === 'BLOCKED') return 'status-dot--warn'
+  if (kr.status === 'COMPLETED' || krPercent(kr) >= 100) return 'status-dot--active'
+  return 'status-dot--info'
+}
+```
+
+**注意**:`krPercent`/`krStatusLabel` 是现有方法,直接复用,不要重造。
+
+- [ ] **步骤 4:侧边抽屉(KR 详情 + 进度更新)**
+
+现有 `updateProgress(kr)` 方法(第 650 行)内部用 `kr._newValue`/`kr._newStatus`/`okrApi.updateProgress(kr.id, {newValue, status, remark})`。抽屉要对接这个现有机制,**不重造**。在 template 末尾追加抽屉:
+
+```html
+<el-drawer v-model="drawerVisible" title="关键结果详情" size="420px" direction="rtl">
+  <div v-if="drawerKr" class="side-drawer__content">
+    <div class="uppercase-label">关键结果</div>
+    <div style="margin: 4px 0 16px; font-weight: 600;">{{ drawerKr.title }}</div>
+
+    <div class="metric-bar">
+      <div class="metric-bar__head">
+        <span class="metric-bar__label">当前进度</span>
+        <span class="metric-bar__value">{{ drawerKr.currentValue }} / {{ drawerKr.targetValue }}
+          <span class="metric-bar__value--muted">({{ krPercent(drawerKr) }}%)</span>
+        </span>
+      </div>
+      <div class="metric-bar__track">
+        <div class="metric-bar__fill" :style="{ width: krPercent(drawerKr) + '%', background: 'var(--color-accent)' }"></div>
+      </div>
+    </div>
+
+    <!-- 进度更新(仅 EXECUTING 阶段,复用现有 updateProgress 机制)-->
+    <template v-if="period?.status === 'EXECUTING' && drawerKr.status !== 'COMPLETED'">
+      <div class="uppercase-label" style="margin-top: 20px;">更新进度</div>
+      <div style="font-size: 11px; color: var(--color-text-muted); margin-top: 4px; margin-bottom: 8px;">
+        当前值:{{ drawerKr.currentValue }}
+      </div>
+      <el-input-number v-model="drawerKr._newValue" :min="0" style="width: 100%;"
+                       :placeholder="'新值(当前 ' + drawerKr.currentValue + ')'" />
+      <div class="uppercase-label" style="margin-top: 16px;">状态</div>
+      <el-select v-model="drawerKr._newStatus" style="width: 100%; margin-top: 4px;">
+        <el-option label="进行中" value="IN_PROGRESS" />
+        <el-option label="阻塞" value="BLOCKED" />
+        <el-option label="已完成" value="COMPLETED" />
+      </el-select>
+      <el-button type="primary" style="width: 100%; margin-top: 16px;"
+                 :disabled="drawerKr._newValue == null" @click="submitDrawerProgress">保存进度</el-button>
+    </template>
+    <div v-else-if="drawerKr.status === 'COMPLETED'" class="uppercase-label"
+         style="margin-top: 20px; color: var(--color-grade-success);">
+      ✓ 该 KR 已完成
+    </div>
+  </div>
+</el-drawer>
+```
+
+抽屉初始化时设置 `_newValue`/`_newStatus`(对接现有方法),补充方法:
+
+```js
+function openKrDrawer(kr) {
+  // 复制一份并初始化现有 updateProgress 机制所需的临时字段
+  drawerKr.value = { ...kr, _newValue: kr.currentValue, _newStatus: kr.status }
+  drawerVisible.value = true
+}
+async function submitDrawerProgress() {
+  if (!drawerKr.value) return
+  await updateProgress(drawerKr.value)  // 调用现有方法(内部读 _newValue/_newStatus)
+  drawerVisible.value = false
+  await loadData()
+}
+```
+
+**注意**:`updateProgress(kr)` 是现有 async 方法(第 650 行),其内部已调用 `okrApi.updateProgress(kr.id, {newValue: kr._newValue, status: kr._newStatus, remark: ''})` 并刷新数据。抽屉只负责设置 `_newValue`/`_newStatus` 后调用它。先读第 650-660 行确认完整签名与副作用(是否会自动 loadData)。
+
+- [ ] **步骤 5:上级 OKR 与待审核区表格化(OKR_ALIGN 阶段)**
+
+找到当前 `superior-okr` / `pending-reviews` 展示区,改为同样的 data-table 结构。上级 OKR 的 KR 行加一个「指派给我」小标签:
+
+```html
+<div v-if="superiorOkr?.objectives?.length" style="margin-top: 24px;">
+  <div class="section-band">上级 OKR · 对齐参考</div>
+  <div class="data-table">
+    <!-- 同上 data-table__header + 遍历 superiorOkr.objectives -->
+    <!-- KR 行若 assignedToMe 则加标签 -->
+    <span v-if="kr.assignedToMe" class="assign-tag">指派给我</span>
+  </div>
+</div>
+```
+
+```css
+.assign-tag {
+  display: inline-block;
+  background: var(--section-band-bg);
+  color: var(--section-band-color);
+  padding: 1px 6px;
+  border-radius: 3px;
+  font-size: 10px;
+  margin-left: 8px;
+}
+```
+
+- [ ] **步骤 6:删除所有 page-hero / section-badge / hint-card / okr-card 样式**
+
+在 `<style scoped>` 中,删除以下类定义(它们不再被 template 引用):
+- `.page-hero` / `.hero-info` / `.hero-title` / `.hero-subtitle`
+- `.section-badge` 及其修饰符(`.personal` / `.superior` / `.review`)
+- `.section-head`
+- `.hint-card` 及子元素
+- `.okr-card` / `.okr-grid` / `.okr-item`
+- `.rules-card`
+
+保留 `.okr-workbench`(verify 依赖)、`.okr-empty`、`.assign-tag`、`.side-drawer__content` 及上述新样式。
+
+- [ ] **步骤 7:删除 template 中所有 effect="dark" round 的 el-tag**
+
+全文搜索 `effect="dark"` 和 `round`,移除这些属性。状态展示统一改用 `.status-dot`。
+
+- [ ] **步骤 8:dev server 目测全部状态**
+
+运行:`cd frontend && npm run dev`
+切换 period 状态(或用测试账号),检查:
+- OKR_ALIGN 有 myOkr:表格只读 + 工具栏「编辑」按钮
+- OKR_ALIGN 无 myOkr:空态 + 工具栏「制定」按钮 + 上级 OKR 表格
+- EXECUTING:表格 + 点 KR 行打开抽屉可更新进度
+- ASSESSING:表格只读 + 工具栏「去完成自评」
+
+- [ ] **步骤 9:运行 verify 脚本**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:`okr-workbench` 断言通过。**注意**:当前脚本还检查 `okr-standard-panel`(第 90 行附近),需在任务 7 中同步移除此断言。本任务先确保 `okr-workbench` 在。
+
+- [ ] **步骤 10:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/views/okr/MyOkrView.vue
+git commit -m "feat(frontend): MyOkrView 表格化重构(工具栏 + O/KR 表格 + 侧边抽屉)"
+```
+
+---
+
+## 任务 6:ScoringEditView 三列对照改造
+
+**文件:**
+- 修改:`frontend/src/views/scoring/ScoringEditView.vue`(template 重构为 compare-grid-3 + section-band + 底部总览栏)
+
+**前置说明(关键数据结构):** 先读现有代码确认。当前变量结构:
+- `krScores` = `ref([])`,元素是 `{title, score}`(第 321 行 `kr.value.map(kr => ({title: kr.title, score: 0.6}))`)→ 绑定 `krScores[i].score`
+- `dimScores` = `ref([])`,元素是 `{id, name, maxScore, score}`(第 326 行)→ 绑定 `dimScores[i].score`
+- `dimensions` = `ref([])`(第 233 行),从 `dimensionApi.list()` 加载
+- 预览总分 = `computedTotal`(computed,第 256 行,**不是** computedPreviewTotal)
+- 等级 = `computedGrade`(已有,第 261 行,S/A/B/C)
+- 加减分 = `form.bonus` / `form.penalty`(reactive form,第 232 行 `const form = reactive({bonus:0, penalty:0, remark:''})`)
+- 自评数据:`selfKrScore(i)` / `selfDimScore(name)` / `selfDimDesc(name)` 均为现有函数
+- `okrRecords` = `computed(() => data.value.okrRecords || [])`(第 236 行)
+- 保留所有评分逻辑。保留 `review-workbench` 类名(内部 evidence-panel/decision-panel 替换为三列对照)。
+
+- [ ] **步骤 1:template 顶部改 data-toolbar**
+
+替换 `page-hero`:
+
+```html
+<div class="review-workbench">
+  <div class="data-toolbar">
+    <span class="data-toolbar__title">绩效考评</span>
+    <span class="data-toolbar__sep">|</span>
+    <span class="data-toolbar__meta">{{ data.targetUser?.name }} · {{ data.targetUser?.role }}</span>
+    <span class="data-toolbar__sep">|</span>
+    <span class="data-toolbar__meta">{{ data.period?.name }}</span>
+    <span class="data-toolbar__spacer"></span>
+    <el-button v-if="data.finalScore && data.finalScore.status !== 'PUBLISHED'"
+               type="primary" size="small" @click="submitFinal">提交终评</el-button>
+  </div>
+  ...(步骤 2-4 填充)
+</div>
+```
+
+- [ ] **步骤 2:三列对照表头 + OKR 区段**
+
+替换原 evidence-panel/decision-panel:
+
+```html
+<div class="scoring-body">
+  <!-- 对照表头 -->
+  <div class="compare-grid-3__header">
+    <span>证据 · 员工自评</span>
+    <span style="text-align: center;">自评分</span>
+    <span>你的终评</span>
+  </div>
+
+  <!-- OKR 区段 -->
+  <div class="section-band">OKR 目标达成 · 满分 60</div>
+
+  <div v-for="(kr, i) in okrRecords[0]?.keyResults || []" :key="'kr'+i"
+       class="compare-grid-3">
+    <!-- 左:证据 -->
+    <div>
+      <div style="color: var(--color-text); font-weight: 500;">KR{{ i+1 }} {{ kr.title }}</div>
+      <div style="color: var(--color-text-muted); font-size: 11px; margin-top: 2px;">
+        实际 {{ kr.currentValue }}/{{ kr.targetValue }} · 完成率
+        <span style="color: var(--color-brand); font-weight: 600;">{{ krCompletionPct(kr) }}%</span>
+      </div>
+    </div>
+    <!-- 中:自评分 -->
+    <div class="compare-grid-3__self" :style="{ color: selfKrScoreColor(i) }">
+      {{ selfKrScore(i) !== null ? selfKrScore(i) : '—' }}
+    </div>
+    <!-- 右:终评下拉(绑定 krScores[i].score)-->
+    <div>
+      <el-select v-model="krScores[i].score" size="small" style="width: 140px;" @change="markDirty" placeholder="选择评级">
+        <el-option v-for="g in gradeOptions" :key="g.value" :label="g.label" :value="g.value" />
+      </el-select>
+    </div>
+  </div>
+
+  <!-- 绩效区段 -->
+  <div class="section-band" style="margin-top: 8px; border-top: 1px solid var(--table-border);">绩效维度 · 满分 40</div>
+
+  <div v-for="(dim, i) in dimensions" :key="'dim'+i" class="compare-grid-3">
+    <div>
+      <div style="color: var(--color-text); font-weight: 500;">{{ dim.name }}</div>
+      <div v-if="selfDimDesc(dim.name)" style="color: var(--color-text-muted); font-size: 11px; margin-top: 2px;">
+        {{ selfDimDesc(dim.name) }}
+      </div>
+    </div>
+    <div class="compare-grid-3__self" :style="{ color: selfDimScore(dim.name) !== null ? 'var(--color-text-secondary)' : 'var(--color-text-muted)' }">
+      {{ selfDimScore(dim.name) !== null ? selfDimScore(dim.name) : '—' }}
+    </div>
+    <div>
+      <el-input-number v-model="dimScores[i].score" :min="0" :max="dim.maxScore || 10" size="small" style="width: 140px;" @change="markDirty" />
+    </div>
+  </div>
+
+  <!-- 加减分(绑定 form.bonus / form.penalty)-->
+  <div class="compare-grid-3">
+    <div style="color: var(--color-text); font-weight: 500;">加分(满分内)</div>
+    <div class="compare-grid-3__self" style="color: var(--color-grade-warn, #c98219);">
+      +{{ data.selfScore?.bonus || 0 }}
+    </div>
+    <div>
+      <el-input-number v-model="form.bonus" :min="0" :max="10" size="small" style="width: 140px;" @change="markDirty" />
+    </div>
+  </div>
+  <div class="compare-grid-3">
+    <div style="color: var(--color-text); font-weight: 500;">扣分</div>
+    <div class="compare-grid-3__self" style="color: var(--color-text-muted);">
+      -{{ data.selfScore?.penalty || 0 }}
+    </div>
+    <div>
+      <el-input-number v-model="form.penalty" :min="0" :max="10" size="small" style="width: 140px;" @change="markDirty" />
+    </div>
+  </div>
+
+  ...(步骤 3 底部总览栏)
+</div>
+```
+
+补充 computed/method(基于现有数据,确认字段名后再写):
+
+```js
+const gradeOptions = [
+  { label: '卓越 1.0', value: 1.0 },
+  { label: '优秀 0.9', value: 0.9 },
+  { label: '良好 0.8', value: 0.8 },
+  { label: '达标 0.6', value: 0.6 },
+  { label: '不达标 0.3', value: 0.3 },
+]
+function krCompletionPct(rec) {
+  if (!rec.targetValue) return 0
+  return Math.min(100, Math.round((rec.currentValue / rec.targetValue) * 100))
+}
+function selfKrScoreColor(i) {
+  const s = selfKrScore(i)
+  return s === null ? 'var(--color-text-muted)' : (s >= 0.8 ? 'var(--color-grade-success, #0d7a5f)' : 'var(--color-text-secondary)')
+}
+```
+
+**注意**:`okrRecords`、`selfKrScore`、`selfDimScore`、`selfDimDesc`、`krScores`、`dimScores`、`bonusScore` 均为现有变量,先读现有代码确认名称与类型。
+
+- [ ] **步骤 3:底部固定总览栏(含自评差异提示)**
+
+在 `scoring-body` 末尾追加:
+
+```html
+<!-- 底部总览栏 -->
+<div class="review-summary-bar">
+  <span class="uppercase-label">终评总分</span>
+  <span class="review-summary-bar__score">
+    {{ computedTotal?.toFixed(1) || '0.0' }}
+    <span style="font-size: 11px; color: var(--color-text-muted); font-weight: 400;">/ 100</span>
+  </span>
+  <span class="status-label">
+    <span class="status-dot" :class="previewGradeDot"></span>
+    {{ computedGrade }}
+  </span>
+  <span style="margin-left: auto;" class="uppercase-label" :style="{ color: diffColor }">
+    自评 {{ (data.selfScore?.total || 0).toFixed(1) }} · 差异 {{ scoreDiff >= 0 ? '+' : '' }}{{ scoreDiff.toFixed(1) }}
+  </span>
+  <el-tooltip v-if="Math.abs(scoreDiff) >= 10" content="打分偏差较大,建议复核">
+    <span style="color: var(--color-grade-danger, #c94a4a); cursor: help; font-size: 12px;">!</span>
+  </el-tooltip>
+</div>
+```
+
+补充 computed(`computedTotal`/`computedGrade` 复用现有,仅新增差异相关):
+
+```js
+const scoreDiff = computed(() => {
+  const preview = computedTotal.value || 0
+  const selfT = data.value.selfScore?.total || 0
+  return Math.round((preview - selfT) * 10) / 10
+})
+const diffColor = computed(() => {
+  return Math.abs(scoreDiff.value) >= 10 ? 'var(--color-grade-danger, #c94a4a)' : 'var(--color-text-muted, #8a97a8)'
+})
+const previewGradeDot = computed(() => {
+  const t = computedTotal.value || 0
+  if (t >= 75) return 'status-dot--active'
+  if (t >= 60) return 'status-dot--warn'
+  return 'status-dot--danger'
+})
+```
+
+**注意**:`computedTotal`(第 256 行)和 `computedGrade`(第 261 行,返回 S/A/B/C)是现有 computed,直接复用。`markDirty` 若不存在则新增 `const markDirty = () => { /* 触发响应式更新,computed 会自动重算 */ }` 或删除 `@change="markDirty"`(el-select/el-input-number 的 v-model 已触发响应式,computed 自动更新,markDirty 可能多余——先读代码确认是否已有 dirty 状态管理)。
+
+- [ ] **步骤 4:响应式降级(<1280px)**
+
+在 `<style scoped>` 追加:
+
+```css
+@media (max-width: 1279px) {
+  .compare-grid-3,
+  .compare-grid-3__header {
+    grid-template-columns: 1fr;
+    gap: 6px;
+  }
+  .compare-grid-3__self {
+    text-align: left;
+  }
+}
+```
+
+- [ ] **步骤 5:删除旧样式**
+
+删除 `<style scoped>` 中:`.page-hero` / `.evidence-panel` / `.decision-panel` / `.okr-record-card` / `.orr-kr-card` / `.self-block` / `.self-total-bar` / `.result-preview` 及相关。保留 `.review-workbench`、`.scoring-body` 及新样式。
+
+- [ ] **步骤 6:删除 template 中所有 effect="dark" round**
+
+全文移除 `effect="dark"` 和 `round`。
+
+- [ ] **步骤 7:dev server 目测**
+
+运行:`cd frontend && npm run dev`
+进入评分页,检查:三列对照、区段色带、底部总览栏(含自评差异)、小屏降级。
+
+- [ ] **步骤 8:运行两个 verify 脚本**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs && node scripts/verify-scoring-contract.mjs`
+预期:评分契约 PASS(口径不变);theme 脚本中 `review-workbench` 通过,但 `evidence-panel`/`decision-panel` 断言会 FAIL —— 这是预期的,将在任务 7 移除。
+
+- [ ] **步骤 9:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/views/scoring/ScoringEditView.vue
+git commit -m "feat(frontend): ScoringEditView 三列对照重构(证据·自评·终评 + 总览栏)"
+```
+
+---
+
+## 任务 7:MyResultView 删圆环 + 数据视图
+
+**文件:**
+- 修改:`frontend/src/views/scoring/MyResultView.vue`(template 重构 result 态,保留 self-form 态不动)
+
+**前置说明:** 现有数据字段:`data.finalScore.total/grade/okrScore/kpiScore/bonus/penalty/confirmedAt`、`data.selfScore.total`。自评表单态(`showSelfForm` 为 true)**保持现状不动**。
+
+- [ ] **步骤 1:template 顶部改 data-toolbar**
+
+替换 `page-hero`(仅结果态部分):
+
+```html
+<div class="result-workbench">
+  <div class="data-toolbar">
+    <span class="data-toolbar__title">我的绩效</span>
+    <span class="data-toolbar__sep">|</span>
+    <span class="data-toolbar__meta">{{ data.period?.name }}</span>
+    <span class="data-toolbar__spacer"></span>
+    <el-button size="small" @click="goHistory">历史记录</el-button>
+  </div>
+  ...(步骤 2-4 填充)
+</div>
+```
+
+- [ ] **步骤 2:result-grid 主结果区(左大数字 + 右构成条)**
+
+替换 `score-hero`(grade-ring 区):
+
+```html
+<div v-if="data.finalScore" class="result-grid">
+  <!-- 左:分数 + 等级 -->
+  <div class="result-grid__main">
+    <div class="uppercase-label">本期终评</div>
+    <div class="result-score">{{ data.finalScore.total }}</div>
+    <div class="result-score-max">/ 100 分</div>
+    <div class="result-grade-tag">
+      <span class="status-dot" :class="gradeDotClass(data.finalScore.grade)"></span>
+      <span class="result-grade-letter">{{ data.finalScore.grade }}</span>
+      <span class="result-grade-desc">{{ gradeDesc(data.finalScore.grade) }}</span>
+    </div>
+    <div class="result-vs-last">
+      <div class="uppercase-label">较上期</div>
+      <div :style="{ color: vsLastColor }">{{ vsLastText }}</div>
+    </div>
+  </div>
+
+  <!-- 右:构成条 -->
+  <div class="result-grid__breakdown">
+    <div class="uppercase-label" style="margin-bottom: 12px;">得分构成</div>
+
+    <div class="metric-bar">
+      <div class="metric-bar__head">
+        <span class="metric-bar__label">OKR 目标达成</span>
+        <span class="metric-bar__value">{{ data.finalScore.okrScore }}<span class="metric-bar__value--muted"> / 60</span></span>
+      </div>
+      <div class="metric-bar__track"><div class="metric-bar__fill" :style="{ width: (data.finalScore.okrScore/60*100)+'%', background: 'var(--color-brand)' }"></div></div>
+    </div>
+
+    <div class="metric-bar">
+      <div class="metric-bar__head">
+        <span class="metric-bar__label">绩效维度</span>
+        <span class="metric-bar__value">{{ data.finalScore.kpiScore }}<span class="metric-bar__value--muted"> / 40</span></span>
+      </div>
+      <div class="metric-bar__track"><div class="metric-bar__fill" :style="{ width: (data.finalScore.kpiScore/40*100)+'%', background: 'var(--color-accent)' }"></div></div>
+    </div>
+
+    <div class="metric-bar">
+      <div class="metric-bar__head">
+        <span class="metric-bar__label">专项加分</span>
+        <span class="metric-bar__value" style="color: var(--color-grade-success);">+{{ data.finalScore.bonus }}</span>
+      </div>
+      <div class="metric-bar__track"><div class="metric-bar__fill" :style="{ width: Math.min(100, data.finalScore.bonus/5*100)+'%', background: 'var(--color-grade-success)' }"></div></div>
+    </div>
+
+    <div class="metric-bar">
+      <div class="metric-bar__head">
+        <span class="metric-bar__label">专项扣分</span>
+        <span class="metric-bar__value" style="color: var(--color-grade-danger);">-{{ data.finalScore.penalty }}</span>
+      </div>
+      <div class="metric-bar__track"><div class="metric-bar__fill" :style="{ width: Math.min(100, data.finalScore.penalty/5*100)+'%', background: 'var(--color-grade-danger)' }"></div></div>
+    </div>
+  </div>
+</div>
+```
+
+补充 computed/method(历史趋势兜底):
+
+```js
+const historyList = ref([])
+const vsLastText = computed(() => {
+  if (!historyList.value.length) return '首次考评'
+  const last = historyList.value[historyList.value.length - 1]
+  const diff = (data.value.finalScore?.total || 0) - (last.total || 0)
+  if (diff > 0) return `▲ +${diff.toFixed(1)} 分`
+  if (diff < 0) return `▼ ${diff.toFixed(1)} 分`
+  return '持平'
+})
+const vsLastColor = computed(() => {
+  if (!historyList.value.length) return 'var(--color-text-muted)'
+  const last = historyList.value[historyList.value.length - 1]
+  const diff = (data.value.finalScore?.total || 0) - (last.total || 0)
+  return diff > 0 ? 'var(--color-grade-success)' : (diff < 0 ? 'var(--color-grade-danger)' : 'var(--color-text-muted)')
+})
+function gradeDotClass(g) {
+  const map = { S: 'status-dot--active', A: 'status-dot--active', B: 'status-dot--warn', C: 'status-dot--warn', D: 'status-dot--danger' }
+  return map[g] || 'status-dot--info'
+}
+```
+
+- [ ] **步骤 3:自评 vs 终评对照行**
+
+在 result-grid 下方(保留现有 `compare-row` 逻辑,仅调整样式类):
+
+```html
+<div v-if="data.selfScore && data.finalScore" class="result-compare">
+  <div class="uppercase-label" style="margin-bottom: 8px;">自评 vs 终评</div>
+  <div class="result-compare-grid">
+    <div>
+      <div class="uppercase-label">OKR</div>
+      <div><span style="color: var(--color-text-secondary);">自评 {{ data.selfScore.okrScore?.toFixed(1) }}</span> → <span style="color: var(--color-brand); font-weight: 600;">终评 {{ data.finalScore.okrScore?.toFixed(1) }}</span></div>
+    </div>
+    <div>
+      <div class="uppercase-label">绩效</div>
+      <div><span style="color: var(--color-text-secondary);">自评 {{ data.selfScore.kpiScore?.toFixed(1) }}</span> → <span style="color: var(--color-brand); font-weight: 600;">终评 {{ data.finalScore.kpiScore?.toFixed(1) }}</span></div>
+    </div>
+    <div>
+      <div class="uppercase-label">总分</div>
+      <div><span style="color: var(--color-text-secondary);">自评 {{ data.selfScore.total?.toFixed(1) }}</span> → <span style="color: var(--color-brand); font-weight: 600;">终评 {{ data.finalScore.total?.toFixed(1) }}</span></div>
+    </div>
+  </div>
+</div>
+```
+
+**注意**:`data.selfScore` 是否含 `okrScore`/`kpiScore` 拆分字段需先读现有代码确认;若无拆分则只显示总分对照。
+
+- [ ] **步骤 4:操作栏(扁平化,删 emoji)+ 历史趋势**
+
+```html
+<div class="result-actions">
+  <el-button v-if="data.finalScore && !data.finalScore.confirmedAt"
+             class="result-action--primary" @click="confirmResult">确认结果</el-button>
+  <el-button v-else-if="data.finalScore?.confirmedAt" disabled>已确认</el-button>
+  <el-button @click="goAppeal">提起申诉</el-button>
+  <el-button @click="goFeedback">查看面谈记录</el-button>
+</div>
+
+<!-- 历史趋势 -->
+<div class="result-trend">
+  <div class="uppercase-label" style="margin-bottom: 12px;">历史趋势({{ Math.max(1, historyList.length) + 1 }} 期)</div>
+  <div v-if="historyList.length" class="history-trend">
+    <div v-for="(h, i) in historyList" :key="i" class="history-trend__item">
+      <div class="history-trend__bar" :style="{ height: barHeight(h.total) + 'px' }"></div>
+      <span class="uppercase-label">{{ h.periodName }}</span>
+      <span style="font-size: 10px; color: var(--color-text-secondary);">{{ h.total?.toFixed(1) }}</span>
+    </div>
+    <!-- 本期高亮 -->
+    <div class="history-trend__item">
+      <div class="history-trend__bar history-trend__bar--current" :style="{ height: barHeight(data.finalScore?.total) + 'px' }"></div>
+      <span class="uppercase-label" style="color: var(--color-brand); font-weight: 600;">{{ data.period?.name }}</span>
+      <span style="font-size: 10px; color: var(--color-brand); font-weight: 600;">{{ data.finalScore?.total?.toFixed(1) }}</span>
+    </div>
+  </div>
+  <div v-else class="uppercase-label" style="padding: 24px; text-align: center;">暂无历史成绩</div>
+</div>
+```
+
+补充方法:
+
+```js
+function barHeight(score) {
+  if (!score) return 10
+  return Math.max(10, Math.min(48, (score / 100) * 48))
+}
+```
+
+- [ ] **步骤 5:历史数据兜底加载(前端轮询,无新后端接口)**
+
+在 `loadData` 或 `onMounted` 中追加(方案一:轮询最近 4 个归档周期):
+
+```js
+async function loadHistory() {
+  try {
+    const periods = await periodApi.list({ status: 'ARCHIVED', pageSize: 4 })
+    const archived = periods.data?.records || periods.records || []
+    const results = []
+    for (const p of archived) {
+      try {
+        const r = await scoreApi.getMyResult(p.id)
+        if (r.data?.finalScore?.total != null) {
+          results.push({ periodName: p.name, total: r.data.finalScore.total })
+        }
+      } catch { /* 跳过无数据的周期 */ }
+    }
+    historyList.value = results
+  } catch {
+    historyList.value = []  // 方案二:失败则隐藏趋势
+  }
+}
+```
+
+**注意**:`periodApi.list` 的参数与返回结构需先读 `frontend/src/api/index.js` 确认;`scoreApi.getMyResult` 已确认存在。若 `periodApi.list` 不支持 status 过滤,则取全部后在前端过滤 ARCHIVED。
+
+- [ ] **步骤 6:删除旧样式**
+
+删除 `<style scoped>` 中:`.page-hero` / `.score-hero` / `.grade-ring` / `.grade-letter` / `.grade-ring.grade-*` / `.score-breakdown` / `.compare-row`(旧版)。保留 `.result-workbench`(verify 依赖)及新样式。追加新样式:
+
+```css
+.result-grid {
+  display: grid;
+  grid-template-columns: 200px 1fr;
+  border-bottom: 1px solid var(--toolbar-border);
+}
+.result-grid__main {
+  padding: 20px 18px;
+  border-right: 1px solid var(--table-border);
+  background: var(--table-header-bg);
+}
+.result-score {
+  font-size: 40px;
+  font-weight: 700;
+  color: var(--color-text, #18212f);
+  line-height: 1;
+  margin-top: 6px;
+}
+.result-score-max { color: var(--color-text-muted); font-size: 11px; margin-top: 2px; }
+.result-grade-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 12px;
+  padding: 4px 10px;
+  background: var(--section-band-bg);
+  border-radius: 4px;
+}
+.result-grade-letter { color: var(--section-band-color); font-weight: 700; font-size: 13px; }
+.result-grade-desc { color: var(--section-band-color); font-size: 11px; }
+.result-vs-last { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--table-border); }
+.result-vs-last div:last-child { font-weight: 600; font-size: 13px; margin-top: 2px; }
+.result-grid__breakdown { padding: 16px 18px; }
+.result-compare {
+  padding: 12px 18px;
+  border-bottom: 1px solid var(--table-border);
+  background: var(--table-header-bg);
+}
+.result-compare-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr;
+  gap: 12px;
+  font-size: 11px;
+}
+.result-actions {
+  display: flex;
+  gap: 8px;
+  padding: 10px 14px;
+  border-bottom: 1px solid var(--toolbar-border);
+}
+.result-action--primary {
+  background: var(--table-row-group-bg);
+  border-color: var(--table-border);
+  font-weight: 500;
+}
+.result-trend { padding: 12px 18px; }
+```
+
+- [ ] **步骤 7:dev server 目测**
+
+运行:`cd frontend && npm run dev`
+进入我的绩效页,检查:无圆环、左大数字 + 右构成条、自评vs终评对照、历史趋势、扁平操作按钮。
+
+- [ ] **步骤 8:运行 verify 脚本**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:`result-workbench` 通过。`grade-ring`/`score-hero` 断言需在任务 8 移除(若当前脚本有检查的话,先确认)。
+
+- [ ] **步骤 9:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/src/views/scoring/MyResultView.vue
+git commit -m "feat(frontend): MyResultView 删等级圆环,改数据卡 + 构成条 + 历史趋势"
+```
+
+---
+
+## 任务 8:更新 verify-medtech-theme.mjs 断言
+
+**文件:**
+- 修改:`frontend/scripts/verify-medtech-theme.mjs`(移除旧类断言,新增 v2 类断言 + 禁用模式)
+
+- [ ] **步骤 1:移除已废弃的类断言**
+
+在脚本中,找到以下断言行并**删除**(v2 已用新结构替代):
+
+```js
+// 删除这些行:
+if (!myOkr.includes('okr-standard-panel')) failures.push('...')  // 第 90 行附近
+if (!scoringEdit.includes('evidence-panel')) failures.push('...')
+if (!scoringEdit.includes('decision-panel')) failures.push('...')
+```
+
+- [ ] **步骤 2:新增 v2 类断言**
+
+在 `if (!scoringList.includes('scoring-worklist'))` 之后,追加:
+
+```js
+// v2 信息架构断言
+if (!myOkr.includes('data-toolbar')) failures.push('MyOkrView must use data-toolbar (replaced page-hero)')
+if (!myOkr.includes('data-table')) failures.push('MyOkrView must use data-table (table-based O/KR)')
+if (!myOkr.includes('side-drawer__content') && !myOkr.includes('el-drawer')) failures.push('MyOkrView must use side drawer for KR detail/progress')
+
+if (!scoringEdit.includes('compare-grid-3')) failures.push('ScoringEditView must use compare-grid-3 (3-column comparison)')
+if (!scoringEdit.includes('section-band')) failures.push('ScoringEditView must use section-band')
+if (!scoringEdit.includes('review-summary-bar')) failures.push('ScoringEditView must use review-summary-bar')
+
+const myResult = readFileSync(join(root, 'src/views/scoring/MyResultView.vue'), 'utf8')
+if (!myResult.includes('metric-bar')) failures.push('MyResultView must use metric-bar (replaced grade-ring)')
+if (myResult.includes('conic-gradient')) failures.push('MyResultView must not use conic-gradient (grade-ring removed)')
+
+if (!login.includes('underline-input')) failures.push('LoginView must use underline-input')
+if (!login.includes('login-brand')) failures.push('LoginView must use login-brand (full-screen split)')
+```
+
+- [ ] **步骤 3:新增禁用模式(全 src 扫描)**
+
+在 `const bannedEmoji = ...` 等定义后,新增:
+
+```js
+const bannedLegacyPatterns = [
+  { re: /page-hero/, msg: '禁止 page-hero(改用 data-toolbar)' },
+  { re: /section-badge/, msg: '禁止 section-badge 胶囊徽章' },
+  { re: /grade-ring/, msg: '禁止 grade-ring 圆环' },
+  { re: /score-hero/, msg: '禁止 score-hero 渐变区' },
+  { re: /hint-card/, msg: '禁止 hint-card 大引导卡' },
+  { re: /conic-gradient/, msg: '禁止 conic-gradient' },
+]
+```
+
+在 `for (const file of allVueFiles)` 循环(遍历所有 .vue 的那段)中,追加检查:
+
+```js
+for (const { re, msg } of bannedLegacyPatterns) {
+  if (re.test(text)) failures.push(`${relativePath}: ${msg}`)
+}
+```
+
+**注意**:需要先读现有脚本,找到遍历 .vue 文件的循环位置(通常在 emoji/decor 检查附近),把禁用模式检查加进去。
+
+- [ ] **步骤 4:移除 migratedMarkers 中的废弃类(可选,避免误报)**
+
+检查 `migratedMarkers` 数组,若有 `okr-standard-panel` 之外的、v2 已删除的类名,移除。保留 `okr-workbench`/`review-workbench`/`result-workbench`/`clinical-shell`/`clinical-sidebar`/`internal-login`(这些 v2 仍保留)。
+
+- [ ] **步骤 5:运行脚本验证全部通过**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs`
+预期:PASS(所有 v2 断言通过,无 legacy 模式残留)
+
+若有 FAIL,逐条排查:是页面未改干净(回去修页面),还是断言写错(修脚本)。
+
+- [ ] **步骤 6:运行评分契约脚本**
+
+运行:`cd frontend && node scripts/verify-scoring-contract.mjs`
+预期:PASS(口径 OKR 60 + 绩效 40 不变)
+
+- [ ] **步骤 7:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add frontend/scripts/verify-medtech-theme.mjs
+git commit -m "test(frontend): 更新 verify 脚本断言(v2 信息架构类 + 禁用 legacy 模式)"
+```
+
+---
+
+## 任务 9:构建 + 部署同步
+
+**文件:**
+- 修改:`backend/src/main/resources/static/`(构建产物覆盖)
+
+- [ ] **步骤 1:前端构建**
+
+运行:`cd frontend && npm run build`
+预期:构建成功,`frontend/dist/` 生成。
+
+- [ ] **步骤 2:复制产物到 backend static**
+
+运行:
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+rm -rf backend/src/main/resources/static/assets
+cp -r frontend/dist/* backend/src/main/resources/static/
+```
+
+**注意**:删除旧 assets 后整体复制,避免孤儿文件(v1 发现的问题)。
+
+- [ ] **步骤 3:确认 index.html 引用正确**
+
+运行:`grep -oE 'assets/index-[A-Za-z0-9_-]+\.(js|css)' backend/src/main/resources/static/index.html | sort -u`
+预期:输出 1 个 js + 1 个 css,且文件存在于 `assets/` 目录。
+
+- [ ] **步骤 4:清理孤儿文件**
+
+运行:`ls backend/src/main/resources/static/assets/ | wc -l`,对比构建产物数量。确认无未被 index.html 引用的孤儿文件。
+
+- [ ] **步骤 5:后端启动冒烟测试(可选)**
+
+运行:`cd backend && mvn spring-boot:run`
+打开 http://localhost:18080,登录后逐一访问 5 个样板页,确认渲染正常、无控制台报错。
+
+- [ ] **步骤 6:运行两个 verify 脚本最终确认**
+
+运行:`cd frontend && node scripts/verify-medtech-theme.mjs && node scripts/verify-scoring-contract.mjs`
+预期:两个脚本均 PASS。
+
+- [ ] **步骤 7:Commit**
+
+```bash
+cd /Users/destiny/dev/emoon/projects/okr-performance
+git add backend/src/main/resources/static/
+git commit -m "build: 同步前端 v2 构建产物到 backend static(单 JAR 部署)"
+```
+
+---
+
+## 任务 10:验收对照清单
+
+**文件:** 无(对照规格第 5 节验收标准人工核对)
+
+- [ ] **步骤 1:信息架构验收**
+
+逐条核对:
+- [ ] 5 页无 `page-hero` / `section-badge` / `grade-ring` / `score-hero` / `hint-card` / `conic-gradient`(verify 脚本保证)
+- [ ] MyOkrView 用 `data-toolbar` + `data-table`,KR 更新走抽屉
+- [ ] ScoringEditView 用 `compare-grid-3` + `section-band` + `review-summary-bar`(含自评差异)
+- [ ] MyResultView 用 `metric-bar` + 历史趋势,无圆环
+- [ ] MainLayout 侧边栏无 emoji,2px 选中边框
+- [ ] LoginView 全屏分栏 + 下划线输入
+
+- [ ] **步骤 2:视觉规范验收**
+
+- [ ] 无硬编码 hex(除 SVG 描边)
+- [ ] 无 emoji、无渐变胶囊、无营销口号
+- [ ] 状态用 status-dot,无 effect="dark" round
+- [ ] 区段用 section-band
+
+- [ ] **步骤 3:数据契约验收**
+
+- [ ] 评分口径 OKR 60 + 绩效 40(`verify-scoring-contract.mjs` PASS)
+- [ ] 自评/终评/结果展示口径一致
+
+- [ ] **步骤 4:构建部署验收**
+
+- [ ] `verify-medtech-theme.mjs` PASS
+- [ ] `verify-scoring-contract.mjs` PASS
+- [ ] `npm run build` 成功
+- [ ] 产物已同步 backend/static,无孤儿文件
+
+- [ ] **步骤 5:全页面人工回归(5 个样板页 + 登录)**
+
+登录后逐页操作,确认业务功能未受影响(OKR 编辑/进度更新、评分提交、结果确认、申诉、面谈查看)。
+
+---
+
+## 风险与回退
+
+| 风险 | 缓解 |
+|---|---|
+| 表格化后小屏挤压 | 任务 6 步骤 4 预设 <1280px 降级 |
+| 历史趋势数据缺失 | 任务 7 步骤 5 前端轮询,失败隐藏趋势区 |
+| 抽屉与现有 dialog 冲突 | 抽屉只承载进度更新,编辑 OKR 仍走现有 dialog |
+| 字段名与计划不符 | 每个任务都标注「先读现有代码确认字段名」 |
+| verify 断言与页面不一致 | 任务 8 最后统一更新脚本 |
+
+**回退**:所有改动在 5 页 + theme.css + components.css + verify 脚本 + static 产物范围内。`git revert` 对应 commit 即可完整回退,不影响后端与业务逻辑。
+
+---
+
+## 自检清单(执行前阅读)
+
+执行此计划的代理请注意:
+1. **先读后写**:每个涉及现有页面的任务,动手前必须先 Read 该文件,确认字段名/方法名/现有结构,再按计划改造。计划中的字段名(如 `kr.currentValue`、`data.finalScore.okrScore`)是基于代码探查的推断,可能与实际有出入。
+2. **保留 verify 依赖的类名**:`okr-workbench`/`review-workbench`/`result-workbench`/`clinical-shell`/`clinical-sidebar`/`internal-login` 必须保留。
+3. **每任务一 commit**:按计划节奏频繁 commit,便于回退。
+4. **verify 脚本先行**:任务 1-7 完成后,任务 8 之前,部分 legacy 断言会 FAIL(因为页面已改但脚本未更新)——这是预期的,不要在任务 1-7 中改脚本。
+5. **口径不动**:OKR 60 + 绩效 40 是基线,任何改动不得触碰。