You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/.trae/skills/ink-training-theme/SKILL.md

61 KiB

name description
ink-training-theme Apply ink-minimal style to student training pages (Vue 3 + Element Plus). Use when restyling student pages, sidebar el-menu, step tabs, buttons, tables, or type scale in this repo. Do not use for admin pages or business-logic changes.

教育蓝极简 · 学生实训页设计规范

本技能用于把学生端实训页(.student-training-shell 体系)统一为 「教育蓝极简」风格:纯白画布、教育蓝主操作、中性灰文字层次、紧凑排版。 教育蓝标准样例:src/views/foundation/new-product-survey.vue (根类 .new-product-value-page.student-training-shell); 步骤导航以本规范「步骤导航:按钮切换风」一节为准, 左侧课程菜单换肤见「侧边栏 el-menu 教育蓝换肤」与「折叠把手」两节。

注意:src/views/foundation/product-development-process.vue 一页仍沿用旧「墨黑极简」, 属遗留页面。新页面一律按本文档教育蓝极简落地。

边界

  • 只动 template 结构与样式;script 业务逻辑(判题/保存/草稿/上传/接口)一律不碰。
  • 仅作用于当前页根类,不得影响其他页面;但必须同步修改全局 src/assets/styles/student-training.scss(见下一节「全局 !important 覆盖 机制」),否则页面样式会被带 !important 的旧蓝色规则碾压。
  • 表格类内容区不铺任何底色,对错只靠文字反馈,不靠行背景。

选择器边界(防污染教师端/超管端)

必须遵守的规则

改全局 student-training.scss 时,所有视觉样式选择器必须以以下锚点开头 之一——否则会漏到教师端或超管端的普通页面:

安全锚点 覆盖范围
.student-training-shell 实训页面专用容器类(学生端 + 教师端预览共用,是实训风格)
#app .studentLayout .app-main:has(.xxx-page) 精确限定在学生端某个路由页面
#app .teacherLayout .app-main:has(.student-training-shell) 限定在教师端预览实训页面(如需差异化)

危险锚点(绝对不能用):

危险选择器 后果
.el-button { ... } 污染全站所有 Element Plus 按钮(教师端/超管端/学生端都会被改)
.btn-primary { ... } 同上
.task-actions { ... } (裸写,不带前置锚) 教师端/超管端如果也有 .task-actions 类就会被改
button.outline-action { ... } (裸写) 全站所有 outline-action 按钮被改
:root { --xxx: ... } (视觉令牌) CSS 自定义属性会全局继承,但字体/字号/间距这类无害

项目实际验证:

  • 教师端 views/teacher/**/*.vue 和超管端 views/admin/**/*.vue 均不直接使用 .student-training-shell 类
  • 教师端预览学生实训页面时,才会通过布局嵌套间接使用
  • 超管端完全不涉及实训模块,所以以 .student-training-shell 为锚点最安全

页面非 scoped 块的边界

每个页面自己写的非 scoped <style> 块也必须加页面根类锚点:

/* ✅ 正确:以页面根类为锚,只影响当前页面 */
#app .studentLayout section.app-main:has(.industry-pest-page) .step-indicator { ... }
#app .studentLayout section.app-main:has(.industry-pest-page) .step-tab { ... }

/* ❌ 错误:无页面锚点,会影响所有步骤导航 */
.step-indicator { ... }
.step-tab { ... }

组件级修改的边界

改 TrainingMaterialButton.vue / TrainingTaskToolbar.vue 这类共用组件时:

  • 如果组件只在 .student-training-shell 内使用 → 改组件本身即可(安全)
  • 如果组件可能被教师端/超管端复用 → 加 :deep(.student-training-shell) 或让调用方传 prop 控制
  • 改完必须 Grep 确认该组件在教师端/超管端没有被意外引用

全局 !important 覆盖机制(必须先处理)

根因

