---
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` 落地的「墨黑极简」风格:纯白画布、
墨黑主操作、中性灰层次、紧凑排版。该 Vue 文件是整体标准样例;**步骤导航**
以本规范「步骤导航: 按钮切换风」一节为准( foundation 三页均已落地),
**左侧课程菜单**换肤见「侧边栏 el-menu 墨黑换肤」与「折叠把手」两节。
拿不准时先读这些文件。
## 边界
- 只动 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(31, 41, 55, 0.4) !important; /* 深灰描边 */
border-radius: 10px !important; /* 统一 10px */
color: var(--student-text, #1f2937 ) !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, #1f2937 ) !important;
color: #ffffff !important;
background: var(--student-accent, #1f2937 ) !important; /* 墨黑实底 */
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.28) !important;
}
/* hover 态 */
.student-training-shell .task-actions > button:hover:not(:disabled) {
border-color: var(--student-accent, #1f2937 ) !important;
background: var(--student-accent-soft, #f3f4f6 ) !important;
color: var(--student-accent, #1f2937 ) !important;
transform: translateY(-1px) !important;
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12) !important;
}
.student-training-shell .task-actions > .btn-primary:hover:not(:disabled) {
border-color: var(--student-accent-deep, #111827 ) !important;
background: var(--student-accent-deep, #111827 ) !important;
color: #ffffff !important;
transform: translateY(-1px) !important;
box-shadow: 0 5px 14px rgba(17, 24, 39, 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(31, 41, 55, 0.4) !important;
border-radius: 10px !important;
color: var(--student-accent, #1f2937 ) !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, #1f2937 ) !important;
background: var(--student-accent-soft, #f3f4f6 ) !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. 在页面 scoped 样式的根类块内定义全部令牌(见下),让所有
`var(--student-*)` 规则自动重着色,不要逐处打补丁。
3. 新增一个非 scoped `<style lang="scss">` 块,选择器全部以页面根类为锚:
- 覆盖祖先 `app-main` 画布背景( scoped 触及不到祖先);
- 重写步骤导航(全局规则带 `!important` , scoped 无法取胜,见步骤导航一节)。
4. 按按钮四级层级、字号梯度、间距规则逐区收紧。
5. 完成后用 GetDiagnostics 校验,并全文搜索蓝色旧值确认无残留。
## 颜色令牌
在页面根类中定义:
```scss
.xxx-page {
/* 主色:墨黑 */
--student-accent: #1f2937 ;
--student-accent-deep: #111827 ;
--student-accent-light: #4b5563 ;
--student-accent-soft: #f3f4f6 ;
/* 不使用橙色点缀;需要强调时只用墨黑或语义红/绿 */
/* 语义色 */
--student-danger: #d9554c ;
--student-success: #4a9d6e ;
/* 三层次标识:石墨 / 青灰 / 灰紫(仅小色条,不做行底色) */
--student-layer-1: #374151 ;
--student-layer-2: #43a294 ;
--student-layer-3: #8b6fc0 ;
/* 结构色 */
--student-panel-border: #e5e7eb ;
--student-card-bg: #ffffff ;
--student-panel-bg: #fafbfc ;
--student-text: #1f2937 ;
--student-text-secondary: #6b7280 ;
}
```
非 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" 胶囊。
## 步骤导航:按钮切换风
每个步骤是一枚等宽分段按钮:白底灰边、圆角矩形,编号收成左侧小徽章;
当前步骤墨黑实底,切换感明确。已在 foundation 三页全部落地, DOM 结构
( `.step-indicator > .step-tab > .step-bar/.step-no/.step-name`)保持不变,
只换样式。
### 视觉规格
| 元素 | 规格 |
|------|------|
| `.step-indicator` | flex 水平居中,`align-items: stretch`, gap 10px; 无 border/底色; margin `4px 0 20px` |
| `.step-tab` | `flex: 1 1 0` , max-width 220px; 横向排列( 徽章+名称) 居中, gap 8px; min-height 42px; padding `8px 12px` ; border `1px solid #e5e7eb` ; radius 8px; 白底; 字 `#6b7280` 、13px/600, 名称允许换行 |
| `.step-bar` | `display: none` (按钮风不需要引导线) |
| `.step-no` | 22px 小徽章: radius 6px、`#f3f4f6` 底、等宽 12px/700; `::after` 圆点隐藏 |
| `.step-name` | `color: inherit` |
三态:
- **未到达**: 白底灰边灰字; hover 边转 `#1f2937` 、底 `#f9fafb` 、字墨黑,
徽章变墨黑底白字。
- **已完成**:边 `#dce9e2` 、字 `#374151` ,徽章 12% 绿底 + `#3f8a5f` 字;
hover 边转绿、徽章变绿实底白字。
- **当前**: 墨黑实底白字( 边同色) , 徽章反转为白底黑字; hover 加深 `#111827` 。
### 覆盖机制(不做这步,页面仍是浅蓝大胶囊)
全局 `student-training.scss` 对 `.student-training-shell .step-indicator` 与
`.step-tab` 全部以 `!important` 锁定浅蓝底(`#f0f6fd`)、全胶囊圆角、固定高度、
`white-space: nowrap` ,并在 ≤1100px 用 `!important` 强制纵向堆叠。
**scoped 样式无论怎么写特异性都打不赢**( scoped 属性选择器不增加 id 级特异性)。
正确做法——规则放进页面**非 scoped** 块,选择器带 id 锚点且每条声明 `!important` :
```scss
#app .studentLayout section.app-main:has(.xxx-page) .step-indicator {
/* 容器 */ !important;
.step-tab {
/* 按钮 + 三态,全部 !important; 推荐直接嵌套, 减少重复书写 */
}
}
```
必须显式重置的全局残留:容器的 `padding / border / border-radius / background /
box-shadow`; tab 的 `min-height / height / padding / border / border-radius` 。
窄屏在非 scoped 块自行处理,**不要**接受全局 1100px 纵向规则:
```scss
@media (max-width: 900px) {
#app …:has(.xxx-page) .step-indicator {
justify-content: flex-start;
overflow-x: auto; /* 横向滑动 */
}
#app …:has(.xxx-page) .step-tab { flex: 0 0 148px; max-width: none; }
}
```
## 侧边栏 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-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` ,普通声明无效。
- 一级菜单自定义墨黑块:`#1f2937` 底白字 14px/600, hover `#111827` ,
激活 `#111827` + 左 3px 白条; color/background 全部 `!important` 。
- 二级菜单: hover `#f3f4f6` 、激活墨黑底白字,**背景必须 `!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. ** 一级主操作**:墨黑实底 `#1f2937` 白字 + 中性柔影, hover `#111827` 。
用于下一步/提交任务/AI 发送/智能评分。
2. ** 二级描边**:白底 + 描边 + `#1f2937` 字, hover 墨黑描边 + `#f3f4f6` 底。
用于上一步/下载/重新实训/导出/清空。
3. ** 三级浅灰底**: `#f3f4f6` 底 + `#374151` 字, hover `#e9ecef` 。用于保存草稿。
4. ** 危险文字**:无底色,`#d9554c` 文字链接, hover 深红加下划线。用于删除。
禁用态统一 `opacity: 0.5` + `not-allowed` , `transform: none`、去掉阴影,形状不跳变。
loading 态保持原配色。
### 位置 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(31, 41, 55, 0.4)` ,字色 `var(--student-accent)` 。
- hover: `translateY(-1px)` + 描边变实色墨黑 + 底变 `var(--student-accent-soft)`
+ 阴影 `0 3px 10px rgba(31,41,55,.12)` ; active 回落 `translateY(0)` 。
- primary: 墨黑实底 + 阴影 `0 3px 10px rgba(31,41,55,.28)` , hover 加深为
`#111827` 、阴影 `0 5px 14px rgba(31,41,55,.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 字**,白底 + `#d1d5db` 描边 + `#374151` 字; hover 描边和字变 `#1f2937` 、
底 `#f9fafb` 、阴影 `0 2px 6px rgba(17,24,39,.1)` 。图标 14px 且 `color: inherit` 。
- 提交按钮(`.task-toolbar-button--submit`):墨黑实底白字 +
`0 2px 8px rgba(17,24,39,.22)` , hover `#111827` + 阴影加深。
- loading 态强制保持白底灰描边,不随 Element Plus 默认变蓝。
- 无资料时的 plain 态:字色 `#9ca3af` , hover 只到 `#6b7280` + `#f9fafb` 。
- 步骤标题行内的小号导出按钮可再缩到 32px 高、12px 字。
## 表格规则
- 表头、单元格、外容器一律不设 background( 默认透明白) , 包括 hover、选中、判题后。
- 表头仅用 12– 13px、800– 900 字重,无渐变底。
- 只保留 `#e5e7eb` 浅灰边框与行间分隔线。
- 序号用 22px 浅灰圆角方块(`#f3f4f6`) + 等宽数字。
- 判题结果:单元格内文字反馈(绿/红)+ 底部汇总横幅( 8– 10px 圆角、8% 同色底)。
- 三层次概念仅靠维度名前 3– 4px 小色条标识。
## 间距(紧凑)
- 主列 gap 10px; 卡片内 padding 12px; 字段组 padding 12px。
- 步骤标题下 10px、说明下 14px、字段块间 12px、表格上 14px。
- 行高:页面 1.55,标题 1.35,说明 1.6,表格次要文字 1.55。
## 子组件换肤要点
- AI 侧栏面板、气泡、输入框、发送按钮全部中性灰白;其内部 Element Plus
主色按钮通过覆盖组件内 CSS 变量或 `.el-button--primary` 改墨黑。
- 工具栏 `TrainingTaskToolbar` / `TrainingMaterialButton` 按「按钮规范 · 位置 B」
换肤;提交按钮保留墨黑实底作为主操作。
- Element Plus 单选:选中圆点 `#1f2937` 填充白勾,覆盖默认蓝;带边框单选项
选中墨黑描边黑字。下拉选中值用深色 + `-webkit-text-fill-color` 防透明。