From 332d33bd322e6757dfe51d3bd5fd28f4ffccac20 Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Fri, 16 Oct 2026 09:50:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(training):=20=E4=BF=AE=E5=A4=8D=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E8=87=AA=E6=B5=8B=E7=AD=94=E9=A2=98=E5=9B=9E=E6=98=BE?= =?UTF-8?q?=E4=B8=8E=E8=AF=84=E9=98=85=E9=A2=84=E8=A7=88=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 知识自测回显: - 新产品调查与分析 watch 漏了 stepOneChoiceAnswers,学生只答自测时本地草稿不落盘,刷新即丢 - applyStep1Answer 只在表格分类有值时返回 true,纯答题场景后端回显后 clearDraft 不生效 - 兼容 toolbox DOM 快照 legacy-training-snapshot-v3(教师端评阅预览返回的就是这类数据), 按「选项以 A 开头分组 + 遇非字母选项停止」还原逐题作答 - 第 4 步 AI 应用价值分析在结构化数据为空时,用快照尾部 textarea 兜底回填(route 限定页面) - 行业需求分析 / 竞争环境分析同步加上快照兜底 提交链路: - new-product-survey 工具栏未传 on-submit/on-reset,提交走 DOM 兜底导致第 4 步内容从未落库, 补上 :on-submit="submitTask" :on-reset="resetExample" - 工具栏兜底快照改写 dynamicStepAnswers,不再覆盖 step1Answer 里的结构化答题 评阅预览: - 步骤导航判定仅靠文案太窄(行业/竞争页胶囊文案是「01 PEST宏观环境分析 当前步骤 →」), 改为容器白名单放行,文案正则补充「步骤 N / 点击切换 / 当前步骤」 - 预览只读拦截与加载失败时透出真实错误信息,不再静默失败 --- src/utils/stepOneChoiceQuestions.js | 75 +++++++++ src/views/components/TrainingTaskToolbar.vue | 156 +++++++++++++++++- .../competitive-environment-analysis.vue | 5 +- src/views/demand/industry-demand-analysis.vue | 5 +- src/views/foundation/new-product-survey.vue | 48 +++++- .../teacherEnd/score/TaskReviewPanel.vue | 6 +- .../taskReview/StudentTaskReadonlyPreview.vue | 39 ++++- 7 files changed, 317 insertions(+), 17 deletions(-) diff --git a/src/utils/stepOneChoiceQuestions.js b/src/utils/stepOneChoiceQuestions.js index 98c9dca..328b615 100644 --- a/src/utils/stepOneChoiceQuestions.js +++ b/src/utils/stepOneChoiceQuestions.js @@ -23,6 +23,81 @@ export function createStepOneChoiceQuestion(index = 0) { }; } +/** + * 兼容「legacy-training-snapshot-v3」整页 DOM 快照: + * 工具栏的提交/保存兜底会把整页 input/textarea 的选中状态拍成快照塞进 step1Answer, + * 这类数据里没有结构化 choiceAnswers,只能按 DOM 顺序把单选组还原成逐题作答。 + * 分组规则:选项 key 恒为 A~F 单字母,且每组以 "A" 开头;遇到非单字母选项(如第二步的"京东") + * 说明自测区已结束,停止收集。 + */ +export function parseLegacySnapshotChoiceAnswers(value, questions = []) { + let source = value; + if (typeof value === "string") { + try { + source = JSON.parse(value); + } catch { + return null; + } + } + if (!source || typeof source !== "object" || !Array.isArray(source.fields)) return null; + const normalized = Array.isArray(questions) ? questions : parseStepOneChoiceQuestions(questions); + if (!normalized.length) return null; + const groups = []; + let current = null; + for (const field of source.fields) { + if (!field || field.type !== "radio") continue; + const key = String(field.value ?? "").trim().toUpperCase(); + if (!/^[A-F]$/.test(key)) { + // 已开始收集自测题后遇到非字母选项,说明后面是其它步骤的控件 + if (current) break; + continue; + } + if (key === "A") { + current = []; + groups.push(current); + } + if (!current) continue; + current.push({ key, checked: Boolean(field.checked) }); + } + const answers = {}; + groups.slice(0, normalized.length).forEach((group, index) => { + const picked = group.find((option) => option.checked); + if (picked) answers[normalized[index].id] = picked.key; + }); + return Object.keys(answers).length ? answers : null; +} + +/** + * 兼容 legacy 快照里的尾部 textarea(如新产品调查与分析第 4 步的三层次 AI 应用价值分析): + * 快照没有字段名,只能按 DOM 顺序取最后 N 个 textarea。调用方必须用 route 限定页面, + * 避免把其它页面的控件误当成第 4 步内容回填给教师端。 + */ +export function parseLegacySnapshotTailTextareas(value, fields = [], route = "") { + let source = value; + if (typeof value === "string") { + try { + source = JSON.parse(value); + } catch { + return null; + } + } + if (!source || typeof source !== "object" || !Array.isArray(source.fields)) return null; + if (route && String(source.route || "") !== route) return null; + if (!Array.isArray(fields) || !fields.length) return null; + const textareas = source.fields.filter((field) => field && (field.type === "textarea" || field.tag === "textarea")); + // 至少要有第 3 步的 rows 加上第 4 步的字段,才可能是“第 4 步在末尾”的结构 + if (textareas.length < fields.length + 3) return null; + const tail = textareas.slice(-fields.length); + const result = {}; + let filled = false; + tail.forEach((field, index) => { + const text = String(field?.value ?? "").trim(); + if (text) filled = true; + result[fields[index]] = text; + }); + return filled ? result : null; +} + export function parseStepOneChoiceQuestions(value) { let source = value; if (typeof value === "string") { diff --git a/src/views/components/TrainingTaskToolbar.vue b/src/views/components/TrainingTaskToolbar.vue index c77a3b9..8fbc272 100644 --- a/src/views/components/TrainingTaskToolbar.vue +++ b/src/views/components/TrainingTaskToolbar.vue @@ -10,6 +10,15 @@ :ai-manual-name="resolvedAiManual.name" :ai-manual-url="resolvedAiManual.url" /> + + + +
+
+
+

老师评分

+

{{ formatScoreValue(teacherScore) }}分

+

{{ formatScoreTime(teacherScoredAt) }}

+
+
+

AI 评分

+

{{ formatScoreValue(aiScore) }}分

+
+
+
+

老师评语

+

{{ teacherComment }}

+
+
+

参考答案

+

{{ referenceAnswer }}

+
+ +
+