fix: 完善学生实训校验与权重恢复

master
陈沅 2 days ago
parent 7b59ca6422
commit d8b861bb68

@ -1078,6 +1078,7 @@ export function checkProductDevelopmentProcessStepTwo(items) {
method: "post", method: "post",
data: { items }, data: { items },
headers: { repeatSubmit: false }, headers: { repeatSubmit: false },
skipErrorMessage: true,
}); });
} }
@ -1910,10 +1911,10 @@ export function checkCompetitiveEnvironmentAnalysisStepFour(payload) {
const value = payload || {}; const priorities = Array.isArray(value.priorities) ? value.priorities : []; const value = payload || {}; const priorities = Array.isArray(value.priorities) ? value.priorities : [];
if (isStudentDemo()) { if (isStudentDemo()) {
const types = ["so", "wo", "st", "wt"]; const ranks = priorities.map((item) => Number(item?.priority)); const types = ["so", "wo", "st", "wt"]; const ranks = priorities.map((item) => Number(item?.priority));
if (priorities.length !== types.length || types.some((type) => !priorities.some((item) => item?.strategyType === type)) || new Set(ranks).size !== 4 || ranks.some((rank) => ![1, 2, 3, 4].includes(rank)) || !String(value.overallEvaluation || "").trim() || priorities.some((item) => !String(item?.reason || "").trim()) || !String(value.coreRiskOne || "").trim() || !String(value.coreRiskTwo || "").trim() || !String(value.conclusion || "").trim()) return Promise.reject(new Error("请完整填写策略优先级、总体判断、风险点和总结结论")); if (priorities.length !== types.length || types.some((type) => !priorities.some((item) => item?.strategyType === type)) || new Set(ranks).size !== 4 || ranks.some((rank) => ![1, 2, 3, 4].includes(rank)) || priorities.some((item) => !String(item?.reason || "").trim()) || !String(value.coreRiskOne || "").trim() || !String(value.coreRiskTwo || "").trim() || !String(value.conclusion || "").trim()) return Promise.reject(new Error("请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } }); return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
} }
return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-4/validate", method: "post", data: value, headers: { repeatSubmit: false } }); return request({ url: "/api/student-training-answers/competitive-environment-analysis/step-4/validate", method: "post", data: value, headers: { repeatSubmit: false }, skipErrorMessage: true });
} }
const consumerScenarioJourneyOrder = ["problem-awareness", "information-search", "evaluation-decision", "purchase-conversion", "post-purchase"]; const consumerScenarioJourneyOrder = ["problem-awareness", "information-search", "evaluation-decision", "purchase-conversion", "post-purchase"];

@ -14,7 +14,7 @@
<component v-if="!route.meta.link" v-show="!studentTaskLoading" :is="Component" :key="route.path" /> <component v-if="!route.meta.link" v-show="!studentTaskLoading" :is="Component" :key="route.path" />
</keep-alive> </keep-alive>
</router-view> </router-view>
<DynamicTrainingStepNavigator v-if="Number(role) === 4 && route.meta?.pageKey" v-show="!studentTaskLoading" :task-key="route.meta.pageKey" /> <DynamicTrainingStepNavigator v-if="Number(role) === 4 && route.meta?.pageKey && !route.meta?.hideSupplementSteps" v-show="!studentTaskLoading" :task-key="route.meta.pageKey" />
<div v-if="studentTaskLoading" class="student-task-loading" role="status" aria-live="polite"> <div v-if="studentTaskLoading" class="student-task-loading" role="status" aria-live="polite">
<div class="student-task-loading__panel"> <div class="student-task-loading__panel">
<span class="student-task-loading__label">正在加载当前实训配置</span> <span class="student-task-loading__label">正在加载当前实训配置</span>

@ -288,7 +288,7 @@ export const dynamicRoutes = [
path: "index", path: "index",
component: () => import("@/views/product/market-opportunity-selection.vue"), component: () => import("@/views/product/market-opportunity-selection.vue"),
name: "product", name: "product",
meta: { title: "市场机会识别与选品", icon: "产品规划", affix: true, pageKey: "market-opportunity-selection" }, meta: { title: "市场机会识别与选品", icon: "产品规划", affix: true, pageKey: "market-opportunity-selection", hideSupplementSteps: true },
}, },
// 产品定价 // 产品定价
{ {

@ -102,6 +102,7 @@ service.interceptors.response.use(
(error) => { (error) => {
console.log("err" + error); console.log("err" + error);
let { message } = error; let { message } = error;
const serverMessage = String(error?.response?.data?.msg || "").trim();
if (message == "Network Error") { if (message == "Network Error") {
message = "后端接口连接异常"; message = "后端接口连接异常";
} else if (message.includes("timeout")) { } else if (message.includes("timeout")) {
@ -121,8 +122,11 @@ service.interceptors.response.use(
isRelogin.show = false; isRelogin.show = false;
}); });
} else if (message.includes("code 400")) { } else if (message.includes("code 400")) {
// ElNotification.error({ title: error.response.data }) if (!error.config?.skipErrorMessage) {
ElMessage({ message: error.response.data.msg, type: "error" }); ElMessage({ message: serverMessage || "请求参数不符合要求", type: "error" });
} else {
error.message = serverMessage || "请求参数不符合要求";
}
} }
message = "系统接口" + message.substr(message.length - 3) + "异常"; message = "系统接口" + message.substr(message.length - 3) + "异常";
} }

