fix: constrain business feasibility to configured steps

bugfix/ai-assessment-evidence-rescore
陈沅 1 month ago
parent 0ca08d1086
commit 8d7a33f336

@ -96,10 +96,9 @@
<template v-if="!report.url"><el-icon class="upload-orbit"><Document /></el-icon><h3>上传可行性论证报告</h3><p>支持 PDF、Word、PNG、JPG,文件不超过 20MB。</p><button class="action-button primary" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '选择报告附件' }}</button></template> <template v-if="!report.url"><el-icon class="upload-orbit"><Document /></el-icon><h3>上传可行性论证报告</h3><p>支持 PDF、Word、PNG、JPG,文件不超过 20MB。</p><button class="action-button primary" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '选择报告附件' }}</button></template>
<template v-else><div class="report-file-card"><div class="report-file-icon"><el-icon><Document /></el-icon></div><div class="report-file-meta"><strong>{{ report.name || '可行性论证报告' }}</strong><span>{{ reportKindLabel }} · {{ readableSize(report.size) }}</span></div><div class="report-file-actions"><button type="button" @click="openReportPreview"><el-icon><View /></el-icon>预览</button><button type="button" @click="downloadReport"><el-icon><Download /></el-icon>下载</button><button type="button" class="danger" @click="clearReport"><el-icon><Delete /></el-icon>移除</button></div></div><div class="report-replace"><button class="text-action" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '更换附件' }}</button><span>附件已上传,可随时预览或下载核对。</span></div></template> <template v-else><div class="report-file-card"><div class="report-file-icon"><el-icon><Document /></el-icon></div><div class="report-file-meta"><strong>{{ report.name || '可行性论证报告' }}</strong><span>{{ reportKindLabel }} · {{ readableSize(report.size) }}</span></div><div class="report-file-actions"><button type="button" @click="openReportPreview"><el-icon><View /></el-icon>预览</button><button type="button" @click="downloadReport"><el-icon><Download /></el-icon>下载</button><button type="button" class="danger" @click="clearReport"><el-icon><Delete /></el-icon>移除</button></div></div><div class="report-replace"><button class="text-action" type="button" :disabled="uploading" @click="openReportPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? '上传中…' : '更换附件' }}</button><span>附件已上传,可随时预览或下载核对。</span></div></template>
</section> </section>
<div class="step-actions"><button class="action-button primary" type="button" :disabled="saving || uploading" @click="saveStepThree">{{ saving ? '校验并保存中…' : '保存并进入第 4 步' }}</button></div> <div class="step-actions"><button class="action-button primary" type="button" :disabled="saving || uploading" @click="saveStepThree">{{ saving ? '校验并提交中…' : '保存并提交任务' }}</button></div>
<el-dialog v-model="reportPreviewVisible" class="report-preview-dialog" :title="report.name || '可行性论证报告预览'" width="min(1100px, 94vw)" destroy-on-close append-to-body><div class="report-preview"><el-image v-if="reportKind === 'image'" :src="report.url" fit="contain" :preview-src-list="[report.url]" preview-teleported /><iframe v-else :src="reportPreviewUrl" title="可行性论证报告预览"></iframe></div><template #footer><button class="action-button" type="button" @click="downloadReport"><el-icon><Download /></el-icon>下载附件</button></template></el-dialog> <el-dialog v-model="reportPreviewVisible" class="report-preview-dialog" :title="report.name || '可行性论证报告预览'" width="min(1100px, 94vw)" destroy-on-close append-to-body><div class="report-preview"><el-image v-if="reportKind === 'image'" :src="report.url" fit="contain" :preview-src-list="[report.url]" preview-teleported /><iframe v-else :src="reportPreviewUrl" title="可行性论证报告预览"></iframe></div><template #footer><button class="action-button" type="button" @click="downloadReport"><el-icon><Download /></el-icon>下载附件</button></template></el-dialog>
</section> </section>
<section v-else class="step-card placeholder"><p>第 {{ currentStep }} 步将在后续实训页面中配置。</p><button class="action-button" type="button" @click="currentStep = 3">返回第 3 步</button></section>
</section> </section>
</main> </main>
<TrainingAiSidebar study-tip="先从案例资料中提炼可验证的市场、用户与成本依据,再形成一致的商业论证。" reference-title="可行性论证提示" reference-text="• 结论要有数据或前序分析支撑<br />• 价格、成本和毛利需要逻辑一致<br />• 风险应附带可执行的应对措施" :progress-items="progressItems" /> <TrainingAiSidebar study-tip="先从案例资料中提炼可验证的市场、用户与成本依据,再形成一致的商业论证。" reference-title="可行性论证提示" reference-text="• 结论要有数据或前序分析支撑<br />• 价格、成本和毛利需要逻辑一致<br />• 风险应附带可执行的应对措施" :progress-items="progressItems" />
@ -116,7 +115,7 @@ import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue"; import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const TASK_KEY = "business-feasibility"; const TASK_KEY = "business-feasibility";
const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告", "决策结论"]; const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告"];
const overviewFields = [ const overviewFields = [
{ key: "projectBackground", label: "项目背景", placeholder: "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" }, { key: "projectBackground", label: "项目背景", placeholder: "示例:根据案例信息简述公司基本情况,再简要阐述智能家居、个护健康、美妆工具等相关市场发展情况和需求情况。" },
{ key: "productPositioning", label: "产品定位", placeholder: "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" }, { key: "productPositioning", label: "产品定位", placeholder: "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" },
@ -153,7 +152,8 @@ const priorityOptions = [
const defaultTask = { title: "商业可行性论证", background: "在产品开发前,综合市场机会、用户需求、竞争态势、成本收益和风险,判断项目是否具有可推进的商业价值。", goals: ["掌握商业可行性论证的基本维度", "形成市场、用户、成本与收益的一致判断", "识别主要风险并提出应对措施"], requirement: "请以案例资料和前序分析为依据,完成表格全部内容;成本与价格数据需真实、合理且逻辑一致。" }; const defaultTask = { title: "商业可行性论证", background: "在产品开发前,综合市场机会、用户需求、竞争态势、成本收益和风险,判断项目是否具有可推进的商业价值。", goals: ["掌握商业可行性论证的基本维度", "形成市场、用户、成本与收益的一致判断", "识别主要风险并提出应对措施"], requirement: "请以案例资料和前序分析为依据,完成表格全部内容;成本与价格数据需真实、合理且逻辑一致。" };
const { proxy } = getCurrentInstance(); 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 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 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 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); 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 currency(value) { return Number(value || 0).toFixed(2); }
function percent(value) { return value === null || !Number.isFinite(value) ? "--" : `${Number(value).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; } } 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; } } function clampStep(value) { return Math.min(Math.max(1, Number(value) || 1), finalStep.value); }
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 */ } } 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 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) }; } 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; } } 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 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 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; } 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; } } 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) { 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; } 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); } 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); }
</script> </script>

Loading…
Cancel
Save