# 前端去 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 是基线，任何改动不得触碰。