@ -24,7 +24,7 @@
<section v-if="currentStep === 1" class="step-card"> <section v-if="currentStep === 1" class="step-card">
<div class="step-heading"><div><p>Step 01</p><h2>商业可行性论证</h2></div><em>结构化论证</em></div> <div class="step-heading"><div><p>Step 01</p><h2>商业可行性论证</h2></div><em>结构化论证</em></div>
<p class="intro">商业可行性论证需要从多个维度系统展开请基于案例信息完成项目市场用户商业模式成本收益和风险的论证</p> <p class="intro">商业可行性论证需要从多个维度系统展开以下提供了可行性分析的各个子模块根据案例信息完善填写以下信息</p>
<section class="form-section"> <section class="form-section">
<div class="section-title"><span>01</span><div><h3>项目概述</h3><p>结合前序 PESTSWOT 分析说明项目为何值得推进</p></div></div> <div class="section-title"><span>01</span><div><h3>项目概述</h3><p>结合前序 PESTSWOT 分析说明项目为何值得推进</p></div></div>
@ -42,7 +42,7 @@
<section class="form-section"> <section class="form-section">
<div class="section-title"><span>03</span><div><h3>目标用户群体分析</h3><p>从不同消费能力和使用场景中识别最有价值的用户</p></div></div> <div class="section-title"><span>03</span><div><h3>目标用户群体分析</h3><p>从不同消费能力和使用场景中识别最有价值的用户</p></div></div>
<div class="table-wrap"><table><thead><tr><th>用户群体</th><th>典型场景</th><th>核心需求</th><th>消费能力</th></tr></thead><tbody><tr v-for="(user, index) in form.targetUsers" :key="index"><td><textarea v-model.trim="user.userGroup" placeholder="例如25-35 岁健身爱好者" /></td><td><textarea v-model.trim="user.scenario" placeholder="例如:居家训练" /></td><td><textarea v-model.trim="user.coreNeed" placeholder="例如:专业指导与反馈" /></td><td><input v-model.trim="user.spendingPower" placeholder="高 / 中 / 低" /></td></tr></tbody></table></div> <div class="table-wrap"><table><thead><tr><th>用户群体</th><th>典型场景</th><th>核心需求</th><th>消费能力</th></tr></thead><tbody><tr v-for="(user, index) in form.targetUsers" :key="index"><td><textarea v-model.trim="user.userGroup" :placeholder="targetUserExamples[index].userGroup" /></td><td><textarea v-model.trim="user.scenario" :placeholder="targetUserExamples[index].scenario" /></td><td><textarea v-model.trim="user.coreNeed" :placeholder="targetUserExamples[index].coreNeed" /></td><td><input v-model.trim="user.spendingPower" :placeholder="targetUserExamples[index].spendingPower" /></td></tr></tbody></table></div>
</section> </section>
<section class="form-section"> <section class="form-section">
@ -115,11 +115,11 @@ 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: "示例:阐述项目产品针对的目标市场用户、核心卖点、使用场景、差异化理由以及用户收益。" },
{ key: "projectGoals", label: "项目目标", placeholder: "填写短期、中期和长期可衡量目标" }, { key: "projectGoals", label: "项目目标", placeholder: "示例:填写 12 个月短期目标24 个月中期目标 36 个月长期目标" },
{ key: "macroEnvironmentConclusion", label: "宏观环境分析结论", placeholder: "总结 PEST 分析对项目的影响" }, { key: "macroEnvironmentConclusion", label: "宏观环境分析结论", placeholder: "示例:项目产品 PEST 分析结论。" },
{ key: "competitiveLandscapeConclusion", label: "竞争态势分析结论", placeholder: "总结 SWOT / 竞争分析带来的机会与压力" }, { key: "competitiveLandscapeConclusion", label: "竞争态势分析结论", placeholder: "示例:项目产品 SWOT 分析结论。" },
]; ];
const marketFields = [ const marketFields = [
{ key: "annualMarketSize", label: "年度市场规模", type: "number", suffix: "亿元", placeholder: "例如 80" }, { key: "annualMarketSize", label: "年度市场规模", type: "number", suffix: "亿元", placeholder: "例如 80" },
@ -128,6 +128,11 @@ const marketFields = [
{ key: "competitorsPriceRange", label: "主要竞品售价区间", suffix: "元", placeholder: "例如 299 - 899" }, { key: "competitorsPriceRange", label: "主要竞品售价区间", suffix: "元", placeholder: "例如 299 - 899" },
{ key: "marketConcentrationCr3", label: "市场集中度CR3", type: "number", suffix: "%", placeholder: "例如 58" }, { key: "marketConcentrationCr3", label: "市场集中度CR3", type: "number", suffix: "%", placeholder: "例如 58" },
]; ];
const targetUserExamples = [
{ userGroup: "示例18-35 岁一二线城市美妆爱好者、美妆博主", scenario: "示例:美妆产品带货、线上美妆教程", coreNeed: "示例补光效果、AI 测肤精度", spendingPower: "示例:高" },
{ userGroup: "示例22-40 岁租房白领、早八通勤族", scenario: "示例:早上上班前快速护肤化妆", coreNeed: "示例:日常化妆、耐用", spendingPower: "示例:中" },
{ userGroup: "示例18-28 岁学生群体、礼品采购人群", scenario: "示例:宿舍美妆尝试、礼物赠送", coreNeed: "示例:宿舍美妆工具、生日礼物", spendingPower: "示例:低" },
];
const feasibilityOptions = [ const feasibilityOptions = [
{ value: "highly-feasible", label: "高度可行,建议立即立项启动", hint: "市场、收益与风险条件均具备" }, { value: "highly-feasible", label: "高度可行,建议立即立项启动", hint: "市场、收益与风险条件均具备" },
{ value: "feasible-after-optimization", label: "基本可行,建议在优化部分条件后立项", hint: "关键条件完善后再推进" }, { value: "feasible-after-optimization", label: "基本可行,建议在优化部分条件后立项", hint: "关键条件完善后再推进" },
@ -186,4 +191,5 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTit
.report-guide { margin-top:24px; padding:18px 20px; border-left:3px solid #39d4f8; color:#c5e6f0; background:rgba(8,83,115,.2); line-height:1.85; }.report-guide p,.report-guide ol { margin:0; }.report-guide ol { margin-top:9px; padding-left:23px; }.report-guide li + li { margin-top:6px; }.report-upload-panel { display:grid; min-height:300px; place-content:center; justify-items:center; gap:12px; margin-top:30px; padding:36px; border:1px dashed rgba(83,218,255,.72); border-radius:16px; background:radial-gradient(circle at 50% 22%,rgba(15,149,201,.23),transparent 42%),rgba(1,21,34,.55); text-align:center; }.upload-orbit { display:grid; width:68px; height:68px; place-items:center; border:1px solid rgba(104,225,255,.72); border-radius:22px; color:#74e9ff; background:rgba(9,120,164,.22); font-size:32px; box-shadow:0 0 0 9px rgba(45,193,233,.06); }.report-upload-panel h3 { margin:8px 0 0; color:#f1fcff; font-size:21px; }.report-upload-panel > p { margin:0; color:#8ebfce; }.report-file-card { display:flex; align-items:center; width:min(760px,100%); gap:15px; padding:17px; border:1px solid rgba(93,217,252,.45); border-radius:13px; background:rgba(2,17,29,.7); text-align:left; }.report-file-icon { display:grid; flex:0 0 48px; width:48px; height:48px; place-items:center; border-radius:12px; color:#83e8ff; background:rgba(10,129,176,.28); font-size:25px; }.report-file-meta { min-width:0; flex:1; }.report-file-meta strong,.report-file-meta span { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.report-file-meta strong { color:#effdff; }.report-file-meta span { margin-top:5px; color:#83b7c8; font-size:12px; }.report-file-actions { display:flex; gap:8px; }.report-file-actions button,.text-action { display:inline-flex; align-items:center; gap:5px; border:0; color:#93eaff; background:transparent; font:inherit; font-size:13px; cursor:pointer; }.report-file-actions .danger { color:#ffaaa1; }.report-replace { display:flex; align-items:center; gap:13px; color:#85b7c7; font-size:12px; }.text-action:disabled { cursor:not-allowed; opacity:.6; }.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; }.report-preview { display:grid; min-height:64vh; place-items:center; background:#0a1520; }.report-preview .el-image { width:100%; height:64vh; }.report-preview iframe { width:100%; height:64vh; border:0; background:#fff; } .report-guide { margin-top:24px; padding:18px 20px; border-left:3px solid #39d4f8; color:#c5e6f0; background:rgba(8,83,115,.2); line-height:1.85; }.report-guide p,.report-guide ol { margin:0; }.report-guide ol { margin-top:9px; padding-left:23px; }.report-guide li + li { margin-top:6px; }.report-upload-panel { display:grid; min-height:300px; place-content:center; justify-items:center; gap:12px; margin-top:30px; padding:36px; border:1px dashed rgba(83,218,255,.72); border-radius:16px; background:radial-gradient(circle at 50% 22%,rgba(15,149,201,.23),transparent 42%),rgba(1,21,34,.55); text-align:center; }.upload-orbit { display:grid; width:68px; height:68px; place-items:center; border:1px solid rgba(104,225,255,.72); border-radius:22px; color:#74e9ff; background:rgba(9,120,164,.22); font-size:32px; box-shadow:0 0 0 9px rgba(45,193,233,.06); }.report-upload-panel h3 { margin:8px 0 0; color:#f1fcff; font-size:21px; }.report-upload-panel > p { margin:0; color:#8ebfce; }.report-file-card { display:flex; align-items:center; width:min(760px,100%); gap:15px; padding:17px; border:1px solid rgba(93,217,252,.45); border-radius:13px; background:rgba(2,17,29,.7); text-align:left; }.report-file-icon { display:grid; flex:0 0 48px; width:48px; height:48px; place-items:center; border-radius:12px; color:#83e8ff; background:rgba(10,129,176,.28); font-size:25px; }.report-file-meta { min-width:0; flex:1; }.report-file-meta strong,.report-file-meta span { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.report-file-meta strong { color:#effdff; }.report-file-meta span { margin-top:5px; color:#83b7c8; font-size:12px; }.report-file-actions { display:flex; gap:8px; }.report-file-actions button,.text-action { display:inline-flex; align-items:center; gap:5px; border:0; color:#93eaff; background:transparent; font:inherit; font-size:13px; cursor:pointer; }.report-file-actions .danger { color:#ffaaa1; }.report-replace { display:flex; align-items:center; gap:13px; color:#85b7c7; font-size:12px; }.text-action:disabled { cursor:not-allowed; opacity:.6; }.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; }.report-preview { display:grid; min-height:64vh; place-items:center; background:#0a1520; }.report-preview .el-image { width:100%; height:64vh; }.report-preview iframe { width:100%; height:64vh; border:0; background:#fff; }
@media (max-width:1280px) { .feasibility-page { grid-template-columns:1fr; }.step-indicator { overflow-x:auto; }.step-tab { min-width:170px; }.market-table { grid-template-columns:1fr; }.market-table label { border-right:0; }.cost-layout { grid-template-columns:1fr; } } @media (max-width:1280px) { .feasibility-page { grid-template-columns:1fr; }.step-indicator { overflow-x:auto; }.step-tab { min-width:170px; }.market-table { grid-template-columns:1fr; }.market-table label { border-right:0; }.cost-layout { grid-template-columns:1fr; } }
@media (max-width:720px) { .feasibility-page { padding:10px; }.task-header,.workbench,.step-card { padding:16px; }.task-header h1 { font-size:25px; }.workbench-actions { justify-content:flex-start; flex-wrap:wrap; }.form-table label { grid-template-columns:1fr; }.form-table label > span { justify-content:flex-start; }.step-heading em { display:none; }.step-actions { flex-wrap:wrap; }.decision-grid,.priority-grid { grid-template-columns:1fr; }.decision-section { padding:16px; }.hint { margin-left:0; }.report-upload-panel { padding:22px 14px; }.report-file-card { align-items:flex-start; flex-wrap:wrap; }.report-file-actions { width:100%; justify-content:flex-end; }.report-replace { flex-wrap:wrap; justify-content:center; } } @media (max-width:720px) { .feasibility-page { padding:10px; }.task-header,.workbench,.step-card { padding:16px; }.task-header h1 { font-size:25px; }.workbench-actions { justify-content:flex-start; flex-wrap:wrap; }.form-table label { grid-template-columns:1fr; }.form-table label > span { justify-content:flex-start; }.step-heading em { display:none; }.step-actions { flex-wrap:wrap; }.decision-grid,.priority-grid { grid-template-columns:1fr; }.decision-section { padding:16px; }.hint { margin-left:0; }.report-upload-panel { padding:22px 14px; }.report-file-card { align-items:flex-start; flex-wrap:wrap; }.report-file-actions { width:100%; justify-content:flex-end; }.report-replace { flex-wrap:wrap; justify-content:center; } }
.table-wrap th { display:table-cell; text-align:center; }
</style> </style>

@ -29,12 +29,11 @@
</template> </template>
<template v-else-if="currentStep === 4"> <template v-else-if="currentStep === 4">
<div class="step-title-row"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><span>最终决策</span></div> <div class="step-title-row"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><span>最终决策</span></div>
<p class="step-desc">结合前述 SWOT 分析完成市场进入决策策略优先级排序风险识别与竞争环境总结</p> <p class="step-desc">结合前述 SWOT 分析完成策略优先级排序风险识别与竞争环境总结</p>
<p v-if="!stepThreePassed" class="step-warning"> 3 SWOT </p> <p v-if="!stepThreePassed" class="step-warning"> 3 SWOT </p>
<section class="decision-section"><h3>1总体竞争态势评估</h3><p>请选择该产品进入目标市场的总体竞争判断</p><div class="radio-options"><label v-for="option in evaluationOptions" :key="option"><input v-model="finalForm.overallEvaluation" :disabled="!stepThreePassed" type="radio" name="competitive-evaluation" :value="option" /><span>{{ option }}</span></label></div></section> <section class="decision-section"><h3>1优先策略排序</h3><p class="section-tip">策略内容由第 3 步填写的具体策略建议自动带入请完成优先级排序并说明理由</p><div class="priority-table-wrap"><table class="priority-table"><thead><tr><th>优先级排序</th><th>策略</th><th>理由</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td><select v-model.number="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`"><option :value="null">请选择</option><option v-for="rank in 4" :key="rank" :value="rank">{{ rank }}</option></select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><textarea v-model.trim="finalForm.priorities[row.key].reason" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section>
<section class="decision-section"><h3>2优先策略排序</h3><p class="section-tip">策略内容由第 3 步填写的具体策略建议自动带入请完成优先级排序并说明理由</p><div class="priority-table-wrap"><table class="priority-table"><thead><tr><th>优先级排序</th><th>策略</th><th>理由</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td><select v-model.number="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`"><option :value="null">请选择</option><option v-for="rank in 4" :key="rank" :value="rank">{{ rank }}</option></select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><textarea v-model.trim="finalForm.priorities[row.key].reason" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section> <section class="decision-section"><h3>2需关注的风险点</h3><p>请至少指出 2 个风险点</p><div class="risk-grid"><label><span>核心风险点 1</span><textarea v-model.trim="finalForm.coreRiskOne" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><textarea v-model.trim="finalForm.coreRiskTwo" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><textarea v-model.trim="finalForm.otherRisk" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
<section class="decision-section"><h3>3需关注的风险点</h3><p>请至少指出 2 个风险点</p><div class="risk-grid"><label><span>核心风险点 1</span><textarea v-model.trim="finalForm.coreRiskOne" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><textarea v-model.trim="finalForm.coreRiskTwo" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><textarea v-model.trim="finalForm.otherRisk" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section> <section class="decision-section"><h3>3总结 SWOT 竞争环境评估结论</h3><textarea v-model.trim="finalForm.conclusion" class="conclusion-textarea" :disabled="!stepThreePassed" placeholder="综合说明优先策略、风险应对方向及最终结论" /></section>
<section class="decision-section"><h3>4总结 SWOT 竞争环境评估结论</h3><textarea v-model.trim="finalForm.conclusion" class="conclusion-textarea" :disabled="!stepThreePassed" placeholder="综合说明总体判断、优先策略与风险应对方向" /></section>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveAndCompleteTraining"></button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> </button></div> <div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveAndCompleteTraining"></button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> </button></div>
</template> </template>
<div v-else class="step-placeholder"><p> {{ currentStep }} 步内容待配置</p><span> 1 SWOT 分类已保存可从上方步骤栏返回查看</span></div> <div v-else class="step-placeholder"><p> {{ currentStep }} 步内容待配置</p><span> 1 SWOT 分类已保存可从上方步骤栏返回查看</span></div>
@ -72,14 +71,13 @@ const strategyDefinitions = [
{ key: "wt", label: "WT策略劣势×威胁", first: "weaknesses", firstLabel: "劣势", second: "threats", secondLabel: "威胁" }, { key: "wt", label: "WT策略劣势×威胁", first: "weaknesses", firstLabel: "劣势", second: "threats", secondLabel: "威胁" },
]; ];
const defaultTask = { title: "竞争环境分析", background: "通过 SWOT 模型归类案例资料中的竞争环境信息,为后续竞品比较与竞争策略判断提供依据。", goals: ["掌握 SWOT 四维度的分类方法", "提炼案例中的竞争环境关键信息", "形成竞争分析的基础材料"], requirement: "请阅读案例资料,将相关信息归类为优势、劣势、机会和威胁。" }; 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 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(() => 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" : "" }))); 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); onMounted(async () => { await loadTaskConfig(); loadDraft(); await loadSavedAnswer(); draftReady.value = true; }); watch(() => JSON.stringify(buildDraft()), persistDraft);
function createForm() { return { strengths: "", weaknesses: "", opportunities: "", threats: "" }; } function createForm() { return { strengths: "", weaknesses: "", opportunities: "", threats: "" }; }
function createStrategyForm() { return { so: createStrategyRow(), wo: createStrategyRow(), st: createStrategyRow(), wt: createStrategyRow() }; } function createStrategyForm() { return { so: createStrategyRow(), wo: createStrategyRow(), st: createStrategyRow(), wt: createStrategyRow() }; }
function createStrategyRow() { return { recommendation: "", expectedEffect: "" }; } function createStrategyRow() { return { recommendation: "", expectedEffect: "" }; }
function createFinalForm() { return { overallEvaluation: "", priorities: Object.fromEntries(strategyDefinitions.map((row) => [row.key, { priority: null, reason: "" }])), coreRiskOne: "", coreRiskTwo: "", otherRisk: "", conclusion: "" }; } function createFinalForm() { return { priorities: Object.fromEntries(strategyDefinitions.map((row) => [row.key, { priority: null, reason: "" }])), coreRiskOne: "", coreRiskTwo: "", otherRisk: "", conclusion: "" }; }
function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; 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 (error) { const list = String(value).split(/[,|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } } function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; 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 (error) { const list = String(value).split(/[,|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } }
async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } } async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } }
async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { restoreOutcome(answer.step1Answer, form); stepOnePassed.value = true; clearDraft(); } if (answer?.step2Answer) { restoreOutcome(answer.step2Answer, supplementForm, "supplements"); stepTwoPassed.value = true; } if (answer?.step3Answer) { restoreStrategyOutcome(answer.step3Answer); stepThreePassed.value = true; } if (answer?.step4Answer) restoreFinalOutcome(answer.step4Answer); if (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep); } catch (error) { /* Keep local draft when unavailable. */ } } async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { restoreOutcome(answer.step1Answer, form); stepOnePassed.value = true; clearDraft(); } if (answer?.step2Answer) { restoreOutcome(answer.step2Answer, supplementForm, "supplements"); stepTwoPassed.value = true; } if (answer?.step3Answer) { restoreStrategyOutcome(answer.step3Answer); stepThreePassed.value = true; } if (answer?.step4Answer) restoreFinalOutcome(answer.step4Answer); if (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep); } catch (error) { /* Keep local draft when unavailable. */ } }
@ -92,7 +90,7 @@ function switchStep(step) { if (step === 2 && !stepOnePassed.value) { proxy?.$mo
async function saveAndGoToStepTwo() { if (saving.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepOne(form.value); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; clearDraft(); currentStep.value = 2; proxy?.$modal?.msgSuccess("已保存,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写 SWOT 四类信息"); } finally { saving.value = false; } } async function saveAndGoToStepTwo() { if (saving.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepOne(form.value); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; clearDraft(); currentStep.value = 2; proxy?.$modal?.msgSuccess("已保存,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写 SWOT 四类信息"); } finally { saving.value = false; } }
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepTwo(supplementForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; clearDraft(); currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请为每个 SWOT 维度至少补充 1 条信息"); } finally { saving.value = false; } } async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepTwo(supplementForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; clearDraft(); currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请为每个 SWOT 维度至少补充 1 条信息"); } finally { saving.value = false; } }
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepThree({ strategies: strategyDefinitions.map((row) => ({ strategyType: row.key, ...strategyForm.value[row.key] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; clearDraft(); currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成四类 SWOT 策略建议和预期效果"); } finally { saving.value = false; } } async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepThree({ strategies: strategyDefinitions.map((row) => ({ strategyType: row.key, ...strategyForm.value[row.key] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; clearDraft(); currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成四类 SWOT 策略建议和预期效果"); } finally { saving.value = false; } }
async function saveAndCompleteTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ overallEvaluation: finalForm.value.overallEvaluation, priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("已保存并完成实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成总体判断、策略排序、风险点和总结结论"); } finally { saving.value = false; } } async function saveAndCompleteTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("已保存并完成实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"); } finally { saving.value = false; } }
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); strategyForm.value = createStrategyForm(); finalForm.value = createFinalForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" }); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已清空填写内容"); } async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); strategyForm.value = createStrategyForm(); finalForm.value = createFinalForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" }); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已清空填写内容"); }
function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); } function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function resetStepThree() { strategyForm.value = createStrategyForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); } function resetStepThree() { strategyForm.value = createStrategyForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }

