fix(task-review): 评阅预览改用最新学生页并修复新版样式门控失配

1. previewMap 三处指向已废弃的旧页面,改为 router 实际引用的最新页面:
   industry-demand-analysis      -> demand/industry-demand-analysis.vue
   competitive-environment-analysis -> demand/competitive-environment-analysis.vue
   market-opportunity-selection  -> product/market-opportunity-selection.vue
   并在文件头补充契约注释(必须与 router 一致,不得指向废弃页面)。

2. 修复评阅预览"看起来仍在用废弃页面"的根因,两重原因叠加:
   a) /task-review-preview 是脱离 Layout 的独立路由,DOM 里没有 .studentLayout
      与 section.app-main,导致学生页新版皮肤的门控选择器
      `#app .studentLayout section.app-main:has(.xxx-page) …` 集体失配,
      页面静默回落全局旧版样式(胶囊满宽、步骤说明小字重新出现、按钮配色不一致)。
      模板外层补两级空壳祖先容器,只补壳、不渲染顶栏侧栏。
   b) 预览 iframe 宽约 1000px,命中 student-training.scss 的
      @media (max-width: 1100px):`#app .student-training-shell .step-tab
      { width: 100% }`(特异性 1,2,0)压过预览自身 :deep() 覆盖(0,4,0),
      步骤胶囊被撑成整行、只剩第一个可见并出现横向滚动条。
      新增非 scoped 样式用更高特异性复位宽度,并压掉布局级背景与内边距。

实测(真实 Chrome,1000px 视口,与预览 iframe 同宽):
修复前 胶囊宽 952px x4、导航区 scrollWidth 3874/952(溢出);
修复后 214/169/199/200px、952/952(无溢出),与学生端外观一致。
dev-QQq
qinzhenpen 7 hours ago
parent 89d08986cf
commit 511abe73b9

