refactor(student-training): 重构实训页面样式与结构,统一墨黑极简主题

1.  重构TrainingMaterialButton.vue按钮样式,替换为墨黑极简风格
2.  全局样式student-training.scss替换旧蓝色主题为墨黑主题,修复!important覆盖问题
3.  重构行业需求分析页面结构,新增任务简介折叠功能
4.  将原生textarea替换为ElInput组件,优化交互体验
5.  重构页面样式,统一使用CSS变量实现主题切换,适配响应式布局
6.  重写步骤导航样式,改为按钮切换风格
fix/exam-question-access-material-excel-delete-draft
qinzhenpen 3 weeks ago
parent 8637c17ff4
commit f89a5649fa

@ -15,18 +15,176 @@ description: Apply ink-minimal style to student training pages (Vue 3 + Element
## 边界 ## 边界
- 只动 template 结构与样式;script 业务逻辑(判题/保存/草稿/上传/接口)一律不碰。 - 只动 template 结构与样式;script 业务逻辑(判题/保存/草稿/上传/接口)一律不碰。
- 仅作用于当前页根类,不得影响其他页面;不修改全局 `student-training.scss`。 - 仅作用于当前页根类,不得影响其他页面;**但必须同步修改全局
`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。 1. 读目标页,确认根类(如 `.xxx-page.student-training-shell`)和现有 DOM。
2. 在页面 scoped 样式的根类块内定义全部令牌(见下),让所有 2. 在页面 scoped 样式的根类块内定义全部令牌(见下),让所有
`var(--student-*)` 规则自动重着色,不要逐处打补丁。 `var(--student-*)` 规则自动重着色,不要逐处打补丁。
3. 新增一个非 scoped `<style lang="scss">` 块,选择器全部以页面根类为锚: 3. 新增一个非 scoped `<style lang="scss">` 块,选择器全部以页面根类为锚:
- 覆盖祖先 `app-main` 画布背景(scoped 触及不到祖先); - 覆盖祖先 `app-main` 画布背景(scoped 触及不到祖先);
- 重写步骤导航(全局规则带 `!important`,scoped 无法取胜,见步骤导航一节); - 重写步骤导航(全局规则带 `!important`,scoped 无法取胜,见步骤导航一节)。
- 给子组件(AI 侧栏、工具栏等)换肤。
4. 按按钮四级层级、字号梯度、间距规则逐区收紧。 4. 按按钮四级层级、字号梯度、间距规则逐区收紧。
5. 完成后用 GetDiagnostics 校验,并全文搜索蓝色旧值确认无残留。 5. 完成后用 GetDiagnostics 校验,并全文搜索蓝色旧值确认无残留。

@ -442,46 +442,51 @@ aside {
border-color: rgba(0, 170, 255, 0.55) !important; border-color: rgba(0, 170, 255, 0.55) !important;
} }
.pagination-container .el-pagination__total, /* === 以下规则原本是裸选择器,全站强制改成暗色主题。
.pagination-container .el-pagination__jump, 已加 :not(.studentLayout) 锚点,只在教师端/超管端生效。
.el-pagination__total, 学生端(.studentLayout)和实训页(.student-training-shell)
.el-pagination__jump { 不再被这些裸规则污染。 */
:not(.studentLayout) .pagination-container .el-pagination__total,
:not(.studentLayout) .pagination-container .el-pagination__jump,
:not(.studentLayout) .el-pagination__total,
:not(.studentLayout) .el-pagination__jump {
color: #9ab3d0; color: #9ab3d0;
} }
.pagination-container .el-pagination .btn-prev, :not(.studentLayout) .pagination-container .el-pagination .btn-prev,
.pagination-container .el-pagination .btn-next, :not(.studentLayout) .pagination-container .el-pagination .btn-next,
.el-pagination .btn-prev, :not(.studentLayout) .el-pagination .btn-prev,
.el-pagination .btn-next { :not(.studentLayout) .el-pagination .btn-next {
background-color: rgba(3, 24, 36, 0.85) !important; background-color: rgba(3, 24, 36, 0.85) !important;
color: #cde4ff !important; color: #cde4ff !important;
border: 1px solid rgba(45, 143, 207, 0.32); border: 1px solid rgba(45, 143, 207, 0.32);
} }
.pagination-container .el-pagination .btn-prev:disabled, :not(.studentLayout) .pagination-container .el-pagination .btn-prev:disabled,
.pagination-container .el-pagination .btn-next:disabled, :not(.studentLayout) .pagination-container .el-pagination .btn-next:disabled,
.el-pagination .btn-prev:disabled, :not(.studentLayout) .el-pagination .btn-prev:disabled,
.el-pagination .btn-next:disabled { :not(.studentLayout) .el-pagination .btn-next:disabled {
background-color: rgba(3, 24, 36, 0.5) !important; background-color: rgba(3, 24, 36, 0.5) !important;
color: #5f7d9c !important; color: #5f7d9c !important;
} }
.pagination-container .el-pagination .el-select .el-input__wrapper, :not(.studentLayout) .pagination-container .el-pagination .el-select .el-input__wrapper,
.el-pagination .el-select .el-input__wrapper { :not(.studentLayout) .el-pagination .el-select .el-input__wrapper {
background: rgba(3, 24, 36, 0.9) !important; background: rgba(3, 24, 36, 0.9) !important;
border: 1px solid rgba(45, 143, 207, 0.9) !important; border: 1px solid rgba(45, 143, 207, 0.9) !important;
box-shadow: none !important; box-shadow: none !important;
} }
.pagination-container .el-pagination .el-select .el-input__inner, // :not(.studentLayout) .pagination-container .el-pagination .el-select .el-input__inner,
.el-pagination .el-select .el-input__inner { // :not(.studentLayout) .el-pagination .el-select .el-input__inner {
color: #eef5ff !important; // color: #eef5ff !important;
} // }
.pagination-container .el-input__inner, // :not(.studentLayout) .pagination-container .el-input__inner,
.el-input__inner { // :not(.studentLayout) .el-input__inner {
color: #eef5ff !important; // color: #eef5ff !important;
} // }
.text-center { .text-center {
text-align: center; text-align: center;

@ -29,15 +29,54 @@
--student-aux-font: 13px; --student-aux-font: 13px;
--student-radius-sm: 6px; --student-radius-sm: 6px;
--student-radius-pill: 999px; --student-radius-pill: 999px;
/* 浅色科技蓝主题 */ /* 墨黑极简主题令牌 */
--student-accent: #1e88e5; --student-accent: #1f2937;
--student-accent-deep: #1565c0; --student-accent-deep: #111827;
--student-accent-light: #42a5f5; --student-accent-light: #4b5563;
--student-panel-border: #cfe2f5; --student-accent-soft: #f3f4f6;
--student-panel-border: #e5e7eb;
--student-card-bg: #ffffff; --student-card-bg: #ffffff;
--student-panel-bg: #f0f6fd; --student-panel-bg: #fafbfc;
--student-text: #1e3a5f; --student-text: #1f2937;
--student-text-secondary: #5a7a9a; --student-text-secondary: #6b7280;
/* ===== Element Plus 亮白主题覆盖 =====
index.scss 全局把 Element Plus CSS 变量设成了暗色主题
(--el-input-text-color: #e0f0ff 极浅蓝 / --el-input-bg-color: rgba(0,30,60,.6)),
教师端/超管端的暗色页面需要这些值,但学生实训页
.student-training-shell 是亮白底,必须在这里覆盖回来。 */
--el-text-color-primary: #1f2937;
--el-text-color-regular: #374151;
--el-text-color-secondary: #6b7280;
--el-text-color-placeholder: #9ca3af;
--el-text-color-disabled: #9ca3af;
--el-text-color: #1f2937;
--el-input-bg-color: #ffffff;
--el-input-text-color: #1f2937;
--el-input-border-color: #e5e7eb;
--el-input-hover-border-color: #1f2937;
--el-input-focus-border-color: #1f2937;
--el-input-focus-ring-color: rgba(31, 41, 55, 0.15);
--el-input-disabled-bg-color: #f3f4f6;
--el-input-disabled-text-color: #9ca3af;
--el-input-disabled-border-color: #e5e7eb;
--el-textarea-bg-color: #ffffff;
--el-textarea-text-color: #1f2937;
--el-textarea-border-color: #e5e7eb;
--el-textarea-hover-border-color: #1f2937;
--el-textarea-focus-border-color: #1f2937;
--el-textarea-focus-ring-color: rgba(31, 41, 55, 0.15);
--el-select-bg-color: #ffffff;
--el-select-text-color: #1f2937;
--el-select-border-color-hover: #1f2937;
--el-fill-color-blank: #ffffff;
--el-border-color: #e5e7eb;
--el-border-color-hover: #1f2937;
--el-border-radius-base: 6px;
} }
/* /*
@ -500,10 +539,10 @@
.student-training-shell .training-actions button.training-action, .student-training-shell .training-actions button.training-action,
.student-training-shell .task-actions > button, .student-training-shell .task-actions > button,
.student-training-shell .taskfooter .el-button { .student-training-shell .taskfooter .el-button {
border: 1px solid #1e88e5 !important; border: 1px solid rgba(31, 41, 55, 0.4) !important;
border-radius: var(--student-radius-pill) !important; border-radius: 10px !important;
color: #1e3a5f !important; color: var(--student-text, #1f2937) !important;
background: #e3f2fd !important; background: #ffffff !important;
font-weight: 700 !important; font-weight: 700 !important;
} }
@ -511,9 +550,31 @@
.student-training-shell .task-actions > .btn-submit, .student-training-shell .task-actions > .btn-submit,
.student-training-shell .task-actions > .btn-primary, .student-training-shell .task-actions > .btn-primary,
.student-training-shell .taskfooter .el-button:first-child { .student-training-shell .taskfooter .el-button:first-child {
border-color: #1565c0 !important; border-color: var(--student-accent, #1f2937) !important;
color: #ffffff !important; color: #ffffff !important;
background: linear-gradient(110deg, #1e88e5, #1565c0) !important; background: var(--student-accent, #1f2937) !important;
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.28) !important;
}
.student-training-shell .training-actions button.training-action:hover:not(:disabled),
.student-training-shell .task-actions > button:hover:not(:disabled),
.student-training-shell .taskfooter .el-button:hover:not(.is-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 .training-actions .training-action--submit:hover:not(:disabled),
.student-training-shell .task-actions > .btn-primary:hover:not(:disabled),
.student-training-shell .task-actions > .btn-submit:hover:not(:disabled),
.student-training-shell .taskfooter .el-button:first-child:hover:not(.is-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;
} }
.student-training-shell .task-actions > button .el-icon { .student-training-shell .task-actions > button .el-icon {
@ -535,15 +596,16 @@
height: 40px !important; height: 40px !important;
min-height: 40px !important; min-height: 40px !important;
padding: 0 18px !important; padding: 0 18px !important;
border: 1px solid #1e88e5 !important; border: 1px solid rgba(31, 41, 55, 0.4) !important;
border-radius: var(--student-radius-pill) !important; border-radius: 10px !important;
color: #1e3a5f !important; color: var(--student-accent, #1f2937) !important;
background: #e3f2fd !important; background: #ffffff !important;
box-shadow: none !important; box-shadow: none !important;
font-size: 14px !important; font-size: 13px !important;
font-weight: 800 !important; font-weight: 700 !important;
line-height: 1 !important; line-height: 1 !important;
letter-spacing: 0 !important; letter-spacing: 0 !important;
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 .el-button.ghost-action:hover:not(.is-disabled), .student-training-shell .el-button.ghost-action:hover:not(.is-disabled),
@ -553,9 +615,10 @@
.student-training-shell button.outline-action:hover:not(:disabled), .student-training-shell button.outline-action:hover:not(:disabled),
.student-training-shell .workbench-actions button.material-download:hover:not(:disabled) { .student-training-shell .workbench-actions button.material-download:hover:not(:disabled) {
transform: translateY(-1px) !important; transform: translateY(-1px) !important;
border-color: #1565c0 !important; border-color: var(--student-accent, #1f2937) !important;
color: #1565c0 !important; color: var(--student-accent, #1f2937) !important;
background: #bbdefb !important; background: var(--student-accent-soft, #f3f4f6) !important;
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12) !important;
} }
/* /*

@ -82,21 +82,26 @@ async function handleDownload() {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 4px; gap: 6px;
min-height: 28px; min-height: 40px;
padding: 0 8px; padding: 0 18px;
border-color: rgba(99, 217, 255, .55); border: 1px solid rgba(31, 41, 55, 0.4);
border-radius: var(--student-radius-sm); border-radius: 10px;
color: #e9fbff; color: #1f2937;
background: rgba(17, 126, 178, .36); background: #ffffff;
font-size: var(--student-step-font); font-family: inherit;
font-weight: 800 font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
} }
.material-button:hover { .material-button:hover {
color: #e9fbff; color: #1f2937;
border-color: rgba(116, 235, 255, .85); border-color: #1f2937;
background: rgba(18, 146, 204, .5) background: #f3f4f6;
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12);
} }
.image-preview { .image-preview {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save