diff --git a/src/api/trainingTask.js b/src/api/trainingTask.js index fe35e08..73f2d86 100644 --- a/src/api/trainingTask.js +++ b/src/api/trainingTask.js @@ -1,5 +1,9 @@ import request from "@/utils/request"; +// 学生页面和全局“补充步骤”导航会在同一轮路由渲染中读取同一任务配置。 +// 仅合并进行中的请求:教师保存后下一次读取仍会获得最新配置,不引入陈旧缓存。 +const pendingTaskRequests = new Map(); + export function listTrainingTasks(params) { return request({ url: "/api/training-tasks", @@ -9,10 +13,20 @@ export function listTrainingTasks(params) { } export function getTrainingTaskByKey(taskKey) { - return request({ - url: `/api/training-tasks/key/${taskKey}`, + const normalizedTaskKey = String(taskKey || "").trim(); + if (pendingTaskRequests.has(normalizedTaskKey)) { + return pendingTaskRequests.get(normalizedTaskKey); + } + const requestPromise = request({ + url: `/api/training-tasks/key/${normalizedTaskKey}`, method: "get", }); + pendingTaskRequests.set(normalizedTaskKey, requestPromise); + requestPromise.then( + () => pendingTaskRequests.delete(normalizedTaskKey), + () => pendingTaskRequests.delete(normalizedTaskKey) + ); + return requestPromise; } export function updateTrainingTask(id, data) { diff --git a/src/views/demand/business-feasibility.vue b/src/views/demand/business-feasibility.vue index 203ac1c..c87e0c4 100644 --- a/src/views/demand/business-feasibility.vue +++ b/src/views/demand/business-feasibility.vue @@ -105,6 +105,7 @@ import { Check, Delete, Document, Download, RefreshLeft, UploadFilled, View } from "@element-plus/icons-vue"; import { checkBusinessFeasibilityStepOne, checkBusinessFeasibilityStepTwo, checkBusinessFeasibilityStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } 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"; @@ -142,7 +143,7 @@ const priorityOptions = [ const defaultTask = { title: "商业可行性论证", background: "在产品开发前,综合市场机会、用户需求、竞争态势、成本收益和风险,判断项目是否具有可推进的商业价值。", goals: ["掌握商业可行性论证的基本维度", "形成市场、用户、成本与收益的一致判断", "识别主要风险并提出应对措施"], requirement: "请以案例资料和前序分析为依据,完成表格全部内容;成本与价格数据需真实、合理且逻辑一致。" }; const { proxy } = getCurrentInstance(); const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const uploading = ref(false); const completed = ref(false); const stepTwoCompleted = ref(false); const stepThreeCompleted = ref(false); const reportInput = ref(null); const reportPreviewVisible = ref(false); const report = ref(createReport()); const form = ref(createForm()); const stepTwo = ref(createStepTwo()); -const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, DEFAULT_STEPS)); +const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS)); const totalCost = computed(() => form.value.costs.reduce((sum, cost) => sum + numeric(cost.amount), 0)); const unitGrossProfit = computed(() => numeric(form.value.targetPrice) - totalCost.value); const grossMargin = computed(() => totalCost.value > 0 ? unitGrossProfit.value / totalCost.value * 100 : null); const progressItems = computed(() => [{ text: completed.value ? "项目论证已保存" : "待完成项目论证", status: completed.value ? "done" : "" }, { text: totalCost.value > 0 ? "成本与毛利已计算" : "待填写成本与售价", status: totalCost.value > 0 ? "done" : "" }, { text: form.value.risks.filter((risk) => risk.description && risk.response && risk.impact).length === 4 ? "风险评估已完成" : "待完成风险评估", status: form.value.risks.filter((risk) => risk.description && risk.response && risk.impact).length === 4 ? "done" : "" }]); const selectedPriorityCount = computed(() => stepTwo.value.priorities.length); diff --git a/src/views/demand/competitive-environment-analysis.vue b/src/views/demand/competitive-environment-analysis.vue index d327d39..296df6a 100644 --- a/src/views/demand/competitive-environment-analysis.vue +++ b/src/views/demand/competitive-environment-analysis.vue @@ -49,6 +49,7 @@ import { Download, RefreshLeft, TrendCharts } from "@element-plus/icons-vue"; import { checkCompetitiveEnvironmentAnalysisStepOne, checkCompetitiveEnvironmentAnalysisStepTwo, checkCompetitiveEnvironmentAnalysisStepThree, checkCompetitiveEnvironmentAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import useUserStore from "@/store/modules/user"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; @@ -73,7 +74,7 @@ const strategyDefinitions = [ const defaultTask = { title: "竞争环境分析", background: "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。", goals: ["掌握 SWOT 四维度的分类方法", "提炼案例中的竞争环境关键信息", "形成竞争分析的基础材料"], requirement: "请阅读案例资料,将相关信息归类为优势、劣势、机会和威胁。" }; const evaluationOptions = ["优势明显,建议快速进入", "机会大于挑战,建议积极布局", "挑战与机会并存,需谨慎推进", "劣势突出,建议暂缓进入"]; const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const draftReady = ref(false); const stepOnePassed = ref(false); const stepTwoPassed = ref(false); const stepThreePassed = ref(false); const form = ref(createForm()); const supplementForm = ref(createForm()); const strategyForm = ref(createStrategyForm()); const finalForm = ref(createFinalForm()); -const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, DEFAULT_STEPS)); const progressItems = computed(() => swotRows.map((row) => ({ text: `${strategyForm.value.so.recommendation ? "已制定策略" : supplementForm.value[row.key] ? "已补充" : form.value[row.key] ? "已归类" : "待填写"}:${row.label}`, status: strategyForm.value.so.recommendation || supplementForm.value[row.key] || form.value[row.key] ? "done" : "" }))); +const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS)); const progressItems = computed(() => swotRows.map((row) => ({ text: `${strategyForm.value.so.recommendation ? "已制定策略" : supplementForm.value[row.key] ? "已补充" : form.value[row.key] ? "已归类" : "待填写"}:${row.label}`, status: strategyForm.value.so.recommendation || supplementForm.value[row.key] || form.value[row.key] ? "done" : "" }))); onMounted(async () => { await loadTaskConfig(); loadDraft(); await loadSavedAnswer(); draftReady.value = true; }); watch(() => JSON.stringify(buildDraft()), persistDraft); function createForm() { return { strengths: "", weaknesses: "", opportunities: "", threats: "" }; } function createStrategyForm() { return { so: createStrategyRow(), wo: createStrategyRow(), st: createStrategyRow(), wt: createStrategyRow() }; } diff --git a/src/views/demand/consumer-behavior.vue b/src/views/demand/consumer-behavior.vue index 98c3a99..6ddd57e 100644 --- a/src/views/demand/consumer-behavior.vue +++ b/src/views/demand/consumer-behavior.vue @@ -275,6 +275,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; const TASK_KEY = "consumer-behavior"; @@ -480,7 +481,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "消费者需求 const taskBackground = computed(() => taskConfig.value?.background || defaultBackground); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); -const flowSteps = computed(() => parseArray(taskConfig.value?.steps, []).slice(0, 4)); +const flowSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, []).slice(0, 4)); const activeStepName = computed(() => flowSteps.value[form.value.activeStep] || ""); const progressItems = computed(() => [ diff --git a/src/views/demand/consumer-scenario.vue b/src/views/demand/consumer-scenario.vue index 7fe7541..60b18b5 100644 --- a/src/views/demand/consumer-scenario.vue +++ b/src/views/demand/consumer-scenario.vue @@ -261,6 +261,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, checkConsumerScenarioStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { isStudentDemo } from "@/utils/studentDemo"; @@ -468,7 +469,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "消费场景解 const taskBackground = computed(() => taskConfig.value?.background || defaultBackground); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); -const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, [])); +const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, [])); const hasConfiguredSteps = computed(() => trainingSteps.value.length > 0); const activeStepName = computed(() => trainingSteps.value[form.value.activeStep - 1] || ""); const orderedJourneyStages = computed(() => diff --git a/src/views/demand/environment.vue b/src/views/demand/environment.vue index e73096b..e646b5b 100644 --- a/src/views/demand/environment.vue +++ b/src/views/demand/environment.vue @@ -132,6 +132,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue"; @@ -226,7 +227,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "竞争环境分 const taskBackground = computed(() => taskConfig.value?.background || defaultBackground); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); -const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, [])); +const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, [])); const activeStepName = computed(() => trainingSteps.value[currentStep.value - 1] || trainingSteps.value[0] || ""); watch(trainingSteps, (steps) => { diff --git a/src/views/demand/feature-conversion.vue b/src/views/demand/feature-conversion.vue index 5013d93..557dec1 100644 --- a/src/views/demand/feature-conversion.vue +++ b/src/views/demand/feature-conversion.vue @@ -230,6 +230,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { checkFeatureConversionStepOne, checkFeatureConversionStepTwo, checkFeatureConversionStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; const TASK_KEY = "feature-conversion"; @@ -375,7 +376,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "需求转化为 const taskBackground = computed(() => taskConfig.value?.background || defaultBackground); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); -const flowSteps = computed(() => parseArray(taskConfig.value?.steps)); +const flowSteps = computed(() => parseTrainingArray(taskConfig.value?.steps)); const hasConfiguredSteps = computed(() => flowSteps.value.length > 0); const activeStepName = computed(() => flowSteps.value[form.value.activeStep - 1] || ""); diff --git a/src/views/demand/index.vue b/src/views/demand/index.vue index b7858de..1b7d355 100644 --- a/src/views/demand/index.vue +++ b/src/views/demand/index.vue @@ -126,6 +126,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue"; @@ -222,7 +223,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "行业需求分 const taskBackground = computed(() => taskConfig.value?.background || defaultBackground); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); -const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, [])); +const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, [])); const activeStepName = computed(() => trainingSteps.value[currentStep.value - 1] || trainingSteps.value[0] || ""); watch(trainingSteps, (steps) => { diff --git a/src/views/demand/industry-demand-analysis.vue b/src/views/demand/industry-demand-analysis.vue index 934df1b..5c4f4cd 100644 --- a/src/views/demand/industry-demand-analysis.vue +++ b/src/views/demand/industry-demand-analysis.vue @@ -90,6 +90,7 @@ import { DataAnalysis, Download, RefreshLeft } from "@element-plus/icons-vue"; import { checkIndustryDemandAnalysisStepOne, checkIndustryDemandAnalysisStepTwo, checkIndustryDemandAnalysisStepThree, checkIndustryDemandAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import useUserStore from "@/store/modules/user"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; @@ -134,7 +135,7 @@ const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); -const trainingSteps = computed(() => parseArray(taskConfig.value?.steps, DEFAULT_STEPS)); +const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS)); const progressItems = computed(() => pestRows.map((row) => ({ text: `${conclusionForm.value.summary ? "已总结" : analysisForm.value[row.key].opportunities ? "已分析" : supplementForm.value[row.key] ? "已补充" : form.value[row.key] ? "已归类" : "待填写"}:${row.label}`, status: conclusionForm.value.summary || analysisForm.value[row.key].opportunities || supplementForm.value[row.key] || form.value[row.key] ? "done" : "" }))); onMounted(async () => { await loadTaskConfig(); loadDraft(); await loadSavedAnswer(); draftReady.value = true; }); diff --git a/src/views/demand/people.vue b/src/views/demand/people.vue index 9fa9253..489aeed 100644 --- a/src/views/demand/people.vue +++ b/src/views/demand/people.vue @@ -330,6 +330,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import { getTrainingTaskByKey } from "@/api/trainingTask"; +import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { checkTargetUserProfileStepFive, checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; const TASK_KEY = "target-user-profile"; @@ -539,7 +540,7 @@ const taskBackground = computed(() => taskConfig.value?.background || defaultBac const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement); const flowSteps = computed(() => { - const configuredSteps = parseArray(taskConfig.value?.steps, DEFAULT_FLOW_STEPS); + const configuredSteps = parseTrainingArray(taskConfig.value?.steps, DEFAULT_FLOW_STEPS); return configuredSteps.length >= DEFAULT_FLOW_STEPS.length ? configuredSteps : DEFAULT_FLOW_STEPS; }); const steps = computed(() => diff --git a/src/views/foundation/product-development-factors.vue b/src/views/foundation/product-development-factors.vue index a5f258f..3b77b40 100644 --- a/src/views/foundation/product-development-factors.vue +++ b/src/views/foundation/product-development-factors.vue @@ -139,6 +139,7 @@