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" ? "查看实训任务" : "编辑实训任务" }}
-
管理员维护系统默认实训配置,教学班未单独配置时学生端使用这里的实训背景、目标、要求和案例文档。
+
管理员维护系统默认实训配置;步骤数量与学生端保持一致,只能修改步骤名称。
-
+
@@ -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" ? "查看实训任务" : "编辑实训任务" }}
-
老师可维护任务显示名称、实训说明、目标、要求和步骤,并可新增或删除步骤。
+
老师可维护任务显示名称、实训说明、目标、要求和步骤名称;步骤数量与学生端保持一致。
-
+
@@ -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");