From 42963412028b8aa5cf66f11297e87256ba96ea9c Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 4 Aug 2026 16:54:23 +0800 Subject: [PATCH] fix: render student training step names --- src/layout/components/Navbar.vue | 3 +- src/layout/components/Sidebar/index.vue | 3 +- ...ation.cjs => studentTrainingNavigation.js} | 33 +++++-------- src/views/foundation/new-product-survey.vue | 3 +- src/views/training/taskKeyMap.js | 4 ++ .../student-training-navigation-esm.test.mjs | 26 ++++++++++ tests/student-training-navigation.test.cjs | 48 ------------------- tests/training-step-name.test.mjs | 9 ++++ 8 files changed, 55 insertions(+), 74 deletions(-) rename src/utils/{studentTrainingNavigation.cjs => studentTrainingNavigation.js} (74%) create mode 100644 tests/student-training-navigation-esm.test.mjs delete mode 100644 tests/student-training-navigation.test.cjs create mode 100644 tests/training-step-name.test.mjs diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index f882979..d613f4c 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -83,7 +83,7 @@ import useSettingsStore from "@/store/modules/settings"; import * as indexApi from "@/api/teacher"; import { listTrainingTasks } from "@/api/trainingTask"; import { getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo"; -import studentTrainingNavigation from "@/utils/studentTrainingNavigation.cjs"; +import { getComprehensiveTopTask } from "@/utils/studentTrainingNavigation"; const { proxy } = getCurrentInstance(); const router = useRouter(); const appStore = useAppStore(); @@ -92,7 +92,6 @@ const settingsStore = useSettingsStore(); const showModel = ref(false); const form = ref({}); const role = computed(() => Number(userStore.userInfo.roleId)); -const { getComprehensiveTopTask } = studentTrainingNavigation; const studentTopTask = ref(null); const mentList = ref([ { diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue index d6643fa..bcf1ff7 100644 --- a/src/layout/components/Sidebar/index.vue +++ b/src/layout/components/Sidebar/index.vue @@ -69,7 +69,7 @@ import usePermissionStore from "@/store/modules/permission"; import { constantRoutes, dynamicRoutes, teacherRoutes, platformAdminRoutes, schoolAdminRoutes } from "@/router/index.js"; import useUserStore from "@/store/modules/user"; import { listTrainingTasks } from "@/api/trainingTask"; -import studentTrainingNavigation from "@/utils/studentTrainingNavigation.cjs"; +import { buildStudentTaskSections } from "@/utils/studentTrainingNavigation"; import * as ElementPlusIconsVue from "@element-plus/icons-vue"; const { proxy } = getCurrentInstance(); const userStore = useUserStore(); @@ -78,7 +78,6 @@ const router = useRouter(); const appStore = useAppStore(); const settingsStore = useSettingsStore(); const permissionStore = usePermissionStore(); -const { buildStudentTaskSections } = studentTrainingNavigation; const sidebarRouters = ref([...constantRoutes]); const visibleSidebarRouters = computed(() => filterRoutesByOrganizationMode(sidebarRouters.value)); const sidebarMenuKey = computed(() => visibleSidebarRouters.value.map((route) => route.path).join("|")); diff --git a/src/utils/studentTrainingNavigation.cjs b/src/utils/studentTrainingNavigation.js similarity index 74% rename from src/utils/studentTrainingNavigation.cjs rename to src/utils/studentTrainingNavigation.js index c3a09e9..bb27469 100644 --- a/src/utils/studentTrainingNavigation.cjs +++ b/src/utils/studentTrainingNavigation.js @@ -1,6 +1,6 @@ const COMPREHENSIVE_TASK_KEY = "comprehensive-case-training"; -const KNOWN_TASK_PATHS = Object.freeze({ +const knownTaskPaths = Object.freeze({ "new-product-survey": "/foundation/new-product-survey", "product-development-factors": "/foundation/product-development-factors", "product-development-process": "/foundation/product-development-process", @@ -26,18 +26,17 @@ const KNOWN_TASK_PATHS = Object.freeze({ "product-diagnosis-iteration": "/assessment/diagnosis", }); -function taskSort(task) { - const sort = Number(task?.sort); - return Number.isFinite(sort) ? sort : Number.MAX_SAFE_INTEGER; +function taskPath(taskKey) { + return knownTaskPaths[taskKey] || `/training/task/${encodeURIComponent(taskKey)}`; } -function taskPath(taskKey) { - return KNOWN_TASK_PATHS[taskKey] || `/training/task/${encodeURIComponent(taskKey)}`; +function taskSort(task) { + return Number.isFinite(Number(task?.sort)) ? Number(task.sort) : Number.MAX_SAFE_INTEGER; } -function buildStudentTaskSections(tasks) { +export function buildStudentTaskSections(tasks = []) { const sections = new Map(); - [...(tasks || [])] + [...tasks] .filter((task) => task?.taskKey && task.taskKey !== COMPREHENSIVE_TASK_KEY) .sort((left, right) => taskSort(left) - taskSort(right)) .forEach((task) => { @@ -51,17 +50,9 @@ function buildStudentTaskSections(tasks) { return [...sections.values()]; } -function getComprehensiveTopTask(tasks) { - const task = (tasks || []).find((item) => item?.taskKey === COMPREHENSIVE_TASK_KEY); - if (!task) return null; - return { - title: task.taskName || "综合实训", - path: "/comprehensive/index", - activePrefix: "/comprehensive", - }; +export function getComprehensiveTopTask(tasks = []) { + const task = tasks.find((item) => item?.taskKey === COMPREHENSIVE_TASK_KEY); + return task + ? { title: task.taskName || "综合实训", path: "/comprehensive/index", activePrefix: "/comprehensive" } + : null; } - -module.exports = { - buildStudentTaskSections, - getComprehensiveTopTask, -}; diff --git a/src/views/foundation/new-product-survey.vue b/src/views/foundation/new-product-survey.vue index 84f23ca..7f8e381 100644 --- a/src/views/foundation/new-product-survey.vue +++ b/src/views/foundation/new-product-survey.vue @@ -265,6 +265,7 @@ import { import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import useUserStore from "@/store/modules/user"; +import { extractTrainingStepNames } from "@/views/training/taskKeyMap"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; @@ -305,7 +306,7 @@ const taskRequirement = computed( ); const steps = computed(() => { - const configured = parseArray(taskConfig.value?.steps, defaultSteps.map((item) => item.name)).slice(0, 4); + const configured = extractTrainingStepNames(taskConfig.value?.steps, defaultSteps.map((item) => item.name)).slice(0, 4); return defaultSteps.map((step, index) => ({ ...step, name: configured[index] || step.name, diff --git a/src/views/training/taskKeyMap.js b/src/views/training/taskKeyMap.js index 62899cb..f4cf6f0 100644 --- a/src/views/training/taskKeyMap.js +++ b/src/views/training/taskKeyMap.js @@ -60,3 +60,7 @@ export function parseTrainingArray(value, fallback = []) { .filter(Boolean); return result.length ? result : fallback; } + +export function extractTrainingStepNames(value, fallback = []) { + return parseTrainingArray(value, fallback); +} diff --git a/tests/student-training-navigation-esm.test.mjs b/tests/student-training-navigation-esm.test.mjs new file mode 100644 index 0000000..cbe14a2 --- /dev/null +++ b/tests/student-training-navigation-esm.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { buildStudentTaskSections, getComprehensiveTopTask } from "../src/utils/studentTrainingNavigation.js"; + +const sections = buildStudentTaskSections([ + { + taskKey: "teacher-added-task", + taskName: "教师新增任务", + projectName: "市场洞察与需求分析", + sort: 1, + }, +]); + +assert.deepEqual(sections, [ + { + title: "市场洞察与需求分析", + children: [{ title: "教师新增任务", path: "/training/task/teacher-added-task" }], + }, +]); + +assert.deepEqual(getComprehensiveTopTask([{ taskKey: "comprehensive-case-training", taskName: "综合实训" }]), { + title: "综合实训", + path: "/comprehensive/index", + activePrefix: "/comprehensive", +}); + +console.log("student training navigation ESM contract passed"); diff --git a/tests/student-training-navigation.test.cjs b/tests/student-training-navigation.test.cjs deleted file mode 100644 index 63ee910..0000000 --- a/tests/student-training-navigation.test.cjs +++ /dev/null @@ -1,48 +0,0 @@ -const assert = require("assert"); -const { - buildStudentTaskSections, - getComprehensiveTopTask, -} = require("../src/utils/studentTrainingNavigation.cjs"); - -const tasks = [ - { taskKey: "comprehensive-case-training", taskName: "综合案例实训", projectName: "综合实训", sort: 1 }, - { taskKey: "market-task", taskName: "需求访谈", projectName: "市场洞察与需求分析", sort: 3 }, - { taskKey: "new-product-survey", taskName: "新产品调查与分析", projectName: "互联网产品开发基础认知", sort: 2 }, - { taskKey: "teacher-added-task", taskName: "教师新增任务", projectName: "市场洞察与需求分析", sort: 4 }, -]; - -const sections = buildStudentTaskSections(tasks); - -assert.deepStrictEqual(sections, [ - { - title: "互联网产品开发基础认知", - children: [{ title: "新产品调查与分析", path: "/foundation/new-product-survey" }], - }, - { - title: "市场洞察与需求分析", - children: [ - { title: "需求访谈", path: "/training/task/market-task" }, - { title: "教师新增任务", path: "/training/task/teacher-added-task" }, - ], - }, -]); -assert.deepStrictEqual(getComprehensiveTopTask(tasks), { - title: "综合案例实训", - path: "/comprehensive/index", - activePrefix: "/comprehensive", -}); - -const fs = require("fs"); -const path = require("path"); -const frontendRoot = path.resolve(__dirname, ".."); -const sidebarSource = fs.readFileSync(path.join(frontendRoot, "src/layout/components/Sidebar/index.vue"), "utf8"); -const navbarSource = fs.readFileSync(path.join(frontendRoot, "src/layout/components/Navbar.vue"), "utf8"); -const routerSource = fs.readFileSync(path.join(frontendRoot, "src/router/index.js"), "utf8"); - -assert(sidebarSource.includes("loadStudentTaskNavigation"), "student sidebar must load its task navigation from the task API"); -assert(sidebarSource.includes("studentTaskSections"), "student sidebar must render dynamic task sections"); -assert(navbarSource.includes("getComprehensiveTopTask"), "top navigation must derive comprehensive training from the task API"); -assert(navbarSource.includes("studentTopTask"), "top navigation must hold the published comprehensive task"); -assert(routerSource.includes('path: "task/:pageKey"'), "teacher-added tasks must have a generic student route"); - -console.log("student training navigation contract passed"); diff --git a/tests/training-step-name.test.mjs b/tests/training-step-name.test.mjs new file mode 100644 index 0000000..7e9a686 --- /dev/null +++ b/tests/training-step-name.test.mjs @@ -0,0 +1,9 @@ +import assert from "node:assert/strict"; +import { extractTrainingStepNames } from "../src/views/training/taskKeyMap.js"; + +assert.deepEqual( + extractTrainingStepNames('[{"id":"custom-123","kind":"builtin","name":"电商产品调研"}]'), + ["电商产品调研"] +); + +console.log("training step names contract passed");