@ -0,0 +1,61 @@
# 2026-10-14
## 摸清教师端「任务评阅」页面结构(是否复用学生端组件)
结论:**不是复制页面,而是同一个学生端 SFC 被动态复用**(源码同一份,非副本)。
链路:
- `views/teacherEnd/score/TaskReviewPanel.vue`(左侧班级/任务/学生列表 + 打分)右侧用 `<iframe src="...#/task-review-preview?taskKey=..&reviewPreviewKey=..">`
- 隐藏教师路由 `/task-review-preview`(roles:["teacher"])→ `views/teacherEnd/taskReview/StudentTaskReadonlyPreview.vue`
- 该组件用 `<component :is>` 动态加载 `studentTaskPreviewMap.js`:25 个 taskKey → 真实学生页组件(`@/views/foundation|demand|product|channel|test|assessment/**.vue`);仅 comprehensive-case-training 通过 props 传 previewTaskKey
数据注入(不走 props,走 sessionStorage + API 层拦截):
- `utils/trainingReviewPreview.js`:写 `teacher-task-review-preview:<previewKey>` = {taskKey, teachingClassId, taskConfig, answer}
- `api/studentTrainingAnswer.js#getStudentTrainingAnswer` 命中 preview 直接返回 preview.answer
- `api/trainingTask.js#getTrainingTaskByKey` 返回该班生效 taskConfig(教师覆盖优先,否则管理员案例)
- `utils/request.js` 拦截 POST/PUT/PATCH/DELETE(答案/学习时长/上传)直接 reject「只读模式」
只读保障三层:①点击/按键捕获 guardPreviewInteraction(拦 input/select/contenteditable/按钮,放行步骤导航)②scoped `:deep` 隐藏顶部 chrome、AI 侧栏、底部动作区,表单 pointer-events:none ③网络层拦截写请求
iframe 高度:子页 ResizeObserver + MutationObserver 测量 → postMessage(`teacher-task-review-preview-height`)→ 父页设置高度
风险点:学生页任何类名/结构改动会波及评阅预览(大量 :deep 依赖命名契约);学生页新增写接口需同步加入 `isTrainingReviewPreviewRequest` 名单。
### 追加:覆盖率与差异排查(重要发现)
- taskKey 全覆盖(25 个),不会出现"不支持预览";但**3 个 key 的预览加载的是废弃旧页面文件**(仅 previewMap 引用,router 已无引用):
| taskKey | 学生端实际文件 | 预览加载文件 |
|---|---|---|
| industry-demand-analysis | demand/industry-demand-analysis.vue | demand/index.vue |
| competitive-environment-analysis | demand/competitive-environment-analysis.vue | demand/environment.vue |
| market-opportunity-selection | product/market-opportunity-selection.vue | product/index.vue |
证据:旧页面模板头带 `TrainingTaskToolbar` + `TrainingTaskBrief` + `training-core`;新页面是自定义 `task-header/task-brief-toggle` + `xxx-core`
- 另有 5 个 key 存在"一个 taskKey 对应多个路由页面",previewMap 只映射其一:/demand/user(user.vue 旧6KB) 与 /demand/people;/test/channelas(test/channel.vue 3KB) 与 /test/index;/test/marketing(marketing.vue 旧) 与 /test/promotion;/channel/purchase(purchase.vue) 与 /channel/index;/assessment/index(index.vue 旧16KB)+five/kano/market/traffic 与 /assessment/diagnosis
- 有意差异(只读外壳):隐藏顶部标题/brief/各 toolbar、AI 侧栏与 AI 评测、底部动作区(55% 透明+灰度+不可点)、浮层弹窗;`student-training-shell` 改单列;步骤导航强制横排胶囊(132~220px);表单 pointer-events:none;数据走 sessionStorage 注入(该班生效 taskConfig + 学生 answer);iframe 无应用布局(学生端有顶栏/TagsView/侧边菜单)
- 待办建议:修正 previewMap 三条指向新版文件;评估废弃重复路由(/demand/user、/test/channelas、/test/marketing、/channel/purchase、/assessment/index 等)
### 已执行:previewMap 全部改指最新页面
- `src/views/teacherEnd/taskReview/studentTaskPreviewMap.js`:三条改为 demand/industry-demand-analysis.vue、demand/competitive-environment-analysis.vue、product/market-opportunity-selection.vue;顶部加注释声明"必须与 router 保持一致,不得指向废弃页面"
- `src/views/teacherEnd/taskReview/StudentTaskReadonlyPreview.vue`:补两个类名 —— 隐藏列表加 `.task-action-strip`(否则工具栏被隐藏后残留 10px 空带)、白卡规则加 `.industry-step-card`(行业页步骤容器类名不同)
- 校验:node 脚本交叉核对 = 映射 24 条、文件全部存在、router 全部引用、废弃页面残留引用 0;@vue/compiler-sfc parse + compileStyle 无错误
- 遗留风险(previewMap 换新页后新增):新版页面采用 localStorage 草稿(loadDraft/persistDraft,键含当前用户 id),评阅预览同源会读写教师自己的草稿,理论上可能先闪出教师草稿;涉及 5 个页面(new-product-survey、product-development-factors、product-development-process、industry-demand-analysis、competitive-environment-analysis),可在这些页面用 getTrainingReviewPreviewKey() 判断预览态跳过草稿读写。待用户确认是否执行
### 已修复:评阅预览"看起来还在用废弃页面"的根因(两个叠加原因)
用户反馈截图里步骤导航是一条满宽蓝色大条,误判为废弃页面。实测根因 **不是页面文件错了**,而是样式失配:
1. **学生页"新版皮肤"全部由布局门控选择器承载**:`#app .studentLayout section.app-main:has(.xxx-page) …`(非 scoped 全局块,写在每个实训页底部)。
而 `/task-review-preview` 路由**脱离 Layout**(`component` 直接指向组件,无 Layout 包裹)→ 没有 `.studentLayout`、没有 `section.app-main` → 这些新样式全部失配,页面静默退化到全局旧版样式(步骤说明 `small`「当前步骤」重新出现、胶囊样式丢失、按钮配色不一致)。
`studentLayout` 类由 `layout/index.vue` 的 `classObj` 计算(仅 role 4 挂载);`.app-main` 由 `layout/components/AppMain.vue` 渲染且样式是 scoped。
2. **预览 iframe 宽度约 1000px**(`TaskReviewPanel.vue` 左栏 `minmax(440px,520px)` + 右栏自适应),命中 `student-training.scss` 的 `@media (max-width: 1100px)`:
`.student-training-shell .step-indicator { flex-direction: column }` + `#app .student-training-shell .step-tab { width: 100% !important }`。
后者带 `#app`(ID 级特异性 1,2,0),压过预览自身 `:deep(.step-tab){max-width:220px}`(0,4,0)→ 每个步骤胶囊被撑成整行、只剩第一个可见并出现横向滚动条。
修复(`src/views/teacherEnd/taskReview/StudentTaskReadonlyPreview.vue`,单文件):
- 模板外层补空壳祖先:`<div class="studentLayout student-task-preview-root"><section class="app-main">…`,让门控样式集体生效
- 新增一段非 scoped `<style>`:用 `#app .studentLayout.student-task-preview-root …`(1,3,0/1,4,0)压掉布局级背景与内边距,并复位 `.step-tab/.flow-pill/.flow-step/.training-action` 的 `width: 100%`
- 保留原有兜底:`:deep()` 里 `flex-direction: row`、隐藏 chrome、白卡等规则不变
验证方式(可复用):用本地 `sass` 编译 `index.scss` + 页面非 scoped 样式块 + 预览组件样式块,拼成静态 HTML(`.studentLayout > section.app-main > main.student-task-readonly-preview` + 4 个 `.step-tab`),用本机 Chrome `--headless=new --window-size=1000,700 --screenshot=xxx.png` 出图,页面内探针把 computed style 写进 `<pre>` 一起截进图里。
结果:修复前 tabWidths=952px×4(=容器满宽)、navScroll=3874/952 溢出、small=inline(与用户截图完全一致);修复后 tabWidths=214/169/199/200px 自然宽、navScroll=952/952 不溢出、small=none,与图二学生端一致。
注:Chrome for Windows 为 GUI 子系统,`--dump-dom` 不写 stdout,只能靠 `--screenshot` 落盘取证。
### 学生端菜单/页面全貌(roleId=4)
菜单不是硬编码:`dynamicRoutes` 里大量 `hidden:true`,真正侧边菜单由 `utils/studentTrainingNavigation.js#buildStudentTaskSections()` 按后端「课程分配」动态生成(按 projectName 分组),路径查 `knownTaskPaths`。
静态模块:`/foundation`(3) / `/demand`(8: index、environment、people、user、business-feasibility、consumer-behavior、consumer-scenario、feature-conversion) / `/product`(7: index、price、differentiation、structure、after-sales-service、requirements-doc、hardware-cost) / `/test`(4: index、promotion、channelas、marketing) / `/channel`(3: index、purchase、supplier) / `/assessment`(3: index、diagnosis、optimization) / `/comprehensive` / `/training/task/:pageKey` / `/training-step/:taskKey/:stepId` / `/assistant` / `/ai/learning-situation-*` / `/analysis` / `/report` / `/bi` / `/examination`(index、take/:id、result/:id) / `/student-demo`。
遗留:5 处重复 pageKey(一个 taskKey 对多个路由入口);`demand/index.vue`、`demand/environment.vue`、`product/index.vue` 三个文件已零引用但仍在磁盘(其内部仍保留 `:has(.industry-demand-page)` 等门控块,是现新页的前身)。

