diff --git a/src/views/schoolAdmin/trainingTask/index.vue b/src/views/schoolAdmin/trainingTask/index.vue index a03e5fb..3557550 100644 --- a/src/views/schoolAdmin/trainingTask/index.vue +++ b/src/views/schoolAdmin/trainingTask/index.vue @@ -72,7 +72,7 @@
{{ dialogMode === "view" ? "Training Task Preview" : "Training Task Editor" }}

{{ dialogMode === "view" ? "查看实训任务" : "编辑实训任务" }}

-

管理员维护系统默认实训配置,教学班未单独配置时学生端使用这里的实训背景、目标、要求和案例文档。

+

管理员维护系统默认实训配置;步骤数量与学生端保持一致,只能修改步骤名称。

@@ -166,16 +166,14 @@
- +
- 新增步骤
@@ -198,6 +196,7 @@ import { updateTrainingTask, uploadTrainingTaskMaterial, } from "@/api/trainingTask"; +import { getStudentTrainingStepCount } from "@/views/training/taskKeyMap"; const { proxy } = getCurrentInstance(); @@ -315,7 +314,7 @@ function normalizeTask(task) { requirements: task.requirements || "", materialName: material.name, materialUrl: material.url, - steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), + steps: normalizeTaskSteps(task.taskKey, parseArray(task.steps, [])), sort: task.sort || 0, enabled: task.enabled !== false, }; @@ -354,17 +353,16 @@ function parseArray(value, fallback) { } } -function normalizeSteps(steps) { +function normalizeTaskSteps(taskKey, steps) { const source = Array.isArray(steps) ? steps : []; - const result = source - .map((item, index) => { - const raw = item && typeof item === "object" ? item : {}; - const name = typeof item === "object" ? String(raw.name || "").trim() : String(item || "").trim(); - const kind = raw.kind === "custom" ? "custom" : index < 4 ? "builtin" : "custom"; - return createStep(name, kind, raw.id); - }) - .filter((item) => item.name); - return result.length ? result : [createStep()]; + const expectedCount = getStudentTrainingStepCount(taskKey); + return Array.from({ length: expectedCount }, (_, index) => { + const item = source[index]; + const raw = item && typeof item === "object" ? item : {}; + const savedName = typeof item === "object" ? String(raw.name || "").trim() : String(item || "").trim(); + const name = savedName || `步骤 ${String(index + 1).padStart(2, "0")}`; + return createStep(name, "builtin", `builtin-${index + 1}`); + }); } function createBuiltinSteps() { @@ -390,7 +388,7 @@ function cloneTask(task) { return { ...task, objectives: [...(task.objectives?.length ? task.objectives : [""])], - steps: normalizeSteps(task.steps?.length ? task.steps : createBuiltinSteps()), + steps: normalizeTaskSteps(task.taskKey, task.steps), }; } @@ -403,15 +401,6 @@ function removeObjective(index) { taskForm.objectives.splice(index, 1); } -function addStep() { - taskForm.steps.push(createStep()); -} - -function removeStep(index) { - if (taskForm.steps.length <= 1) return; - taskForm.steps.splice(index, 1); -} - async function handleImportChange(file) { if (!file?.raw) return; await importTrainingTasks(file.raw); @@ -466,6 +455,7 @@ async function submitTask() { } function buildPayload() { + const fixedSteps = normalizeTaskSteps(taskForm.taskKey, taskForm.steps); return { projectName: taskForm.project, taskKey: taskForm.taskKey, @@ -473,7 +463,7 @@ function buildPayload() { background: taskForm.background, objectives: JSON.stringify(taskForm.objectives.map((item) => item.trim()).filter(Boolean)), requirements: taskForm.requirements, - steps: JSON.stringify(taskForm.steps.map((item) => ({ ...item, name: item.name.trim() })).filter((item) => item.name)), + steps: JSON.stringify(fixedSteps), materialName: taskForm.materialName, materialUrl: taskForm.materialUrl, sort: taskForm.sort, @@ -1195,10 +1185,6 @@ function buildPayload() { flex: 1 1 260px; } - .add-step-button { - border-style: dashed; - } - .step-arrow { color: #7ddaff; font-weight: 800; diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue index 81370e5..75239c8 100644 --- a/src/views/teacherEnd/trainingTask/index.vue +++ b/src/views/teacherEnd/trainingTask/index.vue @@ -104,7 +104,7 @@
{{ dialogMode === "view" ? "Training Task Preview" : "Training Task Editor" }}

{{ dialogMode === "view" ? "查看实训任务" : "编辑实训任务" }}

-

老师可维护任务显示名称、实训说明、目标、要求和步骤,并可新增或删除步骤。

+

老师可维护任务显示名称、实训说明、目标、要求和步骤名称;步骤数量与学生端保持一致。

@@ -188,16 +188,14 @@
- +
- 新增步骤
@@ -235,6 +233,7 @@ import { import { createStudentDemoSession } from "@/api/studentDemo"; import * as indexApi from "@/api/teacher"; import useUserStore from "@/store/modules/user"; +import { getStudentTrainingStepCount } from "@/views/training/taskKeyMap"; const { proxy } = getCurrentInstance(); const userStore = useUserStore(); @@ -410,7 +409,7 @@ function normalizeTask(task) { requirements: task.requirements || "", materialName: material.name, materialUrl: material.url, - steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), + steps: normalizeTaskSteps(task.taskKey, parseArray(task.steps, [])), sort: task.sort || 0, enabled: task.enabled !== false, }; @@ -467,17 +466,16 @@ function parseArray(value, fallback) { } } -function normalizeSteps(steps) { +function normalizeTaskSteps(taskKey, steps) { const source = Array.isArray(steps) ? steps : []; - const result = source - .map((item, index) => { - const raw = item && typeof item === "object" ? item : {}; - const name = typeof item === "object" ? String(raw.name || "").trim() : String(item || "").trim(); - const kind = raw.kind === "custom" ? "custom" : index < 4 ? "builtin" : "custom"; - return createStep(name, kind, raw.id); - }) - .filter((item) => item.name); - return result.length ? result : [createStep()]; + const expectedCount = getStudentTrainingStepCount(taskKey); + return Array.from({ length: expectedCount }, (_, index) => { + const item = source[index]; + const raw = item && typeof item === "object" ? item : {}; + const savedName = typeof item === "object" ? String(raw.name || "").trim() : String(item || "").trim(); + const name = savedName || `步骤 ${String(index + 1).padStart(2, "0")}`; + return createStep(name, "builtin", `builtin-${index + 1}`); + }); } function createBuiltinSteps() { @@ -502,7 +500,7 @@ function cloneTask(task) { return { ...task, objectives: [...(task.objectives?.length ? task.objectives : [""])], - steps: normalizeSteps(task.steps?.length ? task.steps : createBuiltinSteps()), + steps: normalizeTaskSteps(task.taskKey, task.steps), }; } @@ -515,15 +513,6 @@ function removeObjective(index) { taskForm.objectives.splice(index, 1); } -function addStep() { - taskForm.steps.push(createStep()); -} - -function removeStep(index) { - if (taskForm.steps.length <= 1) return; - taskForm.steps.splice(index, 1); -} - async function handleMaterialChange(file) { const rawFile = file?.raw; if (!rawFile) return; @@ -632,6 +621,7 @@ async function confirmRestoreDefault(action) { } function buildPayload() { + const fixedSteps = normalizeTaskSteps(taskForm.taskKey, taskForm.steps); return { projectName: taskForm.project, taskKey: taskForm.taskKey, @@ -639,7 +629,7 @@ function buildPayload() { background: taskForm.background, objectives: JSON.stringify(taskForm.objectives.map((item) => item.trim()).filter(Boolean)), requirements: taskForm.requirements, - steps: JSON.stringify(taskForm.steps.map((item) => ({ ...item, name: item.name.trim() })).filter((item) => item.name)), + steps: JSON.stringify(fixedSteps), materialName: taskForm.materialName, materialUrl: taskForm.materialUrl, sort: taskForm.sort, @@ -1358,10 +1348,6 @@ function buildPayload() { flex: 1 1 260px; } - .add-step-button { - border-style: dashed; - } - .step-arrow { color: #7ddaff; font-weight: 800; diff --git a/src/views/training/taskKeyMap.js b/src/views/training/taskKeyMap.js index f4cf6f0..d3e2c64 100644 --- a/src/views/training/taskKeyMap.js +++ b/src/views/training/taskKeyMap.js @@ -33,6 +33,40 @@ export const trainingTaskKeyByPath = { "/assessment/optimization": "category-optimization", }; +// Each training page implements a fixed number of student-facing steps. Keep the +// teacher editor aligned with those page capabilities instead of the legacy +// four-step backend default. +export const studentTrainingStepCountByTaskKey = Object.freeze({ + "new-product-survey": 4, + "product-development-factors": 4, + "product-development-process": 4, + "industry-demand-analysis": 4, + "competitive-environment-analysis": 4, + "market-opportunity-selection": 6, + "business-feasibility": 3, + "target-user-profile": 5, + "consumer-behavior": 4, + "consumer-scenario": 4, + "feature-conversion": 4, + "product-value-positioning": 4, + "product-structure": 4, + "after-sales-service": 3, + "requirements-doc": 3, + "hardware-cost": 4, + "supplier-evaluation": 3, + "purchase-demand-forecast": 4, + "product-pricing": 5, + "category-optimization": 5, + "release-channel-evaluation": 4, + "promotion-effect-optimization": 4, + "product-diagnosis-iteration": 3, + "comprehensive-case-training": 10, +}); + +export function getStudentTrainingStepCount(taskKey, fallback = 4) { + return studentTrainingStepCountByTaskKey[String(taskKey || "").trim()] || fallback; +} + export function resolveTrainingTaskKey(route, explicitTaskKey = "") { return explicitTaskKey || route?.meta?.pageKey || trainingTaskKeyByPath[route?.path] || ""; } diff --git a/tests/training-task-fixed-steps.static.test.cjs b/tests/training-task-fixed-steps.static.test.cjs new file mode 100644 index 0000000..ae9974f --- /dev/null +++ b/tests/training-task-fixed-steps.static.test.cjs @@ -0,0 +1,60 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const root = path.resolve(__dirname, ".."); +const read = (file) => fs.readFileSync(path.join(root, file), "utf8"); + +const stepCounts = read("src/views/training/taskKeyMap.js"); +const teacherEditor = read("src/views/teacherEnd/trainingTask/index.vue"); +const adminEditor = read("src/views/schoolAdmin/trainingTask/index.vue"); + +const expectedCounts = { + "new-product-survey": 4, + "product-development-factors": 4, + "product-development-process": 4, + "industry-demand-analysis": 4, + "competitive-environment-analysis": 4, + "market-opportunity-selection": 6, + "business-feasibility": 3, + "target-user-profile": 5, + "consumer-behavior": 4, + "consumer-scenario": 4, + "feature-conversion": 4, + "product-value-positioning": 4, + "product-structure": 4, + "after-sales-service": 3, + "requirements-doc": 3, + "hardware-cost": 4, + "supplier-evaluation": 3, + "purchase-demand-forecast": 4, + "product-pricing": 5, + "category-optimization": 5, + "release-channel-evaluation": 4, + "promotion-effect-optimization": 4, + "product-diagnosis-iteration": 3, + "comprehensive-case-training": 10, +}; + +for (const [taskKey, count] of Object.entries(expectedCounts)) { + assert( + new RegExp(`"${taskKey}"\\s*:\\s*${count}(?:,|\\n)`).test(stepCounts), + `${taskKey} should be fixed to ${count} student steps` + ); +} + +for (const [name, editor] of [ + ["teacher", teacherEditor], + ["admin", adminEditor], +]) { + assert(/import \{ getStudentTrainingStepCount \} from "@\/views\/training\/taskKeyMap";/.test(editor)); + assert(/const expectedCount = getStudentTrainingStepCount\(taskKey\);/.test(editor)); + assert(/Array\.from\(\{ length: expectedCount \}/.test(editor)); + assert(/steps: JSON\.stringify\(fixedSteps\)/.test(editor)); + assert(/步骤名称(固定 \$\{taskForm\.steps\.length\} 步)/.test(editor)); + assert(!/@click="addStep"/.test(editor), `${name} editor must not add steps`); + assert(!/@click="removeStep/.test(editor), `${name} editor must not remove steps`); + assert(!/新增步骤/.test(editor), `${name} editor must not show add-step control`); +} + +console.log("teacher and admin training task fixed-step contracts passed");