@ -10,23 +10,23 @@
< template v-if ="currentStep === 1" >
< template v-if ="currentStep === 1" >
< div class = "step-heading" > < div > < p > Step 01 < / p > < h2 > { { trainingSteps [ 0 ] } } < / h2 > < / div > < em > 指标分类练习 < / em > < / div >
< div class = "step-heading" > < div > < p > Step 01 < / p > < h2 > { { trainingSteps [ 0 ] } } < / h2 > < / div > < em > 指标分类练习 < / em > < / div >
< p class = "intro" > 请对实训数据表中的各项指标 , 按照市场规模 、 市场潜力 、 竞争强度 、 运营难度 4 个类别进行分类 。 < / p >
< p class = "intro" > 请对实训数据表中的各项指标 , 按照市场规模 、 市场潜力 、 竞争强度 、 运营难度 4 个类别进行分类 。 < / p >
< div class = "table-wrap" > < table > < thead > < tr > < th > 序号 < / th > < th > 指标 < / th > < th > 分类 < / th > < / tr > < / thead > < tbody > < tr v-for ="(item, index) in indicators" :key="item.id"><td>{{ index + 1 }}</td><td class="metric">{{ item.name }}</td><td><select v-model="answers[item.id]" : aria-label="`${item.name} 的分类`"><option value="">请选择</option><option v-for="category in categories" :key="category" :value ="category" > {{ category }} < / option > < / select > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "table-wrap" > < table > < thead > < tr > < th > 序号 < / th > < th > 指标 < / th > < th > 分类 < / th > < / tr > < / thead > < tbody > < tr v-for ="(item, index) in indicators" :key="item.id"><td>{{ index + 1 }}</td><td class="metric">{{ item.name }}</td><td><select v-model="answers[item.id]" : class="{ 'input-error': stepOneCheckResults[item.id] && !stepOneCheckResults[item.id].correct }" : aria-label="`${item.name} 的分类`" @change="clearStepOneCheck(item.id) "><option value="">请选择</option><option v-for="category in categories" :key="category" :value ="category" > {{ category }} < / option > < / select > < small v-if ="stepOneCheckResults[item.id] && !stepOneCheckResults[item.id].correct" class="classification-error" > {{ stepOneCheckResults [ item.id ] .message }} < / small > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" :disabled ="saving" @click ="checkAndContinue" > 判 题 并 进 入 第 2 步 < / button > < button class = "btn" type = "button" :disabled ="saving" @click ="resetStepOne" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< div class = "step-actions" > < button class = "btn primary" type = "button" :disabled ="saving" @click ="checkAndContinue" > 判 题 并 进 入 第 2 步 < / button > < button class = "btn" type = "button" :disabled ="saving" @click ="resetStepOne" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< / template >
< template v -else -if = " currentStep = = = 2 " >
< template v -else -if = " currentStep = = = 2 " >
< div class = "step-heading" > < div > < p > Step 02 < / p > < h2 > { { trainingSteps [ 1 ] } } < / h2 > < / div > < em > 指标权重设定 < / em > < / div >
< div class = "step-heading" > < div > < p > Step 02 < / p > < h2 > { { trainingSteps [ 1 ] } } < / h2 > < / div > < em > 指标权重设定 < / em > < / div >
< p class = "intro" > 根据案例信息, 为各二级指标设定权重 。 系统将自动汇总一级指标 权重。 < / p >
< p class = "intro" > 第 1 步分类结果已自动带入 。 请填写四类一级指标权重和各维度内二级指标权重 , 系统将自动计算对应总 权重。 < / p >
< p v-if ="!stepOnePassed" class="warning" > 请 先 完 成 并 保 存 第 1 步 的 指 标 分 类 , 再 填 写 本 步 骤 。 < / p >
< p v-if ="!stepOnePassed" class="warning" > 请 先 完 成 并 保 存 第 1 步 的 指 标 分 类 , 再 填 写 本 步 骤 。 < / p >
< div class = "weight-summary" > < span > 权重合计< / span > < strong : class = "{ valid: totalWeight === 100 }"> { { totalWeight } } % < / strong > < small > 每项建议填写 1 – 100 , 合计必 须为 100 % < / small > < / div >
< div class = "weight-summary" > < span > 一级指标 权重合计< / span > < strong : class = "{ valid: primaryWeightTotal === 100 }"> { { primaryWeigh tT otal } } % < / strong > < span > 对应总权重合计 < / span > < strong : class = "{ valid: corresponding WeightTotal === 100 }"> { { correspondingWeightTotal } } % < / strong > < small > 一级权重合计、 每类维度内权重合计 、 对应总权重均 须为 100 % < / small > < / div >
< div class = "table-wrap" > < table class = "weight-table" > < thead > < tr > < th > 一级指标 < / th > < th > 二级指标< / th > < th > 权重( % ) < / th > < / tr > < / thead > < tbody > < t r v-for ="item in indicators" :key="item.id"><td class="first-level">{{ item.group }}( {{ groupWeight(item.group) }}%) </td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的权重`" /><span class="percent"> % < / span > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "table-wrap" > < table class = "weight-table" > < thead > < tr > < th > 一级指标 < / th > < th > 权重占比( % ) < / th > < th > 二级指标< / th > < th > 维度内 权重( % ) < / th > < th > 对应总权重 < / th > < / tr > < / thead > < tbody > < t emplate v-for ="group in indicatorGroups" :key="group.name"><tr v-for="(item, index) in group.items" :key="item.id"><td v-if="index === 0" :rowspan="group.items.length" class="first-level">{{ group.name }}</td><td v-if="index === 0" :rowspan="group.items.length" class="primary-weight"><input v-model.number="primaryWeights[group.name]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${group.name} 的一级指标权重`" /><span class="percent">%</span><small>建议 {{ group.suggestion }}</small> </td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的维度内权重`" /><span class="percent">%</span><small>建议 {{ item.suggestion }}</small></td><td class="corresponding-weight"> {{ formatWeight ( correspondingWeight ( item ) ) }} % < / td > < / tr > < / template > < / tbody > < / table > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepOnePassed" @click ="saveAndGoToStepThree" > 保 存 并 进 入 第 3 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepOnePassed" @click ="resetStepTwo" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepOnePassed" @click ="saveAndGoToStepThree" > 保 存 并 进 入 第 3 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepOnePassed" @click ="resetStepTwo" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< / template >
< template v -else -if = " currentStep = = = 3 " >
< template v -else -if = " currentStep = = = 3 " >
< div class = "step-heading" > < div > < p > Step 03 < / p > < h2 > { { trainingSteps [ 2 ] } } < / h2 > < / div > < em > 加权评分计算 < / em > < / div >
< div class = "step-heading" > < div > < p > Step 03 < / p > < h2 > { { trainingSteps [ 2 ] } } < / h2 > < / div > < em > 加权评分计算 < / em > < / div >
< p class = "intro" > 请根据数据表及第 2 步权重 , 使用 Min - Max 归一法计算各二级指标得分 , 再采用加权评分法填写产品 综合得分。 < / p >
< p class = "intro" > 通过 Min - Max 归一法计算各二级指标得分 , 采用加权评分法计算产品品类 综合得分。 < / p >
< p v-if ="!stepTwoPassed" class="warning" > 请 先 完 成 并 保 存 第 2 步 的 指 标 权 重 设 定 , 再 填 写 本 步 骤 。 < / p >
< p v-if ="!stepTwoPassed" class="warning" > 请 先 完 成 并 保 存 第 2 步 的 指 标 权 重 设 定 , 再 填 写 本 步 骤 。 < / p >
< div class = "score-table-wrap" > < table class = "score-table" > < thead > < tr > < th > 序号 < / th > < th > 产品品类 < / th > < th > 综合得分 < / th > < / tr > < / thead > < tbody > < tr v-for ="(product, index) in products" :key="product.id"><td>{{ index + 1 }}</td><td class="metric">{{ product.name }}</td><td><input v-model.number="scores[product.id]" :disabled="!stepTwoPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的综合得分`" /><span class="percent" > 分 < / span > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "score-table-wrap" > < table class = "score-table" > < thead > < tr > < th > 序号 < / th > < th > 产品品类 < / th > < th > 综合得分 < / th > < / tr > < / thead > < tbody > < tr v-for ="(product, index) in products" :key="product.id"><td>{{ index + 1 }}</td><td class="metric">{{ product.name }}</td><td><input v-model.number="scores[product.id]" :disabled="!stepTwoPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的综合得分`" /><span class="percent" > 分 < / span > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "calculation-guide" > < div > < h3 > 推荐工具及方法如下 , 自行选择 : < / h3 > < p > 1. Excel / WPS 表格 , 自行操作计算 , 可参考 《 实训操作说明 》 完成 。 < / p > < p > 2. 下载 Excel 模板 , 填写关键数据 , 完成指标计算 。 < / p > < p > 3. 导出实训数据表 , 上传到通用 AI 大模型 , 参考提示词完成计算 。 < / p > < p > 请根据数据表及各指标权重设定表, 通过 Min - Max 归一法计算各二级指标得分 , 采用加权评分法计算综合 得分。 < / p > < / div > < div class = "download-stack" > < button class = "btn" type = "button" @click ="downloadInstructions" > 下 载 实 训 操 作 说 明 < / button > < button class = "btn primary" type = "button" @click ="downloadTemplate" > 下 载 自 动 化 模 板 < / button > < / div > < / div >
< div class = "calculation-guide" > < div > < h3 > 推荐工具及方法如下 , 自行选择 : < / h3 > < p > 1. Excel / WPS 表格 , 自行操作计算 , 可参考 《 实训操作说明 》 完成 。 < / p > < p > 2. 下载 自动化模板, 填写案例数据及第 2 步权重 , 完成指标计算 。 < / p > < p > 3. 导出实训数据表 , 上传到通用 AI 大模型 , 参考提示词完成计算 。 < / p > < p > 先按 Min - Max 归一法和加权评分法计算品类综合得分 ; 如案例资料提供三段式时间权重 , 则分别按第 1 – 20 天 、 第 21 – 40 天 、 第 41 – 60 天的权重计算最终 得分。 < / p > < / div > < div class = "download-stack" > < button class = "btn" type = "button" @click ="downloadInstructions" > 下 载 实 训 操 作 说 明 < / button > < button class = "btn primary" type = "button" @click ="downloadTemplate" > 下 载 自 动 化 模 板 < / button > < / div > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepTwoPassed" @click ="saveAndGoToStepFour" > 保 存 并 进 入 第 4 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepTwoPassed" @click ="resetStepThree" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepTwoPassed" @click ="saveAndGoToStepFour" > 保 存 并 进 入 第 4 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepTwoPassed" @click ="resetStepThree" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< / template >
< template v -else -if = " currentStep = = = 4 " >
< template v -else -if = " currentStep = = = 4 " >
@ -39,8 +39,23 @@
< p > 支持 PNG 、 JPG 、 WEBP 格式 , 文件不超过 5 MB 。 < / p >
< p > 支持 PNG 、 JPG 、 WEBP 格式 , 文件不超过 5 MB 。 < / p >
< div v-if ="visualization.url" class="visualization-preview"><el-image :src="visualization.url" :preview-src-list="[visualization.url]" fit="cover" preview-teleported /><div><strong>{{ visualization.name || "数据可视化结果图" }}</strong><span>点击缩略图可放大查看</span><button class="text-button" type="button" @click="clearVisualization" > 移 除 图 片 < / button > < / div > < / div >
< div v-if ="visualization.url" class="visualization-preview"><el-image :src="visualization.url" :preview-src-list="[visualization.url]" fit="cover" preview-teleported /><div><strong>{{ visualization.name || "数据可视化结果图" }}</strong><span>点击缩略图可放大查看</span><button class="text-button" type="button" @click="clearVisualization" > 移 除 图 片 < / button > < / div > < / div >
< / div >
< / div >
< div class = "visual-guide" > < h3 > 推荐工具及方法如下 , 自行选择 : < / h3 > < p > 1. 使用 Excel / WPS 表格计算三类产品的平均得分 、 最高分和最低分 , 并生成图表 。 < / p > < p > 2. 下载 Excel 模板 , 填写关键数据 , 完成指标计算和可视化 。 < / p > < p > 3. 导出实训数据表 , 上传到通用 AI 大模型 , 参考提示词生成可视化结果 。 < / p > < / div >
< div class = "visual-guide" > < div > < h3 > 推荐工具及方法如下 , 自行选择 : < / h3 > < p > 1. 使用 Excel / WPS 表格计算三类产品的平均得分 、 最高分和最低分 , 并生成图表 。 < / p > < p > 2. 下载自动化模板 , 填写关键数据 , 完成指标计算和可视化 。 < / p > < p > 3. 导出实训数据表 , 上传到通用 AI 大模型 , 参考提示词完成计算与图表生成 。 < / p > < p > 请根据数据表及各指标权重设定表 , 使用 Min - Max 归一法与加权评分法计算产品综合得分 , 并汇总平均分 、 最高分和最低分 。 < / p > < / div > < div class = "download-stack" > < button class = "btn" type = "button" @click ="downloadVisualizationInstructions" > 下 载 实 训 操 作 说 明 < / button > < button class = "btn primary" type = "button" @click ="downloadVisualizationTemplate" > 下 载 自 动 化 模 板 < / button > < / div > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || uploading || !stepThreePassed" @click ="saveAndCompleteTraining" > 保 存 并 完 成 实 训 < / button > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || uploading || !stepThreePassed" @click ="saveAndGoToStepFive" > 保 存 并 进 入 第 5 步 < / button > < / div >
< / template >
< template v -else -if = " currentStep = = = 5 " >
< div class = "step-heading" > < div > < p > Step 05 < / p > < h2 > { { trainingSteps [ 4 ] } } < / h2 > < / div > < em > 评论数据洞察 < / em > < / div >
< p class = "intro" > 结合本实训提供的智能健身镜 、 智能卫浴镜和智能美妆镜三类产品评论数据 , 分析用户的痛点及未被满足的需求 。 < / p >
< p v-if ="!stepFourPassed" class="warning" > 请 先 完 成 并 保 存 第 4 步 的 数 据 可 视 化 结 果 , 再 填 写 本 步 骤 。 < / p >
< div class = "table-wrap" > < table class = "insight-table" > < thead > < tr > < th > 品类 < / th > < th > 好评率 ( % ) < / th > < th > 用户痛点 < / th > < th > 未被满足的需求 < / th > < / tr > < / thead > < tbody > < tr v-for ="product in products" :key="product.id"><td class="metric">{{ product.name }}</td><td><input v-model.number="reviewInsights[product.id].favorableRate" :disabled="!stepFourPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的好评率`" /><span class="percent" > % < / span > < / td > < td > < textarea v -model .trim = " reviewInsights [ product.id ] .painPoint " :disabled ="!stepFourPassed" : aria -label = " ` $ { product.name } 的 用 户 痛 点 ` " placeholder = "请根据评论数据提炼用户痛点" / > < / td > < td > < textarea v -model .trim = " reviewInsights [ product.id ] .unmetNeed " :disabled ="!stepFourPassed" : aria -label = " ` $ { product.name } 的 未 被 满 足 的 需 求 ` " placeholder = "请根据评论数据提炼未被满足的需求" / > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "insight-guide" > < div > < h3 > 可参考以下方法 : < / h3 > < p > 1. 自行根据评价数据计算好评率 , 阅读评论后分析用户痛点及未被满足的需求 。 < / p > < p > 2. 导出评论数据表 , 上传到通用 AI 大模型 , 通过文本分析完成计算与提炼 。 < / p > < p > 提示词 : 请计算 Excel 表中产品评论数据的好评率 , 提炼用户痛点和未被满足的需求 。 < / p > < / div > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepFourPassed" @click ="saveAndGoToStepSix" > 保 存 并 进 入 第 6 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepFourPassed" @click ="resetStepFive" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< template v -else -if = " currentStep = = = 6 " >
< div class = "step-heading" > < div > < p > Step 06 < / p > < h2 > { { trainingSteps [ 5 ] } } < / h2 > < / div > < em > 选品结论 < / em > < / div >
< p class = "intro" > 结合前面的数据分析 , 请说明应该进行开发什么产品 , 具体原因是什么 。 < / p >
< p v-if ="!stepFivePassed" class="warning" > 请 先 完 成 并 保 存 第 5 步 的 用 户 需 求 洞 察 , 再 填 写 本 步 骤 。 < / p >
< div class = "conclusion-field" > < label for = "market-opportunity-conclusion" > 开发建议与原因 < / label > < textarea id = "market-opportunity-conclusion" v -model .trim = " developmentConclusion " :disabled ="!stepFivePassed" placeholder = "请结合综合得分、评论痛点与未被满足的需求,说明建议开发的产品及具体原因。" / > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepFivePassed" @click ="saveAndCompleteTraining" > 保 存 并 完 成 实 训 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepFivePassed" @click ="resetStepSix" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< / template >
< div v -else class = "placeholder" > < p > 第 { { currentStep } } 步内容待配置 < / p > < span > 已保存的步骤可从上方步骤栏返回查看 。 < / span > < / div >
< div v -else class = "placeholder" > < p > 第 { { currentStep } } 步内容待配置 < / p > < span > 已保存的步骤可从上方步骤栏返回查看 。 < / span > < / div >
< / section >
< / section >
@ -52,54 +67,77 @@
< script setup >
< script setup >
import { Download , RefreshLeft , UploadFilled } from "@element-plus/icons-vue" ;
import { Download , RefreshLeft , UploadFilled } from "@element-plus/icons-vue" ;
import { checkMarketOpportunitySelectionStepOne , checkMarketOpportunitySelectionStepTwo , checkMarketOpportunitySelectionStepThree , checkMarketOpportunitySelectionStepFour , getStudentTrainingAnswer, saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { checkMarketOpportunitySelectionStepOne , checkMarketOpportunitySelectionStepTwo , checkMarketOpportunitySelectionStepThree , checkMarketOpportunitySelectionStepFour , checkMarketOpportunitySelectionStepFive, checkMarketOpportunitySelectionStepSix , getStudentTrainingAnswer, saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { parseTrainingArray } from "@/views/training/taskKeyMap" ;
import { isStudentDemo } from "@/utils/studentDemo" ;
import { isStudentDemo } from "@/utils/studentDemo" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue" ;
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue" ;
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue" ;
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue" ;
const TASK _KEY = "market-opportunity-selection" ;
const TASK _KEY = "market-opportunity-selection" ;
const DEFAULT _STEPS = [ "指标分类" , " 市场机会分析", "选品方案设计 ", "选品结论" ] ;
const DEFAULT _STEPS = [ "指标分类" , " 指标权重设置", "综合得分计算" , "数据可视化" , "用户需求洞察 ", "选品结论" ] ;
const categories = [ "市场规模" , "市场潜力" , "竞争强度" , "运营难度" ] ;
const categories = [ "市场规模" , "市场潜力" , "竞争强度" , "运营难度" ] ;
const indicators = [
const indicators = [
{ id : "search-count" , name : "搜索次数" , group : "市场规模" } , { id : "transaction-amount" , name : "成交金额(元)" , group : "市场规模" } , { id : "transaction-growth" , name : "成交金额增速" , group : "市场潜力" } , { id : "category-transaction-growth" , name : "类目成交增速" , group : "市场潜力" } ,
{ id : "search-count" , name : "搜索次数(万次)" , group : "市场规模" , suggestion : "25– 35" } , { id : "demand-supply-ratio" , name : "需供比" , group : "竞争强度" , suggestion : "25– 35" } , { id : "transaction-amount" , name : "成交金额(万元)" , group : "市场规模" , suggestion : "65– 75" } ,
{ id : "demand-supply-ratio" , name : "需供比" , group : "竞争强度" } , { id : "online-products" , name : "在线商品数" , group : "竞争强度" } , { id : "online-merchants" , name : "在线商家数" , group : "竞争强度" } , { id : "organic-traffic-ratio" , name : "自然流占比" , group : "运营难度" } ,
{ id : "transaction-growth" , name : "成交金额增速(%) " , group : "市场潜力" , suggestion : "55– 65" } , { id : "category-transaction-growth" , name : "类目成交增速(%) " , group : "市场潜力" , suggestion : "35– 45" } , { id : "online-products" , name : "在线商品数" , group : "竞争强度" , suggestion : "35– 45" } ,
{ id : "online-merchants" , name : "在线商家数" , group : "竞争强度" , suggestion : "25– 35" } , { id : "organic-traffic-ratio" , name : "自然流占比(%) " , group : "运营难度" , suggestion : "55– 65" } , { id : "return-rate" , name : "退货率(%) " , group : "运营难度" , suggestion : "35– 45" } ,
] ;
] ;
const groupSuggestions = { "市场规模" : "25– 35" , "市场潜力" : "25– 35" , "竞争强度" : "20– 30" , "运营难度" : "10– 20" } ;
const products = [ { id : "smart-fitness-mirror" , name : "智能健身镜" } , { id : "smart-bathroom-mirror" , name : "智能卫浴镜" } , { id : "smart-beauty-mirror" , name : "智能美妆镜" } ] ;
const products = [ { id : "smart-fitness-mirror" , name : "智能健身镜" } , { id : "smart-bathroom-mirror" , name : "智能卫浴镜" } , { id : "smart-beauty-mirror" , name : "智能美妆镜" } ] ;
const defaultTask = { title : "市场机会识别与选品" , background : "基于实训数据中的市场、供给与流量指标,完成市场机会识别和选品判断。" , goals : [ "理解选品数据指标的业务含义" , "掌握市场规模、潜力、竞争和运营难度的分类方法" , "为后续选品分析建立指标基础" ] , requirement : "阅读案例资料, 完成 8 项指标的分类,并依据 判题反馈修改。" } ;
const defaultTask = { title : "市场机会识别与选品" , background : "基于实训数据中的市场、供给与流量指标,完成市场机会识别和选品判断。" , goals : [ "理解选品数据指标的业务含义" , "掌握市场规模、潜力、竞争和运营难度的分类方法" , "为后续选品分析建立指标基础" ] , requirement : "阅读案例资料, 自行完成 9 项指标的分类,并依据系统 判题反馈修改。" } ;
const { proxy } = getCurrentInstance ( ) ;
const { proxy } = getCurrentInstance ( ) ;
const taskConfig = ref ( null ) ; const currentStep = ref ( 1 ) ; const saving = ref ( false ) ; const uploading = ref ( false ) ; const visualizationInput = ref ( null ) ; const visualization = ref ( { url : "" , name : "" } ) ; const stepOnePassed = ref ( false ) ; const stepTwoPassed = ref ( false ) ; const stepThreePassed = ref ( false ) ; const answers = ref ( createAnswers ( ) ) ; const weights = ref ( createWeights ( ) ) ; const scores = ref ( createScores ( ) ) ;
const taskConfig = ref ( null ) ; const currentStep = ref ( 1 ) ; const saving = ref ( false ) ; const uploading = ref ( false ) ; const visualizationInput = ref ( null ) ; const visualization = ref ( { url : "" , name : "" } ) ; const stepOnePassed = ref ( false ) ; const stepTwoPassed = ref ( false ) ; const stepThreePassed = ref ( false ) ; const stepFourPassed = ref ( false ) ; const stepFivePassed = ref ( false ) ; const answers = ref ( createAnswers ( ) ) ; const primaryWeights = ref ( createPrimaryWeights ( ) ) ; const weights = ref ( createWeights ( ) ) ; const scores = ref ( createScores ( ) ) ; const reviewInsights = ref ( createReviewInsights ( ) ) ; const developmentConclusion = ref ( "" ) ; const dynamicStepAnswers = ref ( { } ) ; const stepOneCheckResults = ref ( { } ) ;
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 ( ( ) => parse Array( taskConfig . value ? . steps , DEFAULT _STEPS ) ) ;
const taskTitle = computed ( ( ) => taskConfig . value ? . taskName || defaultTask . title ) ; const taskBackground = computed ( ( ) => taskConfig . value ? . background || defaultTask . background ) ; const taskGoals = computed ( ( ) => parseArray ( taskConfig . value ? . objectives , defaultTask . goals ) ) ; const taskRequirement = computed ( ( ) => taskConfig . value ? . requirements || defaultTask . requirement ) ; const trainingSteps = computed ( ( ) => { const configured = parse Training Array( taskConfig . value ? . steps , DEFAULT _STEPS ) ; return DEFAULT _STEPS . map ( ( fallback , index ) => configured [ index ] || fallback ) ; } ) ;
const progressItems = computed ( ( ) => indicators . map ( ( item ) => ( { text : ` ${ answers . value [ item . id ] ? "已分类" : "待分类" } : ${ item . name } ` , status : answers . value [ item . id ] ? "done" : "" } ) ) ) ;
const progressItems = computed ( ( ) => indicators . map ( ( item ) => ( { text : ` ${ answers . value [ item . id ] ? "已分类" : "待分类" } : ${ item . name } ` , status : answers . value [ item . id ] ? "done" : "" } ) ) ) ;
const totalWeight = computed ( ( ) => indicators . reduce ( ( total , item ) => total + ( Number ( weights . value [ item . id ] ) || 0 ) , 0 ) ) ;
const indicatorGroups = computed ( ( ) => categories . map ( ( name ) => ( { name , suggestion : groupSuggestions [ name ] , items : indicators . filter ( ( item ) => answers . value [ item . id ] === name ) } ) ) ) ;
const primaryWeightTotal = computed ( ( ) => categories . reduce ( ( total , group ) => total + ( Number ( primaryWeights . value [ group ] ) || 0 ) , 0 ) ) ;
const correspondingWeightTotal = computed ( ( ) => roundWeight ( indicators . reduce ( ( total , item ) => total + correspondingWeight ( item ) , 0 ) ) ) ;
onMounted ( async ( ) => { await loadTaskConfig ( ) ; await loadSavedAnswer ( ) ; } ) ;
onMounted ( async ( ) => { await loadTaskConfig ( ) ; await loadSavedAnswer ( ) ; } ) ;
function createAnswers ( ) { return Object . fromEntries ( indicators . map ( ( item ) => [ item . id , "" ] ) ) ; }
function createAnswers ( ) { return Object . fromEntries ( indicators . map ( ( item ) => [ item . id , "" ] ) ) ; }
function createPrimaryWeights ( ) { return Object . fromEntries ( categories . map ( ( group ) => [ group , null ] ) ) ; }
function createWeights ( ) { return Object . fromEntries ( indicators . map ( ( item ) => [ item . id , null ] ) ) ; }
function createWeights ( ) { return Object . fromEntries ( indicators . map ( ( item ) => [ item . id , null ] ) ) ; }
function createScores ( ) { return Object . fromEntries ( products . map ( ( product ) => [ product . id , null ] ) ) ; }
function createScores ( ) { return Object . fromEntries ( products . map ( ( product ) => [ product . id , null ] ) ) ; }
function createReviewInsights ( ) { return Object . fromEntries ( products . map ( ( product ) => [ product . id , { favorableRate : null , painPoint : "" , unmetNeed : "" } ] ) ) ; }
function isValidRate ( value ) { return Number . isFinite ( Number ( value ) ) && Number ( value ) >= 0 && Number ( value ) <= 100 && Math . round ( Number ( value ) * 100 ) === Number ( value ) * 100 ; }
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 = true ; } if ( answer ? . step2Answer ) { const parsed = JSON . parse ( answer . step2Answer ) ; weights . value = { ... createWeights ( ) , ... ( parsed ? . weights || parsed || { } ) } ; stepTwoPassed . value = true ; } if ( answer ? . step3Answer ) { const parsed = JSON . parse ( answer . step3Answer ) ; scores . value = { ... createScores ( ) , ... ( parsed ? . scores || parsed || { } ) } ; stepThreePassed . value = true ; } if ( answer ? . step4Answer ) { const parsed = JSON . parse ( answer . step4Answer ) ; visualization . value = { url : parsed ? . visualization ? . url || "" , name : parsed ? . visualization ? . name || "" } ; } if ( Number ( answer ? . currentStep ) > 0 ) currentStep . value = Number ( answer . currentStep ) ; } 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 ) => Boolean ( answers . value [ item . id ] ) ) ; } 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 . val ue; } 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 , weights: { ... weights . value } , primaryWeights : Object . fromEntries ( [ "市场规模" , "市场潜力" , "竞争强度" , "运营难度" ] . map ( ( group ) => [ group , groupWeight ( group ) ] ) ) } ; }
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 } } ; }
function buildStepFourOutcome ( ) { return { title : taskTitle . value , visualization : { ... visualization . value } } ; }
function buildStepFourOutcome ( ) { return { title : taskTitle . value , visualization : { ... visualization . value } } ; }
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 ] } ) ) ) ; 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 buildStepFiveOutcome ( ) { return { title : taskTitle . value , reviewInsights : Object . fromEntries ( products . map ( ( product ) => [ product . id , { ... reviewInsights . value [ product . id ] } ] ) ) } ; }
function groupWeight ( group ) { return indicators . filter ( ( item ) => item . group === group ) . reduce ( ( total , item ) => total + ( Number ( weights . value [ item . id ] ) || 0 ) , 0 ) ; }
function buildStepSixOutcome ( ) { return { title : taskTitle . value , developmentConclusion : developmentConclusion . value . trim ( ) } ; }
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 ; } currentStep . value = step ; }
function buildDynamicStepAnswers ( overrides ) { return { ... dynamicStepAnswers . value , ... overrides } ; }
async function saveAndGoToStepThree ( ) { if ( saving . value || ! stepOnePassed . value ) return ; saving . value = true ; try { await checkMarketOpportunitySelectionStepTwo ( { weights : 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 || "请填写合理的二级指标权重,且合计为 100%" ) ; } 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 ; } }
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 isIntegerWeight ( value ) { return value !== null && value !== "" && Number . isInteger ( Number ( value ) ) && Number ( value ) >= 1 && Number ( value ) <= 100 ; }
function resetStepOne ( ) { answers . value = createAnswers ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 1 步填写内容" ) ; }
function dimensionWeightTotal ( group ) { return indicators . filter ( ( item ) => answers . value [ item . id ] === group ) . reduce ( ( total , item ) => total + ( Number ( weights . value [ item . id ] ) || 0 ) , 0 ) ; }
function resetStepTwo ( ) { weights . value = createWeights ( ) ; proxy ? . $modal ? . msgSuccess ( "已清空第 2 步填写内容" ) ; }
function correspondingWeight ( item ) { return roundWeight ( ( ( Number ( primaryWeights . value [ answers . value [ item . id ] ] ) || 0 ) * ( Number ( weights . value [ item . id ] ) || 0 ) ) / 100 ) ; }
function roundWeight ( value ) { return Math . round ( ( value + Number . EPSILON ) * 100 ) / 100 ; }
function formatWeight ( value ) { return Number . isInteger ( value ) ? value : value . toFixed ( 2 ) ; }
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 ; 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 ; } }
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 步填写内容" ) ; }
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 resetStepSix ( ) { developmentConclusion . value = "" ; proxy ? . $modal ? . msgSuccess ( "已清空第 6 步填写内容" ) ; }
function openVisualizationPicker ( ) { visualizationInput . value ? . click ( ) ; }
function openVisualizationPicker ( ) { visualizationInput . value ? . click ( ) ; }
function clearVisualization ( ) { if ( visualization . value . url . startsWith ( "blob:" ) ) URL . revokeObjectURL ( visualization . value . url ) ; visualization . value = { url : "" , name : "" } ; }
function clearVisualization ( ) { if ( visualization . value . url . startsWith ( "blob:" ) ) URL . revokeObjectURL ( visualization . value . url ) ; visualization . value = { url : "" , name : "" } ; stepFourPassed . value = false ; stepFivePassed . value = false ; }
async function handleVisualizationUpload ( event ) { const file = event . target ? . files ? . [ 0 ] ; if ( ! file ) return ; const acceptedTypes = [ "image/jpeg" , "image/png" , "image/webp" ] ; if ( ! acceptedTypes . includes ( file . type ) ) { proxy ? . $modal ? . msgError ? . ( "请上传 PNG、JPG 或 WEBP 格式的图片" ) ; event . target . value = "" ; return ; } if ( file . size > 5 * 1024 * 1024 ) { proxy ? . $modal ? . msgError ? . ( "图片大小不能超过 5MB" ) ; event . target . value = "" ; return ; } uploading . value = true ; try { if ( isStudentDemo ( ) ) { clearVisualization ( ) ; visualization . value = { url : URL . createObjectURL ( file ) , name : file . name } ; } else { const formData = new FormData ( ) ; formData . append ( "file" , file ) ; const result = await uploadStudentTrainingFile ( formData ) ; const url = result ? . url || result ? . data ? . url ; if ( ! url ) throw new Error ( "图片上传失败,请重试" ) ; visualization . value = { url , name : file . name } ; } proxy ? . $modal ? . msgSuccess ( "可视化结果图上传成功" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "图片上传失败,请重试" ) ; } finally { uploading . value = false ; event . target . value = "" ; } }
async function handleVisualizationUpload ( event ) { const file = event . target ? . files ? . [ 0 ] ; if ( ! file ) return ; const acceptedTypes = [ "image/jpeg" , "image/png" , "image/webp" ] ; if ( ! acceptedTypes . includes ( file . type ) ) { proxy ? . $modal ? . msgError ? . ( "请上传 PNG、JPG 或 WEBP 格式的图片" ) ; event . target . value = "" ; return ; } if ( file . size > 5 * 1024 * 1024 ) { proxy ? . $modal ? . msgError ? . ( "图片大小不能超过 5MB" ) ; event . target . value = "" ; return ; } uploading . value = true ; try { if ( isStudentDemo ( ) ) { clearVisualization ( ) ; visualization . value = { url : URL . createObjectURL ( file ) , name : file . name } ; } else { const formData = new FormData ( ) ; formData . append ( "file" , file ) ; const result = await uploadStudentTrainingFile ( formData ) ; const url = result ? . url || result ? . data ? . url ; if ( ! url ) throw new Error ( "图片上传失败,请重试" ) ; visualization . value = { url , name : file . name } ; } proxy ? . $modal ? . msgSuccess ( "可视化结果图上传成功" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "图片上传失败,请重试" ) ; } finally { uploading . value = false ; event . target . value = "" ; } }
async function saveAndCompleteTraining ( ) { if ( saving . value || ! stepThreePassed . value ) return ; saving . value = true ; try { await checkMarketOpportunitySelectionStepFour ( { visualizationUrl : visualization . value . url } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step4Answer : JSON . stringify ( buildStepFourOutcome ( ) ) , currentStep : 4 , submitted : true , saveAction : "SUBMIT" } ) ; proxy ? . $modal ? . msgSuccess ( "实训成果已保存,恭喜完成本次实训" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请上传数据可视化结果图" ) ; } finally { saving . value = false ; } }
async function saveAndGoToStepFive ( ) { if ( saving . value || ! stepThreePassed . value ) return ; saving . value = true ; try { await checkMarketOpportunitySelectionStepFour ( { visualizationUrl : visualization . value . url } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step4Answer : JSON . stringify ( buildStepFourOutcome ( ) ) , currentStep : 5 , submitted : false , saveAction : "SAVE" } ) ; stepFourPassed . value = true ; currentStep . value = 5 ; proxy ? . $modal ? . msgSuccess ( "可视化结果已保存,已进入第 5 步" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请上传数据可视化结果图" ) ; } finally { saving . value = false ; } }
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 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. 将综合得分填写回第 3 步表格,保留两位小数。" , "市场机会识别与选品-实训操作说明.txt" , "text/plain;charset=utf-8" ) ; }
function downloadInstructions ( ) { downloadFile ( "市场机会识别与选品 - 实训操作说明\n\n1. 汇总三类产品的原始数据。\n2. 对正向指标计算 (x-min)/(max-min),对逆向指标计算 (max-x)/(max-min)。\n3. 将归一化得分乘以第 2 步的对应总权重后求和。\n4. 如案例资料提供三段式时间权重,分别计算第 1– 20 天、第 21– 40 天和第 41– 60 天的加权得分。\n5. 将产品品类综合得分填写回第 3 步表格,保留两位小数。" , "市场机会识别与选品-实训操作说明.txt" , "text/plain;charset=utf-8" ) ; }
function downloadTemplate ( ) { const header = [ "产品品类" , ... indicators . map ( ( item ) => item . name ) , "综合得分" ] ; const rows = products . map ( ( product ) => [ product . name , ... indicators . map ( ( ) => "" ) , "" ] ) ; const weightRow = [ "二级指标权重(%)" , ... indicators . map ( ( item ) => weights . value [ item . id ] ? ? "" ) , "" ] ; downloadFile ( [ header , weightRow , ... rows ] . map ( ( row ) => row . join ( "," ) ) . join ( "\n" ) , "市场机会识别与选品-自动化模板.csv" , "text/csv;charset=utf-8" ) ; }
function downloadTemplate ( ) { const header = [ "产品品类" , ... indicators . map ( ( item ) => item . name ) , "综合得分" ] ; const rows = products . map ( ( product ) => [ product . name , ... indicators . map ( ( ) => "" ) , "" ] ) ; const primaryWeightRow = [ "一级指标权重(%)" , ... indicators . map ( ( item ) => primaryWeights . value [ item . group ] ? ? "" ) , "" ] ; const dimensionWeightRow = [ "维度内权重(%)" , ... indicators . map ( ( item ) => weights . value [ item . id ] ? ? "" ) , "" ] ; const correspondingWeightRow = [ "对应总权重(%)" , ... indicators . map ( ( item ) => correspondingWeight ( item ) ) , "" ] ; downloadFile ( [ header , primaryWeightRow , dimensionWeightRow , correspondingWeightRow , ... rows ] . map ( ( row ) => row . join ( "," ) ) . join ( "\n" ) , "市场机会识别与选品-自动化模板.csv" , "text/csv;charset=utf-8" ) ; }
function exportOutcome ( ) { const blob = new Blob ( [ JSON . stringify ( { taskName : taskTitle . value , step1 : buildStepOneOutcome ( ) , step2 : buildStepTwoOutcome ( ) , step3 : buildStepThreeOutcome ( ) , step4 : buildStepFourOutcome ( ) , 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 ) ; }
function downloadVisualizationInstructions ( ) { downloadFile ( "市场机会识别与选品 - 数据可视化操作说明\n\n1. 按智能健身镜、智能卫浴镜和智能美妆镜汇总产品综合得分。\n2. 分别计算各品类的平均分、最高分和最低分。\n3. 使用柱状图、折线图或组合图完成可视化,并清晰标注品类和得分。\n4. 导出图表为 PNG、JPG 或 WEBP 格式后上传。" , "市场机会识别与选品-数据可视化操作说明.txt" , "text/plain;charset=utf-8" ) ; }
function downloadVisualizationTemplate ( ) { const header = [ "产品品类" , "平均得分" , "最高分" , "最低分" ] ; const rows = products . map ( ( product ) => [ product . name , "" , "" , "" ] ) ; downloadFile ( [ header , ... rows ] . map ( ( row ) => row . join ( "," ) ) . join ( "\n" ) , "市场机会识别与选品-数据可视化模板.csv" , "text/csv;charset=utf-8" ) ; }
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 ) ; }
< / script >
< / script >
< style lang = "scss" scoped >
< style lang = "scss" scoped >