@ -1,8 +1,23 @@
<template>
<!--
教师端任务评阅右侧预览(脱离 Layout 的独立路由,不渲染顶栏/侧栏)。
必须补齐 .studentLayout 与 section.app-main 两级祖先:
学生端实训页的「新版皮肤」全部以
#app .studentLayout section.app-main:has(.xxx-page) …
作为样式门控前缀。本路由没有走 Layout,天然缺少这两级祖先,
会导致这些新样式集体失配,页面静默退化到全局旧版样式
(步骤导航变 100% 宽竖排、步骤说明文字重新出现、动作按钮配色不一致)。
这里只补空壳容器,不引入顶栏、侧栏或 TagsView。
-->
<div class="studentLayout student-task-preview-root">
<section class="app-main">
<main ref="previewRoot" class="student-task-readonly-preview" @click.capture="guardPreviewInteraction" @keydown.capture="guardPreviewInteraction">
<component v-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
<el-empty v-else :description="emptyDescription" :image-size="112" />
</main>
</section>
</div>
</template>
<script setup>
@ -126,6 +141,7 @@ function isStepNavigation(element) {
.student-task-readonly-preview :deep(.workbench-head),
.student-task-readonly-preview :deep(.workbench-head--actions-only),
.student-task-readonly-preview :deep(.workbench-actions),
.student-task-readonly-preview :deep(.task-action-strip),
.student-task-readonly-preview :deep(.training-task-toolbar),
.student-task-readonly-preview :deep(.training-legacy-actions),
.student-task-readonly-preview :deep(.new-product-survey-toolbar),
@ -232,6 +248,7 @@ function isStepNavigation(element) {
/* ===== 6. 步骤内容卡片:保留学生端白卡 + 灰边样式 ===== */
.student-task-readonly-preview :deep(.step-card),
.student-task-readonly-preview :deep(.industry-step-card),
.student-task-readonly-preview :deep(.process-step-card),
.student-task-readonly-preview :deep(.factor-step-card),
.student-task-readonly-preview :deep(.step-content > .step-card),
@ -268,3 +285,49 @@ function isStepNavigation(element) {
.student-task-readonly-preview :deep(.el-popper),
.student-task-readonly-preview :deep(.el-overlay) { display: none !important; }
</style>
<!--
非 scoped:补齐 .studentLayout / section.app-main 空壳祖先之后,
需要压掉布局级背景与内边距;同时复位窄屏媒体查询。
选择器统一带 #app + .student-task-preview-root,确保 ID 级特异性高于
student-training.scss / sidebar.scss 里的
#app .studentLayout .app-main:has(.student-training-shell) …
这类规则,避免预览壳被学生端布局背景与内边距污染。
-->
<style lang="scss">
#app .studentLayout.student-task-preview-root {
min-height: 0 !important;
background: #ffffff !important;
}
#app .studentLayout.student-task-preview-root .app-main,
#app .studentLayout.student-task-preview-root .app-main:has(.student-training-shell) {
height: auto !important;
min-height: 0 !important;
padding: 0 !important;
overflow: visible !important;
background: #ffffff !important;
}
/*
评阅预览的 iframe 宽度约 1000px(左栏 440~520px + 右栏自适应),
会命中 student-training.scss 的 @media (max-width: 1100px):
.student-training-shell .step-indicator { flex-direction: column }
#app .student-training-shell .step-tab { width: 100% }
后者带 #app(ID 级特异性),会压过本组件 :deep() 里的宽度覆盖,
于是预览里的步骤胶囊被撑成整行、只剩第一个可见(横向滚动条)。
预览需要呈现学生端「桌面端」外观,这里用更高特异性把宽度复位。
*/
#app .student-task-preview-root .step-indicator .step-tab,
#app .student-task-preview-root .step-tabs .step-tab,
#app .student-task-preview-root .comprehensive-step-strip .step-tab,
#app .student-task-preview-root .training-intro__steps .step-tab,
#app .student-task-preview-root .step-indicator .flow-pill,
#app .student-task-preview-root .flow-steps .flow-pill,
#app .student-task-preview-root .flow-rail .flow-step,
#app .student-task-preview-root .training-actions .el-button.training-action,
#app .student-task-preview-root .training-actions button.training-action {
width: auto !important;
}
</style>

