feat: 完善学生实训资料下载与步骤页面

master
陈沅 4 days ago
parent e1ad967b1b
commit 03a5967382

@ -8,10 +8,14 @@
<nav class="step-tabs" aria-label=""><button type="button" :class="{ active: currentStep === 1 }" @click="currentStep = 1"><b>01</b><span>全链路漏斗诊断<small>转化率与异常节点</small></span></button><i></i><button type="button" :class="{ active: currentStep === 2 }" @click="currentStep = 2"><b>02</b><span>用户评论根因分析<small>词云与情感倾向</small></span></button><i></i><button type="button" :class="{ active: currentStep === 3 }" @click="currentStep = 3"><b>03</b><span>AB 测试验证<small>改进方案有效性</small></span></button></nav>
<template v-if="currentStep === 1">
<p class="lead">从曝光到复购逐层计算渠道转化率并与行业均值比较定位产品运营环节中的异常流失</p>
<p class="lead">从用户触达产品的全路径出发搭建从曝光到复购的完整数据漏斗逐层计算各环节转化率并对比行业均值定位产品在哪个环节出现了明显异常流失</p>
<div class="diagnosis-materials" aria-label=" 1 ">
<div class="diagnosis-materials__item"><div><strong>节点转化率计算</strong><span>使用案例数据计算从曝光到复购的各节点转化率</span></div><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadNodeGuide"></button><button type="button" class="secondary-button" @click="downloadNodeTemplate">Excel</button></div></div>
<div class="diagnosis-materials__item"><div><strong>全链路转化漏斗可视化</strong><span>根据节点转化率绘制各渠道与行业均值的漏斗对比图</span></div><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadFunnelGuide"></button><button type="button" class="secondary-button" @click="downloadFunnelTemplate">Excel</button></div></div>
</div>
<section class="section-block"><div class="section-heading"><span></span><div><h3>计算各节点转化率</h3><p>根据案例数据填写各渠道链路节点转化率数值以百分比%填写</p></div></div><div class="table-wrap"><table class="funnel-table"><thead><tr><th>链路节点</th><th>天猫</th><th>京东</th><th>抖音</th><th>拼多多</th><th>行业均值</th></tr></thead><tbody><tr v-for="row in conversionRates" :key="row.linkNode"><th>{{ row.linkNode }}</th><td v-for="field in rateFields" :key="field"><el-input-number v-model="row[field]" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td></tr></tbody></table></div></section>
<section class="section-block visual-section"><div class="section-heading"><span></span><div><h3>全链路转化漏斗可视化</h3><p>根据上述转化率绘制各渠道与行业均值的节点转化漏斗对比图</p></div></div><input ref="chartInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleChartUpload" /><div class="chart-upload-area"><template v-if="funnelChart.url"><div class="chart-preview"><el-image :src="funnelChart.url" :preview-src-list="[funnelChart.url]" fit="cover" preview-teleported /><div><strong>{{ funnelChart.name || '节点转化漏斗对比图' }}</strong><span>点击缩略图可放大预览</span><button type="button" @click="clearChart"></button></div></div><button type="button" class="secondary-button" :disabled="uploading" @click="chartInput?.click()">{{ uploading ? '' : '' }}</button></template><template v-else><button type="button" class="upload-button" :disabled="uploading" @click="chartInput?.click()">{{ uploading ? '' : '' }}</button><p>支持 PNGJPGWEBP 格式图片不超过 5MB上传后可点击预览</p></template></div></section>
<section class="section-block diagnosis-section"><div class="section-heading"><span></span><div><h3>定位异常节点</h3><p>对比行业均值数据定位异常节点并填写分析与判定</p></div></div><div class="table-toolbar"><span>默认 1 最多 5 渠道必须来源于上表</span><div><button type="button" class="table-button" :disabled="anomalyRows.length >= 5" @click="addAnomalyRow"></button><button type="button" class="table-button ghost" :disabled="anomalyRows.length <= 1" @click="removeAnomalyRow"></button></div></div><div class="table-wrap"><table class="diagnosis-table"><thead><tr><th>渠道</th><th>链路节点</th><th>店铺转化率</th><th>行业均值转化率</th><th>异常分析与判定</th></tr></thead><tbody><tr v-for="(row, index) in anomalyRows" :key="index"><td><el-select v-model="row.channel" placeholder="请选择"><el-option v-for="channel in channels" :key="channel.key" :label="channel.label" :value="channel.label" /></el-select></td><td><el-select v-model="row.linkNode" placeholder="请选择"><el-option v-for="node in nodes" :key="node" :label="node" :value="node" /></el-select></td><td><el-input-number v-model="row.storeConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input-number v-model="row.industryAverageConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input v-model="row.anomalyAnalysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="请填写异常原因或正常判定" /></td></tr></tbody></table></div></section>
<section class="section-block diagnosis-section"><div class="section-heading"><span></span><div><h3>定位异常节点</h3><p>对比行业均值数据定位异常节点并填写分析与判定</p></div></div><div class="table-toolbar"><span>默认 1 最多 5 渠道必须来源于上表</span><div><button type="button" class="table-button" :disabled="anomalyRows.length >= 5" @click="addAnomalyRow"></button><button type="button" class="table-button ghost" :disabled="anomalyRows.length <= 1" @click="removeAnomalyRow"></button></div></div><div class="table-wrap"><table class="diagnosis-table"><thead><tr><th>渠道</th><th>链路节点</th><th>店铺转化率</th><th>行业均值转化率</th><th>异常分析与判定</th></tr></thead><tbody><tr v-for="(row, index) in anomalyRows" :key="index"><td><el-select v-model="row.channel" placeholder="请选择"><el-option v-for="channel in channels" :key="channel.key" :label="channel.label" :value="channel.label" /></el-select></td><td><el-select v-model="row.linkNode" placeholder="请选择"><el-option v-for="node in nodes" :key="node" :label="node" :value="node" /></el-select></td><td><el-input-number v-model="row.storeConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input-number v-model="row.industryAverageConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input v-model="row.anomalyAnalysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="请填写异常原因或正常判定" /></td></tr></tbody></table></div><p class="diagnosis-step-one-notice"> 5 </p></section>
<div class="actions"><button type="button" class="secondary-button" :disabled="saving" @click="saveStepOneDraft">稿</button><button type="button" class="submit-button" :disabled="saving || uploading" @click="validateStepOne">{{ saving ? '' : ' 2 ' }}</button></div>
</template>
@ -24,11 +28,11 @@
</template>
<template v-else>
<p class="lead">找到根因后还需要通过 AB 测试验证改进方案是否真正有效将用户随机分为维持原方案的对照组和应用改进方案的实验组用关键指标差异判断方案是否值得落地</p>
<section class="section-block"><div class="section-heading"><span></span><div><h3>确定测试方案</h3><p>请基于前两步的漏斗异常与评论根因设计 AB 测试方案</p></div></div><el-input v-model="abTestPlan" type="textarea" :rows="6" maxlength="1000" show-word-limit placeholder="说明对照组与实验组设置、改进内容、随机分流方式、观察周期及成功判定标准" /></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>AB 测试数据填报</h3><p>填写案例资料中的对照组与实验组数据并逐项给出判定与分析</p></div></div><div class="table-wrap"><table class="ab-table"><thead><tr><th>测试指标</th><th>对照组数据</th><th>实验组数据</th><th>判定与分析</th></tr></thead><tbody><tr v-for="row in abRows" :key="row.metric"><th>{{ row.metric }}</th><td><el-input-number v-model="row.controlValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input-number v-model="row.experimentValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input v-model="row.analysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="如:提升 8pp显著提升建议采用" /></td></tr></tbody></table></div></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>可视化呈现</h3><p>绘制 AB 测试结果对比图并上传作为改进方案的可视化证据</p></div></div><input ref="abChartInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleAbChartUpload" /><div class="chart-upload-area"><template v-if="abChart.url"><div class="chart-preview"><el-image :src="abChart.url" :preview-src-list="[abChart.url]" fit="cover" preview-teleported /><div><strong>{{ abChart.name || 'AB 测试结果对比图' }}</strong><span>点击缩略图可放大预览</span><button type="button" @click="clearAbChart"></button></div></div><button type="button" class="secondary-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '' : '' }}</button></template><template v-else><button type="button" class="upload-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '' : ' AB ' }}</button><p>支持 PNGJPGWEBP 格式图片不超过 5MB上传后可点击预览</p></template></div></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>诊断结论与迭代优化建议</h3><p>结合全链路漏斗评论根因与 AB 测试对照结果明确产品问题及下一轮优化动作</p></div></div><el-input v-model="iterationRecommendation" type="textarea" :rows="6" maxlength="1200" show-word-limit placeholder="说明验证结论、需要保留或放大的改进方案、待优化问题及后续迭代建议" /></section>
<p class="lead">找到根因后还需验证改进方案是否真的有效避免拍脑袋迭代AB测试对照诊断法将用户随机分为对照组维持原方案和实验组应用改进方案对比两组关键指标差异用数据判断改进方案是否值得落地</p>
<section class="section-block"><div class="section-heading"><span></span><div><h3>确定测试方案</h3><p>请基于案例资料针对步骤一和步骤二分析结果设计 AB 测试方案</p></div></div><el-input v-model="abTestPlan" type="textarea" :rows="6" maxlength="1000" show-word-limit placeholder="填写对照组与实验组设置、改进方案、随机分流方式、观察周期和成功判定标准" /></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>AB测试数据填报</h3><p>请从案例资料中获取 AB 测试两组数据计算后填入下表并逐项判定</p></div></div><div class="table-wrap"><table class="ab-table"><thead><tr><th>测试指标</th><th>对照组数据</th><th>实验组数据</th><th>判定与分析</th></tr></thead><tbody><tr v-for="row in abRows" :key="row.metric"><th>{{ row.metric }}</th><td><el-input-number v-model="row.controlValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="56" /><span class="unit">%</span></td><td><el-input-number v-model="row.experimentValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="64" /><span class="unit">%</span></td><td><el-input v-model="row.analysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="如:提升8pp显著提升采纳" /></td></tr></tbody></table></div></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>可视化呈现</h3><p>绘制 AB 测试结果对比图并上传作为改进方案的可视化证据</p></div></div><input ref="abChartInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleAbChartUpload" /><div class="chart-upload-area"><template v-if="abChart.url"><div class="chart-preview"><el-image :src="abChart.url" :preview-src-list="[abChart.url]" fit="cover" preview-teleported /><div><strong>{{ abChart.name || 'AB 测试结果对比图' }}</strong><span>点击缩略图可放大预览</span><button type="button" @click="clearAbChart"></button></div></div><button type="button" class="secondary-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '' : '' }}</button></template><template v-else><button type="button" class="upload-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '' : 'AB' }}</button><p>支持 PNGJPGWEBP 格式图片不超过 5MB上传后可点击预览</p></template></div><div class="ab-materials"><div><ol><li>使用 Excel/WPS 绘图功能自主完成 AB 测试结果对比图可参考实训操作说明</li><li>下载 Excel 模板填写关键数据自动计算 AB 测试数据并生成测试结果对比图</li><li>也可导出数据表上传到通用 AI 大模型通过对照组和实验组数据对比生成组合柱状图</li></ol></div><div class="ab-materials__actions"><button type="button" class="secondary-button" @click="downloadAbGuide"></button><button type="button" class="secondary-button" @click="downloadAbTemplate">Excel</button></div></div></section>
<section class="section-block"><div class="section-heading"><span></span><div><h3>诊断结论与迭代优化建议</h3><p>结合全链路数据漏斗诊断售后评论分析法及 AB 测试对照诊断分析产品存在的问题并提出具体迭代优化建议</p></div></div><el-input v-model="iterationRecommendation" type="textarea" :rows="6" maxlength="1200" show-word-limit placeholder="请填写产品问题诊断结论及具体迭代优化建议" /></section>
<div class="actions"><button type="button" class="secondary-button" :disabled="saving" @click="saveStepThreeDraft">稿</button><button type="button" class="submit-button" :disabled="saving || uploading" @click="validateStepThree">{{ saving ? '' : ' 3 ' }}</button></div>
</template>
</section>
@ -49,6 +53,19 @@ import { analyzeProductDiagnosisIterationStepTwo, getStudentTrainingAnswer, save
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
const NODE_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E1.docx";
const NODE_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%881.xlsx";
const FUNNEL_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E2.docx";
const FUNNEL_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%882.xlsx";
const AB_TEST_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E3.docx";
const AB_TEST_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%883.xlsx";
function downloadBuiltInFile(url) { window.open(url, "_blank", "noopener"); }
function downloadNodeGuide() { downloadBuiltInFile(NODE_GUIDE_URL); }
function downloadNodeTemplate() { downloadBuiltInFile(NODE_TEMPLATE_URL); }
function downloadFunnelGuide() { downloadBuiltInFile(FUNNEL_GUIDE_URL); }
function downloadFunnelTemplate() { downloadBuiltInFile(FUNNEL_TEMPLATE_URL); }
function downloadAbGuide() { downloadBuiltInFile(AB_TEST_GUIDE_URL); }
function downloadAbTemplate() { downloadBuiltInFile(AB_TEST_TEMPLATE_URL); }
const TASK_KEY = "product-diagnosis-iteration"; const nodes = ["曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"]; const channels = [{ key: "tmall", label: "天猫" }, { key: "jd", label: "京东" }, { key: "douyin", label: "抖音" }, { key: "pinduoduo", label: "拼多多" }]; const rateFields = [...channels.map((item) => item.key), "industryAverage"];
const { proxy } = getCurrentInstance(); const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const uploading = ref(false); const analyzing = ref(false); const chartInput = ref(null); const commentInput = ref(null); const abChartInput = ref(null); const wordCloudRef = ref(null); let wordCloudChart = null;
const conversionRates = ref(createConversionRates()); const anomalyRows = ref([createAnomalyRow()]); const funnelChart = ref({ url: "", name: "" }); const analysisResult = ref(null); const commentPreview = ref({ fileName: "", totalComments: 0, comments: [] }); const wordCloud = ref([]); const topIssues = ref(["", "", ""]); const sentiments = ref(createSentiments()); const rootCauseAnalysis = ref(""); const abTestPlan = ref(""); const abRows = ref(createAbRows()); const abChart = ref({ url: "", name: "" }); const iterationRecommendation = ref("");
@ -72,5 +89,7 @@ onMounted(async () => { try { taskConfig.value = (await getTrainingTaskByKey(TAS
</script>
<style scoped lang="scss">
.diagnosis-materials{display:grid;gap:10px;margin:14px 0}.diagnosis-materials__item{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:12px 14px;border:1px solid rgba(76,202,242,.32);border-radius:10px;background:rgba(3,42,61,.45)}.diagnosis-materials__item strong,.diagnosis-materials__item span{display:block}.diagnosis-materials__item strong{color:#e5faff;font-size:15px}.diagnosis-materials__item span{margin-top:4px;color:#93baca;font-size:13px}.diagnosis-materials__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.diagnosis-step-one-notice{margin:12px 0 0;color:#ff7178;font-size:13px;line-height:1.6}@media (max-width:860px){.diagnosis-materials__item{align-items:flex-start;flex-direction:column}.diagnosis-materials__actions{justify-content:flex-start}}
.ab-materials{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:18px;padding:14px 16px;border-left:3px solid #19c9ff;background:rgba(3,42,61,.45);color:#cde9f2}.ab-materials ol{margin:0;padding-left:20px;line-height:1.8}.ab-materials__actions{display:grid;flex:0 0 178px;gap:8px}@media (max-width:860px){.ab-materials{align-items:stretch;flex-direction:column}.ab-materials__actions{grid-template-columns:repeat(2,minmax(0,1fr));flex-basis:auto}}
.ab-table{width:100%;min-width:860px;border-collapse:collapse}.ab-table th,.ab-table td{border:1px solid rgba(139,213,241,.45)}.ab-table thead th{padding:14px 10px;color:#fff;background:#079ed9;font-size:16px}.ab-table tbody th{width:25%;padding:14px 10px;color:#dff8ff;background:rgba(6,67,94,.34);text-align:center}.ab-table td{padding:9px 11px;text-align:center}.ab-table td:nth-child(2),.ab-table td:nth-child(3){width:18%}.ab-table td:last-child{width:39%}
</style>

@ -110,6 +110,20 @@
<div class="pareto-actions"><button type="button" class="outline-action" :disabled="uploadingPareto" @click="paretoInput?.click()"><el-icon><UploadFilled /></el-icon></button><button type="button" class="outline-action danger-action" @click="removeParetoImage"><el-icon><Delete /></el-icon></button></div>
</template>
</div>
<section class="pareto-materials" aria-label="">
<div>
<h3>参考方法及工具如下</h3>
<ol>
<li>下载案例数据表使用 Excel 计算各品类占比累计占比并进行 ABC 分层判定绘制帕累托图</li>
<li>下载 Excel 模板填写关键数据完成帕累托图绘制</li>
<li>也可导出数据表并使用通用 AI 工具辅助完成计算与可视化</li>
</ol>
</div>
<div class="pareto-materials__actions">
<button type="button" class="action-button" @click="downloadParetoGuide"></button>
<button type="button" class="action-button action-button--primary" @click="downloadParetoTemplate"> Excel </button>
</div>
</section>
<div class="rule-notice">
<strong>上传校验说明</strong>
<p>帕累托图必须根据步骤二已完成的ABC分层数据绘制未上传图片时不能进入下一步或提交任务</p>
@ -181,6 +195,8 @@ import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
const TASK_KEY = "category-optimization";
const PARETO_GUIDE_URL = "/file/training-materials/category-optimization/20.实训操作说明.docx";
const PARETO_TEMPLATE_URL = "/file/training-materials/category-optimization/20.帕累托绘制自动化模版.xlsx";
const MAX_ROWS = 30;
const taskConfig = ref(null);
const activeStep = ref(1);
@ -291,6 +307,16 @@ async function handleParetoUpload(event) {
} catch (error) { ElMessage.error(error?.message || "帕累托图上传失败,请重试"); } finally { uploadingPareto.value = false; }
}
function removeParetoImage() { if (paretoImage.value.url.startsWith("blob:")) URL.revokeObjectURL(paretoImage.value.url); paretoImage.value = { url: "", name: "" }; }
function downloadParetoGuide() { downloadBuiltInFile(PARETO_GUIDE_URL, "20.实训操作说明.docx"); }
function downloadParetoTemplate() { downloadBuiltInFile(PARETO_TEMPLATE_URL, "20.帕累托绘制自动化模版.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 handleProductChange() { if (stepFour.value.recommendedClassification === selectedProduct.value?.abcClassification) stepFour.value.recommendedClassification = ""; }
async function validateStepFour() {
try {
@ -359,6 +385,7 @@ onMounted(async () => {
.category-step{padding:24px}.instruction{margin:0 0 20px;color:#d8edf5;font-size:15px;line-height:1.75}.table-toolbar{display:flex;justify-content:space-between;gap:14px;margin-bottom:12px;color:var(--muted);font-size:13px}.action-button--muted{border-color:rgba(158,192,207,.35);background:rgba(75,101,112,.25)}.category-table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px}.category-table{width:100%;min-width:1260px;border-collapse:collapse}.category-table th,.category-table td{padding:10px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);text-align:center}.category-table th{color:#e6f9ff;background:rgba(23,112,150,.42);font-size:13px;white-space:nowrap}.category-table td{background:rgba(2,18,28,.32)}.category-table tr:last-child td{border-bottom:0}.category-table th:last-child,.category-table td:last-child{border-right:0}.empty-cell{height:88px;color:var(--muted)}.abc-table{min-width:900px}.abc-table :deep(.el-select){width:100%;min-width:180px}
.rule-notice{margin-top:20px;padding:16px 18px;border:1px solid rgba(255,112,112,.46);border-radius:12px;background:rgba(126,31,35,.15);color:#ffd5d5}.rule-notice strong{color:#ff9292}.rule-notice p{margin:8px 0 0;line-height:1.65}.analysis-step{padding:24px}.analysis-tabs{display:flex;gap:10px;margin-bottom:18px}.analysis-tabs button{padding:9px 14px;border:1px solid var(--line);border-radius:8px;color:var(--muted);background:transparent;font:inherit;cursor:pointer}.analysis-tabs button.active{color:white;border-color:#71dfff;background:rgba(12,114,159,.48)}.business-workspace{overflow:hidden;border:1px solid var(--line);border-radius:14px}.step-navigation{justify-content:center;padding:20px 24px 0}.nav-button--save,.nav-button--primary{border-color:#71dfff;background:linear-gradient(100deg,#0d80b7,#09638f)}.training-actions{justify-content:center;gap:28px;padding:26px 24px}.training-actions :deep(.training-action){min-width:160px;height:50px;border-color:#2c83aa;color:#dff7ff;background:rgba(13,64,88,.62);font-size:17px;font-weight:800}
.pareto-upload-panel{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;min-height:330px;padding:26px;border:1px dashed rgba(113,223,255,.7);border-radius:16px;background:linear-gradient(135deg,rgba(5,47,69,.7),rgba(4,23,35,.58));text-align:center}.file-input{display:none}.upload-icon{font-size:42px;color:#71dfff}.pareto-upload-panel>strong{font-size:20px}.pareto-upload-panel>span,.upload-file-meta span{color:var(--muted);font-size:13px}.pareto-preview{width:min(100%,760px);height:270px;border:1px solid var(--line);border-radius:10px;background:rgba(0,10,18,.65);cursor:zoom-in}.upload-file-meta{display:grid;gap:5px;max-width:100%;word-break:break-all}.pareto-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:10px}.danger-action{border-color:rgba(255,145,145,.6);color:#ffd7d7;background:rgba(130,43,43,.23)}
.pareto-materials{display:flex;align-items:center;justify-content:space-between;gap:22px;margin-top:20px;padding:18px 20px;border-left:3px solid var(--accent);border-radius:0 14px 14px 0;background:rgba(11,79,109,.22);color:#d8edf5}.pareto-materials h3{margin:0 0 8px;font-size:16px}.pareto-materials ol{margin:0;padding-left:20px;line-height:1.8}.pareto-materials__actions{display:grid;flex:0 0 178px;gap:10px}.action-button--primary{border-color:#71dfff;background:linear-gradient(100deg,#0d80b7,#09638f)}
.strategy-step{display:grid;gap:16px}.subsection-heading{margin:8px 0 0;font-size:18px}.subsection-copy{margin:-8px 0 0;color:var(--muted);line-height:1.6}.category-matrix{position:relative;width:min(100%,820px);height:430px;margin:12px auto 22px;padding:38px 52px 52px 78px}.matrix-grid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:100%;height:100%;border-left:2px solid rgba(185,240,255,.82);border-bottom:2px solid rgba(185,240,255,.82);background:linear-gradient(135deg,rgba(24,169,209,.12),rgba(3,25,38,.16))}.matrix-cell{display:grid;place-content:center;gap:7px;padding:18px;border-right:1px solid rgba(185,240,255,.65);border-bottom:1px solid rgba(185,240,255,.65);text-align:center}.matrix-cell:nth-child(2n){border-right:0}.matrix-cell:nth-child(n+3){border-bottom:0}.matrix-cell strong{font-size:18px}.matrix-cell small{color:#8dddf5}.matrix-y-title,.matrix-x-title,.matrix-y-high,.matrix-y-low,.matrix-x-high,.matrix-x-low{position:absolute;color:#d9f8ff;font-weight:800}.matrix-y-title{top:5px;left:78px}.matrix-x-title{right:0;bottom:15px}.matrix-y-high{top:63px;left:39px}.matrix-y-low{bottom:61px;left:39px}.matrix-x-low{bottom:29px;left:79px}.matrix-x-high{right:44px;bottom:29px}.matrix-inputs{display:grid;grid-template-columns:minmax(210px,.42fr) 1fr;gap:16px}.matrix-inputs label{display:grid;gap:8px;color:#d9f8ff;font-weight:700}.strategy-table{min-width:1120px}.strategy-table td{vertical-align:top}.strategy-table :deep(.el-select){width:100%;min-width:160px}.strategy-table :deep(.el-textarea__inner){min-height:78px}.current-classification{display:inline-block;padding:8px 10px;border-radius:8px;color:#a5efff;background:rgba(17,126,178,.24);white-space:nowrap}
.operations-table{min-width:1050px}.operations-table th{color:#fff;background:linear-gradient(100deg,#08a6df,#098bc1);font-size:16px}.operations-table td{vertical-align:middle}.operations-table td:first-child{width:17%;font-weight:800}.operations-table td:nth-child(2){width:34%}.operations-table td:nth-child(3){width:25%}.operations-table td:last-child{width:24%}.operations-table :deep(.el-textarea__inner){min-height:86px;text-align:left}.operations-table :deep(.el-select){width:100%;min-width:160px}.layer-cell{color:#dff8ff;font-size:16px;background:rgba(15,103,137,.18)!important}
:deep(.el-input__wrapper){background:rgba(1,15,25,.76);box-shadow:0 0 0 1px rgba(94,213,245,.28) inset}:deep(.el-input__inner){color:#fff;text-align:center}@media(max-width:1100px){.optimization{grid-template-columns:1fr}}@media(max-width:720px){.optimization{padding:12px}.training-core{padding:16px;border-radius:20px}.workbench-head,.table-toolbar{flex-direction:column;align-items:stretch}.workbench-actions,.table-toolbar>div,.step-navigation,.training-actions{justify-content:stretch}.workbench-actions>*,.action-button,.nav-button,.training-actions :deep(.training-action){flex:1;width:100%}.category-step,.analysis-step{padding:16px}}

@ -154,6 +154,7 @@ import codemirror from "@/components/codemirror/index.vue";
import popModel from "@/views/components/popModal.vue";
const { proxy } = getCurrentInstance();
const TASK_KEY = "purchase-demand-forecast";
const PURCHASE_DEMAND_FORECAST_GUIDE_URL = "/file/training-materials/purchase-demand-forecast/18.实训操作说明.docx";
const taskConfig = ref(null);
const activeStep = ref(1);
const saving = ref(false);
@ -537,14 +538,15 @@ const drawForecastChart = () => {
window.addEventListener("resize", () => myChart.resize());
};
function downloadPracticeGuide() {
const content = "采购需求预测实训操作说明\n\n1. 根据案例给出的 12 个月销量数据,使用 FORECAST.LINEARWPS 可使用 FORECAST函数预测 t=13、t=14、t=15。\n2. 将每个月的预测结果取整后,填写到预测表中。\n3. 将已知 12 个月销量和未来 3 个月预测值制作成散点图,并添加趋势线。\n4. 导出图形后上传至第 2 步,确认图中能清晰呈现历史销量、预测值和整体走势。";
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
downloadBuiltInFile(PURCHASE_DEMAND_FORECAST_GUIDE_URL, "18.实训操作说明.docx");
}
function downloadBuiltInFile(url, fileName) {
const link = document.createElement("a");
link.href = url;
link.download = "采购需求预测-实训操作说明.txt";
link.download = fileName;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
}
function clearDemandForecastChart() {
const imageUrl = String(demandForecastChart.value?.url || "");

@ -163,6 +163,19 @@
已编辑 {{ form.questionnaireQuestions.length }} {{ form.questionnaireImport?.name ? "已导入 1 份 Word 问卷,可随时预览。" : "也可导入已完成的 .docx 问卷。" }}
</p>
<div class="questionnaire-template-guide">
<div>
<h5>推荐工具及方法如下自行选择</h5>
<p>1. 使用本系统问卷编辑器自行编辑问卷</p>
<p>2. 下载参考问卷 Word 模板在本地修改后上传导入</p>
<p>3. 也可使用 AI 辅助生成问卷应包含基本信息购买行为需求偏好与开放建议</p>
</div>
<el-button class="questionnaire-template-action" @click="downloadQuestionnaireTemplate">
<el-icon><Download /></el-icon>
下载模板
</el-button>
</div>
<el-dialog v-model="questionnaireEditorVisible" title="消费者需求调研问卷编辑器" width="min(1120px, 94vw)" class="questionnaire-editor-dialog" append-to-body>
<div class="questionnaire-editor-dialog__intro">
<p>请覆盖基本信息购买行为需求偏好和开放建议四部分需求偏好至少设置一题 Likert 五级量表</p>
@ -279,6 +292,7 @@ import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "consumer-behavior";
const QUESTIONNAIRE_TEMPLATE_URL = "/file/training-materials/consumer-behavior/9.问卷调查报告模板.docx";
const KANO_NEED_LIMIT = 20;
let questionnaireQuestionSequence = 0;
const { proxy } = getCurrentInstance();
@ -882,16 +896,21 @@ function surveyReportContentPlaceholder(index) {
return ["说明本次问卷调研的背景、目标用户与调研目的。", "说明样本数量、调研时间、调研渠道与数据来源。", "结合问卷数据,分析影响用户购买决策的核心因素。", "比较不同人群在需求、价格、渠道或购买场景上的差异。", "总结调研结论,并提出产品功能、定价或营销建议。"][index] || "请输入报告内容";
}
function downloadSurveyReportTemplate() {
const rows = Array.from({ length: 5 }, (_, index) => `<tr><td>${surveyReportTitlePlaceholder(index)}</td><td>${surveyReportContentPlaceholder(index)}</td></tr>`).join("");
const html = `<!doctype html><html><head><meta charset="utf-8"><title>消费者需求调研报告模板</title><style>body{font-family:SimSun,serif;padding:32px;color:#222}h1{font-size:22px;text-align:center}p{line-height:1.75}table{width:100%;border-collapse:collapse;margin-top:24px}th,td{border:1px solid #777;padding:14px;vertical-align:top}th{background:#14a3df;color:#fff}td:first-child{width:22%}</style></head><body><h1>消费者需求调研报告</h1><p>请将步骤三设计的调查问卷导入在线问卷调查平台开展调研,并基于收集到的数据完成本报告。</p><table><thead><tr><th>报告模块标题</th><th>内容</th></tr></thead><tbody>${rows}</tbody></table></body></html>`;
const blob = new Blob([html], { type: "application/msword;charset=utf-8" });
const url = URL.createObjectURL(blob);
function downloadBuiltInFile(url, fileName) {
const link = document.createElement("a");
link.href = url;
link.download = "消费者需求调研报告模板.doc";
link.download = fileName;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
}
function downloadQuestionnaireTemplate() {
downloadBuiltInFile(QUESTIONNAIRE_TEMPLATE_URL, "9.问卷调查报告模板.docx");
}
function downloadSurveyReportTemplate() {
downloadBuiltInFile(QUESTIONNAIRE_TEMPLATE_URL, "9.问卷调查报告模板.docx");
}
</script>
@ -1572,6 +1591,41 @@ function downloadSurveyReportTemplate() {
font-size: 13px;
}
.questionnaire-template-guide {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-top: 22px;
padding: 18px 20px;
border-left: 3px solid #25cfff;
border-radius: 0 14px 14px 0;
color: #c7e7f3;
background: rgba(15, 104, 145, 0.18);
h5,
p {
margin: 0 0 5px;
line-height: 1.72;
}
h5 {
color: #ffffff;
font-size: 15px;
}
}
.questionnaire-template-action {
flex: 0 0 auto;
min-width: 132px;
height: 42px;
border: 1px solid rgba(93, 218, 255, 0.68);
border-radius: 10px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-weight: 800;
}
:deep(.questionnaire-editor-dialog .el-dialog),
:deep(.questionnaire-preview-dialog .el-dialog) {
overflow: hidden;
@ -2210,13 +2264,15 @@ function downloadSurveyReportTemplate() {
.questionnaire-design-head,
.questionnaire-editor-launcher,
.questionnaire-template-guide,
.questionnaire-editor-dialog__intro {
align-items: stretch;
flex-direction: column;
}
.questionnaire-import-action,
.questionnaire-editor-launcher__action {
.questionnaire-editor-launcher__action,
.questionnaire-template-action {
width: 100%;
}
@ -2309,4 +2365,3 @@ function downloadSurveyReportTemplate() {
border-bottom: 0;
}
</style>

@ -266,6 +266,7 @@ import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsum
import { isStudentDemo } from "@/utils/studentDemo";
const TASK_KEY = "consumer-scenario";
const JOURNEY_TEMPLATE_URL = "/file/training-materials/consumer-scenario/10.用户购买旅程模版.pptx";
const { proxy } = getCurrentInstance();
const defaultRequirement =
@ -818,25 +819,16 @@ async function ensureJourneyOptimizationValid() {
}
function downloadJourneyMapTemplate() {
const escapeCsvValue = (value) => `"${String(value || "").replaceAll('"', '""')}"`;
const header = ["序号", "旅程阶段", "用户行为描述", "关键触点", "情感体验", "关键决策因素", "痛点标记", "机会点标记"];
const rows = form.value.journeyAnalysis.map((row, index) => [
index + 1,
stageTitle(row.stageId),
row.behaviorDescription,
row.keyTouchpoints,
row.emotionExperience,
row.decisionFactors,
"",
"",
]);
const csv = [header, ...rows].map((row) => row.map(escapeCsvValue).join(",")).join("\r\n");
const url = URL.createObjectURL(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }));
downloadBuiltInFile(JOURNEY_TEMPLATE_URL, "10.用户购买旅程模版.pptx");
}
function downloadBuiltInFile(url, fileName) {
const link = document.createElement("a");
link.href = url;
link.download = "用户购买旅程图模板.csv";
link.download = fileName;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
}
function buildOutcome() {
@ -2274,4 +2266,3 @@ function exportOutcome() {
}
</style>

@ -69,6 +69,15 @@
</el-button>
</div>
<p class="upload-tip">仅支持 .xls.xlsx 文件上传后可预览首个工作表的表头和前 20 行数据</p>
<div class="step-one-materials" aria-label="RFM ">
<p>推荐工具及方法如下自行选择</p>
<p>1. 使用 Excel/WPS 完成 RFM 用户分层分析可参考实训操作说明</p>
<p>2. 下载 Excel 模板填写案例数据并完成计算与可视化</p>
<div class="step-one-materials__actions">
<el-button class="preview-action" @click="downloadRfmInstructions"></el-button>
<el-button class="upload-action" @click="downloadRfmTemplate"> Excel </el-button>
</div>
</div>
<el-dialog v-model="previewDialogVisible" title="RFM 用户分层分析结果预览" width="min(960px, 92vw)" append-to-body>
<div class="excel-preview-meta">
@ -334,6 +343,8 @@ import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkTargetUserProfileStepFive, checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "target-user-profile";
const RFM_INSTRUCTIONS_URL = "/file/training-materials/target-user-profile/8.实训操作说明.docx";
const RFM_TEMPLATE_URL = "/file/training-materials/target-user-profile/8.Excel计算与可视化模版.xlsx";
const RFM_CHARTS = [
{ id: "segment-share", index: 1, title: "客户分层占比饼图", buttonText: "上传客户分层占比饼图" },
{ id: "average-spend", index: 2, title: "客户分层平均消费对比柱状图", buttonText: "上传客户分层平均消费对比柱状图" },
@ -700,6 +711,23 @@ function triggerFileSelect() {
fileInputRef.value?.click();
}
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 downloadRfmInstructions() {
downloadBuiltInFile(RFM_INSTRUCTIONS_URL, "8.实训操作说明.docx");
}
function downloadRfmTemplate() {
downloadBuiltInFile(RFM_TEMPLATE_URL, "8.Excel计算与可视化模版.xlsx");
}
function setChartInputRef(chartId, element) {
if (element) {
chartFileInputRefs.value[chartId] = element;
@ -1852,6 +1880,28 @@ function exportOutcome() {
line-height: 1.7;
}
.step-one-materials {
margin-top: 18px;
padding: 16px 18px;
border-left: 3px solid #21c5ff;
border-radius: 0 12px 12px 0;
color: #c8e6f2;
background: rgba(14, 91, 128, 0.18);
font-size: 14px;
line-height: 1.75;
p {
margin: 0 0 4px;
}
}
.step-one-materials__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.excel-preview-meta {
margin-bottom: 12px;
color: #52687a;
@ -2427,4 +2477,3 @@ function exportOutcome() {
border-bottom: 0;
}
</style>

@ -163,7 +163,6 @@
<script setup>
import { Download, Operation, RefreshLeft, Upload } from "@element-plus/icons-vue";
import * as XLSX from "xlsx";
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, checkProductDevelopmentProcessStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
@ -175,6 +174,10 @@ import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const TASK_KEY = "product-development-process";
const GANTT_PRACTICE_GUIDE_URL = "/file/training-materials/product-development-process/3.甘特图制作实训操作说明.docx";
const GANTT_PRACTICE_GUIDE_NAME = "3.甘特图制作实训操作说明.docx";
const GANTT_TEMPLATE_URL = "/file/training-materials/product-development-process/3.项目进度管理表-项目进度甘特图模版.xlsx";
const GANTT_TEMPLATE_NAME = "3.项目进度管理表-项目进度甘特图模版.xlsx";
const CLASSIFICATION_OPTIONS = ["关键任务", "非关键任务"];
const DEFAULT_STEPS = ["产品开发关键任务识别", "任务依赖关系分析", "开发排期制定", "流程复盘优化"];
const PROCESS_TASKS = [
@ -529,42 +532,20 @@ function previewGanttImage() {
}
function downloadPracticeGuide() {
const content = [
"产品开发甘特图实训操作说明",
"1. 使用第 2 步填写的任务编号、开始时间、完成时间和项目依赖关系建立任务表。",
"2. 在 Excel 或 WPS 中按日期轴创建堆积条形图;开始时间作为不可见基线,任务工期作为可见条形。",
"3. 为关键任务使用醒目颜色,并标注任务名称、开始及完成时间。",
"4. 导出或截图为 PNG/JPG 后上传至本步骤。",
].join("\n");
downloadBlob(new Blob([content], { type: "text/plain;charset=utf-8" }), "产品开发甘特图-实训操作说明.txt");
downloadBuiltInFile(GANTT_PRACTICE_GUIDE_URL, GANTT_PRACTICE_GUIDE_NAME);
}
function downloadGanttTemplate() {
const rows = processRows.value.map((row) => ({
阶段: row.stage,
任务编号: row.taskNo,
具体工作任务: row.task,
是否关键任务: row.classification,
开始时间: row.startDate,
完成时间: row.endDate,
项目依赖关系: row.dependency,
}));
const sheet = XLSX.utils.json_to_sheet(rows);
sheet["!cols"] = [{ wch: 16 }, { wch: 12 }, { wch: 32 }, { wch: 16 }, { wch: 14 }, { wch: 14 }, { wch: 24 }];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, sheet, "甘特图任务表");
XLSX.writeFile(workbook, "产品开发甘特图绘制模板.xlsx");
downloadBuiltInFile(GANTT_TEMPLATE_URL, GANTT_TEMPLATE_NAME);
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
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);
URL.revokeObjectURL(url);
}
function exportOutcome() {

@ -263,6 +263,7 @@ import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
const TASK_KEY = "product-value-positioning";
const VALUE_PROPOSITION_CANVAS_TEMPLATE_URL = "/file/training-materials/product-value-positioning/12.产品价值主张画布模版.pptx";
const { proxy } = getCurrentInstance();
const saving = ref(false);
const checkingCorePain = ref(false);
@ -608,7 +609,16 @@ function downloadText(content, fileName) {
function exportCanvasSection(name, content) { downloadText(content, `${taskTitle.value}-${name}.md`); }
function downloadCanvasTemplate() {
downloadText("# 产品价值主张画布\n\n## 用户档案\n- 用户目标Jobs\n- 痛点Pains\n- 收益期望Gains\n\n## 产品价值地图\n- 产品与服务:\n- 痛点缓释器Pain Relievers\n- 增益创造器Gain Creators\n\n## 匹配Fit说明\n", "产品价值主张画布模板.md");
downloadBuiltInFile(VALUE_PROPOSITION_CANVAS_TEMPLATE_URL, "12.产品价值主张画布模版.pptx");
}
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 openValueCanvasPicker() { valueCanvasInput.value?.click(); }

@ -319,6 +319,8 @@ import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrain
import { isStudentDemo } from "@/utils/studentDemo";
const TASK_KEY = "hardware-cost";
const HARDWARE_COST_GUIDE_URL = "/file/training-materials/hardware-cost/16.饼图制作操作说明.docx";
const HARDWARE_COST_TEMPLATE_URL = "/file/training-materials/hardware-cost/16.成本占比结构图绘制模版.xlsx";
const MIN_ROWS = 10;
const MAX_ROWS = 40;
const { proxy } = getCurrentInstance();
@ -704,33 +706,18 @@ function downloadTextFile(name, content) {
URL.revokeObjectURL(url);
}
function downloadCostStructureGuide() {
downloadTextFile("智能硬件成本结构图-实训操作指导.txt", [
"智能硬件成本结构图绘制指导",
"1. 分别汇总第 2 步物料 BOM 成本与第 3 步制造成本。",
"2. 小批量图和大批量图分别使用饼图或环形图,清晰标注 BOM、制造成本及主要费用占比。",
"3. 结合占比最高的费用项提出降档、替换、减量或集成方案,并说明预期效果和风险控制措施。",
].join("\n"));
}
async function downloadCostStructureTemplate() {
try {
const XLSX = await import("xlsx");
const data = [
["智能硬件成本结构模板"],
[],
["成本类别", "小批量试产(元/台)", "大批量量产(元/台)"],
["物料 BOM 成本", smallBatchTotal.value, largeBatchTotal.value],
["制造成本", smallBatchManufacturingCost.value, largeBatchManufacturingCost.value],
["单件总成本", smallBatchUnitTotalCost.value, largeBatchUnitTotalCost.value],
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
worksheet["!cols"] = [{ wch: 24 }, { wch: 28 }, { wch: 28 }];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "成本结构");
XLSX.writeFile(workbook, "智能硬件成本结构模板.xlsx");
notifySuccess("Excel 模板已下载");
} catch (error) {
notifyError(error?.message || "模板下载失败,请稍后重试");
downloadBuiltInFile(HARDWARE_COST_GUIDE_URL, "16.饼图制作操作说明.docx");
}
function downloadBuiltInFile(url, fileName) {
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
function downloadCostStructureTemplate() {
downloadBuiltInFile(HARDWARE_COST_TEMPLATE_URL, "16.成本占比结构图绘制模版.xlsx");
}
async function loadAnswer() {
const response = await getStudentTrainingAnswer(TASK_KEY);

@ -77,6 +77,8 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const TASK_KEY = "market-opportunity-selection";
const SCORE_CALCULATION_GUIDE_URL = "/file/training-materials/market-opportunity-selection/6.综合得分计算操作说明.docx";
const SCORE_CALCULATION_TEMPLATE_URL = "/file/training-materials/market-opportunity-selection/6.数据表-抖音电商 - 计算模版.xlsx";
const DEFAULT_STEPS = ["指标分类", "指标权重设置", "综合得分计算", "数据可视化", "用户需求洞察", "选品结论"];
const categories = ["市场规模", "市场潜力", "竞争强度", "运营难度"];
const indicators = [
@ -135,8 +137,10 @@ async function saveAndGoToStepFive() { if (saving.value || !stepThreePassed.valu
async function saveAndGoToStepSix() { if (saving.value || !stepFourPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepFive({ insights: products.map((product) => ({ productId: product.id, ...reviewInsights.value[product.id] })) }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: false, saveAction: "SAVE" }); stepFivePassed.value = true; currentStep.value = 6; proxy?.$modal?.msgSuccess("用户需求洞察已保存,已进入第 6 步"); } 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 downloadInstructions() { downloadFile("市场机会识别与选品 - 实训操作说明\n\n1. 汇总三类产品的原始数据。\n2. 对正向指标计算 (x-min)/(max-min),对逆向指标计算 (max-x)/(max-min)。\n3. 将归一化得分乘以第 2 步的对应总权重后求和。\n4. 如案例资料提供三段式时间权重,分别计算第 120 天、第 2140 天和第 4160 天的加权得分。\n5. 将产品品类综合得分填写回第 3 步表格,保留两位小数。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }
function downloadInstructions() { downloadBuiltInFile(SCORE_CALCULATION_GUIDE_URL, "6.综合得分计算操作说明.docx"); }
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() { 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 downloadVisualizationInstructions() { downloadFile("市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜分别填写每日综合得分。\n2. 使用自动化模板计算各品类的日平均得分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传,并点击缩略图预览确认。", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }
function downloadVisualizationTemplate() { const firstDataRow = 5; const dailyRows = products.flatMap((product) => Array.from({ length: 60 }, (_, dayIndex) => [`${dayIndex + 1}`, product.name, ""])); const lastDataRow = firstDataRow + dailyRows.length - 1; const dailyWorksheet = XLSX.utils.aoa_to_sheet([["市场机会识别与选品 · 数据可视化自动化模板"], ["请填入各产品每日综合得分,汇总结果会自动计算日平均得分、最高分和最低分。"], [], ["日期", "产品品类", "日综合得分"], ...dailyRows]); dailyWorksheet["!cols"] = [{ wch: 14 }, { wch: 20 }, { wch: 16 }]; const summaryWorksheet = XLSX.utils.aoa_to_sheet([["产品品类", "日平均得分", "最高分", "最低分"], ...products.map((product) => [product.name, "", "", ""])]); products.forEach((product, index) => { const row = index + 2; const scoreRange = `'日综合得分录入'!$C$${firstDataRow}:$C$${lastDataRow}`; const productRange = `'日综合得分录入'!$B$${firstDataRow}:$B$${lastDataRow}`; summaryWorksheet[`B${row}`] = { t: "n", f: `AVERAGEIF(${productRange},A${row},${scoreRange})` }; summaryWorksheet[`C${row}`] = { t: "n", f: `MAXIFS(${scoreRange},${productRange},A${row},${scoreRange},"<>")` }; summaryWorksheet[`D${row}`] = { t: "n", f: `MINIFS(${scoreRange},${productRange},A${row},${scoreRange},"<>")` }; }); summaryWorksheet["!cols"] = [{ wch: 20 }, { wch: 16 }, { wch: 14 }, { wch: 14 }]; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, dailyWorksheet, "日综合得分录入"); XLSX.utils.book_append_sheet(workbook, summaryWorksheet, "汇总结果"); XLSX.writeFile(workbook, "市场机会识别与选品-数据可视化模板.xlsx"); }
function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), step5: buildStepFiveOutcome(), step6: buildStepSixOutcome(), currentStep: currentStep.value }, null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${taskTitle.value}-实训结果.json`; link.click(); URL.revokeObjectURL(url); }

@ -163,6 +163,7 @@ import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const { proxy } = getCurrentInstance();
const TASK_KEY = "product-structure";
const PRODUCT_ARCHITECTURE_TEMPLATE_URL = "/file/training-materials/product-architecture/13.产品架构图绘制模版.pptx";
const DEFAULT_STEPS = ["智能硬件分层架构", "产品运行操作链路", "产品场景架构分析", "架构评审与优化"];
const LAYERS = [
{ id: "hardware-physical", name: "硬件物理层", description: "产品的物理基础包含电源模块、主控芯片、传感器、LED灯珠等实体元器件。" },
@ -352,7 +353,17 @@ async function checkAndSaveStepFour() {
if (stepFourPassed.value) proxy?.$modal?.msgSuccess("产品架构设计实训已完成"); else proxy?.$modal?.msgWarning("请上传完整的三张核心功能架构图");
} catch (error) { proxy?.$modal?.msgError(error?.message || "校验失败,请稍后重试"); } finally { checking.value = false; }
}
function downloadArchitectureTemplate() { const content = ["核心功能架构图模板", "请为每个核心功能分别绘制一张图:", "1. 应用功能层:功能入口与触发条件", "2. 算法处理层:识别、分析、决策逻辑", "3. 驱动感知层:信号采集、转换与输出驱动", "4. 硬件物理层:传感器、芯片、执行器与最终效果", "使用箭头标记应用层向下至硬件层的调用和信号流向。"].join("\n"); const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "核心功能架构图-绘制模板.txt"; link.click(); URL.revokeObjectURL(url); }
function downloadArchitectureTemplate() {
downloadBuiltInFile(PRODUCT_ARCHITECTURE_TEMPLATE_URL, "13.产品架构图绘制模版.pptx");
}
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);
}
async function goNextStep() {
if ((currentStep.value === 1 && !stepOnePassed.value) || (currentStep.value === 2 && !stepTwoPassed.value) || (currentStep.value === 3 && !stepThreePassed.value)) return;
currentStep.value = Math.min(currentStep.value + 1, trainingSteps.value.length); await persist("SAVE");

@ -125,6 +125,11 @@
</table>
</div>
<p class="hint">渠道名称同步第二步权重同步第一步每项得分乘以对应权重按百分比折算并相加得到加权得分下一步会按已保存的前两步数据重新计算并校验</p>
<p class="step-three-notice">说明渠道来自第二步权重来自第一步每个维度得分乘以权重后相加即为加权得分系统会校验加权得分填写不正确不能进入下一步</p>
<section class="step-three-materials" aria-label="">
<p>参考方法及工具如下下载案例数据表使用 Excel Min-Max 公式计算各渠道归一化得分可参考实训操作说明完成</p>
<el-button type="primary" class="step-three-materials__button" @click="downloadPracticeGuide"><el-icon><Download /></el-icon></el-button>
</section>
<footer class="footer-actions">
<el-button @click="activeStep = 2">上一步</el-button>
<el-button :loading="saving" @click="saveScoreStep"></el-button>
@ -174,6 +179,7 @@ import { getStudentTrainingAnswer, saveStudentTrainingAnswer, validateReleaseCha
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const TASK_KEY = "release-channel-evaluation";
const RELEASE_CHANNEL_GUIDE_URL = "/file/training-materials/release-channel-evaluation/21.实训操作说明.docx";
const activeStep = ref(1);
const taskConfig = ref(null);
const saving = ref(false);
@ -226,6 +232,17 @@ function syncRankingRows(savedRows = []) { const savedByChannel = new Map(savedR
function openStepFour() { syncRankingRows(rankingRows.value); activeStep.value = 4; }
async function saveRankingStep() { if (saving.value) return; saving.value = true; try { await saveAnswer(4, { step4Answer: JSON.stringify(rankingPayload()) }); ElMessage.success("当前进度已保存"); } finally { saving.value = false; } }
async function validateRankingStep() { if (validating.value) return; validating.value = true; try { await validateReleaseChannelEvaluationStepFour(rankingPayload()); await saveStudentTrainingAnswer(TASK_KEY, { saveAction: "SUBMIT", currentStep: 4, step4Answer: JSON.stringify(rankingPayload()) }); ElMessage.success("渠道排名校验通过,任务已提交"); } catch (error) { ElMessage.error(error?.message || "请检查渠道排名和核心渠道运营建议"); } finally { validating.value = false; } }
function downloadPracticeGuide() {
downloadBuiltInFile(RELEASE_CHANNEL_GUIDE_URL, "21.实训操作说明.docx");
}
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 exportChannelRows() { const headers = ["渠道", "日均总搜索量", "供需比", "平均点击率(%", "平均CPC", "平均转化率(%", "现有相关访客数", "匹配度1-5"]; const escape = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`; const lines = [headers, ...channelRows.value.map((row) => [row.channel, row.dailySearchVolume, row.supplyDemandRatio, row.averageClickRate, row.averageCpc, row.averageConversionRate, row.existingVisitorCount, row.matchingScore])].map((row) => row.map(escape).join(",")); const url = URL.createObjectURL(new Blob([`\uFEFF${lines.join("\n")}`], { type: "text/csv;charset=utf-8" })); const link = document.createElement("a"); link.href = url; link.download = "产品发布渠道评估-渠道数据.csv"; link.click(); URL.revokeObjectURL(url); }
function restoreRows(raw, target, defaults, fields) { try { const saved = typeof raw === "string" ? JSON.parse(raw || "{}") : raw; if (!Array.isArray(saved?.rows)) return; target.value = saved.rows.map((row) => { const value = Object.fromEntries(fields.map((field) => [field, row?.[field]])); return makeRow(target === weightRows ? { ...value, suggestion: weightSuggestions[value.dimension] || "" } : value); }); } catch (error) { target.value = defaults(); } }
async function loadProgress() { try { const result = await getStudentTrainingAnswer(TASK_KEY); const answer = result?.data; restoreRows(answer?.step1Answer, weightRows, defaultWeightRows, ["dimension", "description", "weight"]); restoreRows(answer?.step2Answer, channelRows, defaultChannelRows, ["channel", "dailySearchVolume", "supplyDemandRatio", "averageClickRate", "averageCpc", "averageConversionRate", "existingVisitorCount", "matchingScore"]); let savedScores = []; try { const saved = typeof answer?.step3Answer === "string" ? JSON.parse(answer.step3Answer || "{}") : answer?.step3Answer; savedScores = Array.isArray(saved?.rows) ? saved.rows : []; } catch (error) { savedScores = []; } syncScoreRows(savedScores); let savedRanking = []; try { const saved = typeof answer?.step4Answer === "string" ? JSON.parse(answer.step4Answer || "{}") : answer?.step4Answer; savedRanking = Array.isArray(saved?.rows) ? saved.rows : []; coreChannelAnalysis.value = String(saved?.coreChannelAnalysis || ""); } catch (error) { savedRanking = []; coreChannelAnalysis.value = ""; } syncRankingRows(savedRanking); activeStep.value = Math.min(Math.max(Number(answer?.currentStep) || 1, 1), 4); } catch (error) { syncScoreRows(); syncRankingRows(); } }
@ -234,6 +251,7 @@ onMounted(async () => { try { const result = await getTrainingTaskByKey(TASK_KEY
<style scoped lang="scss">
.release-channel-page{display:grid;grid-template-columns:minmax(720px,1fr) 340px;gap:24px;min-height:calc(100vh - 76px);padding:24px;color:#eef5ff;background:#06111d}.training-core{min-width:0;padding:28px;border:1px solid rgba(0,180,255,.25);border-radius:30px;background:rgba(8,18,28,.6)}.task-header{margin-bottom:24px}.task-badge{display:inline-flex;gap:8px;align-items:center;padding:5px 14px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#dff5ff;background:rgba(17,126,178,.36);font-size:12px;font-weight:700}.task-header h1{margin:12px 0 0;color:#fff;font-size:30px}.task-card{margin-top:28px;padding:24px;border:1px solid rgba(82,174,226,.42);border-radius:24px;background:rgba(0,25,42,.64)}.workbench-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding-bottom:18px;border-bottom:1px solid rgba(0,180,255,.2)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800}.workbench-head h2{margin:4px 0 0;color:#fff;font-size:24px}.step-tabs{display:flex;gap:10px;margin:20px 0}.step-tabs button{min-height:36px;padding:0 15px;border:1px solid rgba(99,183,222,.42);border-radius:8px;color:#a8cde0;background:rgba(3,27,43,.56);font:inherit;font-size:13px;font-weight:800;cursor:pointer}.step-tabs button.active{border-color:#48d5ff;color:#fff;background:rgba(9,125,177,.7)}.lead,.scoring-guide{margin:22px 0;color:#e2f3ff;font-size:16px;line-height:1.9}.scoring-guide{padding:16px 18px;border-left:3px solid #48d5ff;border-radius:0 12px 12px 0;background:rgba(16,85,121,.2)}.table-actions{display:flex;justify-content:flex-end;gap:12px;margin:0 0 16px}.table-actions :deep(.el-button){min-width:110px;height:42px;font-weight:800}.table-wrap{overflow-x:auto;border:1px solid rgba(99,183,222,.46);border-radius:14px}.table-wrap table{width:100%;border-collapse:collapse}.weight-table{min-width:900px}.channel-table{min-width:1320px}.scoring-table{min-width:1780px}.ranking-table{min-width:880px}.table-wrap th,.table-wrap td{padding:12px;border-right:1px solid rgba(99,183,222,.35);border-bottom:1px solid rgba(99,183,222,.35);vertical-align:middle}.table-wrap th{white-space:nowrap;color:#effaff;background:#0c9dd6;font-size:14px}.table-wrap td{background:rgba(2,21,34,.58)}.weight-table tbody td:nth-child(1){width:22%}.weight-table tbody td:nth-child(2){width:43%}.weight-table tbody td:nth-child(3){width:23%}.channel-table td{min-width:134px}.channel-table td:first-child{min-width:110px}.scoring-table th:first-child{min-width:108px}.scoring-table td{min-width:108px}.scoring-table thead tr:nth-child(2) th{background:rgba(7,116,161,.94)}.ranking-table td{min-width:170px}.ranking-table td:first-child{min-width:100px}.rank-cell,.core-cell{text-align:center;font-weight:900}.core-cell .core{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:26px;border:1px solid rgba(99,235,204,.8);border-radius:999px;color:#062f30;background:#68edce}.weight-cell{color:#a8d9eb;text-align:center;font-weight:800}.analysis-field{display:block;margin-top:28px}.analysis-field>span{display:block;margin-bottom:10px;color:#dff5ff;font-size:15px;font-weight:800}.analysis-field :deep(.el-textarea__inner){min-height:180px!important;border:1px solid rgba(111,190,225,.3);border-radius:12px;box-shadow:none;color:#fff;background:#102233;line-height:1.7}.table-wrap :deep(.el-input__wrapper),.table-wrap :deep(.el-input-number){width:100%;background:#102233}.table-wrap :deep(.el-input__wrapper){box-shadow:0 0 0 1px rgba(111,190,225,.3) inset}.table-wrap :deep(.el-input__inner){color:#fff}.table-wrap :deep(.el-input-number .el-input__wrapper){background:#102233}.table-wrap small{display:block;margin-top:7px;color:#8fa8b8;font-size:12px}.operation-column{width:100px;text-align:center}.empty-row{text-align:center;color:#99b5c7}.table-wrap tfoot th,.table-wrap tfoot td{border-bottom:0;background:rgba(7,49,72,.74)}.total-error{color:#ff8c8c;font-weight:900}.hint{margin:18px 0 0;color:#95b2c3;line-height:1.8}.footer-actions{display:flex;justify-content:center;gap:16px;margin-top:30px}.footer-actions :deep(.el-button){min-width:156px;height:44px;font-weight:800}.footer-actions :deep(.el-button .el-icon){margin-left:5px}@media (max-width:1100px){.release-channel-page{grid-template-columns:1fr}}@media (max-width:720px){.release-channel-page{padding:14px}.training-core{padding:18px}.task-card{padding:18px}.workbench-head{flex-direction:column}.table-actions,.footer-actions{align-items:stretch;flex-direction:column}.table-actions :deep(.el-button),.footer-actions :deep(.el-button){width:100%}}
.step-three-notice{margin:20px 0 0;padding:12px 16px;border:1px solid rgba(255,112,112,.52);border-radius:10px;color:#ffb3b3;background:rgba(126,31,35,.16);line-height:1.7}.step-three-materials{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:18px;padding:16px 18px;border-left:3px solid #48d5ff;border-radius:0 12px 12px 0;background:rgba(16,85,121,.2);color:#d7edf8;line-height:1.7}.step-three-materials p{margin:0}.step-three-materials__button{flex:0 0 auto;font-weight:800}
/* 与标准实训页保持一致的步骤条:编号、步骤状态和箭头连接均可清晰识别。 */
.step-tabs{display:flex;align-items:center;gap:8px;margin:20px 0 28px;padding:18px 22px;overflow-x:auto;border:1px solid rgba(0,180,255,.3);border-radius:22px;background:rgba(1,18,31,.62)}.step-tabs .step-tab{display:flex;flex:1 1 0;align-items:center;min-width:145px;min-height:66px;gap:10px;padding:8px 12px;border:1px solid rgba(68,141,177,.35);border-radius:12px;color:#9ab3d0;background:rgba(3,27,43,.52);font:inherit;text-align:left;cursor:pointer;transition:.2s ease}.step-tabs .step-tab:hover{border-color:rgba(89,223,255,.82);color:#fff;background:rgba(8,92,131,.52);transform:translateY(-1px)}.step-tabs .step-tab:focus-visible{outline:2px solid #72e8ff;outline-offset:3px}.step-tabs .step-marker{display:grid;flex:0 0 38px;width:38px;height:38px;place-items:center;border:1px solid rgba(101,176,208,.6);border-radius:50%;color:#a6c4d9;background:rgba(3,25,40,.8);font-size:12px;font-weight:900}.step-tabs .step-copy{display:grid;min-width:0;gap:4px}.step-tabs .step-copy b{overflow:hidden;font-size:14px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.step-tabs .step-copy small{color:#66b6d4;font-size:10px;font-weight:800;letter-spacing:.06em}.step-tabs .step-tab.active{border-color:#53d8fb;color:#fff;background:linear-gradient(135deg,rgba(7,156,207,.85),rgba(13,105,161,.92));box-shadow:inset 0 0 24px rgba(77,213,255,.22)}.step-tabs .step-tab.active .step-marker{border-color:#72e8ff;color:#fff;background:linear-gradient(135deg,#08b7e8,#1375d0);box-shadow:0 0 0 5px rgba(25,179,237,.14),0 0 18px rgba(46,208,255,.58)}.step-tabs .step-tab.complete{color:#dffbff}.step-tabs .step-tab.complete .step-marker{border-color:rgba(84,230,221,.85);color:#062b37;background:#6ef0e1}.step-tabs .step-connector{position:relative;flex:0 0 24px;height:2px;background:rgba(86,198,235,.48)}.step-tabs .step-connector::after{position:absolute;top:50%;right:0;width:7px;height:7px;border-top:1px solid #73b1ce;border-right:1px solid #73b1ce;content:'';transform:translateY(-50%) rotate(45deg)}
@media(max-width:720px){.step-tabs{align-items:stretch;flex-direction:column;padding:14px}.step-tabs .step-tab{flex:0 0 auto;width:100%}.step-tabs .step-connector{flex:0 0 18px;width:2px;height:18px;margin-left:18px}.step-tabs .step-connector::after{top:auto;right:auto;bottom:0;left:50%;transform:translateX(-50%) rotate(135deg)}}

@ -0,0 +1,22 @@
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/assessment/optimization.vue");
const materialsRoot = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/category-optimization"
);
test("品类结构优化第 3 步提供帕累托图资料下载并保留图片预览", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(path.join(materialsRoot, "20.实训操作说明.docx")), "后端应提供实训操作说明");
assert.ok(fs.existsSync(path.join(materialsRoot, "20.帕累托绘制自动化模版.xlsx")), "后端应提供帕累托图 Excel 模板");
assert.match(source, /下载实训操作说明/);
assert.match(source, /下载 Excel 模板/);
assert.match(source, /:preview-src-list="\[paretoImage\.url\]"/);
assert.match(source, /\/file\/training-materials\/category-optimization\/20\.实训操作说明\.docx/);
assert.match(source, /\/file\/training-materials\/category-optimization\/20\.帕累托绘制自动化模版\.xlsx/);
});

@ -0,0 +1,20 @@
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.resolve(__dirname, "../src/views/demand/consumer-behavior.vue"), "utf8");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/consumer-behavior/9.问卷调查报告模板.docx"
);
test("消费者需求与行为第三步从后端下载问卷模板", () => {
assert.ok(fs.existsSync(templatePath), "问卷模板应随后端资源发布");
assert.match(source, /\/file\/training-materials\/consumer-behavior\/9\.问卷调查报告模板\.docx/);
assert.match(source, /下载模板/);
assert.match(source, /function downloadQuestionnaireTemplate\(/);
const reportFunction = source.match(/function downloadSurveyReportTemplate\(\)[\s\S]*?\n}\n<\/script>/)?.[0] || "";
assert.match(reportFunction, /downloadBuiltInFile\(QUESTIONNAIRE_TEMPLATE_URL, "9\.问卷调查报告模板\.docx"\)/);
assert.doesNotMatch(reportFunction, /new Blob/, "报告模板不应由前端临时生成");
});

@ -0,0 +1,20 @@
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.resolve(__dirname, "../src/views/demand/consumer-scenario.vue"), "utf8");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/consumer-scenario/10.用户购买旅程模版.pptx"
);
test("用户购买旅程模板从后端内置资料目录下载", () => {
assert.ok(fs.existsSync(templatePath), "用户购买旅程模板应随后端资源发布");
assert.match(source, /\/file\/training-materials\/consumer-scenario\/10\.用户购买旅程模版\.pptx/);
assert.match(
source,
/function downloadJourneyMapTemplate\(\) \{\s*downloadBuiltInFile\(JOURNEY_TEMPLATE_URL, "10\.用户购买旅程模版\.pptx"\);\s*}/,
"模板按钮应调用后端内置文件下载"
);
});

@ -0,0 +1,21 @@
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/product/hardware-cost.vue");
const guidePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/hardware-cost/16.饼图制作操作说明.docx"
);
test("硬件成本估算操作指导由后端内置 Word 文件提供下载", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(guidePath), "后端应提供饼图制作操作说明 Word 文件");
assert.match(source, /\/file\/training-materials\/hardware-cost\/16\.饼图制作操作说明\.docx/);
assert.match(
source,
/function downloadCostStructureGuide\(\) \{\s*downloadBuiltInFile\(HARDWARE_COST_GUIDE_URL, "16\.饼图制作操作说明\.docx"\);\s*\}/
);
});

@ -0,0 +1,21 @@
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/product/hardware-cost.vue");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/hardware-cost/16.成本占比结构图绘制模版.xlsx"
);
test("硬件成本占比结构图模板由后端内置 Excel 文件提供下载", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(templatePath), "后端应提供成本占比结构图 Excel 模板");
assert.match(source, /\/file\/training-materials\/hardware-cost\/16\.成本占比结构图绘制模版\.xlsx/);
assert.match(
source,
/function downloadCostStructureTemplate\(\) \{\s*downloadBuiltInFile\(HARDWARE_COST_TEMPLATE_URL, "16\.成本占比结构图绘制模版\.xlsx"\);\s*\}/
);
});

@ -0,0 +1,22 @@
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/product/market-opportunity-selection.vue");
const source = fs.readFileSync(sourcePath, "utf8");
const guidePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/market-opportunity-selection/6.综合得分计算操作说明.docx"
);
test("综合得分计算操作说明从后端内置资料目录下载", () => {
assert.ok(fs.existsSync(guidePath), "操作说明应随后端资源发布");
assert.match(
source,
/\/file\/training-materials\/market-opportunity-selection\/6\.综合得分计算操作说明\.docx/,
"下载按钮应指向后端文件服务"
);
const functionBody = source.match(/function downloadInstructions\(\)[\s\S]*?\n}\nfunction downloadTemplate/)?.[0] || "";
assert.doesNotMatch(functionBody, /new Blob/, "操作说明不应再由前端临时生成文本文件");
});

@ -0,0 +1,25 @@
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/product/market-opportunity-selection.vue");
const source = fs.readFileSync(sourcePath, "utf8");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/market-opportunity-selection/6.数据表-抖音电商 - 计算模版.xlsx"
);
test("综合得分计算自动化模板从后端内置资料目录下载", () => {
assert.ok(fs.existsSync(templatePath), "自动化模板应随后端资源发布");
assert.match(
source,
/\/file\/training-materials\/market-opportunity-selection\/6\.数据表-抖音电商 - 计算模版\.xlsx/,
"下载按钮应指向后端文件服务"
);
assert.match(
source,
/function downloadTemplate\(\) \{ downloadBuiltInFile\(SCORE_CALCULATION_TEMPLATE_URL, "6\.数据表-抖音电商 - 计算模版\.xlsx"\); \}\nfunction downloadBuiltInFile/,
"自动化模板应通过后端内置文件下载"
);
});

@ -0,0 +1,24 @@
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/product/structure.vue");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/product-architecture/13.产品架构图绘制模版.pptx"
);
test("产品架构图绘制模板由后端内置 PPTX 提供下载", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(templatePath), "后端应提供产品架构图绘制 PPTX 模板");
assert.match(
source,
/\/file\/training-materials\/product-architecture\/13\.产品架构图绘制模版\.pptx/
);
assert.match(
source,
/function downloadArchitectureTemplate\(\) \{\s*downloadBuiltInFile\(PRODUCT_ARCHITECTURE_TEMPLATE_URL, "13\.产品架构图绘制模版\.pptx"\);\s*\}/
);
});

@ -0,0 +1,21 @@
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.resolve(__dirname, '../src/views/foundation/product-development-process.vue'), 'utf8');
const guidePath = path.resolve(__dirname, '../../dianshang-houduan/src/main/resources/static/file/training-materials/product-development-process/3.甘特图制作实训操作说明.docx');
const templatePath = path.resolve(__dirname, '../../dianshang-houduan/src/main/resources/static/file/training-materials/product-development-process/3.项目进度管理表-项目进度甘特图模版.xlsx');
test('甘特图实训操作说明从后端内置资料目录下载', () => {
assert.ok(fs.existsSync(guidePath), '操作说明应随后端资源发布');
assert.ok(source.includes('/file/training-materials/product-development-process/3.甘特图制作实训操作说明.docx'), '下载按钮应指向后端文件服务');
assert.ok(source.includes('function downloadBuiltInFile'), '下载应使用统一文件下载方法');
assert.doesNotMatch(source.match(/function downloadPracticeGuide\(\)[\s\S]*?\n}\n\nfunction downloadGanttTemplate/)?.[0] || '', /new Blob/, '操作说明不应再由前端临时生成文本文件');
});
test('甘特图绘制模板从后端内置资料目录下载', () => {
assert.ok(fs.existsSync(templatePath), '甘特图模板应随后端资源发布');
assert.ok(source.includes('/file/training-materials/product-development-process/3.项目进度管理表-项目进度甘特图模版.xlsx'), '模板按钮应指向后端文件服务');
assert.doesNotMatch(source.match(/function downloadGanttTemplate\(\)[\s\S]*?\n}\n\nfunction downloadBuiltInFile/)?.[0] || '', /XLSX\.writeFile/, '模板不应再由前端即时生成');
});

@ -0,0 +1,24 @@
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/assessment/diagnosis.vue");
const materialsRoot = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/product-diagnosis-iteration"
);
test("产品问题诊断第 1 步提供漏斗计算与可视化资料下载及图片预览", () => {
const source = fs.readFileSync(sourcePath, "utf8");
["23.实训操作说明1.docx", "23.Excel模版1.xlsx", "23.实训操作说明2.docx", "23.Excel模版2.xlsx"].forEach((name) => {
assert.ok(fs.existsSync(path.join(materialsRoot, name)), `后端应提供 ${name}`);
});
assert.match(source, /节点转化率计算/);
assert.match(source, /全链路转化漏斗可视化/);
assert.match(source, /:preview-src-list="\[funnelChart\.url\]"/);
assert.match(source, /诊断异常节点须从上表已填写渠道中选择/);
assert.match(source, /\/file\/training-materials\/product-diagnosis-iteration\/23\.Excel%E6%A8%A1%E7%89%881\.xlsx/);
assert.match(source, /\/file\/training-materials\/product-diagnosis-iteration\/23\.Excel%E6%A8%A1%E7%89%882\.xlsx/);
});

@ -0,0 +1,24 @@
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/assessment/diagnosis.vue");
const materialsRoot = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/product-diagnosis-iteration"
);
test("产品问题诊断第 3 步提供 AB 测试资料下载与对比图预览", () => {
const source = fs.readFileSync(sourcePath, "utf8");
["23.实训操作说明3.docx", "23.Excel模版3.xlsx"].forEach((name) => {
assert.ok(fs.existsSync(path.join(materialsRoot, name)), `后端应提供 ${name}`);
});
assert.match(source, /找到根因后,还需验证“改进方案是否真的有效”/);
assert.match(source, /AB测试数据填报/);
assert.match(source, /下载实训操作说明/);
assert.match(source, /下载Excel模板/);
assert.match(source, /:preview-src-list="\[abChart\.url\]"/);
assert.match(source, /23\.Excel%E6%A8%A1%E7%89%883\.xlsx/);
});

@ -0,0 +1,24 @@
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/product/differentiation.vue");
const templatePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/product-value-positioning/12.产品价值主张画布模版.pptx"
);
test("产品价值主张画布模板由后端内置 PPTX 提供下载", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(templatePath), "后端应提供产品价值主张画布 PPTX 模板");
assert.match(
source,
/\/file\/training-materials\/product-value-positioning\/12\.产品价值主张画布模版\.pptx/
);
assert.match(
source,
/function downloadCanvasTemplate\(\) \{\s*downloadBuiltInFile\(VALUE_PROPOSITION_CANVAS_TEMPLATE_URL, "12\.产品价值主张画布模版\.pptx"\);\s*\}/
);
});

@ -0,0 +1,21 @@
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/channel/index.vue");
const guidePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/purchase-demand-forecast/18.实训操作说明.docx"
);
test("采购需求预测实训操作说明由后端内置 Word 文件提供下载", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(guidePath), "后端应提供采购需求预测实训操作说明 Word 文件");
assert.match(source, /\/file\/training-materials\/purchase-demand-forecast\/18\.实训操作说明\.docx/);
assert.match(
source,
/function downloadPracticeGuide\(\) \{\s*downloadBuiltInFile\(PURCHASE_DEMAND_FORECAST_GUIDE_URL, "18\.实训操作说明\.docx"\);\s*\}/
);
});

@ -0,0 +1,20 @@
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/test/index.vue");
const guidePath = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/release-channel-evaluation/21.实训操作说明.docx"
);
test("产品发布渠道评估第 3 步提供操作说明并展示加权得分校验规则", () => {
const source = fs.readFileSync(sourcePath, "utf8");
assert.ok(fs.existsSync(guidePath), "后端应提供产品发布渠道评估操作说明 Word 文件");
assert.match(source, /下载实训操作说明/);
assert.match(source, /step-three-notice/);
assert.match(source, /\/file\/training-materials\/release-channel-evaluation\/21\.实训操作说明\.docx/);
assert.match(source, /function downloadPracticeGuide\(\) \{\s*downloadBuiltInFile\(RELEASE_CHANNEL_GUIDE_URL, "21\.实训操作说明\.docx"\);\s*\}/);
});

@ -0,0 +1,19 @@
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.resolve(__dirname, "../src/views/demand/people.vue"), "utf8");
const materialDir = path.resolve(
__dirname,
"../../dianshang-houduan/src/main/resources/static/file/training-materials/target-user-profile"
);
test("目标用户画像第一步从后端下载操作说明与 Excel 模板", () => {
assert.ok(fs.existsSync(path.join(materialDir, "8.实训操作说明.docx")), "操作说明应随后端资源发布");
assert.ok(fs.existsSync(path.join(materialDir, "8.Excel计算与可视化模版.xlsx")), "Excel 模板应随后端资源发布");
assert.match(source, /\/file\/training-materials\/target-user-profile\/8\.实训操作说明\.docx/);
assert.match(source, /\/file\/training-materials\/target-user-profile\/8\.Excel计算与可视化模版\.xlsx/);
assert.match(source, /下载实训操作说明/);
assert.match(source, /下载 Excel 模板/);
});
Loading…
Cancel
Save