src/assets/styles/student-training.scss 对 .student-training-shell 下的 按钮和背景写了大量带 !important 的旧蓝色规则(全部指向 #1e88e5 / #1565c0 / #e3f2fd / #1e3a5f 等蓝色令牌),特异性比页面 scoped 样式更高, scoped 里写的教育蓝按钮永远不生效。这不是"页面打补丁"能解决的——必须改全局。

已知带 !important 的旧蓝色覆盖(L495–599):

选择器 强制值
.task-actions > button border: 1px solid #1e88e5 !important; background: #e3f2fd !important; color: #1e3a5f !important
.task-actions > .btn-primary border-color: #1565c0 !important; background: linear-gradient(#1e88e5, #1565c0) !important
.button.outline-action border: 1px solid #1e88e5 !important; background: #e3f2fd !important; color: #1e3a5f !important; font-weight: 800 !important
.workbench-actions > button height: 34px !important; min-height: 34px !important(矮胖)
.taskfooter .el-button:first-child 同上蓝色渐变

页面 scoped 打不过的原因:即使 scoped 加 !important,Element Plus 的 .el-button 也有自己的内联变量 + --el-button-bg-color 等 CSS 变量机制, 需要 !important 才能覆盖。而且全局选择器是后代组合 (.student-training-shell .task-actions > button),scoped 加了 [hash] 后特异性相同,但全局排在后面且数量多,容易互相强化。

正确做法:改全局,不是打补丁

第一次改时把 L495–599 的按钮规则全部重写成教育蓝极简,后续页面自动继承。 不需要每个页面单独写非 scoped 覆盖(步骤导航除外,它的结构每页不同)。

改造模板:

/* === task-actions / taskfooter 按钮(教育蓝极简替换旧蓝色 !important) === */
.student-training-shell .task-actions > button,
.student-training-shell .taskfooter .el-button {
  border: 1px solid rgba(37, 99, 235, 0.45) !important; /* 教育蓝描边 */
  border-radius: 10px !important; /* 统一 10px */
  color: var(--student-accent, #2563eb) !important; /* 教育蓝字 */
  background: #ffffff !important; /* 白底 */
  font-weight: 700 !important; /* 统一 700 */
  /* 加上 transition 让 hover 平滑 */
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    color 0.16s ease,
    transform 0.16s ease,
    box-shadow 0.16s ease !important;
}

/* 主按钮(提交/保存/下一步) */
.student-training-shell .task-actions > .btn-primary,
.student-training-shell .taskfooter .el-button:first-child {
  border-color: var(--student-accent, #2563eb) !important;
  color: #ffffff !important;
  background: var(--student-accent, #2563eb) !important; /* 教育蓝实底 */
  box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
}

/* hover 态 */
.student-training-shell .task-actions > button:hover:not(:disabled) {
  border-color: var(--student-accent, #2563eb) !important;
  background: var(--student-accent-soft, #eff6ff) !important;
  color: var(--student-accent, #2563eb) !important;
  transform: translateY(-1px) !important;
  box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
}
.student-training-shell .task-actions > .btn-primary:hover:not(:disabled) {
  border-color: var(--student-accent-deep, #1d4ed8) !important;
  background: var(--student-accent-deep, #1d4ed8) !important;
  color: #ffffff !important;
  transform: translateY(-1px) !important;
  box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
}

/* outline-action / ghost-action(顶部工具栏按钮) */
.student-training-shell button.outline-action,
.student-training-shell button.ghost-action {
  height: 40px !important;
  min-height: 40px !important;
  border: 1px solid rgba(37, 99, 235, 0.45) !important;
  border-radius: 10px !important;
  color: var(--student-accent, #2563eb) !important;
  background: #ffffff !important;
  font-size: 13px !important;
  font-weight: 700 !important;
}
.student-training-shell button.outline-action:hover:not(:disabled) {
  border-color: var(--student-accent, #2563eb) !important;
  background: var(--student-accent-soft, #eff6ff) !important;
  transform: translateY(-1px) !important;
}

/* workbench-actions 按钮(恢复 40px,被旧规则压成 34px) */
.student-training-shell .workbench-actions > button {
  height: 40px !important;
  min-height: 40px !important;
}

还要同步检查的组件

组件 旧风格 改法
TrainingMaterialButton.vue 蓝半透明胶囊(28px 高,rgba(17,126,178,.36)) 组件自身改(scoped 无 !important,所以组件内部改即可覆盖全局),40px / 10px / 白底深灰描边
TrainingTaskToolbar.vue 可能也有旧蓝色 同上组件自身改

实施新页面的流程:先 Grep 搜索 #1e88e5|#1565c0|#e3f2fd|#1e3a5f 确认全局和目标页无残留旧蓝值,再改页面。

实施流程

  1. 先全局后页面:Grep 搜索 #1e88e5|#1565c0|#e3f2fd|#1e3a5f|#cfe2f5 定位 student-training.scss 和子组件中的旧蓝色规则;按「全局 !important 覆盖机制」一节的模板替换成教育蓝极简(只做一次,后续页面自动继承)。
  2. 读目标页,确认根类(如 .xxx-page.student-training-shell)和现有 DOM。
  3. 搭建页面头部:按「页面头部:task-header + 内联 task-brief」一节 替换旧 <TrainingTaskBrief> 组件为内联 task-brief,确保有 task-head-main 包裹层和 briefOpen ref。
  4. 在页面 scoped 样式的根类块内定义全部令牌(见下),让所有 var(--student-*) 规则自动重着色,不要逐处打补丁。
  5. 新增一个非 scoped <style lang="scss"> 块,选择器全部以页面根类为锚:
    • 覆盖祖先 app-main 画布背景(scoped 触及不到祖先);
    • 重写步骤导航(全局规则带 !important,scoped 无法取胜,见步骤导航一节)。
  6. 按按钮四级层级、字号梯度、间距规则逐区收紧。
  7. 完成后用 GetDiagnostics 校验,并全文搜索蓝色旧值确认无残留。

颜色令牌

在页面根类中定义:

.xxx-page {
  /* 主色:教育蓝 */
  --student-accent: #2563eb;
  --student-accent-deep: #1d4ed8;
  --student-accent-light: #3b82f6;
  --student-accent-soft: #eff6ff;
  /* 语义色 */
  --student-danger: #ef4444;
  --student-success: #2ba471;
  /* 三层次标识(仅小色条,不做行底色) */
  --student-layer-1: #1d4ed8;
  --student-layer-2: #3b82f6;
  --student-layer-3: #93c5fd;
  /* 结构色 */
  --student-panel-border: #e5e7eb;
  --student-card-bg: #ffffff;
  --student-panel-bg: #fafbfc;
  --student-text: #1f2937;
  --student-text-secondary: #6b7280;
  /* Element Plus 主色变量(el-select / el-switch / 进度条等吃这个) */
  --el-color-primary: #2563eb;
  --el-color-primary-light-3: #60a5fa;
  --el-color-primary-light-5: #93c5fd;
  --el-color-primary-light-7: #bfdbfe;
  --el-color-primary-light-8: #dbeafe;
  --el-color-primary-light-9: #eff6ff;
  --el-color-primary-dark-2: #1d4ed8;
  --el-input-hover-border-color: #3b82f6;
  --el-input-focus-border-color: #2563eb;
  --el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
  --el-textarea-hover-border-color: #3b82f6;
  --el-textarea-focus-border-color: #2563eb;
}

改色判定原则:

  • #1f2937 / #111827 / #4b5563 作为文字色时保留;作为背景/边框/聚焦/ 阴影/实底等交互主色时才改教育蓝。
  • #f3f4f6 作为 accent-soft(序号徽章、hover 底)改 #eff6ff;作为 三级「保存」按钮(.btn-save)中性灰底时保留。
  • 极淡中性阴影(如 rgba(17,24,39,.04) 卡片影)保留;品牌阴影换 rgba(37,99,235,*) / hover rgba(29,78,216,*)。
  • teacher-light.scss L2780-2793 有一条高特异性(0,8,1)默认按钮规则 强制墨黑,需要在 :not() 链里加 :not(.task-toolbar-button):not(.material-button) 才能让页面自己的教育蓝按钮生效(详见文末「特异性陷阱」)。

非 scoped 块中的画布与行高:

#app .studentLayout .app-main:has(.xxx-page) {
  background: #ffffff !important; /* 纯白,无渐变 */
}
#app .studentLayout .app-main:has(.xxx-page) .xxx-page {
  line-height: 1.55 !important; /* 全局为 1.8,本页收紧 */
}

字号层级(紧凑)

覆盖全局字号令牌,整体下调一档:

令牌 值 用途
--student-fs-display 17px 页面主标题(700,行高 1.35)
--student-fs-h1 15px 步骤大标题(700)
--student-fs-h2 14px 面板/行标题(600)
--student-fs-h3 13px 区块小标题
--student-fs-body 13px 正文
--student-fs-body-sm 12px 表格/次要文字
--student-fs-caption 11px 标签/徽章
--student-fs-button / -lg 13px 按钮文字
--student-fs-data 14px 数字/成绩,配等宽字体

标题区三级关系:上方灰色宽字距小标签(11px,#6b7280,letter-spacing 0.12em, 无底色胶囊)→ 黑色步骤标题 → 灰色说明文字。禁止再用实底色 "STEP 01" 胶囊。

页面头部:task-header + 内联 task-brief

每个实训页的头部是一个独立白卡片(.task-header),包含 badge + 标题 + 一行 实训背景(可展开目标与要求),右侧可选工具栏按钮。必须内联实现,禁止使用 TrainingTaskBrief.vue 组件——该组件是旧蓝 tab 切换风,与教育蓝极简不兼容。

标准 DOM 结构

<section class="task-header">
  <div class="task-head-main">
    <span class="task-badge">
      <el-icon><DataAnalysis /></el-icon>
      核心实训 · 市场洞察与需求分析
    </span>
    <h1 class="task-title">{{ taskTitle }}</h1>
    <p class="task-brief">
      <span class="task-brief-text">{{ taskBackground }}</span>
      <button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">{{ briefOpen ? "收起" : "展开目标与要求" }}</button>
      <span v-if="briefOpen" class="task-brief-more">
        <b>实训目标</b>
        <ul>
          <li v-for="goal in taskGoals" :key="goal">{{ goal }}</li>
        </ul>
        <b>实训要求</b>
        <p>{{ taskRequirement }}</p>
      </span>
    </p>
  </div>
  <!-- 右侧工具栏(可选,二选一) -->
  <TrainingTaskToolbar class="xxx-toolbar" :task-key="TASK_KEY" ... />
  <!-- 或 -->
  <div class="xxx-toolbar">
    <TrainingMaterialButton :material-name="..." :material-url="..." />
    <button type="button" class="outline-action" @click="exportOutcome">导出实训成果</button>
  </div>
</section>

Script 必备变量

const briefOpen = ref(false);
const taskTitle = computed(() => taskConfig.value?.taskName || "页面默认标题");
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);

Scoped 样式(直接照抄)

/* 任务头:白卡 + 左侧教育蓝竖条 + 12px 圆角 */
.task-header {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin: 0;
  padding: 18px 20px 18px 24px;
  border: 1px solid var(--student-panel-border);
  border-radius: 12px;
  background: var(--student-card-bg);
  overflow: hidden;

  &::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 4px;
    background: var(--student-accent);
  }
}

.task-head-main {
  display: grid;
  justify-items: start;
  gap: 8px;
  min-width: 0;
  flex: 1 1 auto;
}

.task-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 12px;
  border: 1px solid var(--student-panel-border);
  border-radius: 999px;
  color: var(--student-accent-deep);
  background: var(--student-accent-soft);
  font-size: 12px;
  font-weight: 700;
}

.task-title {
  margin: 0;
  color: var(--student-text);
  font-size: var(--student-fs-display);
  font-weight: 700;
  line-height: 1.35;
}

.task-brief {
  margin: 0;
  color: var(--student-text-secondary);
  font-size: 12px;
  line-height: 1.6;
}

.task-brief-text {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 1;
}

.task-brief-toggle {
  padding: 0;
  border: 0;
  color: var(--student-accent-deep);
  background: transparent;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  &:hover {
    text-decoration: underline;
  }
}

.task-brief-more {
  display: grid;
  gap: 6px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--student-panel-border);
  b {
    color: var(--student-text);
    font-size: 12px;
  }
  ul {
    display: grid;
    gap: 4px;
    margin: 0;
    padding-left: 18px;
  }
  p {
    margin: 0;
  }
}

实现要点

  1. <TrainingTaskBrief> 组件已弃用:旧组件是 tab 切换 + 独立卡片的蓝色风, 与教育蓝极简冲突。直接把实训背景内联在 task-header 里,不要 import 该组件。
  2. task-head-main 必须有:它是 badge + title + brief 的 grid 容器, gap: 8px、flex: 1 1 auto,右侧工具栏才不会被挤到下一行。
  3. task-brief-text 单行截断:-webkit-line-clamp: 1 保证默认只显示 一行背景摘要,点击按钮展开全部目标和要求。
  4. 右侧工具栏可选:简单页直接用 <TrainingTaskToolbar> 一个组件搞定 提交/重置/导出;需要下载案例资料的页用 <TrainingMaterialButton> + 原生 <button class="outline-action"> 组合。

步骤导航:分段连接胶囊(精确规格,直接抄)

所有教育蓝实训页的步骤导航必须使用 分段连接胶囊(像 VS Code tab): gap:0 无缝连接、首尾格各自圆角、中间格无右边框(共用边)、hover z-index:1 / active z-index:2 解决描边冲突。

DOM 结构(.step-indicator > .step-tab > .step-bar/.step-no/.step-name)保持不变, 只换样式。survey / factors / process 三页已验证统一。

视觉规格(与 new-product-survey.vue 完全一致)

元素 精确值
.step-indicator gap: 0 !important;其他:flex 水平居中、stretch 对齐;无 border/底色;margin 0 0 20px
.step-tab flex: 1 1 0;max-width: 240px;min-height: 40px;padding: 8px 14px;border: 1px solid #e5e7eb !important;border-right: none !important;border-radius: 0 !important;白底;字 #6b7280、13px/600;transition: all 0.16s ease
:first-child border-radius: 8px 0 0 8px !important(仅左圆角)
:last-child border-radius: 0 8px 8px 0 !important;border-right: 1px solid #e5e7eb !important(补回右边框)
.step-bar display: none !important
.step-no 22px 小徽章:border-radius: 6px;background: #f3f4f6 !important;color: #6b7280;等宽字体 12px/700;transition: all 0.16s ease;::after 隐藏

三态(z-index 解决 border-color 冲突):

状态 边框 背景 字色 徽章底 徽章字 z-index
未到达 #e5e7eb #ffffff #6b7280 #f3f4f6 #6b7280 —
未到达 hover #2563eb #eff6ff #2563eb #2563eb #ffffff 1
已完成 #e5e7eb #ffffff #374151 rgba(43,164,113,.12) #16915f —
已完成 hover #2ba471 #f0fdf6 — #2ba471 #ffffff 1
当前 #2563eb #2563eb #ffffff 700 #ffffff #2563eb 2
当前 hover #1d4ed8 #1d4ed8 — — — —

完整可复制 CSS(放进页面非 scoped 块)

把 xxx-page 替换为你的页面根类,其余一字不动:

#app .studentLayout section.app-main:has(.xxx-page) .step-indicator {
  display: flex !important;
  flex-wrap: nowrap !important;
  align-items: stretch !important;
  justify-content: center !important;
  gap: 0 !important;
  width: 100% !important;
  margin: 0 0 20px !important;
  padding: 0 !important;
  border: none !important;
  border-radius: 0 !important;
  background: transparent !important;
  box-shadow: none !important;

  .step-tab {


    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    min-width: 0 !important;
    height: auto !important;
    min-height: 40px !important;
    margin: 0 !important;
    padding: 8px 14px !important;
    border: 1px solid #e5e7eb !important;
    border-right: none !important;
    border-radius: 0 !important;
    background: #ffffff !important;
    box-shadow: none !important;
    color: #6b7280 !important;
    font-size: 13px !important;
    font-weight: 600 !important;
    line-height: 1.3 !important;
    white-space: normal !important;
    text-align: center !important;
    cursor: pointer;
    transition: all 0.16s ease !important;

    &:first-child {
      border-radius: 8px 0 0 8px !important;
    }
    &:last-child {
      border-radius: 0 8px 8px 0 !important;
      border-right: 1px solid #e5e7eb !important;
    }

    .step-bar {
      display: none !important;
    }

    .step-no {
      position: static !important;
      flex: 0 0 auto !important;
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      min-width: 22px !important;
      height: 22px !important;
      padding: 0 5px !important;
      border-radius: 6px !important;
      background: #f3f4f6 !important;
      color: #6b7280 !important;
      font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
      font-size: 12px !important;
      font-weight: 700 !important;
      line-height: 1 !important;
      transition: all 0.16s ease !important;
      &::after {
        display: none !important;
      }
    }

    .step-name {
      color: inherit !important;
    }

    &:hover:not(.active):not(.complete) {
      border-color: #2563eb !important;
      background: #eff6ff !important;
      color: #2563eb !important;
      z-index: 1 !important;
      .step-no {
        background: #2563eb !important;
        color: #ffffff !important;
      }
    }

    &.complete:not(.active) {
      border-color: #e5e7eb !important;
      color: #374151 !important;
      .step-no {
        background: rgba(43, 164, 113, 0.12) !important;
        color: #16915f !important;
      }
      &:hover {
        border-color: #2ba471 !important;
        background: #f0fdf6 !important;
        z-index: 1 !important;
        .step-no {
          background: #2ba471 !important;
          color: #ffffff !important;
        }
      }
    }

    &.active {
      border-color: #2563eb !important;
      background: #2563eb !important;
      color: #ffffff !important;
      font-weight: 700 !important;
      z-index: 2 !important;
      .step-no {
        background: #ffffff !important;
        color: #2563eb !important;
      }
      &:hover {
        background: #1d4ed8 !important;
        border-color: #1d4ed8 !important;
      }
    }
  }
}

/* 窄屏:横向滑动 */
@media (max-width: 900px) {
  #app .studentLayout section.app-main:has(.xxx-page) .step-indicator {
    justify-content: flex-start !important;
    overflow-x: auto !important;
  }
  #app .studentLayout section.app-main:has(.xxx-page) .step-indicator .step-tab {
    flex: 0 0 140px !important;
    max-width: none !important;
  }
}

覆盖机制(不做这步,页面仍是浅蓝大胶囊)

全局 student-training.scss 对 .student-training-shell .step-indicator 与 .step-tab 全部以 !important 锁定浅蓝底(#f0f6fd)、全胶囊圆角、固定高度、 white-space: nowrap,并在 ≤1100px 用 !important 强制纵向堆叠。 scoped 样式无论怎么写特异性都打不赢。

正确做法——规则放进页面非 scoped 块,选择器带 #app .studentLayout section.app-main:has(...) 锚点且每条声明 !important。

侧边栏 el-menu 教育蓝换肤

学生端左侧课程菜单使用 Element Plus el-menu(替代手写 nav)。 样例:src/layout/components/Sidebar/index.vue + src/assets/styles/sidebar.scss。

组件配置(script/template)

<el-menu class="student-course-menu" mode="vertical" :default-active="activeMenu" :default-openeds="studentDefaultOpens" :collapse="isCollapse" :collapse-transition="false" background-color="#ffffff" text-color="#374151" popper-class="student-menu-popper" @select="handleStudentMenuSelect"></el-menu>
  • 一级直跳用 el-menu-item;分组用 el-sub-menu;二级 el-menu-item, 无路径时 :disabled="!child.path"。
  • 不要传 active-text-color:会污染折叠弹出层(白底白字)。
  • default-openeds 非受控:异步分组加载完成后,递增菜单的 :key 强制重建(项目内 bumpStudentMenu()),否则分组不展开。

Element Plus 2.2.x 关键机制(踩坑点)

  • EP 给 el-sub-menu__title 注入内联 color(取自 textColor prop)—— 菜单文字色一律 !important,普通声明无效。
  • 一级菜单自定义教育蓝块:#2563eb 底白字 14px/600,hover #1d4ed8, 激活 #1d4ed8 + 左 3px 蓝条;color/background 全部 !important。
  • 二级菜单:hover #eff6ff、激活教育蓝底白字,背景必须 !important (sidebar.scss 旧规则把二级项背景强制 transparent,白字会落白底像消失)。
  • disabled 项:EP 默认 opacity: .25,覆写为 opacity: 1 + 字色 #c0c5cd。
  • 折叠态弹出层 teleport 到 body,脱离 sidebar 作用域——用全局非 scoped 样式按 popper-class 换肤:.el-popper.student-menu-popper, .el-menu--popup-container.student-menu-popper(白底、10px 圆角、 #e5e7eb 边、6px padding;项常态 #374151,激活教育蓝)。
  • toggle collapse 后 EP 更新 DOM 类有约 1 秒延迟;自动化测试中切换后 先等待再读类,立即读到的是旧值。

折叠把手

折叠按钮不放在菜单右上角,而是贴侧边栏右缘垂直居中的极小竖条 (.course-sidebar-toggle,学生/教师共享块):

  • position: absolute; top: 50%; right: 0; transform: translateY(-50%); 14×44px;白底;1px solid #e5e7eb、border-right: 0; 圆角 8px 0 0 8px;箭头常态 #9ca3af;阴影 -2px 0 6px rgba(17,24,39,.08)。
  • 默认展开显 ArrowLeftBold;.sidebar-container.is-collapse 下隐藏左箭头、 显 ArrowRightBold;hover/focus 教育蓝底白箭头。图标 10px。
  • 尺寸声明也要 !important:旧深色/蓝色块选择器更长、特异性更高。
  • SCSS 嵌套坑:.sidebar-container.is-collapse … 规则不能写在 .sidebar-container { } 块内部,否则编译出双容器 (.sidebar-container .sidebar-container.is-collapse)永不命中; 折叠态规则与 .sidebar-container 块同级书写。

按钮规范

扁平无渐变,强制 text-shadow: none;圆角矩形而非全胶囊。标准页有两个按钮位置, 规格不同,不要混用:

四级层级

  1. 一级主操作:教育蓝实底 #2563eb 白字 + 蓝色柔影,hover #1d4ed8。 用于下一步/提交任务/AI 发送/智能评分。
  2. 二级描边:白底 + 教育蓝描边 + #2563eb 字,hover 蓝描边加深 + #eff6ff 底。 用于上一步/下载/重新实训/导出/清空。
  3. 三级浅灰底:#f3f4f6 底 + #374151 字,hover #e9ecef。用于保存草稿。
  4. 危险文字:无底色,#ef4444 文字链接,hover 深红加下划线。用于删除。

禁用态统一 opacity: 0.5 + not-allowed,transform: none、去掉阴影,形状不跳变。 loading 态保持原配色。

Icon 映射(所有按钮必须有 icon)

按钮类型 Icon 位置
上一步 <ArrowLeft /> 文字前
保存当前进度 / 保存草稿 <CircleCheck /> 文字前
清空填写 / 清空本步 <RefreshLeft /> 文字前
保存并进入第 N 步 / 下一步 <ArrowRight /> 文字后
提交任务(底部) <CircleCheck /> 文字后
提交任务(顶部工具栏) <CircleCheck /> 文字前(组件默认)
导出实训成果 <Download /> 文字前
案例资料 / 实训数据 / 指导手册 <FolderOpened /> / <Document />(TrainingMaterialButton 组件自带) 文字前

所有页面必须 import 这些 icon:

import { ArrowLeft, ArrowRight, CircleCheck, Download, RefreshLeft } from "@element-plus/icons-vue";

位置 A:底部步骤导航(40px 标准按钮)

结构(原生 <button>,容器 .step-navigation 居中 flex,gap 14px):

<div class="step-navigation">
  <button class="btn-nav btn-ghost" @click="prevStep">
    <el-icon><ArrowLeft /></el-icon>
    上一步
  </button>
  <button class="btn-nav btn-save" @click="saveCurrentProgress">
    <el-icon><CircleCheck /></el-icon>
    保存当前进度
  </button>
  <button class="btn-nav btn-primary" @click="nextStep">
    下一步
    <el-icon><ArrowRight /></el-icon>
  </button>
  <span class="step-progress">Step 01 / 04</span>
</div>
  • 三按钮统一 40px 高、10px 圆角、700 字重、13px 字、gap 8px; ghost/primary 横向 padding 18px(primary 可 22px)。
  • 次按钮基类(.outline-action, .btn-nav 共用):白底,描边 1px solid rgba(37, 99, 235, 0.45),字色 var(--student-accent)。
  • hover:translateY(-1px) + 描边变实色教育蓝 + 底变 var(--student-accent-soft)
    • 阴影 0 3px 10px rgba(37,99,235,.12);active 回落 translateY(0)。
  • primary:教育蓝实底 + 阴影 0 3px 10px rgba(37,99,235,.28),hover 加深为 #1d4ed8、阴影 0 5px 14px rgba(29,78,216,.32)。
  • save:#f3f4f6 底、#e5e7eb 描边、#374151 字,无重投影;视觉权重 低于 ghost,形成 实底蓝 > 描边白 > 浅灰底 的层级。
  • .step-progress:绝对定位右侧,12px 等宽数字、#6b7280、800 字重; ≤1100px 时改为静态独占一行居中。
  • ≤900px:导航纵向排列,.btn-nav 全部 width: 100%。

位置 B:顶部任务工具栏(36px 紧凑小按钮)

工具栏是子组件(Element Plus .el-button),在页面非 scoped 块中以页面根类为锚 换肤(如 .xxx-page .task-toolbar-button.el-button),容器 gap 8px:

  • 次按钮(下载案例资料/重新实训/导出实训成果):36px 高、8px 圆角、600 字重、 13px 字,白底 + rgba(37,99,235,.45) 描边 + #2563eb 字;hover 边实 #2563eb、 底 #eff6ff、translateY(-1px) + 0 2px 6px rgba(37,99,235,.12)。图标 14px 且 color: inherit。
  • 提交按钮(.task-toolbar-button--submit):#2563eb 实底白字 + 0 1px 2px rgba(37,99,235,.35), 0 3px 10px rgba(37,99,235,.22),hover #1d4ed8 + 阴影加深。
  • loading 态强制保持白底蓝描边,不随 Element Plus 默认变灰或变实底。
  • 无资料时的 plain 态:字色 #9ca3af、边 #e5e7eb,hover 不变蓝(弱化态)。
  • 步骤标题行内的小号导出按钮可再缩到 32px 高、12px 字。

按钮完整可复制 CSS(放进页面非 scoped 块)

把 xxx-page 替换为你的页面根类,其余一字不动。全局 student-training.scss 用 !important 把按钮锁成浅蓝大胶囊,必须用 #app 锚点覆盖。

/* ============================================================
 * 共用组件换肤:TrainingMaterialButton 资料按钮 → 教育蓝
 * ============================================================ */
.xxx-page .workbench-actions .material-button.el-button {
  height: 36px !important;
  min-height: 36px !important;
  border: 1px solid rgba(37, 99, 235, 0.45) !important;
  border-radius: 8px !important;
  color: #2563eb !important;
  background: #ffffff !important;
  font-size: 13px !important;
  font-weight: 600 !important;
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    color 0.16s ease,
    box-shadow 0.16s ease !important;

  &:hover {
    border-color: #2563eb !important;
    color: #2563eb !important;
    background: #eff6ff !important;
    box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
  }

  &.is-plain {
    border-color: #e5e7eb !important;
    color: #9ca3af !important;
    &:hover {
      border-color: #e5e7eb !important;
      color: #9ca3af !important;
      background: #ffffff !important;
      box-shadow: none !important;
    }
  }
}

/* ============================================================
 * 顶部导出按钮(原生 <button.outline-action>)
 * ============================================================ */
#app .studentLayout section.app-main:has(.xxx-page) .workbench-actions > button.outline-action {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  gap: 6px !important;
  height: 36px !important;
  min-height: 36px !important;
  padding: 0 14px !important;
  border: 1px solid rgba(37, 99, 235, 0.45) !important;
  border-radius: 8px !important;
  color: #2563eb !important;
  background: #ffffff !important;
  font-size: 13px !important;
  font-weight: 600 !important;
  line-height: 1 !important;
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    color 0.16s ease,
    box-shadow 0.16s ease !important;
}

#app .studentLayout section.app-main:has(.xxx-page) .workbench-actions > button.outline-action:hover {
  border-color: #2563eb !important;
  color: #2563eb !important;
  background: #eff6ff !important;
  box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
  transform: none !important;
}

/* ============================================================
 * 底部步骤按钮(40px 标准):task-actions > button
 * ============================================================ */
#app .studentLayout section.app-main:has(.xxx-page) .task-actions {
  gap: 14px !important;
  margin-top: 20px !important;
}

#app .studentLayout section.app-main:has(.xxx-page) .task-actions > button {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  gap: 8px !important;
  height: 40px !important;
  min-height: 40px !important;
  padding: 0 18px !important;
  border: 1px solid rgba(37, 99, 235, 0.45) !important;
  border-radius: 10px !important;
  color: #2563eb !important;
  background: #ffffff !important;
  font-size: 13px !important;
  font-weight: 700 !important;
  line-height: 1 !important;
  transition:
    transform 0.16s ease,
    border-color 0.16s ease,
    background 0.16s ease,
    box-shadow 0.16s ease !important;
}

#app .studentLayout section.app-main:has(.xxx-page) .task-actions > button:hover:not(:disabled) {
  transform: translateY(-1px) !important;
  border-color: #2563eb !important;
  color: #2563eb !important;
  background: #eff6ff !important;
  box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
}

/* 主按钮(btn-submit):教育蓝实底 */
#app .studentLayout section.app-main:has(.xxx-page) .task-actions > button.btn-submit {
  border-color: #2563eb !important;
  color: #ffffff !important;
  background: #2563eb !important;
  box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
}

