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/.cursor/skills/ink-training-theme/SKILL.md

1421 lines
61 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
name: ink-training-theme
description: 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>` 块也必须加页面根类锚点:
```scss
/* ✅ 正确:以页面根类为锚,只影响当前页面 */
#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 覆盖(步骤导航除外,它的结构每页不同)。
改造模板:
```scss
/* === 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` 确认全局和目标页无残留旧蓝值,再改页面。
## 实施流程
0. **先全局后页面**:Grep 搜索 `#1e88e5|#1565c0|#e3f2fd|#1e3a5f|#cfe2f5`
定位 `student-training.scss` 和子组件中的旧蓝色规则;按「全局 !important
覆盖机制」一节的模板替换成教育蓝极简(**只做一次,后续页面自动继承**)。
1. 读目标页,确认根类(如 `.xxx-page.student-training-shell`)和现有 DOM。
2. **搭建页面头部**:按「页面头部:task-header + 内联 task-brief」一节
替换旧 `<TrainingTaskBrief>` 组件为内联 `task-brief`,确保有
`task-head-main` 包裹层和 `briefOpen` ref。
3. 在页面 scoped 样式的根类块内定义全部令牌(见下),让所有
`var(--student-*)` 规则自动重着色,不要逐处打补丁。
4. 新增一个非 scoped `<style lang="scss">` 块,选择器全部以页面根类为锚:
- 覆盖祖先 `app-main` 画布背景(scoped 触及不到祖先);
- 重写步骤导航(全局规则带 `!important`,scoped 无法取胜,见步骤导航一节)。
5. 按按钮四级层级、字号梯度、间距规则逐区收紧。
6. 完成后用 GetDiagnostics 校验,并全文搜索蓝色旧值确认无残留。
## 颜色令牌
在页面根类中定义:
```scss
.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 块中的画布与行高:
```scss
#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 结构
```html
<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 必备变量
```js
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 样式(直接照抄)
```scss
/* 任务头:白卡 + 左侧教育蓝竖条 + 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` 替换为你的页面根类**,其余一字不动:
```scss
#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 {
flex: 1 1 0 !important;
max-width: 240px !important;
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)
```html
<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:
```js
import { ArrowLeft, ArrowRight, CircleCheck, Download, RefreshLeft } from "@element-plus/icons-vue";
```
### 位置 A:底部步骤导航(40px 标准按钮)
结构(原生 `<button>`,容器 `.step-navigation` 居中 flex,gap 14px):
```html
<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` 锚点覆盖。
```scss
/* ============================================================
* 共用组件换肤: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`:
```scss
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 骨架(业务绑定保留)
```html
<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 业务结构
```js
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-*)` 自动生效)**:
```scss
.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);
}
```
**特有元素**(其他教育蓝页面直接复用,或按此模式仿写):
```scss
/* 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` 锁死。
```scss
<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 {
flex: 1 1 0 !important; 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()` 链加:
```scss
: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 一字不动