Просмотр исходного кода

feat(frontend): 修复评分口径并改造为医疗科技内部系统风格

评分口径修复(与后端权威口径对齐):
- 自评/终评/结果展示统一为 OKR 40 分 + 工作成果 60 分 + 加分 - 扣分
- 删除结果页误导性的 ×0.6/×0.4 二次加权表述
- 新增 verify-scoring-contract.mjs 契约脚本固化口径,CI 可校验

医疗科技 UI 改造(5 样板页 + PeriodListView):
- theme.css 新增等级语义色、医疗意象色、tint 色、间距阶梯、过渡 token
- 清除营销口号、渐变发光胶囊、emoji、Material 杂色等 AI 味反模式
- 业务页全部 token 化,硬编码十六进制收敛为 var(--color-*)
- 登录页/主布局品牌区注入内联 SVG 医疗意象(零依赖)
- PeriodListView 一并 token 化,圆角/字号/阴影统一到设计系统
wangkangyjy 3 недель назад
Родитель
Сommit
0e04aea7c9

+ 2 - 1
frontend/package.json

@@ -7,7 +7,8 @@
     "dev": "vite",
     "build": "vite build",
     "preview": "vite preview",
-    "verify:theme": "node scripts/verify-medtech-theme.mjs"
+    "verify:theme": "node scripts/verify-medtech-theme.mjs",
+    "verify:scoring": "node scripts/verify-scoring-contract.mjs"
   },
   "dependencies": {
     "vue": "^3.4.21",

+ 35 - 0
frontend/scripts/verify-scoring-contract.mjs

@@ -0,0 +1,35 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
+
+const root = resolve(new URL('..', import.meta.url).pathname)
+const myResult = readFileSync(resolve(root, 'src/views/scoring/MyResultView.vue'), 'utf8')
+const scoringEdit = readFileSync(resolve(root, 'src/views/scoring/ScoringEditView.vue'), 'utf8')
+
+const failures = []
+
+function reject(pattern, message) {
+  if (pattern.test(myResult) || pattern.test(scoringEdit)) failures.push(message)
+}
+
+function requireIn(file, pattern, message) {
+  if (!pattern.test(file)) failures.push(message)
+}
+
+reject(/满分\s*60[\s\S]{0,80}OKR|OKR[\s\S]{0,80}满分\s*60/, 'OKR 自评/终评必须是 40 分制,不能显示满分 60')
+reject(/满分\s*40[\s\S]{0,80}工作成果|工作成果[\s\S]{0,80}满分\s*40/, '工作成果必须是 60 分制,不能显示满分 40')
+reject(/×\s*0\.[46](?!\d)/, '结果展示不能使用二次加权系数 ×0.6 或 ×0.4(排除动画时长 0.4s 等合法值)')
+reject(/\*\s*60\s*\*\s*10/, 'OKR 计算不能乘以 60')
+reject(/OKR\(40%\)|工作成果\(60%\)/, '评分预览应写分值制,不应写成百分比加权')
+
+requireIn(myResult, /OKR 得分 = 平均[\s\S]*× 40[\s\S]*\/ 40/, 'MyResultView 自评 OKR 小计应展示 ×40 与 /40')
+requireIn(myResult, /工作成果得分 =[\s\S]*\/ 60/, 'MyResultView 自评工作成果小计应展示 /60')
+requireIn(scoringEdit, /OKR 得分 = 平均[\s\S]*× 40[\s\S]*\/ 40/, 'ScoringEditView 终评 OKR 小计应展示 ×40 与 /40')
+requireIn(scoringEdit, /工作成果得分 =[\s\S]*\/ 60/, 'ScoringEditView 终评工作成果小计应展示 /60')
+
+if (failures.length) {
+  console.error('评分契约检查失败:')
+  failures.forEach(item => console.error(`- ${item}`))
+  process.exit(1)
+}
+
+console.log('评分契约检查通过')

+ 52 - 11
frontend/src/components/MainLayout.vue

@@ -8,6 +8,9 @@
             <span class="brand-lockup__name">医梦AI</span>
             <span class="brand-lockup__sub">OKR 绩效管理</span>
           </div>
+          <svg class="brand-lockup__pulse" viewBox="0 0 160 18" aria-hidden="true" focusable="false">
+            <path d="M2 9h52l6-6 8 12 9-12 6 6h75" />
+          </svg>
         </div>
         <div class="brand-lockup collapsed" v-show="isCollapse">
           <img src="/logo.png" class="brand-lockup__mini" alt="医梦AI" />
@@ -19,7 +22,7 @@
           :collapse-transition="false"
           background-color="transparent"
           text-color="rgba(255,255,255,0.7)"
-          active-text-color="#3ad4d8"
+          active-text-color="var(--color-accent)"
           router
         >
           <el-menu-item index="/periods">
@@ -121,7 +124,10 @@
                 </div>
               </div>
             </el-popover>
-            <span class="user-info">{{ auth.user?.realName }} <el-tag :color="auth.roleColor" size="small" effect="dark" round style="border:none;vertical-align:middle;margin-left:4px">{{ auth.roleLabel }}</el-tag></span>
+            <span class="user-info">
+              {{ auth.user?.realName }}
+              <el-tag size="small" effect="plain" round class="role-tag">{{ auth.roleLabel }}</el-tag>
+            </span>
             <el-button text type="danger" @click="handleLogout">退出</el-button>
           </div>
         </el-header>
@@ -211,27 +217,45 @@ function handleLogout() {
 .clinical-shell { height: 100vh; color: var(--color-text); }
 .clinical-container { height: 100vh; }
 .clinical-sidebar {
-  background: linear-gradient(180deg, var(--color-brand-strong), #120d4a);
+  background: linear-gradient(180deg, var(--color-brand-strong), var(--color-sidebar-end));
   overflow-y: auto;
   transition: width 0.2s ease;
 }
 .brand-lockup {
+  position: relative;
   display: flex;
   align-items: center;
   gap: 10px;
-  padding: 16px 14px;
-  border-bottom: 1px solid rgba(255,255,255,0.1);
+  padding: 16px 14px 28px;
+  border-bottom: 1px solid var(--color-sidebar-border);
 }
 .brand-lockup.collapsed { justify-content: center; padding: 14px 8px; }
 .brand-lockup__logo { width: 72px; height: 36px; object-fit: contain; flex-shrink: 0; }
 .brand-lockup__mini { width: 38px; height: 22px; object-fit: contain; }
 .brand-lockup__text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
-.brand-lockup__name { font-size: 16px; font-weight: 700; color: #fff; letter-spacing: 1px; }
-.brand-lockup__sub { font-size: 11px; color: rgba(255,255,255,0.58); }
+.brand-lockup__name { font-size: var(--font-size-lg); font-weight: 700; color: var(--color-white); letter-spacing: 0; }
+.brand-lockup__sub { font-size: 11px; color: var(--color-sidebar-muted); }
+.brand-lockup__pulse {
+  position: absolute;
+  left: 14px;
+  right: 14px;
+  bottom: 8px;
+  width: calc(100% - 28px);
+  height: 18px;
+  color: var(--color-accent);
+  opacity: 0.52;
+}
+.brand-lockup__pulse path {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 1.2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
 .menu-group-title {
   padding: 16px 20px 6px;
   font-size: 11px;
-  color: rgba(255,255,255,0.36);
+  color: var(--color-sidebar-muted);
   text-transform: uppercase;
   letter-spacing: 1px;
 }
@@ -239,12 +263,22 @@ function handleLogout() {
   border-right: none !important;
 }
 .el-menu-item {
+  position: relative;
   margin: 2px 8px;
-  border-radius: 6px;
+  border-radius: var(--radius-sm);
 }
 .el-menu-item.is-active {
-  background: rgba(58,212,216,0.1) !important;
-  border-left: 3px solid #3ad4d8;
+  background: var(--color-accent-alpha) !important;
+}
+.el-menu-item.is-active::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 10px;
+  bottom: 10px;
+  width: 2px;
+  border-radius: var(--radius-xs);
+  background: var(--color-accent);
 }
 .clinical-topbar {
   height: var(--layout-topbar);
@@ -258,6 +292,13 @@ function handleLogout() {
 .topbar-left { display: flex; align-items: center; gap: 8px; }
 .topbar-right { display: flex; align-items: center; gap: 12px; }
 .user-info { font-size: 13px; color: var(--color-text-secondary); }
+.role-tag {
+  margin-left: 4px;
+  vertical-align: middle;
+  border-color: var(--color-border);
+  color: var(--color-brand);
+  background: var(--color-surface-tint-brand);
+}
 .clinical-main {
   min-height: calc(100vh - var(--layout-topbar));
   padding: 20px 24px;

+ 37 - 0
frontend/src/styles/theme.css

@@ -6,10 +6,14 @@
   --color-accent: #3ad4d8;
   --color-accent-strong: #17a9ad;
   --color-accent-soft: #e5fbfc;
+  --color-accent-alpha: rgba(58, 212, 216, 0.1);
+  --color-accent-focus: rgba(58, 212, 216, 0.22);
 
   --color-bg: #f5f8fa;
   --color-surface: #ffffff;
   --color-surface-subtle: #f8fafc;
+  --color-surface-muted: #f3f6f9;
+  --color-surface-elevated: #ffffff;
   --color-border: #dfe7ee;
   --color-border-soft: #edf2f6;
   --color-text: #18212f;
@@ -19,15 +23,48 @@
   --color-warning: #c98219;
   --color-danger: #c94a4a;
   --color-info: #4c6f91;
+  --color-grade-s: #0d7a5f;
+  --color-grade-a: #2e9d65;
+  --color-grade-b: #4c6f91;
+  --color-grade-c: #c94a4a;
+  --color-medical: #1a6b7a;
+  --color-surface-tint-brand: #f4f3fb;
+  --color-surface-tint-success: #f0f7f3;
+  --color-surface-tint-warning: #faf6ee;
+  --color-surface-tint-danger: #faf2f2;
+  --color-surface-tint-info: #edf8f9;
+  --color-white: #ffffff;
+  --color-sidebar-end: #120d4a;
+  --color-sidebar-text: rgba(255, 255, 255, 0.72);
+  --color-sidebar-muted: rgba(255, 255, 255, 0.5);
+  --color-sidebar-border: rgba(255, 255, 255, 0.1);
 
   --radius-xs: 4px;
   --radius-sm: 6px;
   --radius-md: 8px;
+  --font-size-xs: 12px;
+  --font-size-sm: 13px;
+  --font-size-base: 14px;
+  --font-size-lg: 16px;
+  --font-size-xl: 18px;
+  --font-size-2xl: 24px;
+  --font-size-3xl: 32px;
   --shadow-panel: 0 1px 2px rgba(15, 23, 42, 0.04);
   --shadow-popover: 0 12px 28px rgba(15, 23, 42, 0.12);
   --layout-sidebar: 220px;
   --layout-topbar: 52px;
 
+  /* 间距阶梯(8 的倍数) */
+  --spacing-xs: 4px;
+  --spacing-sm: 8px;
+  --spacing-md: 12px;
+  --spacing-lg: 16px;
+  --spacing-xl: 24px;
+  --spacing-2xl: 32px;
+
+  /* 过渡 */
+  --transition-fast: 0.2s ease;
+
   --el-color-primary: var(--color-brand);
   --el-color-primary-light-3: #5544b8;
   --el-color-primary-light-5: #7a6bc9;

+ 66 - 17
frontend/src/views/auth/LoginView.vue

@@ -1,11 +1,24 @@
 <template>
   <div class="internal-login">
     <section class="internal-login__brand">
-      <img src="/logo.png" class="internal-login__logo" alt="医梦AI" />
-      <div>
-        <h1>OKR 绩效管理系统</h1>
-        <p>医梦AI 内部绩效运营工作台</p>
+      <div class="internal-login__brand-main">
+        <img src="/logo.png" class="internal-login__logo" alt="医梦AI" />
+        <div>
+          <h1>OKR 绩效管理系统</h1>
+          <p>医梦AI 内部绩效运营工作台</p>
+          <svg class="medical-pulse" viewBox="0 0 220 24" aria-hidden="true" focusable="false">
+            <path d="M2 12h72l8-8 10 16 12-16 8 8h106" />
+          </svg>
+        </div>
       </div>
+      <svg class="molecule-mark" viewBox="0 0 180 150" aria-hidden="true" focusable="false">
+        <path d="M48 44 92 28 134 58 116 112 62 112 48 44Z" />
+        <circle cx="48" cy="44" r="7" />
+        <circle cx="92" cy="28" r="7" />
+        <circle cx="134" cy="58" r="7" />
+        <circle cx="116" cy="112" r="7" />
+        <circle cx="62" cy="112" r="7" />
+      </svg>
     </section>
 
     <section class="internal-login__panel med-panel">
@@ -86,15 +99,24 @@ async function handleSubmit() {
   justify-content: center;
   gap: 56px;
   padding: 40px;
-  background:
-    linear-gradient(135deg, rgba(43,31,153,0.08), transparent 42%),
-    var(--color-bg);
+  background: var(--color-bg);
 }
 
 .internal-login__brand {
+  position: relative;
   display: flex;
   align-items: flex-start;
+  justify-content: space-between;
   gap: 20px;
+  min-height: 180px;
+}
+
+.internal-login__brand-main {
+  display: flex;
+  align-items: flex-start;
+  gap: 20px;
+  position: relative;
+  z-index: 1;
 }
 
 .internal-login__logo {
@@ -107,7 +129,7 @@ async function handleSubmit() {
 .internal-login__brand h1 {
   margin: 8px 0 8px;
   color: var(--color-text);
-  font-size: 30px;
+  font-size: var(--font-size-3xl);
   line-height: 1.2;
   letter-spacing: 0;
 }
@@ -115,7 +137,34 @@ async function handleSubmit() {
 .internal-login__brand p {
   margin: 0;
   color: var(--color-text-secondary);
-  font-size: 15px;
+  font-size: var(--font-size-base);
+}
+
+.medical-pulse {
+  display: block;
+  width: 220px;
+  height: 24px;
+  margin-top: 18px;
+  color: var(--color-accent);
+  opacity: 0.65;
+}
+
+.medical-pulse path,
+.molecule-mark path,
+.molecule-mark circle {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 1.5;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.molecule-mark {
+  width: 180px;
+  height: 150px;
+  color: var(--color-brand);
+  opacity: 0.14;
+  flex-shrink: 0;
 }
 
 .internal-login__panel {
@@ -130,7 +179,7 @@ async function handleSubmit() {
   display: block;
   margin-bottom: 6px;
   color: var(--color-accent-strong);
-  font-size: 12px;
+  font-size: var(--font-size-xs);
   font-weight: 700;
   letter-spacing: 1px;
   text-transform: uppercase;
@@ -139,8 +188,8 @@ async function handleSubmit() {
 .login-panel__header h2 {
   margin: 0;
   color: var(--color-text);
-  font-size: 22px;
-  font-weight: 700;
+  font-size: var(--font-size-xl);
+  font-weight: 500;
 }
 
 .login-form {
@@ -157,7 +206,7 @@ async function handleSubmit() {
 }
 .custom-input :deep(.el-input__wrapper.is-focus) {
   border-color: var(--color-accent);
-  box-shadow: 0 0 0 1px rgba(58,212,216,0.2);
+  box-shadow: 0 0 0 1px var(--color-accent-focus);
 }
 .custom-input :deep(.el-input__inner) {
   color: var(--color-text);
@@ -170,9 +219,9 @@ async function handleSubmit() {
   height: 44px;
   background: var(--color-brand);
   border: none;
-  color: #fff;
-  font-size: 15px;
-  font-weight: 600;
+  color: var(--color-white);
+  font-size: var(--font-size-base);
+  font-weight: 500;
   letter-spacing: 0;
   margin-top: 4px;
 }
@@ -185,7 +234,7 @@ async function handleSubmit() {
 }
 .login-help {
   color: var(--color-text-muted);
-  font-size: 13px;
+  font-size: var(--font-size-sm);
   letter-spacing: 0;
 }
 

+ 108 - 108
frontend/src/views/okr/MyOkrView.vue

@@ -3,7 +3,7 @@
     <div class="page-hero">
       <div class="hero-info">
         <h2>我的 OKR</h2>
-        <p>1O · 3KR · 强对齐 · 看结果</p>
+        <p>本周期目标与关键结果</p>
       </div>
     </div>
 
@@ -23,7 +23,7 @@
           <div class="rule-item"><b>O</b>:定性方向,15-30字,不含数字指标</div>
           <div class="rule-item"><b>KR</b>:2~5条,可量化结果,权重之和=100</div>
           <div class="rule-item"><b>对齐</b>:下级O必须承接上级O方向</div>
-          <div class="rule-item"><b>打分</b>:逐条KR 0-1分,看结果不看过程</div>
+          <div class="rule-item"><b>打分</b>:逐条 KR 0-1 分,以交付结果为依据</div>
         </div>
         <div v-if="positionTemplate" class="position-ref">
           <div class="pr-title">你的岗位指标参考({{ positionTemplate.name }})</div>
@@ -61,7 +61,7 @@
               <div class="kr-display-meta">
                 <span class="kr-target">目标 {{ kr.targetValue }}</span>
                 <el-tag v-if="kr.sourceKrId" size="small" effect="plain" type="info" round>承接上级</el-tag>
-                <el-tag v-if="kr.assignedToName" size="small" effect="dark" type="warning" round>@{{ kr.assignedToName }}</el-tag>
+                <el-tag v-if="kr.assignedToName" size="small" effect="plain" type="warning" round>指派给 {{ kr.assignedToName }}</el-tag>
               </div>
             </div>
           </div>
@@ -97,7 +97,7 @@
                     class="mini-kr" :class="{ 'mine': kr.assignedTo === auth.user?.id }">
                     <span class="mini-kr-num">KR{{ i+1 }}</span>
                     <span class="mini-kr-text">{{ kr.title }}</span>
-                    <el-tag v-if="kr.assignedTo === auth.user?.id" size="small" type="warning" effect="dark" round>@我</el-tag>
+                    <el-tag v-if="kr.assignedTo === auth.user?.id" size="small" type="warning" effect="plain" round>指派给我</el-tag>
                     <span class="mini-val">{{ kr.targetValue }}</span>
                   </div>
                 </div>
@@ -218,7 +218,7 @@
             <span class="align-head-hint">· 勾选你要承接的关键结果,将自动创建引用条目</span>
           </div>
           <el-tag v-if="mandatoryKrCount" type="warning" effect="dark" round size="small">
-            {{ mandatoryKrCount }} 项 @你
+            {{ mandatoryKrCount }} 项指派给我
           </el-tag>
         </div>
 
@@ -237,7 +237,7 @@
               @click="toggleAlignKr(kr, o)">
               <div class="align-kr-left">
                 <span class="align-checkbox" :class="{ checked: isKrAligned(kr.id), locked: kr.assignedTo === auth.user?.id }">
-                  <span v-if="isKrAligned(kr.id)"></span>
+                  <span v-if="isKrAligned(kr.id)">已选</span>
                 </span>
                 <span class="align-kr-num">KR{{ i+1 }}</span>
                 <span class="align-kr-title">{{ kr.title }}</span>
@@ -245,9 +245,9 @@
               <div class="align-kr-right">
                 <span class="align-kr-target">目标 {{ kr.targetValue }}</span>
                 <el-tag v-if="kr.assignedTo === auth.user?.id" type="warning" effect="dark" size="small" round>
-                  @你
+                  指派给我
                 </el-tag>
-                <span v-else-if="isKrAligned(kr.id)" class="align-check"></span>
+                <span v-else-if="isKrAligned(kr.id)" class="align-check">已选</span>
               </div>
             </div>
           </div>
@@ -308,7 +308,7 @@
           </div>
         </div>
         <el-button v-if="newOkr.krs.length < 5" @click="newOkr.krs.push({ title: '', targetValue: 100, weight: 0, assignedTo: null })" size="small" round style="margin-top:8px">
-          + 添加 KR
+          新增 KR
         </el-button>
       </div>
 
@@ -658,7 +658,7 @@ async function doReview(id, approved) {
 }
 
 function krPercent(kr) { if (!kr.targetValue || kr.targetValue === 0) return 0; return Math.min((kr.currentValue / kr.targetValue) * 100, 100) }
-function krColor(s) { return { COMPLETED: '#27ae60', BLOCKED: '#e67e22', IN_PROGRESS: '#3ad4d8', NOT_STARTED: '#c0c4cc' }[s] || '#c0c4cc' }
+function krColor(s) { return { COMPLETED: 'var(--color-success)', BLOCKED: 'var(--color-warning)', IN_PROGRESS: 'var(--color-accent)', NOT_STARTED: 'var(--color-text-muted)' }[s] || 'var(--color-text-muted)' }
 function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进行中', COMPLETED: '已完成', BLOCKED: '阻塞' }[s] || s }
 function truncate(s, n) { if (!s) return ''; return s.length > n ? s.slice(0, n) + '...' : s }
 </script>
@@ -666,155 +666,155 @@ function truncate(s, n) { if (!s) return ''; return s.length > n ? s.slice(0, n)
 <style scoped>
 .page { width: 100%; }
 .page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; }
-.hero-info h2 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0; }
-.hero-info p { font-size: 14px; color: #888; margin: 4px 0 0; }
-.empty-big { display: flex; align-items: center; justify-content: center; height: 300px; font-size: 16px; color: #999; }
+.hero-info h2 { font-size: var(--font-size-2xl); font-weight: 700; color: var(--color-text); margin: 0; }
+.hero-info p { font-size: var(--font-size-base); color: var(--color-text-secondary); margin: 4px 0 0; }
+.empty-big { display: flex; align-items: center; justify-content: center; height: 300px; font-size: var(--font-size-lg); color: var(--color-text-muted); }
 
-.state-banner { padding: 14px 20px; border-radius: 10px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px; font-size: 14px; }
-.banner-OKR_ALIGN { background: #e0f7fa; color: #00838f; border: 1px solid #b2ebf2; }
-.banner-EXECUTING { background: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; }
-.banner-ASSESSING { background: #fef7e8; color: #b88230; border: 1px solid #f5dab1; }
+.state-banner { padding: 14px 20px; border-radius: var(--radius-md); margin-bottom: 16px; display: flex; align-items: center; gap: 10px; font-size: var(--font-size-base); }
+.banner-OKR_ALIGN { background: var(--color-surface-tint-info); color: var(--color-medical); border: 1px solid var(--color-accent-soft); }
+.banner-EXECUTING { background: var(--color-surface-tint-success); color: var(--color-success); border: 1px solid var(--color-border-soft); }
+.banner-ASSESSING { background: var(--color-surface-tint-warning); color: var(--color-warning); border: 1px solid var(--color-border-soft); }
 
-.rules-card { background: #fff; border-radius: 10px; padding: 14px 20px; margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.rules-title { font-weight: 700; color: #2b1f99; font-size: 14px; margin-bottom: 8px; }
+.rules-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 20px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.rules-title { font-weight: 700; color: var(--color-brand); font-size: var(--font-size-base); margin-bottom: 8px; }
 .rules-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
-.rule-item { font-size: 12px; color: #666; }
+.rule-item { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
 
-.position-ref { margin-top: 10px; padding-top: 10px; border-top: 1px solid #e8eaed; }
-.pr-title { font-size: 13px; font-weight: 600; color: #2b1f99; margin-bottom: 6px; }
+.position-ref { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border-soft); }
+.pr-title { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-brand); margin-bottom: 6px; }
 .pr-items { display: flex; flex-wrap: wrap; gap: 6px; }
-.pr-hint { font-size: 11px; color: #aaa; margin-top: 6px; }
+.pr-hint { font-size: 11px; color: var(--color-text-muted); margin-top: 6px; }
 
 .section { margin-bottom: 36px; }
-.section-head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 2px solid #f0f0f0; }
-.section-badge { padding: 6px 18px; border-radius: 8px; font-size: 14px; font-weight: 700; color: #fff; letter-spacing: 2px; }
-.section-badge.personal { background: linear-gradient(135deg, #3ad4d8, #1eada8); box-shadow: 0 2px 8px rgba(58,212,216,0.3); }
-.section-badge.superior { background: linear-gradient(135deg, #2b1f99, #1a1560); box-shadow: 0 2px 8px rgba(43,31,153,0.3); }
-.section-badge.review { background: linear-gradient(135deg, #f39c12, #e67e22); box-shadow: 0 2px 8px rgba(230,126,34,0.3); }
-.section-title { font-size: 18px; font-weight: 700; color: #1a1a2e; }
-.section-hint { font-size: 13px; color: #aaa; }
-
-.hint-card { display: flex; align-items: flex-start; gap: 20px; background: #fff; border-radius: 8px; padding: 28px 32px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); margin-bottom: 24px; }
-.hint-card.waiting { border: 2px solid #fdebd0; background: #fef9e7; }
-.hint-card.top-level { border: 2px solid #d5f5e3; background: #eafaf1; }
+.section-head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--color-border-soft); }
+.section-badge { padding: 4px 12px; border-radius: var(--radius-sm); font-size: var(--font-size-sm); font-weight: 700; color: var(--color-brand); letter-spacing: 0; background: var(--color-surface-tint-brand); border-left: 2px solid var(--color-brand); }
+.section-badge.personal { color: var(--color-medical); background: var(--color-surface-tint-info); border-left-color: var(--color-accent); }
+.section-badge.superior { color: var(--color-brand); background: var(--color-surface-tint-brand); border-left-color: var(--color-brand); }
+.section-badge.review { color: var(--color-warning); background: var(--color-surface-tint-warning); border-left-color: var(--color-warning); }
+.section-title { font-size: var(--font-size-xl); font-weight: 700; color: var(--color-text); }
+.section-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); }
+
+.hint-card { display: flex; align-items: flex-start; gap: 20px; background: var(--color-surface); border-radius: var(--radius-md); padding: 28px 32px; box-shadow: var(--shadow-panel); margin-bottom: 24px; }
+.hint-card.waiting { border: 1px solid var(--color-border-soft); background: var(--color-surface-tint-warning); }
+.hint-card.top-level { border: 1px solid var(--color-border-soft); background: var(--color-surface-tint-success); }
 .hint-icon { font-size: 36px; }
-.hint-title { font-size: 16px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; }
-.hint-desc { font-size: 13px; color: #888; line-height: 1.6; }
+.hint-title { font-size: var(--font-size-lg); font-weight: 700; color: var(--color-text); margin-bottom: 4px; }
+.hint-desc { font-size: var(--font-size-sm); color: var(--color-text-secondary); line-height: 1.6; }
 .action-row { margin-bottom: 24px; }
 
-.rule-hint { font-size: 12px; color: #e67e22; background: #fef9e7; padding: 8px 14px; border-radius: 6px; margin-bottom: 8px; }
+.rule-hint { font-size: var(--font-size-xs); color: var(--color-warning); background: var(--color-surface-tint-warning); padding: 8px 14px; border-radius: var(--radius-sm); margin-bottom: 8px; }
 
 .okr-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
-.okr-card { background: #fff; border-radius: 8px; padding: 20px 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.okr-card.superior-card { border-left: 4px solid #2b1f99; }
-.okr-card.card-APPROVED { border-left: 4px solid #27ae60; }
-.okr-card.card-SUBMITTED { border-left: 4px solid #e67e22; }
-.okr-card.card-REJECTED { border-left: 4px solid #e74c3c; background: #fff5f5; }
-.okr-card.review-card { border-left: 4px solid #f39c12; }
+.okr-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 20px 24px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.okr-card.superior-card { border-left: 4px solid var(--color-brand); }
+.okr-card.card-APPROVED { border-left: 4px solid var(--color-success); }
+.okr-card.card-SUBMITTED { border-left: 4px solid var(--color-warning); }
+.okr-card.card-REJECTED { border-left: 4px solid var(--color-danger); background: var(--color-surface-tint-danger); }
+.okr-card.review-card { border-left: 4px solid var(--color-warning); }
 .okr-card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
-.o-tag { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 8px; font-size: 14px; font-weight: 700; color: #fff; }
-.o-tag.superior { background: #2b1f99; }
-.o-tag.personal { background: #3ad4d8; }
-.o-title { font-size: 15px; font-weight: 600; color: #1a1a2e; flex: 1; }
+.o-tag { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); font-size: var(--font-size-base); font-weight: 700; color: var(--color-white); }
+.o-tag.superior { background: var(--color-brand); }
+.o-tag.personal { background: var(--color-accent); }
+.o-title { font-size: 15px; font-weight: 600; color: var(--color-text); flex: 1; }
 
-.aligned-hint { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; font-size: 12px; color: #999; flex-wrap: wrap; }
+.aligned-hint { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; font-size: var(--font-size-xs); color: var(--color-text-muted); flex-wrap: wrap; }
 
-.mini-kr { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #666; padding: 4px 6px; border-radius: 6px; }
-.mini-kr.mine { background: #fef9e7; border: 1px solid #f9e79f; }
-.mini-kr-num { font-size: 11px; font-weight: 700; color: #2b1f99; width: 32px; }
+.mini-kr { display: flex; align-items: center; gap: 8px; font-size: var(--font-size-sm); color: var(--color-text-secondary); padding: 4px 6px; border-radius: var(--radius-sm); }
+.mini-kr.mine { background: var(--color-surface-tint-warning); border: 1px solid var(--color-border-soft); }
+.mini-kr-num { font-size: 11px; font-weight: 700; color: var(--color-brand); width: 32px; }
 .mini-kr-text { flex: 1; }
-.mini-val { color: #999; }
+.mini-val { color: var(--color-text-muted); }
 
 
-.kr-display { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid #f5f5f5; }
+.kr-display { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid var(--color-border-soft); }
 .kr-display:first-child { border-top: none; }
-.kr-display-num { font-size: 12px; font-weight: 700; color: #3ad4d8; width: 36px; }
-.kr-display-text { flex: 1; font-size: 14px; color: #333; }
+.kr-display-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); width: 36px; }
+.kr-display-text { flex: 1; font-size: var(--font-size-base); color: var(--color-text); }
 .kr-display-meta { display: flex; align-items: center; gap: 8px; }
-.kr-target { font-size: 13px; color: #999; }
+.kr-target { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 
-.kr-progress-row { padding: 16px 0; border-top: 1px solid #f0f0f0; }
+.kr-progress-row { padding: 16px 0; border-top: 1px solid var(--color-border-soft); }
 .kr-progress-row:first-child { border-top: none; }
 .kr-progress-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
-.kr-progress-num { font-size: 12px; font-weight: 700; color: #3ad4d8; width: 36px; }
-.kr-progress-text { flex: 1; font-size: 14px; color: #333; }
-.kr-progress-val { font-size: 13px; color: #999; }
+.kr-progress-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); width: 36px; }
+.kr-progress-text { flex: 1; font-size: var(--font-size-base); color: var(--color-text); }
+.kr-progress-val { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 .kr-progress-ctrl { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
-.kr-status-tag { font-size: 11px; color: #888; width: 60px; }
-.blocked-hint { font-size: 12px; color: #e67e22; margin-top: 6px; }
+.kr-status-tag { font-size: 11px; color: var(--color-text-secondary); width: 60px; }
+.blocked-hint { font-size: var(--font-size-xs); color: var(--color-warning); margin-top: 6px; }
 
 .review-kr { display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 13px; }
-.review-kr-num { font-size: 12px; font-weight: 700; color: #e67e22; width: 32px; }
-.review-kr-text { flex: 1; color: #555; }
-.review-kr-val { color: #999; }
-.review-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
+.review-kr-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-warning); width: 32px; }
+.review-kr-text { flex: 1; color: var(--color-text-secondary); }
+.review-kr-val { color: var(--color-text-muted); }
+.review-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-border-soft); }
 
-.rejected-hint { padding: 14px; background: #fff5f5; border-radius: 8px; font-size: 13px; color: #c62828; margin-top: 12px; }
+.rejected-hint { padding: 14px; background: var(--color-surface-tint-danger); border-radius: var(--radius-md); font-size: var(--font-size-sm); color: var(--color-danger); margin-top: 12px; }
 
 /* Dialog */
-.dialog-rules { background: #f0f8ff; border-radius: 8px; padding: 14px 18px; margin-bottom: 16px; }
-.dr-item { font-size: 13px; color: #333; line-height: 1.8; }
+.dialog-rules { background: var(--color-surface-tint-info); border-radius: var(--radius-md); padding: 14px 18px; margin-bottom: 16px; }
+.dr-item { font-size: var(--font-size-sm); color: var(--color-text); line-height: 1.8; }
 
 /* Alignment section */
-.align-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
-.align-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid #f3f4f6; }
+.align-section { background: var(--color-surface); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md); margin-bottom: 20px; overflow: hidden; }
+.align-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid var(--color-border-soft); }
 .align-head-left { display: flex; align-items: center; gap: 6px; }
-.align-head-icon { font-size: 18px; font-weight: 700; color: #2b1f99; }
-.align-head-title { font-size: 15px; font-weight: 700; color: #1a1a2e; }
-.align-head-hint { font-size: 13px; color: #9ca3af; }
+.align-head-icon { font-size: var(--font-size-xl); font-weight: 700; color: var(--color-brand); }
+.align-head-title { font-size: 15px; font-weight: 700; color: var(--color-text); }
+.align-head-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 
-.align-o-card { padding: 0; border-bottom: 1px solid #f3f4f6; }
+.align-o-card { padding: 0; border-bottom: 1px solid var(--color-border-soft); }
 .align-o-card:last-child { border-bottom: none; }
 .align-o-head { display: flex; align-items: center; gap: 10px; padding: 14px 22px 8px; }
-.align-o-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 6px; background: #2b1f99; color: #fff; font-size: 12px; font-weight: 700; }
-.align-o-title { font-size: 14px; font-weight: 600; color: #1a1a2e; }
+.align-o-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-sm); background: var(--color-brand); color: var(--color-white); font-size: var(--font-size-xs); font-weight: 700; }
+.align-o-title { font-size: var(--font-size-base); font-weight: 600; color: var(--color-text); }
 
 .align-kr-list { padding: 0 22px 14px; display: flex; flex-direction: column; gap: 6px; }
-.align-kr-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 8px; cursor: pointer; transition: all 0.15s; border: 1px solid transparent; }
-.align-kr-row:hover { background: #f9fafb; }
-.align-kr-row.is-selected { background: #f5f3ff; border-color: #d4cdf0; }
-.align-kr-row.is-mandatory { background: #fffdf5; border-color: #fde68a; cursor: default; }
+.align-kr-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: var(--radius-md); cursor: pointer; transition: all 0.15s; border: 1px solid transparent; }
+.align-kr-row:hover { background: var(--color-surface-subtle); }
+.align-kr-row.is-selected { background: var(--color-surface-tint-brand); border-color: var(--color-border); }
+.align-kr-row.is-mandatory { background: var(--color-surface-tint-warning); border-color: var(--color-border-soft); cursor: default; }
 .align-kr-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
-.align-kr-num { font-size: 11px; font-weight: 700; color: #9ca3af; width: 30px; flex-shrink: 0; letter-spacing: 0.5px; }
-.align-checkbox { width: 18px; height: 18px; border-radius: 4px; border: 2px solid #d1d5db; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; color: transparent; flex-shrink: 0; transition: all 0.15s; cursor: pointer; }
-.align-checkbox.checked { background: #2b1f99; border-color: #2b1f99; color: #fff; }
-.align-checkbox.locked { background: #fde68a; border-color: #f59e0b; color: #92400e; cursor: default; }
-.align-kr-title { font-size: 13px; color: #374151; }
+.align-kr-num { font-size: 11px; font-weight: 700; color: var(--color-text-muted); width: 30px; flex-shrink: 0; letter-spacing: 0; }
+.align-checkbox { min-width: 34px; height: 18px; border-radius: var(--radius-xs); border: 1px solid var(--color-border); display: inline-flex; align-items: center; justify-content: center; font-size: 10px; color: transparent; flex-shrink: 0; transition: all 0.15s; cursor: pointer; padding: 0 4px; }
+.align-checkbox.checked { background: var(--color-brand); border-color: var(--color-brand); color: var(--color-white); }
+.align-checkbox.locked { background: var(--color-surface-tint-warning); border-color: var(--color-warning); color: var(--color-warning); cursor: default; }
+.align-kr-title { font-size: var(--font-size-sm); color: var(--color-text); }
 .align-kr-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
-.align-kr-target { font-size: 12px; color: #9ca3af; }
+.align-kr-target { font-size: var(--font-size-xs); color: var(--color-text-muted); }
 
-.align-footer { padding: 12px 22px; background: #f9fafb; border-top: 1px solid #f3f4f6; font-size: 13px; color: #6b7280; text-align: center; }
-.align-footer strong { color: #2b1f99; }
+.align-footer { padding: 12px 22px; background: var(--color-surface-subtle); border-top: 1px solid var(--color-border-soft); font-size: var(--font-size-sm); color: var(--color-text-secondary); text-align: center; }
+.align-footer strong { color: var(--color-brand); }
 
 .parent-align-section { margin-bottom: 20px; }
-.parent-align-label { font-weight: 600; font-size: 15px; color: #1a1a2e; margin-bottom: 8px; }
+.parent-align-label { font-weight: 600; font-size: 15px; color: var(--color-text); margin-bottom: 8px; }
 
 .o-section { margin-bottom: 20px; }
-.o-label { font-weight: 600; font-size: 15px; color: #1a1a2e; margin-bottom: 8px; }
-.o-hint { font-size: 12px; color: #aaa; font-weight: 400; }
+.o-label { font-weight: 600; font-size: 15px; color: var(--color-text); margin-bottom: 8px; }
+.o-hint { font-size: var(--font-size-xs); color: var(--color-text-muted); font-weight: 400; }
 
 .kr-section { margin-bottom: 16px; }
-.kr-section-title { font-weight: 600; font-size: 15px; color: #1a1a2e; margin-bottom: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
-.weight-sum { font-size: 13px; padding: 2px 10px; border-radius: 8px; }
-.weight-ok { background: #eafaf1; color: #1e8449; font-weight: 700; }
-.weight-bad { background: #fdedec; color: #c0392b; font-weight: 700; }
-.kr-form-card { background: #fafbfc; border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; border: 1px solid #f0f0f0; }
-.kr-form-card.has-source { border-left: 3px solid #2b1f99; }
-.kr-source-bar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; padding: 6px 10px; background: #f5f3ff; border-radius: 6px; font-size: 12px; }
-.kr-source-icon { color: #2b1f99; font-weight: 700; }
-.kr-source-text { color: #555; }
+.kr-section-title { font-weight: 600; font-size: 15px; color: var(--color-text); margin-bottom: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+.weight-sum { font-size: var(--font-size-sm); padding: 2px 10px; border-radius: var(--radius-md); }
+.weight-ok { background: var(--color-surface-tint-success); color: var(--color-success); font-weight: 700; }
+.weight-bad { background: var(--color-surface-tint-danger); color: var(--color-danger); font-weight: 700; }
+.kr-form-card { background: var(--color-surface-subtle); border-radius: var(--radius-md); padding: 14px 16px; margin-bottom: 10px; border: 1px solid var(--color-border-soft); }
+.kr-form-card.has-source { border-left: 3px solid var(--color-brand); }
+.kr-source-bar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; padding: 6px 10px; background: var(--color-surface-tint-brand); border-radius: var(--radius-sm); font-size: var(--font-size-xs); }
+.kr-source-icon { color: var(--color-brand); font-weight: 700; }
+.kr-source-text { color: var(--color-text-secondary); }
 .kr-form-head { display: flex; align-items: flex-start; gap: 10px; }
 .kr-form-head .el-textarea { flex: 1; }
-.kr-form-num { font-size: 13px; font-weight: 700; color: #3ad4d8; width: 36px; }
+.kr-form-num { font-size: var(--font-size-sm); font-weight: 700; color: var(--color-accent); width: 36px; }
 .kr-form-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
-.kr-form-label-sm { font-size: 11px; color: #999; }
+.kr-form-label-sm { font-size: 11px; color: var(--color-text-muted); }
 .kr-form-extra { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
-.kr-form-label { font-size: 12px; color: #999; }
+.kr-form-label { font-size: var(--font-size-xs); color: var(--color-text-muted); }
 
-.dialog-kpi-ref { background: #f0f8ff; border: 1px solid #d6eaf8; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; }
-.dkr-title { font-size: 13px; font-weight: 600; color: #2b1f99; margin-bottom: 6px; }
+.dialog-kpi-ref { background: var(--color-surface-tint-info); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md); padding: 12px 16px; margin-bottom: 12px; }
+.dkr-title { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-brand); margin-bottom: 6px; }
 .dkr-tags { display: flex; flex-wrap: wrap; gap: 4px; }
-.dkr-hint { font-size: 11px; color: #999; margin-top: 6px; }
+.dkr-hint { font-size: 11px; color: var(--color-text-muted); margin-top: 6px; }
 
-.dialog-footer-hint { text-align: center; font-size: 12px; color: #aaa; margin-top: 12px; }
+.dialog-footer-hint { text-align: center; font-size: var(--font-size-xs); color: var(--color-text-muted); margin-top: 12px; }
 </style>

+ 257 - 97
frontend/src/views/period/PeriodListView.vue

@@ -216,37 +216,57 @@
     </el-dialog>
 
     <el-dialog v-model="showParticipants" :title="'周期参与人 · ' + selectedPeriod?.name" width="760px">
-      <div class="participant-toolbar">
-        <el-select v-model="participantUserId" filterable placeholder="选择要加入周期的成员" style="width:280px">
-          <el-option v-for="u in candidateUsers" :key="u.id" :label="u.realName + ' · ' + u.username" :value="u.id" />
-        </el-select>
-        <el-button type="primary" @click="handleAddParticipant" :disabled="!participantUserId" round>加入周期</el-button>
-        <span class="participant-count">ACTIVE {{ activeParticipants.length }} / {{ participants.length }}</span>
+      <div class="participant-summary">
+        <div>
+          <div class="participant-summary__title">参与人范围</div>
+          <div class="participant-summary__hint">
+            {{ isDraftPeriod ? '发布 OKR 对齐时将默认纳入全部在职成员。' : '取消员工勾选会记录为本周期豁免,重新勾选会恢复参与。' }}
+          </div>
+        </div>
+        <div class="participant-stat">
+          <strong>{{ participantSelectedCount }}</strong>
+          <span>/ {{ participantTotal }} 人</span>
+        </div>
       </div>
-      <el-table :data="participantRows" size="small" stripe style="width:100%">
-        <el-table-column prop="realName" label="姓名" min-width="120" />
-        <el-table-column prop="position" label="岗位" min-width="140" show-overflow-tooltip />
-        <el-table-column label="状态" width="100">
-          <template #default="{ row }">
-            <el-tag :type="row.status === 'ACTIVE' ? 'success' : 'warning'" size="small" effect="plain" round>
-              {{ row.status === 'ACTIVE' ? '参与' : '豁免' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
-        <el-table-column label="操作" width="180" fixed="right">
-          <template #default="{ row }">
-            <el-button v-if="row.status === 'ACTIVE'" link type="warning" size="small" @click="handleExempt(row)">豁免</el-button>
-            <el-button v-else link type="primary" size="small" @click="handleActivate(row)">恢复参与</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
+      <el-alert
+        v-if="isDraftPeriod"
+        type="info"
+        :closable="false"
+        show-icon
+        class="participant-alert"
+        title="草稿期仅展示默认全员名单,发布后可对个别成员做豁免。"
+      />
+      <el-tree
+        ref="participantTreeRef"
+        v-loading="participantActionLoading"
+        class="participant-tree"
+        :data="participantTree"
+        :props="participantTreeProps"
+        node-key="id"
+        show-checkbox
+        default-expand-all
+        :default-checked-keys="participantCheckKeys"
+        empty-text="暂无可参与成员,请先在组织管理中维护部门和员工"
+        @check-change="handleParticipantCheckChange"
+      >
+        <template #default="{ data }">
+          <div class="participant-node" :class="{ 'is-user': data.type === 'user' }">
+            <span class="participant-node__label">{{ data.label }}</span>
+            <template v-if="data.type === 'user'">
+              <el-tag v-if="data.participantStatus === 'EXEMPT'" size="small" type="warning" effect="plain" round>豁免</el-tag>
+              <el-tag v-else-if="data.participantStatus === 'MISSING'" size="small" type="info" effect="plain" round>未加入</el-tag>
+              <el-tag v-else size="small" type="success" effect="plain" round>参与</el-tag>
+              <span v-if="data.meta" class="participant-node__meta">{{ data.meta }}</span>
+            </template>
+          </div>
+        </template>
+      </el-tree>
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, nextTick, onMounted } from 'vue'
 import { periodApi, orgApi } from '../../api'
 import { useAuthStore } from '../../stores/auth'
 import { ElMessage, ElMessageBox } from 'element-plus'
@@ -260,10 +280,19 @@ const showParticipants = ref(false)
 const selectedPeriod = ref(null)
 const participants = ref([])
 const allUsers = ref([])
-const participantUserId = ref(null)
+const departmentTree = ref([])
+const participantTreeRef = ref(null)
+const participantActionLoading = ref(false)
+const syncingParticipantTree = ref(false)
+const participantCheckKeys = ref([])
 const newPeriod = ref({ name: '', periodType: 'MONTHLY', startDate: '', endDate: '' })
 const typeDays = { WEEKLY: 6, BI_WEEKLY: 13 }
 const typeMonths = { MONTHLY: 1, BI_MONTHLY: 2, QUARTERLY: 3 }
+const participantTreeProps = {
+  children: 'children',
+  label: 'label',
+  disabled: data => data.disabled
+}
 
 function calcEndDate(startDate, periodType) {
   // Parse as UTC midnight (consistent with toISOString formatting used elsewhere)
@@ -343,51 +372,142 @@ onMounted(() => load())
 async function load() { periods.value = await periodApi.list() }
 
 const activeParticipants = computed(() => participants.value.filter(p => p.status === 'ACTIVE'))
-const participantRows = computed(() => participants.value.map(p => {
-  const user = allUsers.value.find(u => u.id === p.userId) || {}
-  return { ...p, realName: user.realName || ('用户 ' + p.userId), username: user.username || '', position: user.position || '' }
-}))
-const candidateUsers = computed(() => {
-  const existing = new Set(participants.value.map(p => p.userId))
-  return allUsers.value.filter(u => !existing.has(u.id) && u.status !== 'DISABLED' && u.status !== 'LEFT')
+const isDraftPeriod = computed(() => selectedPeriod.value?.status === 'DRAFT')
+const participantByUserId = computed(() => {
+  const map = new Map()
+  participants.value.forEach(p => map.set(p.userId, p))
+  return map
+})
+const eligibleUsers = computed(() => allUsers.value.filter(isEligibleParticipantUser))
+const participantTotal = computed(() => eligibleUsers.value.length)
+const participantSelectedCount = computed(() => isDraftPeriod.value ? participantTotal.value : activeParticipants.value.length)
+const activeParticipantKeys = computed(() => {
+  if (isDraftPeriod.value) return eligibleUsers.value.map(u => userNodeId(u.id))
+  return activeParticipants.value.map(p => userNodeId(p.userId))
+})
+const participantTree = computed(() => {
+  const usersByDept = new Map()
+  allUsers.value
+    .filter(u => isEligibleParticipantUser(u) || participantByUserId.value.has(u.id))
+    .forEach(u => {
+      if (!u.departmentId) return
+      if (!usersByDept.has(u.departmentId)) usersByDept.set(u.departmentId, [])
+      usersByDept.get(u.departmentId).push(u)
+    })
+
+  return departmentTree.value
+    .map(dept => buildParticipantDeptNode(dept, usersByDept))
+    .filter(Boolean)
 })
 
 async function openParticipants(period) {
   selectedPeriod.value = period
   showParticipants.value = true
-  participantUserId.value = null
-  participants.value = await periodApi.getParticipants(period.id)
-  allUsers.value = await orgApi.getUsers()
+  participantActionLoading.value = true
+  try {
+    const [participantData, userData, treeData] = await Promise.all([
+      periodApi.getParticipants(period.id),
+      orgApi.getUsers(),
+      orgApi.getTree()
+    ])
+    participants.value = participantData
+    allUsers.value = userData
+    departmentTree.value = treeData
+    await syncParticipantTree()
+  } finally {
+    participantActionLoading.value = false
+  }
 }
 
 async function refreshParticipants() {
   if (!selectedPeriod.value) return
   participants.value = await periodApi.getParticipants(selectedPeriod.value.id)
+  await syncParticipantTree()
 }
 
-async function handleAddParticipant() {
-  await periodApi.addParticipant(selectedPeriod.value.id, participantUserId.value)
-  ElMessage.success('已加入周期')
-  participantUserId.value = null
-  await refreshParticipants()
+function isEligibleParticipantUser(user) {
+  return user.departmentId && user.status !== 'DISABLED' && user.status !== 'LEFT'
 }
 
-async function handleExempt(row) {
-  const { value } = await ElMessageBox.prompt('请输入豁免原因', '豁免参与人', {
-    inputType: 'textarea',
-    inputPlaceholder: '如:本周期休假/新入职不参与考核',
-    confirmButtonText: '确认豁免',
-    cancelButtonText: '取消'
-  })
-  await periodApi.exemptParticipant(selectedPeriod.value.id, row.userId, value)
-  ElMessage.success('已豁免')
-  await refreshParticipants()
+function userNodeId(userId) {
+  return `user-${userId}`
 }
 
-async function handleActivate(row) {
-  await periodApi.activateParticipant(selectedPeriod.value.id, row.userId)
-  ElMessage.success('已恢复参与')
-  await refreshParticipants()
+function buildParticipantDeptNode(dept, usersByDept) {
+  const childDeptNodes = (dept.children || [])
+    .map(child => buildParticipantDeptNode(child, usersByDept))
+    .filter(Boolean)
+  const userNodes = (usersByDept.get(dept.id) || [])
+    .slice()
+    .sort((a, b) => (a.realName || a.username || '').localeCompare(b.realName || b.username || '', 'zh-Hans-CN'))
+    .map(user => {
+      const participant = participantByUserId.value.get(user.id)
+      const status = isDraftPeriod.value ? 'ACTIVE' : (participant?.status || 'MISSING')
+      return {
+        id: userNodeId(user.id),
+        label: user.realName || user.username || `用户 ${user.id}`,
+        type: 'user',
+        userId: user.id,
+        participantStatus: status,
+        disabled: isDraftPeriod.value || participantActionLoading.value,
+        meta: user.position || user.username || ''
+      }
+    })
+  const children = [...childDeptNodes, ...userNodes]
+  if (!children.length) return null
+  return {
+    id: `dept-${dept.id}`,
+    label: dept.name,
+    type: 'dept',
+    disabled: true,
+    children
+  }
+}
+
+async function syncParticipantTree() {
+  participantCheckKeys.value = activeParticipantKeys.value
+  syncingParticipantTree.value = true
+  await nextTick()
+  participantTreeRef.value?.setCheckedKeys(participantCheckKeys.value)
+  await nextTick()
+  syncingParticipantTree.value = false
+}
+
+async function handleParticipantCheckChange(node, checked) {
+  if (syncingParticipantTree.value || participantActionLoading.value || node.type !== 'user') return
+  if (isDraftPeriod.value) {
+    ElMessage.info('草稿期默认全员纳入,发布后可豁免个别成员')
+    await syncParticipantTree()
+    return
+  }
+
+  participantActionLoading.value = true
+  try {
+    const existing = participantByUserId.value.get(node.userId)
+    if (checked) {
+      if (!existing) {
+        await periodApi.addParticipant(selectedPeriod.value.id, node.userId)
+      } else if (existing.status !== 'ACTIVE') {
+        await periodApi.activateParticipant(selectedPeriod.value.id, node.userId)
+      }
+      ElMessage.success('已恢复参与')
+    } else {
+      const { value } = await ElMessageBox.prompt('请输入豁免原因', '豁免参与人', {
+        inputType: 'textarea',
+        inputPlaceholder: '如:本周期休假/新入职不参与考核',
+        confirmButtonText: '确认豁免',
+        cancelButtonText: '取消',
+        inputValidator: val => !!String(val || '').trim() || '请填写豁免原因'
+      })
+      await periodApi.exemptParticipant(selectedPeriod.value.id, node.userId, value)
+      ElMessage.success('已豁免')
+    }
+    await refreshParticipants()
+  } catch {
+    await syncParticipantTree()
+  } finally {
+    participantActionLoading.value = false
+  }
 }
 
 async function handleCreate() {
@@ -454,60 +574,100 @@ function statusGuide(p) {
 
 <style scoped>
 .period-workbench { max-width: 960px; }
-.page-hero { margin-bottom: 18px; }
-.period-metrics { margin-bottom: 18px; }
+.page-hero { margin-bottom: var(--spacing-lg); }
+.period-metrics { margin-bottom: var(--spacing-lg); }
 .empty-state { min-height: 300px; }
-.empty-hint { font-size: 13px; color: #bbb; }
+.empty-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 
 /* Tabs */
-.tab-bar { display: flex; gap: 4px; margin-bottom: 24px; background: #f5f7fa; border-radius: 10px; padding: 4px; }
-.tab-item { padding: 8px 20px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 14px; color: #888; transition: all 0.2s; }
-.tab-item:hover { color: #333; background: #eef0f4; }
-.tab-item.active { background: #fff; color: #2b1f99; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
-.tab-count { font-size: 12px; background: #e8eaed; border-radius: 10px; padding: 1px 8px; color: #888; }
-.tab-item.active .tab-count { background: #2b1f99; color: #fff; }
+.tab-bar { display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-xl); background: var(--color-surface-subtle); border-radius: var(--radius-md); padding: var(--spacing-xs); }
+.tab-item { padding: var(--spacing-sm) var(--spacing-lg); border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-base); color: var(--color-text-muted); transition: all var(--transition-fast); }
+.tab-item:hover { color: var(--color-text); background: var(--color-surface-muted); }
+.tab-item.active { background: var(--color-surface); color: var(--color-brand); font-weight: 600; box-shadow: var(--shadow-panel); }
+.tab-count { font-size: var(--font-size-xs); background: var(--color-surface-muted); border-radius: var(--radius-xs); padding: 1px var(--spacing-sm); color: var(--color-text-muted); }
+.tab-item.active .tab-count { background: var(--color-brand); color: var(--color-white); }
 
 /* Category */
-.category { margin-bottom: 32px; }
-.cat-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
-.cat-badge { padding: 5px 16px; border-radius: 8px; font-size: 14px; font-weight: 700; color: #fff; }
-.cat-badge.active { background: #2b1f99; }
-.cat-badge.upcoming { background: #909399; }
-.cat-badge.archived { background: #b0b4ba; }
-.cat-count { font-size: 13px; color: #999; }
+.category { margin-bottom: var(--spacing-2xl); }
+.cat-head { display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md); }
+.cat-badge { padding: 5px var(--spacing-lg); border-radius: var(--radius-sm); font-size: var(--font-size-base); font-weight: 700; color: var(--color-white); }
+.cat-badge.active { background: var(--color-brand); }
+.cat-badge.upcoming { background: var(--color-info); }
+.cat-badge.archived { background: var(--color-text-muted); }
+.cat-count { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 
 /* Sub-group */
-.sub-group { margin-bottom: 16px; }
-.sub-label { font-size: 13px; font-weight: 600; color: #666; margin-bottom: 8px; padding-left: 4px; }
+.sub-group { margin-bottom: var(--spacing-md); }
+.sub-label { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); padding-left: var(--spacing-xs); }
 
 /* Card */
 .period-card {
-  background: #fff; border-radius: 10px; padding: 0; margin-bottom: 8px;
-  box-shadow: 0 1px 3px rgba(0,0,0,0.04); overflow: hidden; border: 1px solid #f0f0f0;
-  border-left: 5px solid #ddd;
+  background: var(--color-surface); border-radius: var(--radius-md); padding: 0; margin-bottom: var(--spacing-sm);
+  box-shadow: var(--shadow-panel); overflow: hidden; border: 1px solid var(--color-border-soft);
+  border-left: 4px solid var(--color-border);
 }
-.period-card.status-EXECUTING { border-left-color: #3ad4d8; }
-.period-card.status-ASSESSING { border-left-color: #e6a23c; }
-.period-card.status-OKR_ALIGN { border-left-color: #409eff; }
-.period-card.status-DRAFT { border-left-color: #c0c4cc; }
-.period-card.status-ARCHIVED { border-left-color: #909399; opacity: 0.7; }
-
-.card-body { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; gap: 24px; }
-.card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
-.card-info { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
-.card-name { font-size: 15px; font-weight: 700; color: #1a1a2e; }
-.card-date { font-size: 12px; color: #aaa; }
-.card-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
+.period-card.status-EXECUTING { border-left-color: var(--color-accent); }
+.period-card.status-ASSESSING { border-left-color: var(--color-warning); }
+.period-card.status-OKR_ALIGN { border-left-color: var(--color-info); }
+.period-card.status-DRAFT { border-left-color: var(--color-border); }
+.period-card.status-ARCHIVED { border-left-color: var(--color-text-muted); opacity: 0.75; }
+
+.card-body { padding: var(--spacing-md) var(--spacing-lg); display: flex; justify-content: space-between; align-items: center; gap: var(--spacing-lg); }
+.card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--spacing-xs); }
+.card-info { display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; }
+.card-name { font-size: var(--font-size-lg); font-weight: 700; color: var(--color-text); }
+.card-date { font-size: var(--font-size-xs); color: var(--color-text-muted); }
+.card-actions { display: flex; gap: var(--spacing-xs); flex-shrink: 0; align-items: center; }
 .card-actions .el-button { white-space: nowrap; }
-.card-guide { font-size: 12px; color: #888; padding: 8px 20px 10px; background: #fafbfc; border-top: 1px solid #f0f0f0; }
+.card-guide { font-size: var(--font-size-xs); color: var(--color-text-secondary); padding: var(--spacing-sm) var(--spacing-lg) 10px; background: var(--color-surface-subtle); border-top: 1px solid var(--color-border-soft); }
 
 /* Dialog */
-.batch-preview { background: #fafbfc; border-radius: 8px; padding: 12px 16px; margin-top: 8px; border: 1px solid #e8eaed; }
-.bp-title { font-size: 12px; font-weight: 600; color: #2b1f99; margin-bottom: 8px; }
-.bp-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
-.bp-num { width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 50%; background: #3ad4d8; color: #fff; font-size: 11px; font-weight: 700; }
-.bp-name { color: #333; font-weight: 500; min-width: 80px; }
-.bp-date { color: #999; }
-.participant-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
-.participant-count { margin-left: auto; font-size: 12px; color: #667085; }
+.batch-preview { background: var(--color-surface-subtle); border-radius: var(--radius-sm); padding: var(--spacing-sm) var(--spacing-md); margin-top: var(--spacing-sm); border: 1px solid var(--color-border-soft); }
+.bp-title { font-size: var(--font-size-xs); font-weight: 600; color: var(--color-brand); margin-bottom: var(--spacing-sm); }
+.bp-item { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-xs) 0; font-size: var(--font-size-xs); }
+.bp-num { width: var(--font-size-xl); height: var(--font-size-xl); line-height: var(--font-size-xl); text-align: center; border-radius: 50%; background: var(--color-accent); color: var(--color-white); font-size: var(--font-size-xs); font-weight: 700; }
+.bp-name { color: var(--color-text); font-weight: 500; min-width: 80px; }
+.bp-date { color: var(--color-text-muted); }
+.participant-summary {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: var(--spacing-md);
+  padding: var(--spacing-md) var(--spacing-lg);
+  margin-bottom: var(--spacing-sm);
+  border: 1px solid var(--color-border-soft);
+  border-radius: var(--radius-md);
+  background: var(--color-surface-tint-brand);
+}
+.participant-summary__title { font-size: var(--font-size-lg); font-weight: 700; color: var(--color-text); margin-bottom: var(--spacing-xs); }
+.participant-summary__hint { font-size: var(--font-size-sm); color: var(--color-text-secondary); line-height: 1.6; }
+.participant-stat { min-width: 88px; text-align: right; color: var(--color-text-secondary); }
+.participant-stat strong { font-size: var(--font-size-2xl); color: var(--color-brand); line-height: 1; }
+.participant-stat span { font-size: var(--font-size-sm); }
+.participant-alert { margin-bottom: var(--spacing-sm); }
+.participant-tree {
+  max-height: 460px;
+  overflow: auto;
+  padding: var(--spacing-sm) var(--spacing-xs);
+  border: 1px solid var(--color-border-soft);
+  border-radius: var(--radius-md);
+  background: var(--color-surface);
+}
+.participant-node {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: var(--spacing-sm);
+  min-width: 0;
+}
+.participant-node.is-user { color: var(--color-text); }
+.participant-node__label { font-size: var(--font-size-sm); font-weight: 600; }
+.participant-node__meta {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: var(--font-size-xs);
+  color: var(--color-text-muted);
+}
 </style>

+ 87 - 91
frontend/src/views/scoring/MyResultView.vue

@@ -17,7 +17,7 @@
       <div class="form-banner">请完成绩效自评后提交,上级将在此基础进行终审</div>
 
       <div class="form-card">
-        <div class="form-section-title">OKR 目标达成自评 <span class="fst-sub">逐条 KR 打分(0.0-1.0),满分 60</span></div>
+        <div class="form-section-title">OKR 目标达成自评 <span class="fst-sub">逐条 KR 打分(0.0-1.0),满分 40</span></div>
         <div v-if="okrRecords.length" class="kr-score-list">
           <div v-for="(kr, i) in okrRecords[0].keyResults" :key="kr.id" class="kr-score-row">
             <div class="ksr-label">
@@ -33,18 +33,18 @@
                 <el-option label="0.3 · 少量推进" :value="0.3" />
                 <el-option label="0.0 · 未做" :value="0.0" />
               </el-select>
-              <span class="ksr-eq">= {{ (krScores[i].score * 60 / okrRecords[0].keyResults.length).toFixed(1) }} 分</span>
+              <span class="ksr-eq">= {{ (krScores[i].score * 40 / okrRecords[0].keyResults.length).toFixed(1) }} 分</span>
             </div>
           </div>
         </div>
         <div v-else class="no-okr-hint">暂无 OKR 记录</div>
         <div class="okr-subtotal">
-          OKR 得分 = 平均 {{ krAvg.toFixed(2) }} × 60 = <strong>{{ okrTotal.toFixed(1) }} / 60</strong>
+          OKR 得分 = 平均 {{ krAvg.toFixed(2) }} × 40 = <strong>{{ okrTotal.toFixed(1) }} / 40</strong>
         </div>
       </div>
 
       <div class="form-card">
-        <div class="form-section-title">工作成果自评 <span class="fst-sub">按维度自述 + 打分,满分 40</span></div>
+        <div class="form-section-title">工作成果自评 <span class="fst-sub">按维度自述 + 打分,满分 60</span></div>
         <div v-for="(dim, i) in dimScores" :key="i" class="dim-row">
           <div class="dim-head">
             <span class="dim-num">{{ i+1 }}</span>
@@ -56,7 +56,7 @@
           <span class="dim-score-hint">/ {{ dim.maxScore }} 分</span>
         </div>
         <div class="okr-subtotal">
-          工作成果得分 = <strong>{{ dimTotal.toFixed(1) }} / 40</strong>
+          工作成果得分 = <strong>{{ dimTotal.toFixed(1) }} / 60</strong>
         </div>
       </div>
 
@@ -99,11 +99,11 @@
         <div class="score-breakdown">
           <div class="break-item">
             <span class="break-label">OKR 得分</span>
-            <span class="break-value">{{ data.finalScore.okrScore }}<small> × 0.6</small></span>
+            <span class="break-value">{{ data.finalScore.okrScore }}<small> / 40</small></span>
           </div>
           <div class="break-item">
             <span class="break-label">工作成果</span>
-            <span class="break-value">{{ data.finalScore.kpiScore }}<small> × 0.4</small></span>
+            <span class="break-value">{{ data.finalScore.kpiScore }}<small> / 60</small></span>
           </div>
           <div class="break-item bonus">
             <span class="break-label">专项加分</span>
@@ -117,7 +117,11 @@
       </div>
 
       <div v-else-if="data.selfScore" class="score-hero self-submitted">
-        <div class="pending-icon">📤</div>
+        <div class="pending-icon pending-icon--submitted" aria-hidden="true">
+          <svg viewBox="0 0 48 48" focusable="false">
+            <path d="M12 34h24M16 26l8-8 8 8M24 18v20M14 10h20a4 4 0 0 1 4 4v4M10 18v-4a4 4 0 0 1 4-4" />
+          </svg>
+        </div>
         <div class="score-main">
           <div class="score-number">{{ data.selfScore.total }}<small> / 100</small></div>
           <div class="score-label">自评已提交,等待上级终审</div>
@@ -167,7 +171,7 @@
           </div>
         </div>
         <div class="col feedback-col">
-          <div class="col-head">💬 面谈反馈</div>
+          <div class="col-head">面谈反馈</div>
           <div v-if="data.feedback?.interviewNotes" class="fb-content">
             <div class="fb-block" v-if="data.feedback.highlights">
               <div class="fb-label">亮点</div>
@@ -227,7 +231,7 @@ const okrTotal = computed(() => {
   if (okrRecords.value.length && okrRecords.value[0].keyResults?.length) {
     const n = okrRecords.value[0].keyResults.length
     const sum = krScores.value.reduce((a, b) => a + b.score, 0)
-    return Math.round(sum / n * 60 * 10) / 10
+    return Math.round(sum / n * 40 * 10) / 10
   }
   return 0
 })
@@ -333,129 +337,121 @@ function gradeDesc(g) {
 <style scoped>
 .page { width: 100%; }
 .page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
-.hero-info h2 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0; }
-.hero-info p { font-size: 14px; color: #888; margin: 4px 0 0; }
-.empty-state { display: flex; align-items: center; justify-content: center; height: 300px; color: #bbb; font-size: 15px; }
+.hero-info h2 { font-size: var(--font-size-2xl); font-weight: 700; color: var(--color-text); margin: 0; }
+.hero-info p { font-size: var(--font-size-base); color: var(--color-text-secondary); margin: 4px 0 0; }
+.empty-state { display: flex; align-items: center; justify-content: center; height: 300px; color: var(--color-text-muted); font-size: 15px; }
 
-/* Self-assessment form */
-.form-banner { padding: 14px 20px; background: #e0f7fa; border-radius: 10px; font-size: 14px; color: #00838f; border: 1px solid #b2ebf2; margin-bottom: 20px; }
-.form-card { background: #fff; border-radius: 10px; padding: 20px 24px; margin-bottom: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.form-section-title { font-size: 15px; font-weight: 700; color: #1a1a2e; margin-bottom: 16px; }
-.fst-sub { font-size: 12px; color: #aaa; font-weight: 400; }
+.form-banner { padding: 14px 20px; background: var(--color-surface-tint-info); border-radius: var(--radius-md); font-size: var(--font-size-base); color: var(--color-medical); border: 1px solid var(--color-border-soft); margin-bottom: 20px; }
+.form-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 20px 24px; margin-bottom: 16px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.form-section-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 16px; }
+.fst-sub { font-size: var(--font-size-xs); color: var(--color-text-muted); font-weight: 400; }
 
-.kr-score-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-top: 1px solid #f5f5f5; }
+.kr-score-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-top: 1px solid var(--color-border-soft); }
 .kr-score-row:first-child { border-top: none; }
 .ksr-label { flex: 1; min-width: 0; }
-.ksr-num { font-size: 12px; font-weight: 700; color: #3ad4d8; margin-right: 6px; }
-.ksr-title { font-size: 13px; color: #333; }
-.ksr-progress { font-size: 11px; color: #999; margin-left: 6px; }
+.ksr-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); margin-right: 6px; }
+.ksr-title { font-size: var(--font-size-sm); color: var(--color-text); }
+.ksr-progress { font-size: 11px; color: var(--color-text-muted); margin-left: 6px; }
 .ksr-inputs { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
-.ksr-eq { font-size: 12px; color: #999; white-space: nowrap; }
-
-.no-okr-hint { font-size: 13px; color: #ccc; padding: 20px 0; text-align: center; }
-
-.okr-subtotal { text-align: right; font-size: 14px; color: #2b1f99; padding-top: 12px; border-top: 1px dashed #e0e0e0; margin-top: 8px; }
+.ksr-eq { font-size: var(--font-size-xs); color: var(--color-text-muted); white-space: nowrap; }
+.no-okr-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); padding: 20px 0; text-align: center; }
+.okr-subtotal { text-align: right; font-size: var(--font-size-base); color: var(--color-brand); padding-top: 12px; border-top: 1px dashed var(--color-border); margin-top: 8px; }
 
-.dim-row { padding: 14px 0; border-top: 1px solid #f5f5f5; }
+.dim-row { padding: 14px 0; border-top: 1px solid var(--color-border-soft); }
 .dim-row:first-child { border-top: none; }
 .dim-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
-.dim-num { width: 22px; height: 22px; border-radius: 50%; background: #2b1f99; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; }
-.dim-name { font-size: 14px; font-weight: 600; color: #1a1a2e; }
-.dim-max { font-size: 12px; color: #999; }
-.dim-score-hint { font-size: 13px; color: #999; margin-left: 6px; }
-
+.dim-num { width: 22px; height: 22px; border-radius: 50%; background: var(--color-brand); color: var(--color-white); display: flex; align-items: center; justify-content: center; font-size: var(--font-size-xs); font-weight: 700; }
+.dim-name { font-size: var(--font-size-base); font-weight: 600; color: var(--color-text); }
+.dim-max, .dim-score-hint, .bonus-label, .submit-hint { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 .bonus-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
-.bonus-label { font-size: 13px; color: #666; }
 
 .result-preview {
-  background: #f4f2f9; border-radius: 8px;
-  padding: 20px; text-align: center; border: 1px solid #dcd4f0; margin-bottom: 16px;
+  background: var(--color-surface-tint-brand);
+  border-radius: var(--radius-md);
+  padding: 20px;
+  text-align: center;
+  border: 1px solid var(--color-border-soft);
+  margin-bottom: 16px;
 }
-.rp-formula { font-size: 12px; color: #888; }
-.rp-score { font-size: 40px; font-weight: 700; color: #2b1f99; margin: 6px 0; line-height: 1; }
-.rp-score small { font-size: 18px; color: #999; font-weight: 400; }
-.rp-grade { font-size: 14px; color: #666; }
-
+.rp-formula { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
+.rp-score { font-size: 40px; font-weight: 700; color: var(--color-brand); margin: 6px 0; line-height: 1; }
+.rp-score small { font-size: var(--font-size-xl); color: var(--color-text-muted); font-weight: 400; }
+.rp-grade { font-size: var(--font-size-base); color: var(--color-text-secondary); }
 .submit-row { display: flex; align-items: center; gap: 16px; }
-.submit-hint { font-size: 13px; color: #aaa; }
-
-/* Result view */
-.dashboard { }
 
 .score-hero {
   display: flex; align-items: center; gap: 32px;
-  background: #f6f6fb; border-radius: 8px;
-  padding: 32px 40px; margin-bottom: 24px; border: 1px solid #e8e8f0;
+  background: var(--color-surface-tint-brand);
+  border-radius: var(--radius-md);
+  padding: 32px 40px; margin-bottom: 24px; border: 1px solid var(--color-border-soft);
 }
-.score-hero.self-submitted { background: linear-gradient(135deg, #fffcf5, #fef9e7); border-color: #f9e79f; }
+.score-hero.self-submitted { background: var(--color-surface-tint-warning); border-color: var(--color-border-soft); }
 .grade-ring {
   width: 100px; height: 100px; border-radius: 50%;
   display: flex; align-items: center; justify-content: center;
-  border: 4px solid #ddd; flex-shrink: 0;
+  border: 4px solid var(--color-border); flex-shrink: 0;
+  background: var(--color-surface);
 }
-.grade-ring.grade-S { border-color: #f1c40f; background: #fef9e7; }
-.grade-ring.grade-A { border-color: #27ae60; background: #eafaf1; }
-.grade-ring.grade-B { border-color: #3498db; background: #ebf5fb; }
-.grade-ring.grade-C { border-color: #e74c3c; background: #fdedec; }
-.grade-letter { font-size: 48px; font-weight: 800; color: #1a1a2e; }
+.grade-ring.grade-S { border-color: var(--color-grade-s); background: var(--color-surface-tint-success); }
+.grade-ring.grade-A { border-color: var(--color-grade-a); background: var(--color-surface-tint-success); }
+.grade-ring.grade-B { border-color: var(--color-grade-b); background: var(--color-surface-tint-brand); }
+.grade-ring.grade-C { border-color: var(--color-grade-c); background: var(--color-surface-tint-danger); }
+.grade-letter { font-size: 48px; font-weight: 700; color: var(--color-text); }
 
 .score-main { flex: 1; }
-.score-number { font-size: 48px; font-weight: 700; color: #1a1a2e; line-height: 1; }
-.score-number small { font-size: 20px; color: #999; font-weight: 400; }
-.score-label { font-size: 15px; color: #555; margin: 6px 0; }
-.score-meta { font-size: 13px; color: #aaa; }
-
+.score-number { font-size: 48px; font-weight: 700; color: var(--color-text); line-height: 1; }
+.score-number small { font-size: 20px; color: var(--color-text-muted); font-weight: 400; }
+.score-label { font-size: 15px; color: var(--color-text-secondary); margin: 6px 0; }
+.score-meta { font-size: var(--font-size-sm); color: var(--color-text-muted); }
 .score-breakdown { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; flex-shrink: 0; }
 .break-item { text-align: right; }
-.break-label { font-size: 12px; color: #999; display: block; }
-.break-value { font-size: 18px; font-weight: 600; color: #333; }
-.break-value small { font-size: 12px; color: #aaa; font-weight: 400; }
-.break-item.bonus .break-value { color: #27ae60; }
-.break-item.penalty .break-value { color: #e74c3c; }
+.break-label { font-size: var(--font-size-xs); color: var(--color-text-muted); display: block; }
+.break-value { font-size: var(--font-size-xl); font-weight: 600; color: var(--color-text); }
+.break-value small { font-size: var(--font-size-xs); color: var(--color-text-muted); font-weight: 400; }
+.break-item.bonus .break-value { color: var(--color-success); }
+.break-item.penalty .break-value { color: var(--color-danger); }
 
 .score-pending {
-  background: #fff; border-radius: 8px; padding: 60px; text-align: center;
-  margin-bottom: 24px; border: 1px solid #eee;
+  background: var(--color-surface); border-radius: var(--radius-md); padding: 60px; text-align: center;
+  margin-bottom: 24px; border: 1px solid var(--color-border-soft);
 }
-.pending-icon { font-size: 48px; margin-bottom: 12px; }
-.pending-text { font-size: 15px; color: #999; }
+.pending-icon { width: 48px; height: 48px; margin: 0 auto 12px; color: var(--color-warning); }
+.pending-icon svg { width: 48px; height: 48px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
+.pending-text { font-size: 15px; color: var(--color-text-muted); }
 
 .confirm-bar {
   display: flex; align-items: center; justify-content: space-between;
-  background: #fef9e7; border: 2px solid #f9e79f; border-radius: 8px;
+  background: var(--color-surface-tint-warning); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md);
   padding: 18px 24px; margin-bottom: 24px;
 }
-.confirm-info { display: flex; align-items: center; gap: 10px; font-size: 14px; color: #7d6608; }
+.confirm-info { display: flex; align-items: center; gap: 10px; font-size: var(--font-size-base); color: var(--color-warning); }
 .confirm-icon { font-size: 20px; }
 .confirm-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
 .confirmed-bar {
-  background: #eafaf1; border: 1px solid #abebc6; border-radius: 10px;
-  padding: 14px 24px; text-align: center; font-size: 14px; color: #1e8449; margin-bottom: 24px;
+  background: var(--color-surface-tint-success); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md);
+  padding: 14px 24px; text-align: center; font-size: var(--font-size-base); color: var(--color-success); margin-bottom: 24px;
 }
 
 .compare-row { display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 24px; }
-.compare-card { background: #fff; border-radius: 8px; padding: 16px 28px; text-align: center; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.compare-card.self { border-left: 4px solid #3498db; }
-.compare-card.final { border-left: 4px solid #27ae60; }
-.compare-label { font-size: 12px; color: #999; margin-bottom: 4px; }
-.compare-score { font-size: 28px; font-weight: 700; color: #1a1a2e; }
-.compare-score small { font-size: 14px; color: #999; font-weight: 400; }
-.compare-arrow { font-size: 24px; color: #ccc; }
+.compare-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 16px 28px; text-align: center; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.compare-card.self { border-left: 4px solid var(--color-info); }
+.compare-card.final { border-left: 4px solid var(--color-success); }
+.compare-label { font-size: var(--font-size-xs); color: var(--color-text-muted); margin-bottom: 4px; }
+.compare-score { font-size: 28px; font-weight: 700; color: var(--color-text); }
+.compare-score small { font-size: var(--font-size-base); color: var(--color-text-muted); font-weight: 400; }
+.compare-arrow { font-size: 24px; color: var(--color-text-muted); }
 
 .two-col { display: flex; gap: 20px; }
-.col { flex: 1; background: #fff; border-radius: 8px; padding: 20px 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.col-head { font-size: 15px; font-weight: 700; color: #1a1a2e; margin-bottom: 16px; }
-
-.mini-okr { padding: 10px; margin-bottom: 10px; background: #fafbfc; border-radius: 8px; border: 1px solid #f0f0f0; }
-.mini-o { font-weight: 600; font-size: 13px; color: #2b1f99; margin-bottom: 6px; }
-.mini-kr { display: flex; justify-content: space-between; font-size: 12px; color: #666; padding: 2px 0; }
-.mini-val { color: #999; }
-
-.fb-content { }
+.col { flex: 1; background: var(--color-surface); border-radius: var(--radius-md); padding: 20px 24px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.col-head { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 16px; }
+.mini-okr { padding: 10px; margin-bottom: 10px; background: var(--color-surface-subtle); border-radius: var(--radius-md); border: 1px solid var(--color-border-soft); }
+.mini-o { font-weight: 600; font-size: var(--font-size-sm); color: var(--color-brand); margin-bottom: 6px; }
+.mini-kr { display: flex; justify-content: space-between; font-size: var(--font-size-xs); color: var(--color-text-secondary); padding: 2px 0; }
+.mini-val { color: var(--color-text-muted); }
 .fb-block { margin-bottom: 14px; }
-.fb-label { font-size: 11px; font-weight: 600; color: #3ad4d8; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
-.fb-text { font-size: 13px; color: #555; line-height: 1.6; }
-.fb-empty { color: #ccc; font-size: 13px; text-align: center; padding: 30px; }
+.fb-label { font-size: 11px; font-weight: 600; color: var(--color-accent-strong); text-transform: uppercase; letter-spacing: 0; margin-bottom: 4px; }
+.fb-text { font-size: var(--font-size-sm); color: var(--color-text-secondary); line-height: 1.6; }
+.fb-empty { color: var(--color-text-muted); font-size: var(--font-size-sm); text-align: center; padding: 30px; }
 
 @media (max-width: 860px) {
   .score-hero { flex-direction: column; text-align: center; }

+ 86 - 94
frontend/src/views/scoring/ScoringEditView.vue

@@ -189,7 +189,7 @@
 
         <!-- Result -->
         <div class="result-preview">
-          <div class="rp-formula">OKR(40%) + 工作成果(60%) + 加分 − 扣分</div>
+          <div class="rp-formula">OKR 40 分制 + 工作成果 60 分制 + 加分 − 扣分</div>
           <div class="rp-score">{{ computedTotal }} <small>/ 100</small></div>
           <div class="rp-grade">评级 <strong>{{ computedGrade }}</strong></div>
         </div>
@@ -372,7 +372,7 @@ async function handlePublish() {
 }
 
 function krPercent(kr) { if (!kr.targetValue || kr.targetValue === 0) return 0; return Math.min((kr.currentValue / kr.targetValue) * 100, 100) }
-function krColor(s) { return { COMPLETED: '#27ae60', BLOCKED: '#e67e22', IN_PROGRESS: '#3ad4d8', NOT_STARTED: '#c0c4cc' }[s] || '#c0c4cc' }
+function krColor(s) { return { COMPLETED: 'var(--color-success)', BLOCKED: 'var(--color-warning)', IN_PROGRESS: 'var(--color-accent)', NOT_STARTED: 'var(--color-text-muted)' }[s] || 'var(--color-text-muted)' }
 function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进行中', COMPLETED: '已完成', BLOCKED: '阻塞' }[s] || s }
 function krStatusTag(s) { return { COMPLETED: 'success', BLOCKED: 'danger', IN_PROGRESS: '', NOT_STARTED: 'info' }[s] || 'info' }
 function fmtTime(t) { if (!t) return ''; return t.slice(0, 16).replace('T', ' ') }
@@ -381,123 +381,115 @@ function fmtTime(t) { if (!t) return ''; return t.slice(0, 16).replace('T', ' ')
 <style scoped>
 .page { width: 100%; }
 .page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
-.hero-info h2 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0; }
-.hero-info p { font-size: 14px; color: #888; margin: 4px 0 0; }
+.hero-info h2 { font-size: var(--font-size-2xl); font-weight: 700; color: var(--color-text); margin: 0; }
+.hero-info p { font-size: var(--font-size-base); color: var(--color-text-secondary); margin: 4px 0 0; }
 
 .review-workbench { display: flex; gap: 20px; align-items: flex-start; }
 .evidence-panel {
-  flex: 1; background: #fff; border-radius: 8px; padding: 20px 24px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0;
-  max-height: calc(100vh - 160px); overflow-y: auto;
+  flex: 1;
+  background: var(--color-surface);
+  border-radius: var(--radius-md);
+  padding: 20px 24px;
+  box-shadow: var(--shadow-panel);
+  border: 1px solid var(--color-border-soft);
+  max-height: calc(100vh - 160px);
+  overflow-y: auto;
 }
 .decision-panel { width: 480px; flex-shrink: 0; max-height: calc(100vh - 160px); overflow-y: auto; }
-.panel-head { font-size: 15px; font-weight: 700; color: #1a1a2e; margin-bottom: 16px; }
-.panel-empty { color: #ccc; text-align: center; padding: 20px; font-size: 13px; }
+.panel-head { font-size: 15px; font-weight: 700; color: var(--color-text); margin-bottom: 16px; }
+.panel-empty { color: var(--color-text-muted); text-align: center; padding: 20px; font-size: var(--font-size-sm); }
 
-/* OKR Record Cards */
-.okr-record-card { background: #fff; border-radius: 10px; margin-bottom: 14px; border: 1px solid #e8eaed; overflow: hidden; }
-.orr-o-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px 10px; background: #fafbfc; }
-.orr-o-badge { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 6px; background: #2b1f99; color: #fff; font-size: 13px; font-weight: 700; flex-shrink: 0; }
-.orr-o-title { font-size: 14px; font-weight: 700; color: #1a1a2e; flex: 1; }
-.orr-o-meta { font-size: 11px; color: #27ae60; padding: 0 16px 6px; background: #fafbfc; }
+.okr-record-card { background: var(--color-surface); border-radius: var(--radius-md); margin-bottom: 14px; border: 1px solid var(--color-border-soft); overflow: hidden; }
+.orr-o-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px 10px; background: var(--color-surface-subtle); }
+.orr-o-badge { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: var(--radius-sm); background: var(--color-brand); color: var(--color-white); font-size: var(--font-size-sm); font-weight: 700; flex-shrink: 0; }
+.orr-o-title { font-size: var(--font-size-base); font-weight: 700; color: var(--color-text); flex: 1; }
+.orr-o-meta { font-size: 11px; color: var(--color-success); padding: 0 16px 6px; background: var(--color-surface-subtle); }
 
 .orr-krs { padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 8px; }
-.orr-kr-card { padding: 12px 14px; background: #f8f9fb; border-radius: 8px; border: 1px solid #eef0f2; }
-
+.orr-kr-card { padding: 12px 14px; background: var(--color-surface-subtle); border-radius: var(--radius-md); border: 1px solid var(--color-border-soft); }
 .orr-kr-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
-.orr-kr-num { font-size: 12px; font-weight: 700; color: #3ad4d8; width: 30px; flex-shrink: 0; }
-.orr-kr-title { flex: 1; font-size: 13px; color: #333; font-weight: 500; }
+.orr-kr-num, .ksr-num, .ski-num { font-size: var(--font-size-xs); font-weight: 700; color: var(--color-accent); flex-shrink: 0; }
+.orr-kr-num { width: 30px; }
+.orr-kr-title { flex: 1; font-size: var(--font-size-sm); color: var(--color-text); font-weight: 500; }
 
 .orr-kr-metrics { display: flex; gap: 20px; margin-bottom: 8px; }
 .orkm-item { display: flex; flex-direction: column; }
-.orkm-label { font-size: 10px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; }
-.orkm-value { font-size: 14px; font-weight: 600; color: #333; }
+.orkm-label { font-size: 10px; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0; }
+.orkm-value { font-size: var(--font-size-base); font-weight: 600; color: var(--color-text); }
 
-.orr-kr-logs { margin-top: 10px; padding-top: 8px; border-top: 1px dashed #ddd; }
-.orkl-head { font-size: 11px; font-weight: 600; color: #888; margin-bottom: 4px; }
+.orr-kr-logs { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--color-border); }
+.orkl-head { font-size: 11px; font-weight: 600; color: var(--color-text-secondary); margin-bottom: 4px; }
 .orkl-line { display: flex; align-items: center; gap: 8px; font-size: 11px; padding: 2px 0; flex-wrap: nowrap; }
-.orkl-time { color: #aaa; font-family: monospace; width: 120px; flex-shrink: 0; }
-.orkl-change { color: #555; font-family: monospace; width: 55px; flex-shrink: 0; }
-.orkl-status { color: #888; width: 42px; flex-shrink: 0; text-align: center; }
-.orkl-remark { color: #999; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
-.panel-divider { border-top: 2px solid #e8eaed; margin: 20px 0 16px; }
-
-/* Self-assessment blocks */
-.self-block { padding: 10px 0; border-top: 1px solid #f0f0f0; }
-.self-block:first-of-type { border-top: none; padding-top: 0; }
-.self-block-title { font-size: 13px; font-weight: 600; color: #1a1a2e; margin-bottom: 8px; }
-.sb-sub { font-size: 11px; color: #aaa; font-weight: 400; }
-
-.self-kr-list { display: flex; flex-direction: column; gap: 4px; }
-.self-kr-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; background: #f5f3ff; border-radius: 6px; font-size: 12px; }
-.ski-num { font-weight: 700; color: #3ad4d8; width: 28px; flex-shrink: 0; }
-.ski-title { flex: 1; color: #555; }
-.ski-score { font-weight: 600; color: #2b1f99; flex-shrink: 0; }
+.orkl-time { color: var(--color-text-muted); font-family: monospace; width: 120px; flex-shrink: 0; }
+.orkl-change { color: var(--color-text-secondary); font-family: monospace; width: 55px; flex-shrink: 0; }
+.orkl-status { color: var(--color-text-secondary); width: 42px; flex-shrink: 0; text-align: center; }
+.orkl-remark { color: var(--color-text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
+.panel-divider { border-top: 1px solid var(--color-border); margin: 20px 0 16px; }
 
-.self-subtotal { text-align: right; font-size: 12px; color: #2b1f99; margin-top: 6px; padding-top: 4px; border-top: 1px dashed #e8eaed; }
-
-.self-dim-list { display: flex; flex-direction: column; gap: 6px; }
-.self-dim-item { padding: 6px 10px; background: #f5f3ff; border-radius: 6px; }
+.self-block { padding: 10px 0; border-top: 1px solid var(--color-border-soft); }
+.self-block:first-of-type { border-top: none; padding-top: 0; }
+.self-block-title { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text); margin-bottom: 8px; }
+.sb-sub { font-size: 11px; color: var(--color-text-muted); font-weight: 400; }
+.self-kr-list, .self-dim-list { display: flex; flex-direction: column; gap: 6px; }
+.self-kr-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; background: var(--color-surface-tint-brand); border-radius: var(--radius-sm); font-size: var(--font-size-xs); }
+.ski-num { width: 28px; }
+.ski-title { flex: 1; color: var(--color-text-secondary); }
+.ski-score, .sdi-score { font-weight: 600; color: var(--color-brand); flex-shrink: 0; }
+.self-subtotal, .okr-subtotal { text-align: right; font-size: var(--font-size-sm); color: var(--color-brand); margin-top: 6px; padding-top: 8px; border-top: 1px dashed var(--color-border); }
+.self-dim-item { padding: 6px 10px; background: var(--color-surface-tint-brand); border-radius: var(--radius-sm); }
 .sdi-head { display: flex; justify-content: space-between; align-items: center; }
-.sdi-name { font-size: 12px; font-weight: 600; color: #333; }
-.sdi-score { font-size: 13px; font-weight: 600; color: #2b1f99; }
-.sdi-desc { font-size: 11px; color: #888; margin-top: 4px; line-height: 1.5; }
-
-.self-bp { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: #666; }
-.self-bp strong { color: #1a1a2e; }
-
-.self-total-bar {
-  margin-top: 12px; padding: 12px 16px;
-  background: #f4f2f9; border-radius: 10px;
-  text-align: center; font-size: 14px; color: #555;
-  border: 1px solid #dcd4f0;
+.sdi-name { font-size: var(--font-size-xs); font-weight: 600; color: var(--color-text); }
+.sdi-score { font-size: var(--font-size-sm); }
+.sdi-desc { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; line-height: 1.5; }
+.self-bp { display: flex; flex-wrap: wrap; gap: 12px; font-size: var(--font-size-xs); color: var(--color-text-secondary); }
+.self-bp strong { color: var(--color-text); }
+
+.self-total-bar, .result-preview {
+  margin-top: 12px;
+  padding: 20px;
+  background: var(--color-surface-tint-brand);
+  border-radius: var(--radius-md);
+  text-align: center;
+  font-size: var(--font-size-base);
+  color: var(--color-text-secondary);
+  border: 1px solid var(--color-border-soft);
 }
-.self-total-bar strong { font-size: 20px; color: #2b1f99; }
-.stb-grade { font-size: 13px; color: #888; }
-
-.score-card { background: #fff; border-radius: 10px; padding: 16px 20px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
-.sc-header { font-weight: 600; color: #1a1a2e; font-size: 14px; margin-bottom: 10px; }
-.sc-sub { font-size: 12px; color: #aaa; font-weight: 400; }
-.sc-body { }
-.sc-label { font-size: 13px; color: #888; margin-right: 12px; }
+.self-total-bar strong { font-size: 20px; color: var(--color-brand); }
+.stb-grade { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
+
+.score-card { background: var(--color-surface); border-radius: var(--radius-md); padding: 16px 20px; margin-bottom: 12px; box-shadow: var(--shadow-panel); border: 1px solid var(--color-border-soft); }
+.sc-header { font-weight: 600; color: var(--color-text); font-size: var(--font-size-base); margin-bottom: 10px; }
+.sc-sub { font-size: var(--font-size-xs); color: var(--color-text-muted); font-weight: 400; }
+.sc-label { font-size: var(--font-size-sm); color: var(--color-text-secondary); margin-right: 12px; }
 .sc-inline { display: flex; align-items: center; margin-bottom: 8px; }
-.sc-unit { font-size: 14px; color: #999; margin-left: 8px; }
+.sc-unit { font-size: var(--font-size-base); color: var(--color-text-muted); margin-left: 8px; }
 
-.kr-score-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-top: 1px solid #f5f5f5; }
-.kr-score-row:first-child { border-top: none; }
+.kr-score-row, .dim-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-top: 1px solid var(--color-border-soft); }
+.kr-score-row:first-child, .dim-row:first-child { border-top: none; }
 .ksr-label { flex: 1; min-width: 0; }
-.ksr-num { font-size: 12px; font-weight: 700; color: #3ad4d8; margin-right: 6px; }
-.ksr-title { font-size: 12px; color: #555; }
-.ksr-self-ref { font-size: 10px; color: #3498db; display: block; }
+.ksr-num { margin-right: 6px; }
+.ksr-title { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
+.ksr-self-ref, .dim-self-ref { font-size: 10px; color: var(--color-info); display: block; }
 .ksr-inputs { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
-.ksr-eq { font-size: 12px; color: #999; white-space: nowrap; }
-
-.okr-subtotal { text-align: right; font-size: 13px; color: #2b1f99; padding-top: 8px; border-top: 1px dashed #e0e0e0; }
-
-.dim-row { padding: 10px 0; border-top: 1px solid #f5f5f5; }
-.dim-row:first-child { border-top: none; }
+.ksr-eq { font-size: var(--font-size-xs); color: var(--color-text-muted); white-space: nowrap; }
 .dim-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
-.dim-num { width: 20px; height: 20px; border-radius: 50%; background: #2b1f99; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
-.dim-name { font-size: 13px; font-weight: 600; color: #333; }
-.dim-max { font-size: 11px; color: #999; }
-.dim-self-ref { font-size: 11px; color: #3498db; margin-left: auto; }
-.dim-self-desc { font-size: 11px; color: #999; margin-bottom: 4px; padding: 4px 6px; background: #f5f3ff; border-radius: 4px; }
-.dim-score-unit { font-size: 13px; color: #999; margin-left: 6px; }
-
-.result-preview {
-  background: #f4f2f9; border-radius: 8px;
-  padding: 20px; text-align: center; border: 1px solid #dcd4f0; margin-bottom: 16px;
-}
-.rp-formula { font-size: 12px; color: #888; }
-.rp-score { font-size: 40px; font-weight: 700; color: #2b1f99; margin: 6px 0; line-height: 1; }
-.rp-score small { font-size: 18px; color: #999; font-weight: 400; }
-.rp-grade { font-size: 14px; color: #666; }
+.dim-num { width: 20px; height: 20px; border-radius: 50%; background: var(--color-brand); color: var(--color-white); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
+.dim-name { font-size: var(--font-size-sm); font-weight: 600; color: var(--color-text); }
+.dim-max { font-size: 11px; color: var(--color-text-muted); }
+.dim-self-ref { margin-left: auto; }
+.dim-self-desc { font-size: 11px; color: var(--color-text-secondary); margin-bottom: 4px; padding: 4px 6px; background: var(--color-surface-tint-brand); border-radius: var(--radius-xs); }
+.dim-score-unit { font-size: var(--font-size-sm); color: var(--color-text-muted); margin-left: 6px; }
+
+.rp-formula { font-size: var(--font-size-xs); color: var(--color-text-secondary); }
+.rp-score { font-size: 40px; font-weight: 700; color: var(--color-brand); margin: 6px 0; line-height: 1; }
+.rp-score small { font-size: var(--font-size-xl); color: var(--color-text-muted); font-weight: 400; }
+.rp-grade { font-size: var(--font-size-base); color: var(--color-text-secondary); }
 .status-bar {
   display: flex; align-items: center; justify-content: space-between;
-  padding: 12px 14px; margin-bottom: 12px; border-radius: 8px;
-  background: #f7fbfb; border: 1px solid #d7f2f3; color: #2f4f55; font-size: 14px;
+  padding: 12px 14px; margin-bottom: 12px; border-radius: var(--radius-md);
+  background: var(--color-surface-tint-info); border: 1px solid var(--color-border-soft); color: var(--color-medical); font-size: var(--font-size-base);
 }
-.submit-btn { width: 100%; background: #2b1f99; border: none; height: 48px; font-size: 15px; }
+.submit-btn { width: 100%; background: var(--color-brand); border: none; height: 48px; font-size: 15px; }
 
 @media (max-width: 1100px) {
   .review-workbench { flex-direction: column; }