#app .studentLayout section.app-main:has(.xxx-page) .task-actions > button.btn-submit:hover:not(:disabled) {
  border-color: #1d4ed8 !important;
  color: #ffffff !important;
  background: #1d4ed8 !important;
  box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
}

/* ============================================================
 * 顶部任务工具栏(TrainingTaskToolbar 组件)
 * ============================================================ */
.xxx-page .task-toolbar-button.el-button {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  gap: 6px !important;
  min-height: 34px !important;
  height: 34px !important;
  padding: 0 12px !important;
  border: 1px solid rgba(37, 99, 235, 0.45) !important;
  border-radius: 8px !important;
  color: #2563eb !important;
  background: #ffffff !important;
  font-size: 13px !important;
  font-weight: 600 !important;
  transition:
    border-color 0.16s ease,
    background 0.16s ease,
    color 0.16s ease,
    box-shadow 0.16s ease !important;

  &:hover:not(.is-disabled) {
    border-color: #2563eb !important;
    color: #2563eb !important;
    background: #eff6ff !important;
    transform: translateY(-1px) !important;
    box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
  }
}

/* 提交按钮实底 */
.xxx-page .task-toolbar-button--submit {
  border-color: #2563eb !important;
  background: #2563eb !important;
  color: #ffffff !important;
  box-shadow:
    0 1px 2px rgba(37, 99, 235, 0.35),
    0 3px 10px rgba(37, 99, 235, 0.22) !important;
  &:hover:not(.is-disabled) {
    background: #1d4ed8 !important;
    box-shadow:
      0 2px 4px rgba(37, 99, 235, 0.4),
      0 5px 14px rgba(29, 78, 216, 0.28) !important;
  }
}