@ -1,10 +1,18 @@
/*
* 教师端任务评阅预览:taskKey → 学生端页面组件。
*
* 重要:每一条都必须与 src/router/index.js 中学生路由实际引用的页面文件一致,
* 不得指向已废弃的旧版页面(历史上曾误挂 demand、product 目录下的旧版同任务页面,
* 导致评阅预览渲染了学生端根本看不到的旧页面)。
* 新增实训任务或调整学生页文件名时,请同步更新本映射。
*/
export const studentTaskPreviewComponentByKey = {
"new-product-survey": () => import("@/views/foundation/new-product-survey.vue"),
"product-development-factors": () => import("@/views/foundation/product-development-factors.vue"),
"product-development-process": () => import("@/views/foundation/product-development-process.vue"),
"industry-demand-analysis": () => import("@/views/demand/index.vue"),
"competitive-environment-analysis": () => import("@/views/demand/environment.vue"),
"market-opportunity-selection": () => import("@/views/product/index.vue"),
"industry-demand-analysis": () => import("@/views/demand/industry-demand-analysis.vue"),
"competitive-environment-analysis": () => import("@/views/demand/competitive-environment-analysis.vue"),
"market-opportunity-selection": () => import("@/views/product/market-opportunity-selection.vue"),
"business-feasibility": () => import("@/views/demand/business-feasibility.vue"),
"target-user-profile": () => import("@/views/demand/people.vue"),
"consumer-behavior": () => import("@/views/demand/consumer-behavior.vue"),

Loading…
Cancel
Save