From 8d7a33f3363b6d2f7822a0da0452a5c4a09844c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E6=B2=85?= <907037276@qq.com>
Date: Mon, 31 Aug 2026 15:46:38 +0800
Subject: [PATCH] fix: constrain business feasibility to configured steps
---
src/views/demand/business-feasibility.vue | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/views/demand/business-feasibility.vue b/src/views/demand/business-feasibility.vue
index 0e97c2d..292ab83 100644
--- a/src/views/demand/business-feasibility.vue
+++ b/src/views/demand/business-feasibility.vue
@@ -96,10 +96,9 @@
上传可行性论证报告
支持 PDF、Word、PNG、JPG,文件不超过 20MB。
{{ report.name || '可行性论证报告' }}{{ reportKindLabel }} · {{ readableSize(report.size) }}
附件已上传,可随时预览或下载核对。
-
+
- 第 {{ currentStep }} 步将在后续实训页面中配置。
@@ -116,7 +115,7 @@ import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const TASK_KEY = "business-feasibility";
-const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告", "决策结论"];
+const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告"];
const overviewFields = [
{ key: "projectBackground", label: "项目背景", placeholder: "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" },
{ key: "productPositioning", label: "产品定位", placeholder: "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" },
@@ -153,7 +152,8 @@ 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(() => parseTrainingArray(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).slice(0, 3));
+const finalStep = computed(() => Math.max(1, trainingSteps.value.length));
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);
@@ -167,8 +167,9 @@ function numeric(value) { const number = Number(value); return Number.isFinite(n
function currency(value) { return Number(value || 0).toFixed(2); }
function percent(value) { return value === null || !Number.isFinite(value) ? "--" : `${Number(value).toFixed(2)}%`; }
function parseArray(value, fallback) { if (Array.isArray(value) && value.length) return value; if (!value) return fallback; try { const parsed = JSON.parse(value); return Array.isArray(parsed) && parsed.length ? parsed.map((item) => String(item?.name ?? item).trim()).filter(Boolean) : fallback; } catch (_) { const items = String(value).split(/[,,|]/).map((item) => item.trim()).filter(Boolean); return items.length ? items : fallback; } }
-async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (_) { taskConfig.value = null; } }
-async function loadSavedAnswer() { try { const saved = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (!saved) return; if (saved.step1Answer) { const parsed = JSON.parse(saved.step1Answer); form.value = mergeForm(parsed?.form || parsed); completed.value = true; } if (saved.step2Answer) { const parsed = JSON.parse(saved.step2Answer); stepTwo.value = { ...createStepTwo(), ...(parsed?.stepTwo || parsed || {}), priorities: Array.isArray(parsed?.stepTwo?.priorities || parsed?.priorities) ? (parsed?.stepTwo?.priorities || parsed?.priorities) : [] }; stepTwoCompleted.value = true; } if (saved.step3Answer) { const parsed = JSON.parse(saved.step3Answer); report.value = { ...createReport(), ...(parsed?.report || parsed || {}) }; stepThreeCompleted.value = Boolean(report.value.url); } if (Number(saved.currentStep) > 0) currentStep.value = Number(saved.currentStep); } catch (_) { /* keep a new answer editable */ } }
+function clampStep(value) { return Math.min(Math.max(1, Number(value) || 1), finalStep.value); }
+async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; currentStep.value = clampStep(currentStep.value); } catch (_) { taskConfig.value = null; } }
+async function loadSavedAnswer() { try { const saved = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (!saved) return; if (saved.step1Answer) { const parsed = JSON.parse(saved.step1Answer); form.value = mergeForm(parsed?.form || parsed); completed.value = true; } if (saved.step2Answer) { const parsed = JSON.parse(saved.step2Answer); stepTwo.value = { ...createStepTwo(), ...(parsed?.stepTwo || parsed || {}), priorities: Array.isArray(parsed?.stepTwo?.priorities || parsed?.priorities) ? (parsed?.stepTwo?.priorities || parsed?.priorities) : [] }; stepTwoCompleted.value = true; } if (saved.step3Answer) { const parsed = JSON.parse(saved.step3Answer); report.value = { ...createReport(), ...(parsed?.report || parsed || {}) }; stepThreeCompleted.value = Boolean(report.value.url); } if (Number(saved.currentStep) > 0) currentStep.value = clampStep(saved.currentStep); } catch (_) { /* keep a new answer editable */ } }
function mergeForm(value) { const fallback = createForm(); const merged = { ...fallback, ...(value || {}), projectOverview: { ...fallback.projectOverview, ...(value?.projectOverview || {}) }, marketAnalysis: { ...fallback.marketAnalysis, ...(value?.marketAnalysis || {}) }, targetUsers: Array.isArray(value?.targetUsers) && value.targetUsers.length === 3 ? value.targetUsers : fallback.targetUsers, businessModels: Array.isArray(value?.businessModels) && value.businessModels.length === 2 ? value.businessModels : fallback.businessModels, costs: fallback.costs.map((cost) => ({ ...cost, ...(value?.costs || []).find((item) => item?.id === cost.id) })), risks: fallback.risks.map((risk) => ({ ...risk, ...(value?.risks || []).find((item) => item?.type === risk.type) })) }; return merged; }
function payload() { return { ...form.value, costs: form.value.costs.map(({ id, amount }) => ({ id, amount: amount === "" ? null : Number(amount) })), targetPrice: form.value.targetPrice === "" ? null : Number(form.value.targetPrice) }; }
async function validateAndContinue() { if (saving.value) return; saving.value = true; try { const result = await checkBusinessFeasibilityStepOne(payload()); const calculated = result?.data || {}; form.value.costs = form.value.costs.map((cost) => ({ ...cost, amount: cost.amount === "" ? "" : Number(cost.amount) })); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify({ form: form.value, calculations: { unitTotalCost: calculated.unitTotalCost ?? totalCost.value, unitGrossProfit: calculated.unitGrossProfit ?? unitGrossProfit.value, grossMargin: calculated.grossMargin ?? grossMargin.value } }), currentStep: 2, submitted: false, saveAction: "SAVE" }); completed.value = true; currentStep.value = 2; proxy?.$modal?.msgSuccess("已完成校验并保存,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写本步骤内容后再提交"); } finally { saving.value = false; } }
@@ -183,8 +184,8 @@ function readableSize(size) { const value = Number(size || 0); return value > 10
function openReportPreview() { if (report.value.url) reportPreviewVisible.value = true; }
function downloadReport() { if (!report.value.url) return; const link = document.createElement("a"); link.href = report.value.url; link.target = "_blank"; link.rel = "noopener"; link.download = report.value.name || "可行性论证报告"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
function clearReport() { report.value = createReport(); reportPreviewVisible.value = false; stepThreeCompleted.value = false; }
-async function saveStepThree() { if (saving.value) return; saving.value = true; try { await checkBusinessFeasibilityStepThree({ reportUrl: report.value.url, reportName: report.value.name }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify({ report: report.value }), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreeCompleted.value = true; currentStep.value = 4; proxy?.$modal?.msgSuccess("报告已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请先上传可行性论证报告"); } finally { saving.value = false; } }
-function switchStep(step) { if (step > 1 && !completed.value) { proxy?.$modal?.msgWarning("请先完成并保存第 1 步"); return; } if (step > 2 && !stepTwoCompleted.value) { proxy?.$modal?.msgWarning("请先完成并保存第 2 步"); return; } if (step > 3 && !stepThreeCompleted.value) { proxy?.$modal?.msgWarning("请先上传并保存第 3 步报告"); return; } currentStep.value = step; }
+async function saveStepThree() { if (saving.value) return; saving.value = true; try { await checkBusinessFeasibilityStepThree({ reportUrl: report.value.url, reportName: report.value.name }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify({ report: report.value }), currentStep: finalStep.value, submitted: true, saveAction: "SUBMIT" }); stepThreeCompleted.value = true; currentStep.value = finalStep.value; proxy?.$modal?.msgSuccess("报告已保存,任务已提交"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请先上传可行性论证报告"); } finally { saving.value = false; } }
+function switchStep(step) { const targetStep = clampStep(step); if (targetStep > 1 && !completed.value) { proxy?.$modal?.msgWarning("请先完成并保存第 1 步"); return; } if (targetStep > 2 && !stepTwoCompleted.value) { proxy?.$modal?.msgWarning("请先完成并保存第 2 步"); return; } currentStep.value = targetStep; }
function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTitle.value, step: 1, ...payload(), calculations: { unitTotalCost: totalCost.value, unitGrossProfit: unitGrossProfit.value, grossMargin: grossMargin.value } }, null, 2)], { type: "application/json;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "商业可行性论证-第1步.json"; link.click(); URL.revokeObjectURL(link.href); }