表格规则

  • 表头、单元格、外容器一律不设 background(默认透明白),包括 hover、选中、判题后。
  • 表头仅用 12–13px、800–900 字重,无渐变底。
  • 只保留 #e5e7eb 浅灰边框与行间分隔线。
  • 序号用 22px 浅蓝圆角方块(#eff6ff)+ 等宽数字。
  • 判题结果:单元格内文字反馈(绿/红)+ 底部汇总横幅(8–10px 圆角、8% 同色底)。
  • 三层次概念仅靠维度名前 3–4px 小色条标识。

间距(紧凑)

  • 主列 gap 10px;卡片内 padding 12px;字段组 padding 12px。
  • 步骤标题下 10px、说明下 14px、字段块间 12px、表格上 14px。
  • 行高:页面 1.55,标题 1.35,说明 1.6,表格次要文字 1.55。

子组件换肤要点

  • TrainingTaskBrief.vue 已弃用:旧组件是 tab 切换 + 独立卡片的蓝色风, 与教育蓝极简不兼容。新页面禁止 import 该组件,实训背景/目标/要求 直接内联在 .task-header 里(见「页面头部:task-header + 内联 task-brief」)。 旧组件目前仍被 foundation 两页引用,暂不删除,但新页面一律不使用。
  • AI 侧栏面板、气泡、输入框、发送按钮全部中性灰白;其内部 Element Plus 主色按钮通过覆盖组件内 CSS 变量或 .el-button--primary 改教育蓝。
  • 工具栏 TrainingTaskToolbar / TrainingMaterialButton 按「按钮规范 · 位置 B」 换肤;提交按钮保留教育蓝实底作为主操作。
  • Element Plus 单选:选中圆点 #2563eb 填充白勾,覆盖默认蓝;带边框单选项 选中教育蓝描边蓝字。下拉选中值用深色 + -webkit-text-fill-color 防透明。

墨黑遗留页参考(已清零)

foundation 目录三页已全部教育蓝:

  • ✅ new-product-survey.vue(教育蓝基准页)
  • ✅ product-development-factors.vue
  • ✅ product-development-process.vue

其他目录(demand / product / assessment / channel / test / training)仍有墨黑/旧浅蓝/legacy 页约 28 页, 按本规范步骤导航 + 按钮 + 令牌块的可复制 CSS 批量改色即可。

令牌 值 用途
--student-accent #1f2937 主色(实底/选中/聚焦)
--student-accent-deep #111827 hover、active
--student-accent-light #4b5563 次级强调
--student-accent-soft #f3f4f6 浅灰底(hover/选中底)
三层次标识 #374151 / #43a294 / #8b6fc0 表头小色条(石墨/青灰/灰紫)
--student-danger #d9554c 判题错、删除
--student-success #4a9d6e 判题对、完成态
品牌阴影 rgba(17,24,39,*) 墨黑主色阴影

墨黑页的顶部工具栏按钮:共用组件 TrainingMaterialButton / TrainingTaskToolbar 组件自身仍是墨黑(青蓝半透明胶囊 + 墨黑极简), 墨黑遗留页不需要用页面根类覆盖;但如果未来共用组件升级,需要注意。

特异性陷阱(必看!)

teacher-light.scss L2780-2793 在 body.student-light-mode 块里有一条高特异性 默认按钮规则,会覆盖页面自己写的 !important:

body.student-light-mode {
  .el-button:not(.is-text):not(.is-link):not(.el-button--primary) :not(.el-button--success):not(.el-button--danger):not(.el-button--warning) {
    background: #ffffff !important;
    color: var(--text-strong) !important; // #1f2937 强制默认文字色
  }
}
  • 特异性:1 个 body + 7 个类/:not() 内选择器 = 0,8,1
  • 页面非 scoped 规则如 .new-product-value-page .task-toolbar-button.el-button = 0,3,0
  • 双方都有 !important,特异性高的赢 → 页面按钮颜色被强制锁死

修复方式:在 teacher-light.scss L2781 的 :not() 链里加上 :not(.task-toolbar-button):not(.material-button),让实训页工具栏按钮 脱离这条全局规则的控制,由页面自己的颜色样式接管。

同样注意:body.student-light-mode 下还有 .el-button--primary(L2761) 和 .el-button.is-text(L2796)规则,特异性同样高,如需覆盖主按钮或文字按钮 颜色也需在 :not() 链里排除。

验收

  • GetDiagnostics 零错误;Grep 确认旧蓝值 #1e88e5|#1565c0|#e3f2fd|#1e3a5f|#cfe2f5 无残留。
  • Grep #d9554c|#4a9d6e|rgba\(31, ?41, ?55 确认:仅出现在墨黑遗留页或 「墨黑遗留页参考」章节中。
  • 确认改动仅落在目标页文件;全局 scss 与共用组件无 diff(teacher-light.scss 的 :not() 链修复除外,属共享基础设施)。

完整实现样例:new-product-survey.vue

路径:src/views/foundation/new-product-survey.vue(路由 /foundation/new-product-survey) 根类:.new-product-value-page.student-training-shell 三层次:template 268 行 / script 624 行 / scoped style 1179 行 / 非 scoped style 247 行 核心模式:共用组件换肤(TrainingTaskToolbar / TrainingMaterialButton)+ 步骤导航分段胶囊 + 资料按钮教育蓝描边 + task-status 脉动指示

Template 骨架(业务绑定保留)

<div class="new-product-value-page student-training-shell">
  <main class="training-core">
    <!-- 任务头:badge + 脉动状态 + 标题 + 可折叠简介 + 工具栏组件 -->
    <section class="task-header">
      <div class="task-head-main">
        <div class="task-head-top">
          <!-- 新增:徽章与状态横排 -->
          <span class="task-badge">...</span>
          <span class="task-status">进行中 · Step {{ currentStep }}</span>
          <!-- 脉动指示器 -->
        </div>
        <h1 class="task-title">{{ taskTitle }}</h1>
        <p class="task-brief">...</p>
      </div>
      <TrainingTaskToolbar class="new-product-survey-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
    </section>

    <!-- 工作台:步骤导航 + 步骤内容 + 底部按钮 -->
    <section class="task-card">
      <nav class="step-indicator">
        <!-- 步骤导航:分段胶囊,见非 scoped -->
        <button v-for="step in steps" :class="['step-tab', { active, complete }]" />
      </nav>

      <!-- 步骤内容(v-show 切换,不销毁保持草稿) -->
      <section v-show="currentStep === 1" class="step-content active-step">
        <div class="step-card">
          <div class="step-title-row">
            <div class="step-title-main">
              <!-- 眉标 + 标题 -->
              <p class="section-eyebrow">Step 01</p>
              <h3>{{ getStepName(1) }}</h3>
            </div>
            <span class="step-chip">卖点分类练习</span>
            <!-- 步骤小胶囊 -->
          </div>

          <!-- 子组件:单选题目(共用,直接 import) -->
          <TrainingStepOneChoiceQuestions :questions="..." v-model="stepOneChoiceAnswers" />

          <!-- 表格:三层次色条 + el-select -->
          <table class="classification-table">
            <thead>
              <th>
                三层次分类
                <span class="layer-legend">
                  <!-- 三层次色条图例 -->
                  <i style="--dot: var(--student-layer-1)"></i>
                  <i style="--dot: var(--student-layer-2)"></i>
                  <i style="--dot: var(--student-layer-3)"></i>
                </span>
              </th>
            </thead>
            <tbody>
              <el-select v-model="classifications[point.id]" />
            </tbody>
          </table>
        </div>
      </section>

      <section v-show="currentStep === 2" class="step-content active-step">
        <!-- 实地调研:radio-group + input + 图片上传 -->
        <div class="step-two-survey">
          <section class="survey-field-group">
            <p class="survey-field-title">
              <b>01</b>
              选择调研平台
            </p>
            <el-radio-group v-model="step2Form.platform">
              <el-radio v-for="..." :label="option.value" class="survey-option">
                <strong>{{ option.label }}</strong>
                <span>{{ option.description }}</span>
              </el-radio>
            </el-radio-group>
          </section>
          <label class="image-upload-trigger">上传产品图片({{ images.length }}/5)</label>
          <figure v-for="image in images" class="product-image-card">...</figure>
        </div>
      </section>

      <section v-show="currentStep === 3" class="step-content active-step">
        <!-- 三层次对照分析表格 -->
        <table class="layer-table">
          <tbody>
            <el-table-row v-for="row in step3Rows">
              <td class="dimension-cell">
                <strong>{{ row.dimension }}</strong>
                <span>{{ row.definition }}</span>
              </td>
              <td class="example-cell">
                <ol>
                  参考要点
                </ol>
              </td>
              <td class="analysis-input-cell">
                <textarea v-model="row.analysis" class="cell-textarea" />
                <p class="analysis-count">已填写 {{ countStepThreePoints(row.analysis) }} / 至少 2 条</p>
              </td>
            </el-table-row>
          </tbody>
        </table>
      </section>

      <section v-show="currentStep === 4" class="step-content active-step">
        <!-- 价值判断:三个 field-block -->
        <label class="field-block is-layer layer-core">...</label>
        <label class="field-block is-layer layer-tangible">...</label>
        <label class="field-block is-layer layer-extended">...</label>
      </section>

      <!-- 底部按钮:step-nav-left / step-nav-right 双组工具栏 -->
      <div class="step-navigation">
        <div class="step-nav-left">
          <button class="btn-nav btn-ghost">上一步</button>
          <button class="btn-nav btn-save">保存当前进度</button>
        </div>
        <div class="step-nav-right">
          <span class="step-progress">Step 01 / 04</span>
          <button class="btn-nav btn-primary">{{ checking ? '正在判题' : '下一步' }}</button>
        </div>
      </div>
    </section>

    <!-- AI 侧栏(共用组件,直接 import) -->
    <TrainingAiSidebar :study-tip="..." :progress-items="..." />
  </main>
</div>

Script 业务结构

import { /* icons */ } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { /* API */ } from "@/api/studentTrainingAnswer";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import TrainingStepOneChoiceQuestions from "@/views/components/TrainingStepOneChoiceQuestions.vue";

const TASK_KEY = "new-product-survey";
const taskConfig = ref(null);          // 后端任务配置
const saving = ref(false);             // 保存中锁
const checking = ref(false);           // 判题中锁
const uploadingImages = ref(false);     // 上传中锁
const currentStep = ref(1);            // 当前步骤(1-based)
const briefOpen = ref(false);          // 简介展开
const classifications = ref({});       // step1 三层次分类答案
const checkResult = ref(null);         // step1 判题结果
const stepOneChoiceAnswers = ref({});  // step1 单选答案
const step2Form = ref({                // step2 调研表单
  platform, scene, customScene, keyword, productName, images
});
const step3Rows = ref([...]);          // step3 三层次分析行
const step4Form = ref({ coreValue, tangibleValue, extendedValue });

// 本地草稿 key
const getLocalDraftKey = () => `${TASK_KEY}:draft:${userId}`;

// 生命周期:loadTaskConfig → loadSavedAnswer → loadDraft(草稿兜底)
onMounted(async () => { ... });

// watch 整个 draft,自动持久化
watch(() => JSON.stringify(buildDraft()), persistDraft);

Scoped Style 关键块

令牌覆盖(根类内声明,所有 var(--student-*) 自动生效):

.new-product-value-page {
  --student-accent: #2563eb;
  --student-accent-deep: #1d4ed8;
  --student-accent-light: #3b82f6;
  --student-accent-soft: #eff6ff;
  --student-danger: #ef4444;
  --student-success: #2ba471;
  --student-layer-1: #1d4ed8;
  --student-layer-2: #3b82f6;
  --student-layer-3: #93c5fd;
  /* EP 变量一并覆盖 */
  --el-color-primary: #2563eb;
  --el-color-primary-light-3~9: ...;
  --el-input-hover-border-color: #3b82f6;
  --el-input-focus-border-color: #2563eb;
  --el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
}

特有元素(其他教育蓝页面直接复用,或按此模式仿写):

/* task-head-top:badge + 脉动状态横排 */
.task-head-top {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.task-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: 999px;
  background: #eff6ff;
  color: #2563eb;
  font-size: 11px;
  font-weight: 700;
  &::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #2563eb;
    animation: pulse-dot 1.6s ease-in-out infinite;
  }
}