@ -68,7 +68,7 @@
<script setup> <script setup>
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { Download, RefreshLeft, UploadFilled } from "@element-plus/icons-vue"; import { Download, RefreshLeft, UploadFilled } from "@element-plus/icons-vue";
import { checkMarketOpportunitySelectionStepOne, checkMarketOpportunitySelectionStepTwo, checkMarketOpportunitySelectionStepThree, checkMarketOpportunitySelectionStepFour, checkMarketOpportunitySelectionStepFive, checkMarketOpportunitySelectionStepSix, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { checkMarketOpportunitySelectionStepOne, checkMarketOpportunitySelectionStepTwo, checkMarketOpportunitySelectionStepThree, checkMarketOpportunitySelectionStepFour, checkMarketOpportunitySelectionStepFive, checkMarketOpportunitySelectionStepSix, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap"; import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo"; import { isStudentDemo } from "@/utils/studentDemo";
@ -106,7 +106,7 @@ function isValidRate(value) { return Number.isFinite(Number(value)) && Number(va
function hasCompleteReviewInsights() { return products.every((product) => { const insight = reviewInsights.value[product.id] || {}; return isValidRate(insight.favorableRate) && String(insight.painPoint || "").trim() && String(insight.unmetNeed || "").trim(); }); } function hasCompleteReviewInsights() { return products.every((product) => { const insight = reviewInsights.value[product.id] || {}; return isValidRate(insight.favorableRate) && String(insight.painPoint || "").trim() && String(insight.unmetNeed || "").trim(); }); }
function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; 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 (error) { const list = String(value).split(/[;|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } } function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; 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 (error) { const list = String(value).split(/[;|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } }
async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } } async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } }
async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { const parsed = JSON.parse(answer.step1Answer); answers.value = { ...createAnswers(), ...(parsed?.answers || parsed || {}) }; stepOnePassed.value = indicators.every((item) => answers.value[item.id] === item.group); } if (answer?.step2Answer) { const parsed = JSON.parse(answer.step2Answer); primaryWeights.value = { ...createPrimaryWeights(), ...(parsed?.primaryWeights || {}) }; weights.value = { ...createWeights(), ...(parsed?.dimensionWeights || parsed?.weights || {}) }; stepTwoPassed.value = stepOnePassed.value && categories.every((group) => isIntegerWeight(primaryWeights.value[group])) && indicators.every((item) => isIntegerWeight(weights.value[item.id])) && primaryWeightTotal.value === 100 && categories.every((group) => dimensionWeightTotal(group) === 100); } if (answer?.step3Answer) { const parsed = JSON.parse(answer.step3Answer); scores.value = { ...createScores(), ...(parsed?.scores || parsed || {}) }; stepThreePassed.value = stepTwoPassed.value; } if (answer?.step4Answer) { const parsed = JSON.parse(answer.step4Answer); visualization.value = { url: parsed?.visualization?.url || "", name: parsed?.visualization?.name || "" }; stepFourPassed.value = stepThreePassed.value && Boolean(visualization.value.url); } if (answer?.dynamicStepAnswers) { const parsed = JSON.parse(answer.dynamicStepAnswers); dynamicStepAnswers.value = parsed && typeof parsed === "object" ? parsed : {}; reviewInsights.value = { ...createReviewInsights(), ...(dynamicStepAnswers.value?.step5?.reviewInsights || {}) }; developmentConclusion.value = String(dynamicStepAnswers.value?.step6?.developmentConclusion || ""); stepFivePassed.value = stepFourPassed.value && hasCompleteReviewInsights(); } const savedStep = Number(answer?.currentStep) || 1; currentStep.value = stepFivePassed.value ? Math.min(savedStep, 6) : stepFourPassed.value ? Math.min(savedStep, 5) : stepThreePassed.value ? Math.min(savedStep, 4) : stepTwoPassed.value ? Math.min(savedStep, 3) : stepOnePassed.value ? Math.min(savedStep, 2) : 1; } catch (error) { /* Keep the page available when history cannot be restored. */ } } async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { const parsed = JSON.parse(answer.step1Answer); answers.value = { ...createAnswers(), ...(parsed?.answers || parsed || {}) }; stepOnePassed.value = indicators.every((item) => answers.value[item.id] === item.group); } if (answer?.step2Answer) { const parsed = JSON.parse(answer.step2Answer); const savedPrimaryWeights = { ...createPrimaryWeights(), ...(parsed?.primaryWeights || {}) }; const savedDimensionWeights = { ...createWeights(), ...(parsed?.dimensionWeights || parsed?.weights || {}) }; if (isValidSavedStepTwoOutcome(savedPrimaryWeights, savedDimensionWeights)) { primaryWeights.value = savedPrimaryWeights; weights.value = savedDimensionWeights; stepTwoPassed.value = stepOnePassed.value; } else { primaryWeights.value = createPrimaryWeights(); weights.value = createWeights(); stepTwoPassed.value = false; } } if (answer?.step3Answer) { const parsed = JSON.parse(answer.step3Answer); scores.value = { ...createScores(), ...(parsed?.scores || parsed || {}) }; stepThreePassed.value = stepTwoPassed.value; } if (answer?.step4Answer) { const parsed = JSON.parse(answer.step4Answer); visualization.value = { url: parsed?.visualization?.url || "", name: parsed?.visualization?.name || "" }; stepFourPassed.value = stepThreePassed.value && Boolean(visualization.value.url); } if (answer?.dynamicStepAnswers) { const parsed = JSON.parse(answer.dynamicStepAnswers); dynamicStepAnswers.value = parsed && typeof parsed === "object" ? parsed : {}; reviewInsights.value = { ...createReviewInsights(), ...(dynamicStepAnswers.value?.step5?.reviewInsights || {}) }; developmentConclusion.value = String(dynamicStepAnswers.value?.step6?.developmentConclusion || ""); stepFivePassed.value = stepFourPassed.value && hasCompleteReviewInsights(); } const savedStep = Number(answer?.currentStep) || 1; currentStep.value = stepFivePassed.value ? Math.min(savedStep, 6) : stepFourPassed.value ? Math.min(savedStep, 5) : stepThreePassed.value ? Math.min(savedStep, 4) : stepTwoPassed.value ? Math.min(savedStep, 3) : stepOnePassed.value ? Math.min(savedStep, 2) : 1; } catch (error) { /* Keep the page available when history cannot be restored. */ } }
function buildStepOneOutcome() { return { title: taskTitle.value, answers: { ...answers.value } }; } function buildStepOneOutcome() { return { title: taskTitle.value, answers: { ...answers.value } }; }
function buildStepTwoOutcome() { return { title: taskTitle.value, primaryWeights: { ...primaryWeights.value }, dimensionWeights: { ...weights.value }, correspondingWeights: Object.fromEntries(indicators.map((item) => [item.id, correspondingWeight(item)])) }; } function buildStepTwoOutcome() { return { title: taskTitle.value, primaryWeights: { ...primaryWeights.value }, dimensionWeights: { ...weights.value }, correspondingWeights: Object.fromEntries(indicators.map((item) => [item.id, correspondingWeight(item)])) }; }
function buildStepThreeOutcome() { return { title: taskTitle.value, scores: { ...scores.value } }; } function buildStepThreeOutcome() { return { title: taskTitle.value, scores: { ...scores.value } }; }
@ -116,16 +116,45 @@ function buildStepSixOutcome() { return { title: taskTitle.value, developmentCon
function buildDynamicStepAnswers(overrides) { return { ...dynamicStepAnswers.value, ...overrides }; } function buildDynamicStepAnswers(overrides) { return { ...dynamicStepAnswers.value, ...overrides }; }
async function checkAndContinue() { if (saving.value) return; saving.value = true; try { const result = await checkMarketOpportunitySelectionStepOne(indicators.map((item) => ({ id: item.id, category: answers.value[item.id] }))); stepOneCheckResults.value = Object.fromEntries((result?.data?.items || []).map((item) => [item.id, item])); if (!result?.data?.allCorrect) throw new Error(`当前答对 ${result?.data?.correctCount || 0}/${indicators.length} 项,请根据表内提示修改`); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; currentStep.value = 2; proxy?.$modal?.msgSuccess("分类正确,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成指标分类并根据提示修改"); } finally { saving.value = false; } } async function checkAndContinue() { if (saving.value) return; saving.value = true; try { const result = await checkMarketOpportunitySelectionStepOne(indicators.map((item) => ({ id: item.id, category: answers.value[item.id] }))); stepOneCheckResults.value = Object.fromEntries((result?.data?.items || []).map((item) => [item.id, item])); if (!result?.data?.allCorrect) throw new Error(`当前答对 ${result?.data?.correctCount || 0}/${indicators.length} 项,请根据表内提示修改`); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; currentStep.value = 2; proxy?.$modal?.msgSuccess("分类正确,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成指标分类并根据提示修改"); } finally { saving.value = false; } }
function isIntegerWeight(value) { return value !== null && value !== "" && Number.isInteger(Number(value)) && Number(value) >= 1 && Number(value) <= 100; } function isIntegerWeight(value) { return value !== null && value !== "" && Number.isInteger(Number(value)) && Number(value) >= 1 && Number(value) <= 100; }
function dimensionWeightTotal(group) { return indicators.filter((item) => item.group === group).reduce((total, item) => total + (Number(weights.value[item.id]) || 0), 0); } function dimensionWeightTotal(group) { return calculateDimensionWeightTotal(group, weights.value); }
function calculateDimensionWeightTotal(group, dimension) { return indicators.filter((item) => item.group === group).reduce((total, item) => total + (Number(dimension[item.id]) || 0), 0); }
function correspondingWeight(item) { return roundWeight(((Number(primaryWeights.value[item.group]) || 0) * (Number(weights.value[item.id]) || 0)) / 100); } function correspondingWeight(item) { return roundWeight(((Number(primaryWeights.value[item.group]) || 0) * (Number(weights.value[item.id]) || 0)) / 100); }
function roundWeight(value) { return Math.round((value + Number.EPSILON) * 100) / 100; } function roundWeight(value) { return Math.round((value + Number.EPSILON) * 100) / 100; }
function formatWeight(value) { return Number.isInteger(value) ? value : value.toFixed(2); } function formatWeight(value) { return Number.isInteger(value) ? value : value.toFixed(2); }
function isStepTwoReady() { return categories.every((group) => isIntegerWeight(primaryWeights.value[group]) && dimensionWeightTotal(group) === 100) && indicators.every((item) => isIntegerWeight(weights.value[item.id])) && primaryWeightTotal.value === 100 && correspondingWeightTotal.value === 100; } function isValidSavedStepTwoOutcome(primary, dimension) { const primaryTotal = categories.reduce((total, group) => total + (Number(primary[group]) || 0), 0); const correspondingTotal = roundWeight(indicators.reduce((total, item) => total + ((Number(primary[item.group]) || 0) * (Number(dimension[item.id]) || 0)) / 100, 0)); return categories.every((group) => isIntegerWeight(primary[group]) && calculateDimensionWeightTotal(group, dimension) === 100) && indicators.every((item) => isIntegerWeight(dimension[item.id])) && primaryTotal === 100 && correspondingTotal === 100; }
function isStepTwoReady() { return isValidSavedStepTwoOutcome(primaryWeights.value, weights.value); }
function switchStep(step) { if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成第 1 步指标分类"); return; } if (step > 2 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成第 2 步指标权重设定"); return; } if (step > 3 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成第 3 步综合得分计算"); return; } if (step > 4 && !stepFourPassed.value) { proxy?.$modal?.msgWarning("请先完成第 4 步数据可视化"); return; } if (step > 5 && !stepFivePassed.value) { proxy?.$modal?.msgWarning("请先完成第 5 步用户需求洞察"); return; } currentStep.value = step; } function switchStep(step) { if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成第 1 步指标分类"); return; } if (step > 2 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成第 2 步指标权重设定"); return; } if (step > 3 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成第 3 步综合得分计算"); return; } if (step > 4 && !stepFourPassed.value) { proxy?.$modal?.msgWarning("请先完成第 4 步数据可视化"); return; } if (step > 5 && !stepFivePassed.value) { proxy?.$modal?.msgWarning("请先完成第 5 步用户需求洞察"); return; } currentStep.value = step; }
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; if (!isStepTwoReady()) { proxy?.$modal?.msgError?.("请确认一级指标、维度内权重和对应总权重均为 100%"); return; } saving.value = true; try { await checkMarketOpportunitySelectionStepTwo({ primaryWeights: categories.map((group) => ({ group, weight: primaryWeights.value[group] })), dimensionWeights: indicators.map((item) => ({ id: item.id, weight: weights.value[item.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; currentStep.value = 3; proxy?.$modal?.msgSuccess("权重校验通过,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请确认一级指标、维度内权重和对应总权重均满足规则"); } finally { saving.value = false; } } async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; if (!isStepTwoReady()) { proxy?.$modal?.msgError?.("请确认一级指标、维度内权重和对应总权重均为 100%"); return; } saving.value = true; try { await checkMarketOpportunitySelectionStepTwo({ primaryWeights: categories.map((group) => ({ group, weight: primaryWeights.value[group] })), dimensionWeights: indicators.map((item) => ({ id: item.id, weight: weights.value[item.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; currentStep.value = 3; proxy?.$modal?.msgSuccess("权重校验通过,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请确认一级指标、维度内权重和对应总权重均满足规则"); } finally { saving.value = false; } }
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepThree({ scores: products.map((product) => ({ id: product.id, score: scores.value[product.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; currentStep.value = 4; proxy?.$modal?.msgSuccess("综合得分校验通过,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写 0 至 100 分、最多保留两位小数的综合得分"); } finally { saving.value = false; } } async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepThree({ scores: products.map((product) => ({ id: product.id, score: scores.value[product.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; currentStep.value = 4; proxy?.$modal?.msgSuccess("综合得分校验通过,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写 0 至 100 分、最多保留两位小数的综合得分"); } finally { saving.value = false; } }
function clearStepOneCheck(id) { if (stepOneCheckResults.value[id]) { const next = { ...stepOneCheckResults.value }; delete next[id]; stepOneCheckResults.value = next; } } function clearStepOneCheck(id) { if (stepOneCheckResults.value[id]) { const next = { ...stepOneCheckResults.value }; delete next[id]; stepOneCheckResults.value = next; } }
function resetStepOne() { answers.value = createAnswers(); stepOneCheckResults.value = {}; proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); } async function resetStepOne() {
if (saving.value) return;
saving.value = true;
try {
await deleteStudentTrainingAnswer(TASK_KEY);
if (visualization.value.url.startsWith("blob:")) URL.revokeObjectURL(visualization.value.url);
answers.value = createAnswers();
primaryWeights.value = createPrimaryWeights();
weights.value = createWeights();
scores.value = createScores();
visualization.value = { url: "", name: "" };
reviewInsights.value = createReviewInsights();
developmentConclusion.value = "";
dynamicStepAnswers.value = {};
stepOneCheckResults.value = {};
stepOnePassed.value = false;
stepTwoPassed.value = false;
stepThreePassed.value = false;
stepFourPassed.value = false;
stepFivePassed.value = false;
currentStep.value = 1;
proxy?.$modal?.msgSuccess("已清空本实训填写内容");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "清空失败,请重试");
} finally {
saving.value = false;
}
}
function resetStepTwo() { primaryWeights.value = createPrimaryWeights(); weights.value = createWeights(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); } function resetStepTwo() { primaryWeights.value = createPrimaryWeights(); weights.value = createWeights(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function resetStepThree() { scores.value = createScores(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); } function resetStepThree() { scores.value = createScores(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }
function resetStepFive() { reviewInsights.value = createReviewInsights(); stepFivePassed.value = false; proxy?.$modal?.msgSuccess("已清空第 5 步填写内容"); } function resetStepFive() { reviewInsights.value = createReviewInsights(); stepFivePassed.value = false; proxy?.$modal?.msgSuccess("已清空第 5 步填写内容"); }
@ -138,9 +167,13 @@ async function saveAndGoToStepSix() { if (saving.value || !stepFourPassed.value)
async function saveAndCompleteTraining() { if (saving.value || !stepFivePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepSix({ developmentConclusion: developmentConclusion.value }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: true, saveAction: "SUBMIT" }); proxy?.$modal?.msgSuccess("实训成果已保存,恭喜完成本次实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写建议开发的产品及具体原因"); } finally { saving.value = false; } } async function saveAndCompleteTraining() { if (saving.value || !stepFivePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepSix({ developmentConclusion: developmentConclusion.value }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: true, saveAction: "SUBMIT" }); proxy?.$modal?.msgSuccess("实训成果已保存,恭喜完成本次实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写建议开发的产品及具体原因"); } finally { saving.value = false; } }
function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); } function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }
function downloadInstructions() { downloadBuiltInFile(SCORE_CALCULATION_GUIDE_URL, "6.综合得分计算操作说明.docx"); } function downloadInstructions() { downloadBuiltInFile(SCORE_CALCULATION_GUIDE_URL, "6.综合得分计算操作说明.docx"); }
// Excel
/* Excel /* Excel
function downloadTemplate() { const firstProductRow = 5; const lastProductRow = firstProductRow + products.length - 1; const rawStartColumn = 1; const normalizedStartColumn = rawStartColumn + indicators.length; const totalColumn = normalizedStartColumn + indicators.length; const negativeMetricIds = new Set(["online-products", "online-merchants", "return-rate"]); const rows = [["市场机会识别与选品 · Min-Max 加权评分自动化模板"], ["填写原始指标值后,归一得分、对应总权重和综合得分会自动计算。"], [], ["产品品类", ...indicators.map((item) => `${item.name} 原始值`), ...indicators.map((item) => `${item.name} 归一得分`), "综合得分"], ...products.map((product) => [product.name, ...indicators.map(() => ""), ...indicators.map(() => ""), ""]), [], ["权重设置(来自第 2 步)"], ["一级指标权重(%)", ...indicators.map((item) => primaryWeights.value[item.group] ?? ""), ...indicators.map(() => ""), ""], ["维度内权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ...indicators.map(() => ""), ""], ["对应总权重(%)", ...indicators.map(() => ""), ...indicators.map(() => ""), ""]]; const worksheet = XLSX.utils.aoa_to_sheet(rows); indicators.forEach((item, index) => { const rawColumn = XLSX.utils.encode_col(rawStartColumn + index); const normalizedColumn = XLSX.utils.encode_col(normalizedStartColumn + index); const primaryWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 9 }); const dimensionWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 10 }); const correspondingWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 11 }); worksheet[correspondingWeightCell] = { t: "n", f: `${primaryWeightCell}*${dimensionWeightCell}/100` }; products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const rawCell = `${rawColumn}${row}`; const range = `${rawColumn}$${firstProductRow}:${rawColumn}$${lastProductRow}`; const normalizedCell = `${normalizedColumn}${row}`; worksheet[normalizedCell] = { t: "n", f: negativeMetricIds.has(item.id) ? `IFERROR((MAX(${range})-${rawCell})/(MAX(${range})-MIN(${range})),0)` : `IFERROR((${rawCell}-MIN(${range}))/(MAX(${range})-MIN(${range})),0)` }; }); }); products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const scoreCell = `${XLSX.utils.encode_col(totalColumn)}${row}`; const normalizedStart = XLSX.utils.encode_col(normalizedStartColumn); const normalizedEnd = XLSX.utils.encode_col(normalizedStartColumn + indicators.length - 1); const correspondingStart = XLSX.utils.encode_col(rawStartColumn); const correspondingEnd = XLSX.utils.encode_col(rawStartColumn + indicators.length - 1); worksheet[scoreCell] = { t: "n", f: `SUMPRODUCT(${normalizedStart}${row}:${normalizedEnd}${row},$${correspondingStart}$12:$${correspondingEnd}$12)/100` }; }); worksheet["!cols"] = [{ wch: 18 }, ...indicators.map(() => ({ wch: 18 })), ...indicators.map(() => ({ wch: 18 })), { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "综合得分计算"); XLSX.writeFile(workbook, "市场机会识别与选品-自动化模板.xlsx"); } function downloadTemplate() { const firstProductRow = 5; const lastProductRow = firstProductRow + products.length - 1; const rawStartColumn = 1; const normalizedStartColumn = rawStartColumn + indicators.length; const totalColumn = normalizedStartColumn + indicators.length; const negativeMetricIds = new Set(["online-products", "online-merchants", "return-rate"]); const rows = [["市场机会识别与选品 · Min-Max 加权评分自动化模板"], ["填写原始指标值后,归一得分、对应总权重和综合得分会自动计算。"], [], ["产品品类", ...indicators.map((item) => `${item.name} 原始值`), ...indicators.map((item) => `${item.name} 归一得分`), "综合得分"], ...products.map((product) => [product.name, ...indicators.map(() => ""), ...indicators.map(() => ""), ""]), [], ["权重设置(来自第 2 步)"], ["一级指标权重(%)", ...indicators.map((item) => primaryWeights.value[item.group] ?? ""), ...indicators.map(() => ""), ""], ["维度内权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ...indicators.map(() => ""), ""], ["对应总权重(%)", ...indicators.map(() => ""), ...indicators.map(() => ""), ""]]; const worksheet = XLSX.utils.aoa_to_sheet(rows); indicators.forEach((item, index) => { const rawColumn = XLSX.utils.encode_col(rawStartColumn + index); const normalizedColumn = XLSX.utils.encode_col(normalizedStartColumn + index); const primaryWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 9 }); const dimensionWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 10 }); const correspondingWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 11 }); worksheet[correspondingWeightCell] = { t: "n", f: `${primaryWeightCell}*${dimensionWeightCell}/100` }; products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const rawCell = `${rawColumn}${row}`; const range = `${rawColumn}$${firstProductRow}:${rawColumn}$${lastProductRow}`; const normalizedCell = `${normalizedColumn}${row}`; worksheet[normalizedCell] = { t: "n", f: negativeMetricIds.has(item.id) ? `IFERROR((MAX(${range})-${rawCell})/(MAX(${range})-MIN(${range})),0)` : `IFERROR((${rawCell}-MIN(${range}))/(MAX(${range})-MIN(${range})),0)` }; }); }); products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const scoreCell = `${XLSX.utils.encode_col(totalColumn)}${row}`; const normalizedStart = XLSX.utils.encode_col(normalizedStartColumn); const normalizedEnd = XLSX.utils.encode_col(normalizedStartColumn + indicators.length - 1); const correspondingStart = XLSX.utils.encode_col(rawStartColumn); const correspondingEnd = XLSX.utils.encode_col(rawStartColumn + indicators.length - 1); worksheet[scoreCell] = { t: "n", f: `SUMPRODUCT(${normalizedStart}${row}:${normalizedEnd}${row},$${correspondingStart}$12:$${correspondingEnd}$12)/100` }; }); worksheet["!cols"] = [{ wch: 18 }, ...indicators.map(() => ({ wch: 18 })), ...indicators.map(() => ({ wch: 18 })), { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "综合得分计算"); XLSX.writeFile(workbook, "市场机会识别与选品-自动化模板.xlsx"); }
*/ */
//
function downloadGeneratedTemplate() { const firstProductRow = 5; const lastProductRow = firstProductRow + products.length - 1; const rawStartColumn = 1; const normalizedStartColumn = rawStartColumn + indicators.length; const totalColumn = normalizedStartColumn + indicators.length; const negativeMetricIds = new Set(["online-products", "online-merchants", "return-rate"]); const rows = [["市场机会识别与选品 · Min-Max 加权评分自动化模板"], ["填写原始指标值后,归一得分、对应总权重和综合得分会自动计算。"], [], ["产品品类", ...indicators.map((item) => `${item.name} 原始值`), ...indicators.map((item) => `${item.name} 归一得分`), "综合得分"], ...products.map((product) => [product.name, ...indicators.map(() => ""), ...indicators.map(() => ""), ""]), [], ["权重设置(来自第 2 步)"], ["一级指标权重(%)", ...indicators.map((item) => primaryWeights.value[item.group] ?? ""), ...indicators.map(() => ""), ""], ["维度内权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ...indicators.map(() => ""), ""], ["对应总权重(%)", ...indicators.map(() => ""), ...indicators.map(() => ""), ""]]; const worksheet = XLSX.utils.aoa_to_sheet(rows); indicators.forEach((item, index) => { const rawColumn = XLSX.utils.encode_col(rawStartColumn + index); const normalizedColumn = XLSX.utils.encode_col(normalizedStartColumn + index); const primaryWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 9 }); const dimensionWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 10 }); const correspondingWeightCell = XLSX.utils.encode_cell({ c: rawStartColumn + index, r: 11 }); worksheet[correspondingWeightCell] = { t: "n", f: `${primaryWeightCell}*${dimensionWeightCell}/100` }; products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const rawCell = `${rawColumn}${row}`; const range = `${rawColumn}$${firstProductRow}:${rawColumn}$${lastProductRow}`; const normalizedCell = `${normalizedColumn}${row}`; worksheet[normalizedCell] = { t: "n", f: negativeMetricIds.has(item.id) ? `IFERROR((MAX(${range})-${rawCell})/(MAX(${range})-MIN(${range})),0)` : `IFERROR((${rawCell}-MIN(${range}))/(MAX(${range})-MIN(${range})),0)` }; }); }); products.forEach((product, productIndex) => { const row = firstProductRow + productIndex; const scoreCell = `${XLSX.utils.encode_col(totalColumn)}${row}`; const normalizedStart = XLSX.utils.encode_col(normalizedStartColumn); const normalizedEnd = XLSX.utils.encode_col(normalizedStartColumn + indicators.length - 1); const correspondingStart = XLSX.utils.encode_col(rawStartColumn); const correspondingEnd = XLSX.utils.encode_col(rawStartColumn + indicators.length - 1); worksheet[scoreCell] = { t: "n", f: `SUMPRODUCT(${normalizedStart}${row}:${normalizedEnd}${row},$${correspondingStart}$12:$${correspondingEnd}$12)/100` }; }); worksheet["!cols"] = [{ wch: 18 }, ...indicators.map(() => ({ wch: 18 })), ...indicators.map(() => ({ wch: 18 })), { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "综合得分计算"); XLSX.writeFile(workbook, "市场机会识别与选品-自动化模板.xlsx"); }
// rebase conflict resolved: retain the built-in download entry below.
function downloadTemplate() { downloadBuiltInFile(SCORE_CALCULATION_TEMPLATE_URL, "6.数据表-抖音电商 - 计算模版.xlsx"); } function downloadTemplate() { downloadBuiltInFile(SCORE_CALCULATION_TEMPLATE_URL, "6.数据表-抖音电商 - 计算模版.xlsx"); }
function downloadBuiltInFile(url, fileName) { const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadBuiltInFile(url, fileName) { const link = document.createElement("a"); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
function downloadVisualizationInstructions() { downloadFile("市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜分别填写每日综合得分。\n2. 使用自动化模板计算各品类的日平均得分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传,并点击缩略图预览确认。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); } function downloadVisualizationInstructions() { downloadFile("市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜分别填写每日综合得分。\n2. 使用自动化模板计算各品类的日平均得分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传,并点击缩略图预览确认。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }

@ -0,0 +1,30 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const sourcePath = path.resolve(__dirname, "../src/views/demand/business-feasibility.vue");
const backendPath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/java/com/sztzjy/linkCommerce/service/impl/BusinessFeasibilityStepOneServiceImpl.java"
);
test("商业可行性论证第 1 步覆盖七个分析区块并由后端校验计算", () => {
const source = fs.readFileSync(sourcePath, "utf8");
const backend = fs.readFileSync(backendPath, "utf8");
["项目概述", "市场规模与竞争格局", "目标用户群体分析", "商业模式梳理", "成本结构分析", "定价与毛利分析", "风险识别与评估"].forEach((label) => {
assert.match(source, new RegExp(label));
});
assert.match(source, /以下提供了可行性分析的各个子模块/);
assert.match(source, /单位毛利 = 单位目标售价 单位总成本/);
assert.match(source, /unitTotalCost/);
assert.match(backend, /validateOverview\(request\.getProjectOverview\(\)\)/);
assert.match(backend, /validateRisks\(request\.getRisks\(\)\)/);
});
test("目标用户群体表格的表头保持横向表格单元格布局", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.match(source, /\.table-wrap th \{[^}]*display:table-cell[^}]*text-align:center/);
});

@ -0,0 +1,16 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const source = fs.readFileSync(path.join(__dirname, "../src/api/studentTrainingAnswer.js"), "utf8");
test("竞争环境分析第 4 步保留后端中文校验文字且不触发全局 400 提示", () => {
const functionStart = source.indexOf("export function checkCompetitiveEnvironmentAnalysisStepFour(payload)");
const functionEnd = source.indexOf("const consumerScenarioJourneyOrder", functionStart);
const functionSource = source.slice(functionStart, functionEnd);
assert.ok(functionStart >= 0 && functionEnd > functionStart, "应能定位第 4 步校验请求");
assert.doesNotMatch(functionSource, /overallEvaluation/);
assert.match(functionSource, /skipErrorMessage: true/);
});

@ -0,0 +1,18 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const pageSource = fs.readFileSync(path.join(__dirname, "../src/views/product/market-opportunity-selection.vue"), "utf8");
const routerSource = fs.readFileSync(path.join(__dirname, "../src/router/index.js"), "utf8");
const appMainSource = fs.readFileSync(path.join(__dirname, "../src/layout/components/AppMain.vue"), "utf8");
test("清空第 1 步会移除已保存答案,重新进入不会恢复分类选择", () => {
assert.match(pageSource, /import \{[^}]*deleteStudentTrainingAnswer[^}]*\} from "@\/api\/studentTrainingAnswer"/);
assert.match(pageSource, /async function resetStepOne\(\)[\s\S]*?await deleteStudentTrainingAnswer\(TASK_KEY\)[\s\S]*?answers\.value = createAnswers\(\)/);
});
test("数据化选品页不显示右下角补充步骤导航", () => {
assert.match(routerSource, /pageKey: "market-opportunity-selection", hideSupplementSteps: true/);
assert.match(appMainSource, /route\.meta\?\.pageKey && !route\.meta\?\.hideSupplementSteps/);
});

@ -20,10 +20,11 @@ test("第二步按指定顺序渲染固定权重维度,并实时展示每个
test("第二步以固定分组计算对应总权重,并在保存前要求所有合计均为 100%", () => { test("第二步以固定分组计算对应总权重,并在保存前要求所有合计均为 100%", () => {
assert.match(source, /primaryWeights\.value\[item\.group\]/); assert.match(source, /primaryWeights\.value\[item\.group\]/);
const readiness = source.match(/function isStepTwoReady\(\) \{([^}]*)\}/)?.[1] || ""; const validation = source.match(/function isValidSavedStepTwoOutcome\(primary, dimension\) \{([^}]*)\}/)?.[1] || "";
assert.match(readiness, /primaryWeightTotal\.value === 100/); assert.match(validation, /primaryTotal === 100/);
assert.match(readiness, /correspondingWeightTotal\.value === 100/); assert.match(validation, /correspondingTotal === 100/);
assert.match(readiness, /dimensionWeightTotal\(group\) === 100/); assert.match(validation, /calculateDimensionWeightTotal\(group, dimension\) === 100/);
assert.match(source, /function isStepTwoReady\(\) \{ return isValidSavedStepTwoOutcome\(primaryWeights\.value, weights\.value\); \}/);
assert.match(source, /if \(!isStepTwoReady\(\)\) \{ proxy\?\.\$modal\?\.msgError\?\.\("请确认一级指标、维度内权重和对应总权重均为 100%"\); return; \}/); assert.match(source, /if \(!isStepTwoReady\(\)\) \{ proxy\?\.\$modal\?\.msgError\?\.\("请确认一级指标、维度内权重和对应总权重均为 100%"\); return; \}/);
}); });
@ -57,3 +58,9 @@ test("第二步的对应总权重列仅显示计算结果,不提供可编辑
test("恢复历史第一步答案时,只有与固定分组完全一致的答案才能解锁第二步", () => { test("恢复历史第一步答案时,只有与固定分组完全一致的答案才能解锁第二步", () => {
assert.match(source, /stepOnePassed\.value = indicators\.every\(\(item\) => answers\.value\[item\.id\] === item\.group\)/); assert.match(source, /stepOnePassed\.value = indicators\.every\(\(item\) => answers\.value\[item\.id\] === item\.group\)/);
}); });
test("第二步只恢复通过三级 100% 校验的历史权重,失效历史数据保持空白", () => {
assert.match(source, /function isValidSavedStepTwoOutcome\(primary, dimension\)/);
assert.match(source, /if \(isValidSavedStepTwoOutcome\(savedPrimaryWeights, savedDimensionWeights\)\) \{/);
assert.match(source, /primaryWeights\.value = createPrimaryWeights\(\); weights\.value = createWeights\(\);/);
});

@ -4,6 +4,8 @@ const path = require("node:path");
const test = require("node:test"); const test = require("node:test");
const source = fs.readFileSync(path.join(__dirname, "../src/views/foundation/product-development-process.vue"), "utf8"); const source = fs.readFileSync(path.join(__dirname, "../src/views/foundation/product-development-process.vue"), "utf8");
const apiSource = fs.readFileSync(path.join(__dirname, "../src/api/studentTrainingAnswer.js"), "utf8");
const requestSource = fs.readFileSync(path.join(__dirname, "../src/utils/request.js"), "utf8");
test("实训任务三第二步在提交前提示首个未填写字段", () => { test("实训任务三第二步在提交前提示首个未填写字段", () => {
assert.match(source, /function getStepTwoValidationMessage\(\)[\s\S]*?请填写任务 \$\{row\.taskNo\} 的项目依赖关系/); assert.match(source, /function getStepTwoValidationMessage\(\)[\s\S]*?请填写任务 \$\{row\.taskNo\} 的项目依赖关系/);
@ -26,3 +28,8 @@ test("实训任务三第二步使用标准SS依赖示例并采用紧凑表格",
assert.match(source, /\.schedule-table \.stage-cell \{ width: 7%; \}/); assert.match(source, /\.schedule-table \.stage-cell \{ width: 7%; \}/);
assert.match(source, /\.schedule-table \.task-no-cell \{ width: 7%; \}/); assert.match(source, /\.schedule-table \.task-no-cell \{ width: 7%; \}/);
}); });
test("实训任务三第二步只显示后端返回的校验文字", () => {
assert.match(apiSource, /function checkProductDevelopmentProcessStepTwo\(items\)[\s\S]*?skipErrorMessage: true/);
assert.match(requestSource, /message\.includes\("code 400"\)[\s\S]*?if \(!error\.config\?\.skipErrorMessage\)[\s\S]*?error\.message = serverMessage/);
});

Loading…
Cancel
Save