/* step-chip:步骤小胶囊(教育蓝渐变底) */
.step-chip {
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  background: linear-gradient(135deg, #eff6ff, #dbeafe);
  color: #2563eb;
}

/* section-eyebrow:眉标(教育蓝 + 宽字距) */
.section-eyebrow {
  color: #2563eb;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* field-block.is-layer:步骤4三层次块(左侧竖条 + 序号) */
.field-block.is-layer {
  padding: 16px 18px;
  border-radius: 12px;
  background: #fafbfc;
  border-left: 4px solid var(--dot, #2563eb);
}
.field-block.layer-core {
  --dot: #1d4ed8;
}
.field-block.layer-tangible {
  --dot: #3b82f6;
}
.field-block.layer-extended {
  --dot: #93c5fd;
}

/* el-select / el-radio / el-textarea 聚焦覆盖 */
:deep(.el-select .el-input__wrapper.is-focus) {
  box-shadow:
    0 0 0 1px #2563eb inset,
    0 0 0 3px rgba(37, 99, 235, 0.12);
}
:deep(.el-radio__input.is-checked) {
  .el-radio__inner {
    border-color: #2563eb;
    background: #2563eb;
  }
}

非 Scoped Style 覆盖策略(关键!)

为什么需要:共用组件 TrainingTaskToolbar / TrainingMaterialButton 在其他墨黑页继续用,组件自身是墨青蓝,必须用页面根类覆盖。步骤导航全局有 !important 锁死。

<style lang="scss">
/* 1. 共用组件换肤:顶部任务工具栏(36px 小按钮) */
.new-product-value-page .training-task-toolbar .task-toolbar-button.el-button {
  height: 36px !important; min-height: 36px !important;
  border: 1px solid rgba(37,99,235,.45) !important;
  border-radius: 8px !important;
  color: #2563eb !important; background: #ffffff !important;
  font-size: 13px !important; font-weight: 600 !important;
  &:hover:not(.is-disabled) {
    border-color: #2563eb !important; background: #eff6ff !important;
    transform: translateY(-1px) !important;
    box-shadow: 0 2px 6px rgba(37,99,235,.12) !important;
  }
}

/* 2. 共用组件换肤:资料按钮(TrainingMaterialButton) */
.new-product-value-page .training-task-toolbar .material-button.el-button {
  /* 同 task-toolbar-button 规格 */
}

/* 3. 提交按钮实底 */
.new-product-value-page .training-task-toolbar .task-toolbar-button--submit {
  border-color: #2563eb !important; background: #2563eb !important; color: #ffffff !important;
  box-shadow: 0 1px 2px rgba(37,99,235,.35), 0 3px 10px rgba(37,99,235,.22) !important;
  &:hover:not(.is-disabled) { background: #1d4ed8 !important; box-shadow: ... !important; }
}

/* 4. 步骤导航:分段胶囊三态 */
#app .studentLayout section.app-main:has(.new-product-value-page) .step-indicator {
  display: flex !important; gap: 0 !important;
  .step-tab {
     height: 42px !important;
    border-radius: 0 !important; border: 1px solid #e5e7eb !important;
    background: #ffffff !important; color: #6b7280 !important;
    /* 首尾圆角 */
    &:first-child { border-top-left-radius: 8px !important; border-bottom-left-radius: 8px !important; }
    &:last-child  { border-top-right-radius: 8px !important; border-bottom-right-radius: 8px !important; }
    /* 当前态 */
    &.active { background: #2563eb !important; color: #ffffff !important; border-color: #2563eb !important; }
    /* 已完成态 */
    &.complete:not(.active) { border-color: rgba(43,164,113,.25) !important; color: #374151 !important;
      .step-no { background: rgba(43,164,113,.12) !important; color: #16915f !important; } }
  }
}

/* 5. 底部按钮:双组工具栏(step-nav-left / step-nav-right) */
#app .studentLayout section.app-main:has(.new-product-value-page) .step-navigation {
  display: flex !important; justify-content: space-between !important;
  .step-nav-left, .step-nav-right { display: flex !important; gap: 8px !important; align-items: center !important; }
  .btn-nav.btn-primary {
    border-color: #2563eb !important; background: #2563eb !important; color: #ffffff !important;
    box-shadow: 0 3px 10px rgba(37,99,235,.28) !important;
  }
  .btn-nav.btn-save {
    border: 1px solid #e5e7eb !important; background: #f3f4f6 !important; color: #374151 !important;
    /* 三级按钮灰色保留,不教育蓝 */
  }
  .step-progress { color: #6b7280 !important; font-weight: 800 !important; }
}
</style>

改色判定原则(防误改)

场景 教育蓝页处理 例子
#1f2937 作文字色 ✅ 保留 dimension-cell / classification-selling-point 等
#1f2937 作实底/描边/聚焦/阴影 ❌ 改 #2563eb 按钮、步骤胶囊、textarea focus
#f3f4f6 作 accent-soft ❌ 改 #eff6ff 徽章底、hover 底
#f3f4f6 作三级 save 按钮底 ✅ 保留 .btn-save 灰色中性保留
品牌阴影 rgba(31,41,55,*) ❌ 改 rgba(37,99,235,*) 主按钮投影
极淡中性阴影 rgba(17,24,39,.04) ✅ 保留 卡片影
语义红 #d9554c ❌ 改 #ef4444 判题错、删除
语义绿 #4a9d6e / #3f8a5f ❌ 改 #2ba471 / #16915f 判题对、完成态

teacher-light.scss 特异性修复(必做)

全局有一条 body.student-light-mode .el-button:not(.is-text)... 高特异性(0,8,1)规则把默认按钮强制锁为墨黑。在 teacher-light.scss L2781 的 :not() 链加:

:not(.task-toolbar-button): not(.material-button);

否则页面自己的教育蓝按钮样式无法生效。

后续 29 页批量改色模板

本页是教育蓝落地的参考基准。后续墨黑页统一改色时:

  1. 令牌块:直接替换为上述教育蓝令牌(含 EP 变量)
  2. 共用组件换肤:复制上述非 scoped 覆盖(task-toolbar-button / material-button / submit)
  3. 步骤导航:如果有步骤导航,复制分段胶囊三态
  4. 底部按钮:复制 step-navigation + btn-nav 规格
  5. 特有元素:task-head-top 脉动状态、step-chip 小胶囊、section-eyebrow 眉标 按需添加(可无)
  6. 表格:three-layer 页加 layer-legend 色条 + layer-core/tangible/extended 竖条
  7. 业务逻辑:script 一字不动