< template >
< div class = "student-training-page student-training-shell" : class = "{ 'product-development-process-page': isProductDevelopmentProcess }" >
< main class = "training-core" >
< section class = "task-header" >
< div class = "task-head-main" >
< h1 class = "task-title" > { { page . title } } < / h1 >
< p class = "task-brief" >
< span class = "task-brief-text" > { { taskBackground } } < / span >
< button type = "button" class = "task-brief-toggle" @ click = "briefOpen = !briefOpen" >
{ { briefOpen ? "收起" : "展开目标与要求" } }
< / button >
< span v-if ="briefOpen" class="task-brief-more" >
< b > 实训背景 < / b >
< p > { { taskBackground } } < / p >
< b > 实训目标 < / b >
< ul >
< li v-for ="goal in taskGoals" :key ="goal" > {{ goal }} < / li >
< / ul >
< b > 实训要求 < / b >
< p > { { taskRequirement } } < / p >
< / span >
< / p >
< / div >
< / section >
< section class = "task-card" >
< div class = "workbench-head" >
< div >
< p class = "workbench-eyebrow" > COMPREHENSIVE TRAINING · WORKFLOW < / p >
< h2 > { { activeStepName || page . title } } < / h2 >
< / div >
< TrainingTaskToolbar
class = "comprehensive-training-toolbar"
: material - name = "page.material?.name"
: material - url = "page.material?.url"
: task - key = "pageKey"
: on - submit = "submitTask"
: on - reset = "resetTraining"
: on - export = "exportOutcome"
/ >
< / div >
< nav
v - if = "isComprehensiveCaseTraining && indexedConfiguredSteps.length"
ref = "comprehensiveStepNavRef"
: class = "['comprehensive-step-nav', { 'is-dragging': comprehensiveNavDragging }]"
aria - label = "综合实训步骤,可左右拖动浏览"
@ dragstart . prevent
@ pointerdown = "startComprehensiveNavDrag"
@ pointermove = "moveComprehensiveNavDrag"
@ pointerup = "endComprehensiveNavDrag"
@ pointercancel = "endComprehensiveNavDrag"
>
< div class = "comprehensive-step-strip" >
< template v-for ="item in indexedConfiguredSteps" :key="`${item.step}-${item.index}`" >
< button
type = "button"
: class = "['step-tab', { active: currentStep === item.index + 1, complete: currentStep > item.index + 1 }]"
: aria - label = "`切换至第 ${item.index + 1} 步:${item.step}`"
: aria - current = "currentStep === item.index + 1 ? 'step' : undefined"
@ click = "selectComprehensiveStep(item.index + 1)"
>
< span class = "step-marker" aria -hidden = " true " > { { String ( item . index + 1 ) . padStart ( 2 , '0' ) } } < / span >
< span class = "step-copy" > < strong class = "step-name" :title ="item.step" > { { item . step } } < / strong > < span class = "step-action" > { { currentStep === item . index + 1 ? '当前步骤' : '点击切换' } } < span aria -hidden = " true " > → < / span > < / span > < / span >
< / button >
< span v-if ="item.index < indexedConfiguredSteps.length - 1" class="step-connector" aria-hidden="true" > < / span >
< / template >
< / div >
< / nav >
< nav v -else -if = " configuredSteps.length " class = "step-indicator" aria -label = " 实 训 步 骤 " >
< template v-for ="(step, index) in configuredSteps" :key="`${step}-${index}`" >
< button
type = "button"
: class = "['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]"
: aria - label = "`切换至第 ${index + 1} 步:${step}`"
: aria - current = "currentStep === index + 1 ? 'step' : undefined"
@ click = "currentStep = index + 1"
>
< span class = "step-marker" aria -hidden = " true " > { { String ( index + 1 ) . padStart ( 2 , "0" ) } } < / span >
< span class = "step-copy" >
< strong class = "step-name" :title ="step" > { { step } } < / strong >
< span class = "step-action" > { { currentStep === index + 1 ? "当前步骤" : "点击切换" } } < span aria -hidden = " true " > → < / span > < / span >
< / span >
< / button >
< span v-if ="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true" > < / span >
< / template >
< / nav >
< section v-if ="processRows.length" class="process-step-card" >
< div v-if ="configuredSteps.length" class="step-title-row" >
< div >
< p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p >
< h3 > { { activeStepName } } < / h3 >
< / div >
< / div >
< div class = "process-training" >
< p class = "process-intro" > { { page . task } } < / p >
< div class = "process-table-wrap" >
< table class = "process-table" >
< thead >
< tr >
< th class = "serial-col" > 序号 < / th >
< th class = "name-col" > 任务名称 < / th >
< th class = "desc-col" > 任务说明 < / th >
< th > 工作内容 < / th >
< / tr >
< / thead >
< tbody >
< tr v-for ="row in visibleProcessRows" :key="row.no" >
< td class = "serial-cell" > { { row . no } } < / td >
< td class = "task-name-cell" > { { row . taskName } } < / td >
< td class = "desc-cell" > { { row . description } } < / td >
< td >
< textarea
v - model = "row.workContent"
class = "process-textarea"
placeholder = "填写该阶段需要完成的具体工作、产出物、参与角色和预计周期"
/ >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< p class = "upload-tip" > { { page . processUploadTip } } < / p >
< div class = "upload-action" >
< input ref = "uploadInputRef" class = "hidden-file-input" type = "file" accept = "image/*,.xlsx,.xls,.pdf" @change ="handleProcessFileChange" / >
< button type = "button" class = "upload-btn" : disabled = "uploading || isDemo" @click ="triggerUpload" >
< el -icon > < Upload / > < / e l - i c o n >
{ { uploading ? "上传中..." : "上传产品开发全流程甘特图" } }
< / button >
< span v-if ="form.ganttFileName" class="upload-file-name" > {{ form.ganttFileName }} < / span >
< / div >
< / div >
< / section >
< section v-if ="isComprehensiveCaseTraining && currentStep === 1" class="comprehensive-step-card" >
< div class = "step-title-row" >
< div >
< p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p >
< h3 > { { activeStepName } } < / h3 >
< / div >
< / div >
< div class = "comprehensive-analysis-grid" >
< section class = "analysis-panel" aria -labelledby = " pest -heading " >
< h4 id = "pest-heading" > PEST分析 < / h4 >
< p > 1. 请根据案例资料信息进行PEST分析 , 进行上述各项内容归类 , 把文档中相应内容填写到以下四类中 : < / p >
< div class = "analysis-field-grid" >
< label > 政治环境 : < el -input v -model = " comprehensiveStepOne.political " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写政治环境内容" / > < / label >
< label > 经济环境 : < el -input v -model = " comprehensiveStepOne.economic " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写经济环境内容" / > < / label >
< label > 社会环境 : < el -input v -model = " comprehensiveStepOne.social " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写社会环境内容" / > < / label >
< label > 技术环境 : < el -input v -model = " comprehensiveStepOne.technology " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写技术环境内容" / > < / label >
< / div >
< / section >
< section class = "analysis-panel" aria -labelledby = " swot -heading " >
< h4 id = "swot-heading" > SWOT分析 < / h4 >
< p > 2 、 请根据案例信息进行SWOT分析 , 进行各项信息内容提炼归类 , 把文档中相应内容填写到以下四类中 。 < / p >
< div class = "analysis-field-grid" >
< label > 优势 ( S ) : < el -input v -model = " comprehensiveStepOne.strengths " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写优势内容" / > < / label >
< label > 劣势 ( W ) : < el -input v -model = " comprehensiveStepOne.weaknesses " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写劣势内容" / > < / label >
< label > 机会 ( O ) : < el -input v -model = " comprehensiveStepOne.opportunities " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写机会内容" / > < / label >
< label > 威胁 ( T ) : < el -input v -model = " comprehensiveStepOne.threats " type = "textarea" : autosize = "{ minRows: 6, maxRows: 10 }" placeholder = "请填写威胁内容" / > < / label >
< / div >
< / section >
< / div >
< label class = "comprehensive-conclusion" > 3 、 请根据PEST和SWOT分析结果 , 洞察案例企业进入该细分赛道的机会和风险点 , 以及宏观环境和竞争环境评估结论 :
< el -input v -model = " comprehensiveStepOne.conclusion " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写机会、风险点及环境评估结论" / >
< / label >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 2 " class = "comprehensive-step-card comprehensive-step-two" >
< div class = "step-title-row" >
< div >
< p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p >
< h3 > { { activeStepName } } < / h3 >
< / div >
< / div >
< p class = "comprehensive-instruction" > 1. 下载本实训数据 , 根据案例数据 , 利用Min - Max归一法计算产品综合得分 , 请设置指标体系权重 : < / p >
< div class = "comprehensive-table-wrap" >
< table class = "comprehensive-table comprehensive-weight-table" >
< thead > < tr > < th > 一级指标 < / th > < th > 权重占比 ( % ) < / th > < th > 二级指标 < / th > < th > 维度内权重 ( % ) < / th > < th > 对应总权重 < / th > < / tr > < / thead >
< tbody >
< template v-for ="group in comprehensiveIndicators" :key="group.key" >
< tr v-for ="(dimension, index) in group.dimensions" :key="dimension.key" >
< th v-if ="index === 0" :rowspan="group.dimensions.length" > {{ group.label }} < / th >
< td v-if ="index === 0" :rowspan="group.dimensions.length" > < el -input v -model = " comprehensiveStepTwo.primaryWeights [ group.key ] " :placeholder ="group.suggestion" / > < / td >
< td > { { dimension . label } } < / td >
< td > < el -input v -model = " comprehensiveStepTwo.dimensionWeights [ dimension.key ] " :placeholder ="dimension.suggestion" / > < / td >
< td > < el -input v -model = " comprehensiveStepTwo.totalWeights [ dimension.key ] " placeholder = "填写权重" / > < / td >
< / tr >
< / template >
< / tbody >
< / table >
< / div >
< div class = "comprehensive-score-section" >
< div class = "comprehensive-section-line" > < p > 2 、 利用Min - Max归一法计算产品综合得分结果如下所示 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveCategory" > 增 加 品 类 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepTwo.categoryScores.length <= 2" @click ="removeComprehensiveCategory" > 减 少 品 类 < / el -button > < / div > < / div >
< div class = "comprehensive-score-grid" >
< label v-for ="(item, index) in comprehensiveStepTwo.categoryScores" :key="item.id" > 产 品 品 类 {{ index + 1 }} : < el -input v -model = " item.name " placeholder = "填写产品品类" / > < span > 综合得分 : < / span > < el -input v -model = " item.score " placeholder = "填写得分" / > < / label >
< / div >
< / div >
< div class = "comprehensive-review-section" >
< p class = "comprehensive-instruction" > 3 、 结合本实训提供的产品评论数据 , 分析用户的痛点及未被满足的需求 。 < / p >
< div class = "comprehensive-table-wrap" >
< table class = "comprehensive-table comprehensive-review-table" >
< thead > < tr > < th > 品类 < / th > < th > 好评率 ( % ) < / th > < th > 用户痛点 < / th > < th > 未被满足的需求 < / th > < / tr > < / thead >
< tbody > < tr v-for ="(row, index) in comprehensiveStepTwo.reviews" :key="row.id" > < th > { { comprehensiveReviewCategory ( index ) } } < / th > < td > < el -input v -model = " row.favorableRate " placeholder = "填写好评率" / > < / td > < td > < el -input v -model = " row.painPoints " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写用户痛点" / > < / td > < td > < el -input v -model = " row.unmetNeeds " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写未被满足的需求" / > < / td > < / tr > < / tbody >
< / table >
< / div >
< / div >
< label class = "comprehensive-conclusion" > 4 、 结合前面的数据分析 , 请说明应该进行开发什么产品 , 具体原因是什么 。
< el -input v -model = " comprehensiveStepTwo.developmentConclusion " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写产品开发建议及具体原因" / >
< / label >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 3 " class = "comprehensive-step-card comprehensive-step-three" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< p class = "comprehensive-instruction" > 1 、 公司团队通过第三方数据公司 , 采集了电商数据上购买相关产品的用户行为数据 , 包含用户ID 、 最近购买天数 ( R ) 、 购买次数 ( F ) 、 消费金额 ( M ) 等字段 , 需要进行RFM分析 , 完成用户分层 : < / p >
< div class = "rfm-upload-grid" >
< article v-for ="upload in comprehensiveUploadDefinitions" :key="upload.key" class="rfm-upload-card" >
< input : ref = "(element) => setComprehensiveUploadInput(upload.key, element)" class = "hidden-file-input" type = "file" :accept ="upload.accept" @ change = "handleComprehensiveFileChange(upload.key, $event)" / >
< button type = "button" class = "upload-btn" : disabled = "comprehensiveUploading === upload.key || isDemo" @click ="triggerComprehensiveUpload(upload.key)" > {{ comprehensiveUploading = = = upload.key ? ' 上 传 中 ... ' : upload.label }} < / button >
< p > { { upload . hint } } < / p >
< div v-if ="comprehensiveStepThree.attachments[upload.key]?.name" class="comprehensive-file-preview" >
< el -image v -if = " upload.kind = = = ' image ' " :src ="comprehensiveStepThree.attachments[upload.key].url" :preview-src-list ="[comprehensiveStepThree.attachments[upload.key].url]" fit = "cover" / >
< div v -else class = "excel-preview" > < strong > { { comprehensiveStepThree . attachments [ upload . key ] . name } } < / strong > < span > 工作表 : { { comprehensiveStepThree . attachments [ upload . key ] . preview ? . sheetName || '首个工作表' } } < / span > < div v-if ="comprehensiveStepThree.attachments[upload.key].preview?.headers?.length" class="excel-preview-table"><table><thead><tr><th v-for="header in comprehensiveStepThree.attachments[upload.key].preview.headers" :key="header">{{ header }}</th></tr></thead><tbody><tr v-for="(row, index) in comprehensiveStepThree.attachments[upload.key].preview.rows" :key="index"><td v-for="(cell, cellIndex) in row" :key ="cellIndex" > {{ cell }} < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "file-preview-actions" > < a :href ="comprehensiveStepThree.attachments[upload.key].url" target = "_blank" rel = "noopener" > 打开原文件 < / a > < button type = "button" @click ="removeComprehensiveAttachment(upload.key)" > 移 除 < / button > < / div >
< / div >
< / article >
< / div >
< div class = "comprehensive-analysis-section" >
< p class = "comprehensive-instruction" > 2 、 请进行RFM数据可视化分析 , 根据数据可视化结果 , 分析用户群体特点 : < / p >
< div class = "comprehensive-table-wrap" > < table class = "comprehensive-table rfm-analysis-table" > < thead > < tr > < th > 分析维度 < / th > < th > 分析要点 < / th > < th > 你的分析 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveRfmAnalysisRows" :key="row.key" > < th > { { row . label } } < / th > < td > { { row . focus } } < / td > < td > < el -input v -model = " comprehensiveStepThree.rfmAnalyses [ row.key ] " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写你的分析" / > < / td > < / tr > < / tbody > < / table > < / div >
< / div >
< div class = "comprehensive-analysis-section" >
< p class = "comprehensive-instruction" > 3 、 请根据案例资料中重点用户访谈记录 , 分析重点用户画像 : < / p >
< div class = "comprehensive-table-wrap" > < table class = "comprehensive-table rfm-profile-table" > < thead > < tr > < th > 用户信息 < / th > < th > 行为特征 < / th > < th > 需求痛点 < / th > < th > 消费心理 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepThree.profiles" :key="row.id" > < td > < el -input v -model = " row.userInfo " placeholder = "填写用户信息" / > < / td > < td > < el -input v -model = " row.behavior " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写行为特征" / > < / td > < td > < el -input v -model = " row.painPoints " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写需求痛点" / > < / td > < td > < el -input v -model = " row.consumerPsychology " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写消费心理" / > < / td > < / tr > < / tbody > < / table > < / div >
< / div >
< div class = "comprehensive-analysis-section" >
< div class = "comprehensive-section-line" > < p > 4 、 结合RFM用户群画像和重点用户画像的分析结果 , 进一步细分市场 , 寻找产品开发机会 , 从不同细分用户的行为特征和需求痛点分析市场吸引力 , 使用KANO模型对用户需求进行分层 , 进行需求问卷调研 , 通过定量数据验证和深化用户需求洞察 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveKanoRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepThree.kanoRows.length <= 1" @click ="removeComprehensiveKanoRow" > 删 除 一 行 < / el -button > < / div > < / div >
< div class = "comprehensive-table-wrap" > < table class = "comprehensive-table kano-table" > < thead > < tr > < th > 编号 < / th > < th > 功能 < / th > < th > 对应KANO类型 < / th > < th > 对应功能方向 < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepThree.kanoRows" :key="row.id"><th>{{ index + 1 }}</th><td><el-input v-model="row.feature" placeholder="填写功能" /></td><td><el-select v-model="row.kanoType" placeholder="选择类型" > < el -option v -for = " type in comprehensiveKanoTypes " :key ="type" :label ="type" :value ="type" / > < / e l - s e l e c t > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . d i r e c t i o n " p l a c e h o l d e r = " 填 写 功 能 方 向 " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v >
< / div >
< label class = "comprehensive-conclusion" > 5 、 结合前面的实训分析 , 设计一份完整的消费者需求调研问卷 , 包含以下部分 : 基本信息 ( 如 : 年龄 、 性别 、 月收入 、 城市级别等 ) 、 购买行为 ( 产品购买经历 、 购买渠道偏好 、 信息获取渠道 ) 、 需求偏好 ( Likert五级量表对上一步提到的功能列表进行重要度评价 ) 、 开放建议 ( 用户自由表达对智能助眠产品的期望与顾虑 ) , 根据收集到的调研问卷数据进行问卷分析 , 收集分析用户需求 , 撰写问卷分析报告 。
< el -input v -model = " comprehensiveStepThree.questionnaire " type = "textarea" : autosize = "{ minRows: 7, maxRows: 16 }" placeholder = "填写消费者需求调研问卷及分析报告" / >
< / label >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 4 " class = "comprehensive-step-card comprehensive-step-four" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 1 、 请根据访谈记录 、 案例资料 , 分析用户在购买旅程各阶段的行为表现 , 识别关键触点 , 并评估用户的情感体验水平 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table journey-table" > < thead > < tr > < th > 序号 < / th > < th > 旅程阶段 < / th > < th > 用户行为描述 < / th > < th > 关键触点 < / th > < th > 情感体验 < / th > < th > 关键决策因素 < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepFour.journeyRows" :key="row.stage"><th>{{ index + 1 }}</th><th>{{ row.stage }}</th><td><el-input v-model="row.behavior" type="textarea" :autosize="{ minRows: 3, maxRows: 7 }" :placeholder="row.behaviorPlaceholder" /></td><td><el-input v-model="row.touchpoints" type="textarea" :autosize="{ minRows: 3, maxRows: 7 }" :placeholder="row.touchpointPlaceholder" /></td><td><el-select v-model="row.sentiment" placeholder="请选择" > < el -option v -for = " item in comprehensiveSentiments " :key ="item" :label ="item" :value ="item" / > < / e l - s e l e c t > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . d e c i s i o n F a c t o r s " t y p e = " t e x t a r e a " : a u t o s i z e = " { m i n R o w s : 3 , m a x R o w s : 7 } " : p l a c e h o l d e r = " r o w . d e c i s i o n P l a c e h o l d e r " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < / d i v >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 2 、 结合用户购买旅程图 , 分析各阶段存在的痛点及未满足的需求 , 提出产品优化建议 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table journey-pain-table" > < thead > < tr > < th > 旅程阶段 < / th > < th > 识别的痛点 < / th > < th > 优化建议 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepFour.painRows" :key="row.stage" > < th > { { row . stage } } < / th > < td > < el -input v -model = " row.painPoint " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写识别的痛点" / > < / td > < td > < el -input v -model = " row.suggestion " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写优化建议" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 3 、 针对用户原始痛点需求 , 我们需要进行需求整理与清洗 , 把原始需求转化为工程语言 , 请根据实训要求中的原始需求内容 , 进行需求整理与清洗 , 根据示例填写需求转化表 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveRequirementRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepFour.requirements.length <= 1" @click ="removeComprehensiveRequirementRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table requirements-table" > < thead > < tr > < th > 序号 < / th > < th > 用户原声 < / th > < th > 场景洞察 < / th > < th > 转化后的功能定义 < / th > < th > 功能描述 < / th > < th > 技术指标 / 验收标准 < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepFour.requirements" :key="row.id" > < th > { { index + 1 } } < / th > < td > < el -input v -model = " row.userVoice " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写用户原声" / > < / td > < td > < el -input v -model = " row.scenarioInsight " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写场景洞察" / > < / td > < td > < el -input v -model = " row.featureDefinition " placeholder = "填写功能定义" / > < / td > < td > < el -input v -model = " row.featureDescription " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写功能描述" / > < / td > < td > < el -input v -model = " row.acceptanceCriteria " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写技术指标或验收标准" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 4 、 然后我们需要针对具体需求进行量化打分 , 请参考需求优先级评估打分表模板 , 填写以下打分表 : < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table rice-table" > < thead > < tr > < th > 需求ID < / th > < th > 功能名称 < / th > < th > KANO属性 < / th > < th > Reach ( 覆盖范围 ) < / th > < th > Impact ( 影响程度 ) < / th > < th > Confidence ( 置信度 ) < / th > < th > Effort ( 人 / 月 ) < / th > < th > RICE得分 < / th > < th > 优先级 < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepFour.riceRows" :key="row.id"><th>F{{ String(index + 1).padStart(2, '0') }}</th><td><el-input v-model="row.featureName" placeholder="填写功能名称" /></td><td><el-select v-model="row.kanoType" placeholder="请选择"><el-option v-for="item in comprehensiveKanoTypes" :key="item" :label="item" :value="item" /></el-select></td><td><el-input v-model="row.reach" placeholder="0" /></td><td><el-input v-model="row.impact" placeholder="0" /></td><td><el-input v-model="row.confidence" placeholder="例如: 80%" /></td><td><el-input v-model="row.effort" placeholder="0" /></td><td><span class="rice-score">{{ comprehensiveRiceScore(row) }}</span></td><td><el-select v-model="row.priority" placeholder="请选择" > < el -option label = "P0" value = "P0" / > < el -option label = "P1" value = "P1" / > < el -option label = "P2" value = "P2" / > < el -option label = "P3" value = "P3" / > < / e l - s e l e c t > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < / d i v >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 5 、 针对原始痛点需求进行功能转化优先级评估之后 , 针对优先级较高的用户痛点和用户期望 , 我们可以针对性的解决痛点 , 为用户创造收益 , 基于产品价值主张画布 , 形成产品概念 , 提炼出产品价值定位 , 请完善下述产品价值主张 : < / p > < div class = "value-canvas" > < div class = "value-map" > < div class = "value-map-left" > < label > 产品 & 服务 < el -input v-model ="comprehensiveStepFour.valueCanvas.productService" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写产品与服务" /></label><label>痛点缓释器<el-input v-model="comprehensiveStepFour.valueCanvas.painRelievers" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写痛点缓释器" /></label><label>增益创造器<el-input v-model="comprehensiveStepFour.valueCanvas.gainCreators" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写增益创造器" /></label></div><span class="value-fit">契合</span><div class="value-map-right" > < label > 客户痛点 < el -input v -model = " comprehensiveStepFour.valueCanvas.customerPains " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写客户痛点" / > < / label > < label > 客户收益期望 < el -input v -model = " comprehensiveStepFour.valueCanvas.customerGains " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写客户收益期望" / > < / label > < label > 用户目标 < el -input v -model = " comprehensiveStepFour.valueCanvas.customerJobs " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写用户目标" / > < / label > < / div > < / div > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 6 、 根据产品价值主张画布 , 使用经典定位陈述格式 , 撰写简洁有力的产品价值定位陈述 。 针对不同的目标用户 , 设计至少两种产品版本 : < / p > < div class = "version-grid" > < label > 版本1 : < el -input v -model = " comprehensiveStepFour.versions.versionOne " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写版本1产品价值定位陈述" / > < / label > < label > 版本2 : < el -input v -model = " comprehensiveStepFour.versions.versionTwo " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写版本2产品价值定位陈述" / > < / label > < / div > < / div >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 5 " class = "comprehensive-step-card comprehensive-step-five" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 1 、 全面明确产品细分市场用户群体 、 用户痛点需求 , 并把用户痛点需求转化为产品功能定义 , 形成明确的产品概念之后 , 可以进行产品规划与设计 , 针对智能硬件类产品 , 我们首先要明确产品功能逻辑 , 请根据实训案例资料 , 选择3个典型的工作场景 , 分析产品架构各层级的工作 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveArchitectureRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepFive.architectureRows.length <= 1" @click ="removeComprehensiveArchitectureRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table architecture-table" > < thead > < tr > < th > 序号 < / th > < th > 用户场景 < / th > < th > 硬件物理层 ( 感知什么 ) < / th > < th > 驱动感知层 ( 传递什么 ) < / th > < th > 算法处理层 ( 处理什么 ) < / th > < th > 应用功能层 ( 触发什么 ) < / th > < th > 驱动感知层 ( 输出什么 ) < / th > < th > 硬件物理层 ( 产生什么效果 ) < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepFive.architectureRows" :key="row.id" > < th > { { index + 1 } } < / th > < td > < el -input v -model = " row.scene " placeholder = "填写用户场景" / > < / td > < td > < el -input v -model = " row.hardwareInput " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写硬件感知" / > < / td > < td > < el -input v -model = " row.driverInput " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写传递内容" / > < / td > < td > < el -input v -model = " row.algorithm " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写算法处理" / > < / td > < td > < el -input v -model = " row.application " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写应用功能" / > < / td > < td > < el -input v -model = " row.driverOutput " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写输出内容" / > < / td > < td > < el -input v -model = " row.hardwareOutput " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写硬件效果" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 2 、 产品故障与售后服务设计关系到产品能否持续盈利 , 产品经理熟悉产品功能架构之后 , 对产品常见故障发生的原因会有较强认知 , 能够进行故障分类 , 而且互联网产品的售后服务与传统产品有显著区别 。 传统产品售后主要集中在硬件故障维修 , 而互联网产品特别是智能化的产品 , 还需要处理联网问题 、 云端连接故障等软件层面的售后需求 , 且可以通过远程诊断 、 OTA升级等方式降低售后成本 。 请根据4类故障类型 , 设计精细化的分级处理流程 。 请结合实训资料中的售后政策和常见故障案例 , 完成以下处理流程表 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table fault-table" > < thead > < tr > < th > 故障类型 < / th > < th > 具体场景 < / th > < th > 处理流程 < / th > < th > 核心规则 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepFive.faultRows" :key="row.type" > < th > { { row . type } } < / th > < td > < el -input v -model = " row.scene " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" :placeholder ="row.scenePlaceholder" / > < / td > < td > < el -input v -model = " row.process " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写处理流程" / > < / td > < td > < el -input v -model = " row.rule " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写核心规则" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 3 、 根据故障分级处理流程 , 制定面向消费者的完整售后政策 。 售后政策是消费者权益的保障承诺 , 需覆盖退换货 、 质保 、 维修服务 、 响应时效 、 补偿方案等核心维度 , 可直接用于产品说明书附录及电商平台详情页 “ 售后保障 ” 模块 。 < / p > < p class = "comprehensive-instruction" > ①填写售后政策框架表 。 请结合实训资料中的产品信息 、 行业法规及竞品售后政策参考 , 完成以下售后政策框架表 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table policy-table" > < thead > < tr > < th > 政策维度 < / th > < th > 政策内容 < / th > < th > 适用条件与说明 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepFive.policyRows" :key="row.dimension"><th>{{ row.dimension }}</th><td><el-input v-model="row.content" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" placeholder="填写政策内容" /></td><td><el-input v-model="row.conditions" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" placeholder="填写适用条件与说明" /></td></tr></tbody></table></div><p class="comprehensive-instruction">②填写电商详情页售后信息摘要。将售后政策框架表中的核心信息提炼为消费者友好的简明摘要, 用于电商平台详情页“售后保障”模块展示。要求语言简洁、重点突出, 消费者3秒内可获取关键信息。</p><div class="comprehensive-table-wrap"><table class="comprehensive-table after-sales-summary-table"><thead><tr><th>售后信息项</th><th>详情页展示内容</th></tr></thead><tbody><tr v-for="row in comprehensiveStepFive.summaryRows" :key="row.item" > < th > { { row . item } } < / th > < td > < el -input v -model = " row.content " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写详情页展示内容" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 6 " class = "comprehensive-step-card comprehensive-step-six" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 1 、 产品规划设计完成 , PRD文档确定之后 , 正式进入项目落地阶段 , 需要进行BOM估算及供应渠道评估 , 请在满足成本约束条件下 , 比较不同批量规模下的成本差异 。 元器件采购价格与采购量密切相关 : 小批量试产 ( 100 台 ) 议价能力弱 , 大规模量产 ( 10000 台 ) 议价空间大 。 请根据案例资料分别计算两种批量场景下的单台成本 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveBomRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepSix.bomRows.length <= 1" @click ="removeComprehensiveBomRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "cost-constraint-grid" > < label > 成本约束 : 小批量试产BOM成本不高于 < el -input v-model ="comprehensiveStepSix.smallBatchLimit" placeholder="填写金额" />元</label><label>成本约束: 大批量量产BOM成本不高于<el-input v-model="comprehensiveStepSix.largeBatchLimit" placeholder="填写金额" />元</label></div><div class="comprehensive-table-wrap"><table class="comprehensive-table bom-table"><thead><tr><th>物料名称</th><th>选型规格参数</th><th>小批量100台( 元/台)</th><th>大批量10000台( 元/台)</th></tr></thead><tbody><tr v-for="row in comprehensiveStepSix.bomRows" :key="row.id"><th><el-input v-model="row.name" placeholder="填写物料名称" /></th><td><el-input v-model="row.specification" placeholder="填写选型规格参数" /></td><td><el-input v-model="row.smallCost" placeholder="填写成本" /></td><td><el-input v-model="row.largeCost" placeholder="填写成本" /></td></tr><tr class="computed-row" > < th > 合计 < / th > < td > < / td > < td > { { comprehensiveBomTotal ( 'small' ) } } < / td > < td > { { comprehensiveBomTotal ( 'large' ) } } < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 2 、 硬件成本除元器件外 , 还需覆盖制造成本 。 请根据案例资料中的制造成本 , 核算单台总成本 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveManufacturingRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepSix.manufacturingRows.length <= 1" @click ="removeComprehensiveManufacturingRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table manufacturing-table" > < thead > < tr > < th > 序号 < / th > < th > 费用名称 < / th > < th > 小批量试产100台 ( 元 / 台 ) < / th > < th > 大批量量产10000台 ( 元 / 台 ) < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepSix.manufacturingRows" :key="row.id"><th>{{ index + 1 }}</th><td><el-input v-model="row.name" placeholder="填写费用名称" /></td><td><el-input v-model="row.smallCost" placeholder="填写成本" /></td><td><el-input v-model="row.largeCost" placeholder="填写成本" /></td></tr><tr class="computed-row"><th>{{ comprehensiveStepSix.manufacturingRows.length + 1 }}</th><th>制造成本合计</th><td>{{ comprehensiveManufacturingTotal('small') }}</td><td>{{ comprehensiveManufacturingTotal('large') }}</td></tr><tr class="computed-row"><th>{{ comprehensiveStepSix.manufacturingRows.length + 2 }}</th><th>物料BOM成本</th><td>{{ comprehensiveBomTotal('small') }}</td><td>{{ comprehensiveBomTotal('large') }}</td></tr><tr class="computed-row" > < th > { { comprehensiveStepSix . manufacturingRows . length + 3 } } < / th > < th > 单件总成本 < / th > < td > { { comprehensiveUnitCost ( 'small' ) } } < / td > < td > { { comprehensiveUnitCost ( 'large' ) } } < / td > < / tr > < / tbody > < / table > < / div > < / div >
< label class = "comprehensive-conclusion" > 3 、 基于成本结构 , 思考产品的成本是否还有优化空间 , 请简述优化方案的整体思路 , 包括优化优先级 、 预期效果及风险控制措施 。 < el -input v -model = " comprehensiveStepSix.costOptimization " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写成本优化方案" / > < / label >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 4 、 BOM成本估算后 , 企业需要再成本约束下 , 决策 “ 去哪买 ” 。 请从采购成本 、 交货周期 、 品质可靠性 、 采购灵活性 、 服务支持等5个维度 , 且需要考虑小批量试产 ( 100 台 ) 和大规模量产 ( 10000 台 ) 两种不同的场景进行供应渠道评估 , 不同采购场景下各维度重要性不同 , 试产阶段更看重采购灵活性和交货周期 , 量产阶段更看重采购成本和品质可靠性 。 请根据不同场景 , 为评估模型设置权重 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table procurement-weight-table" > < thead > < tr > < th > 评估维度 < / th > < th > 维度说明 < / th > < th > 权重占比 ( % ) 小批量试产 < / th > < th > 权重占比 ( % ) 大批量量产 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepSix.weightRows" :key="row.key" > < th > { { row . label } } < / th > < td > { { row . description } } < / td > < td > < el -input v -model = " row.trialWeight " placeholder = "填写权重" / > < / td > < td > < el -input v -model = " row.massWeight " placeholder = "填写权重" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 5 、 根据案例资料 , 按照统一的标准 , 从采购成本 、 交货周期 、 品质可靠性 、 采购灵活性 、 服务支持等5个维度进行赋分 , 每项分数为0 - 10 分 , 计算试产及量产两种场景下的加权得分 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table procurement-score-table" > < thead > < tr > < th > 渠道 < / th > < th > 采购成本 < / th > < th > 交货周期 < / th > < th > 品质可靠性 < / th > < th > 采购灵活性 < / th > < th > 服务支持 < / th > < th > 试产加权总分 < / th > < th > 量产加权总分 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepSix.channelRows" :key="row.id"><td><el-input v-model="row.channel" placeholder="填写渠道" /></td><td><el-input v-model="row.cost" placeholder="0-10" /></td><td><el-input v-model="row.delivery" placeholder="0-10" /></td><td><el-input v-model="row.quality" placeholder="0-10" /></td><td><el-input v-model="row.flexibility" placeholder="0-10" /></td><td><el-input v-model="row.service" placeholder="0-10" /></td><td class="rice-score">{{ comprehensiveChannelScore(row, 'trial') }}</td><td class="rice-score">{{ comprehensiveChannelScore(row, 'mass') }}</td></tr></tbody></table></div><label class="comprehensive-conclusion" > 根 据 评 分 结 果 , 对 比 分 析 不 同 场 景 下 最 优 渠 道 及 优 劣 , 结 合 供 应 风 险 评 估 , 给 出 综 合 采 购 建 议 : < el -input v -model = " comprehensiveStepSix.procurementRecommendation " type = "textarea" : autosize = "{ minRows: 6, maxRows: 12 }" placeholder = "填写综合采购建议" / > < / label > < / div >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 7 " class = "comprehensive-step-card comprehensive-step-seven" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" >
< p class = "comprehensive-instruction" > 1 、 供应渠道评估与选择之后 , 采购决策还需回答 “ 买多少 、 什么时候买 ” 。 企业需要基于产品历史销量数据 , 运用科学方法预测未来需求 , 结合安全库存和现有库存制定采购计划 。 请基于案例数据 , 采用线性预测法预测未来市场需求数据 , 并填写下表 。 < / p >
< div class = "forecast-rule-list" > < span > 采购规则如下 : < / span > < span > ( 1 ) 安全库存 : 按预测销量的10 % 预留 , 应对突发订单波动 ( 四舍五入取整 ) ; < / span > < span > ( 2 ) 补货周期 : 按月采购 , 每月月初下达采购订单 ; < / span > < span > ( 3 ) 期初库存 : 预测第1个月的期初库存为30台 , 后续月份取上一月末剩余库存 。 < / span > < / div >
< div class = "comprehensive-table-wrap" > < table class = "comprehensive-table demand-forecast-table" > < thead > < tr > < th > 预测月份 < / th > < th > 预测销量 ( 台 ) < / th > < th > 安全库存 ( 台 ) < / th > < th > 期初库存 ( 台 ) < / th > < th > 采购量 ( 台 ) < / th > < / tr > < / thead > < tbody > < tr v-for ="(row, index) in comprehensiveStepSeven.forecastRows" :key="row.month"><th>{{ row.month }}</th><td><el-input v-model="row.forecastSales" placeholder="填写预测销量" /></td><td class="rice-score">{{ comprehensiveSafetyStock(row) }}</td><td class="rice-score">{{ comprehensiveBeginningStock(index) }}</td><td class="rice-score" > {{ comprehensivePurchaseQuantity ( row , index ) }} < / td > < / tr > < / tbody > < / table > < / div >
< / div >
< div class = "comprehensive-analysis-section" >
< p class = "comprehensive-instruction" > 2 、 完成采购需求预测后 , 产品上市前最后决策环节就是定价 。 请综合运用成本加成定价法 、 竞争定价法 、 价值定价法和PSM价格敏感度测试模型 , 测算不同方法下的合理价格区间 , 找到不会引发大规模用户流失 、 同时最大化利润的最终定价 。 < / p >
< div class = "pricing-subsection" > < p > ( 1 ) 成本加成定价法 < / p > < p class = "pricing-formula" > 核心逻辑 : 总成本 × ( 1 + 加成率 ) = 最终售价 。 企业根据渠道类型和品牌定位确定加成率 , 在成本基础上加成定价 。 < / p > < div class = "cost-price-input" > < label > 单台综合成本 : < el -input v-model ="comprehensiveStepSeven.unitCost" placeholder="填写成本" />元</label></div><div class="comprehensive-table-wrap"><table class="comprehensive-table cost-pricing-table"><thead><tr><th>序号</th><th>渠道</th><th>加成率(%) </th><th>加成售价(元)</th></tr></thead><tbody><tr v-for="(row, index) in comprehensiveStepSeven.costPricingRows" :key="row.channel"><th>{{ index + 1 }}</th><th>{{ row.channel }}</th><td><el-input v-model="row.markup" :placeholder="row.suggestion" /></td><td class="rice-score" > {{ comprehensiveCostBasedPrice ( row ) }} < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "pricing-subsection competitor-pricing-section" > < p > ( 2 ) 竞争定价法 < / p > < p > 对标同品类竞品 , 根据产品差异化优势寻找价格空位 。 请分析竞品价格带与差异化定位 , 填写差异化优势 , 并确定本产品可切入的价格空位 。 < / p > < div class = "competitor-pricing-grid" > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table competitor-table" > < thead > < tr > < th > 竞品产品类型 < / th > < th > 品牌 < / th > < th > 电商售价 ( 元 ) < / th > < th > 差异化优势 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepSeven.competitorRows" :key="row.id"><th><el-input v-model="row.type" placeholder="填写竞品类型" /></th><td><el-input v-model="row.brand" placeholder="填写品牌" /></td><td><el-input v-model="row.price" placeholder="填写售价" /></td><td><el-input v-model="row.advantage" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" placeholder="填写差异化优势" /></td></tr></tbody></table></div><div class="competitor-decision"><label>对标竞品:<el-input v-model="comprehensiveStepSeven.competitorDecision.benchmark" placeholder="填写对标竞品" /></label><label>厂家出厂成本:<el-input v-model="comprehensiveStepSeven.competitorDecision.factoryCost" placeholder="填写成本" /><span>元/台</span></label><label>定价:<el-input v-model="comprehensiveStepSeven.competitorDecision.price" placeholder="填写定价" /><span>元</span></label><label>毛利率:<span class="calculated-field">{{ comprehensiveCompetitorMargin }}</span><span>%</span></label><label class="textarea-label" > 定 价 原 因 分 析 : < el -input v -model = " comprehensiveStepSeven.competitorDecision.analysis " type = "textarea" : autosize = "{ minRows: 4, maxRows: 8 }" placeholder = "填写定价原因分析" / > < / label > < / div > < / div > < / div >
< div class = "pricing-subsection" > < div class = "comprehensive-section-line" > < p > ( 3 ) 价值定价法 。 基于用户感知价值定价 , 将产品可替代的功能或产品逐一折算价值并加总 , 得到用户感知总价值 , 再按一定折扣让渡利润给用户 , 确定最终售价 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveValueRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepSeven.valueRows.length <= 1" @click ="removeComprehensiveValueRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "value-pricing-grid" > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table value-pricing-table" > < thead > < tr > < th > 可替代产品 / 功能 / 额外价值感知 < / th > < th > 价值感知 ( 元 ) < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepSeven.valueRows" :key="row.id"><td><el-input v-model="row.item" placeholder="填写价值感知项" /></td><td><el-input v-model="row.value" placeholder="填写价值" /></td></tr><tr class="computed-row"><th>感知价值合计</th><td>{{ comprehensiveValueTotal }}</td></tr></tbody></table></div><div class="value-pricing-result" > < label > 利润让渡折扣率 : < el -input v -model = " comprehensiveStepSeven.valueDiscount " placeholder = "填写比例" / > < span > % < / span > < / label > < p > 价值定价法价格 : < strong > { { comprehensiveValuePrice } } 元 < / strong > < / p > < p > 价值定价法价格 = 感知价值合计 × 折扣率 < / p > < / div > < / div > < / div >
< div class = "pricing-subsection" > < div class = "comprehensive-section-line" > < p > ( 4 ) PSM ( 价格敏感度测试 ) 。 通过四个问题调查用户对价格的敏感程度 , 包括 : 太贵不买 、 太便宜不买 ( 怀疑质量 ) 、 偏高但可接受 、 便宜划算 。 请根据案例资料填写PSM累计占比表 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensivePsmRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepSeven.psmRows.length <= 1" @click ="removeComprehensivePsmRow" > 删 除 一 行 < / el -button > < el -button class = "compact-action" @click ="exportComprehensivePsm" > 导 出 < / el -button > < el -button class = "compact-action" @ click = "psmVisualVisible = !psmVisualVisible" > { { psmVisualVisible ? "收起可视化" : "可视化" } } < / e l - b u t t o n > < / d i v > < / d i v > < d i v c l a s s = " c o m p r e h e n s i v e - t a b l e - w r a p " > < t a b l e c l a s s = " c o m p r e h e n s i v e - t a b l e p s m - t a b l e " > < t h e a d > < t r > < t h > 价 格 ( 元 ) < / t h > < t h > 太 贵 不 买 累 计 占 比 ( % ) < / t h > < t h > 太 便 宜 不 买 累 计 占 比 ( % ) < / t h > < t h > 偏 高 可 接 受 累 计 占 比 ( % ) < / t h > < t h > 便 宜 划 算 累 计 占 比 ( % ) < / t h > < / t r > < / t h e a d > < t b o d y > < t r v - f o r = " r o w i n c o m p r e h e n s i v e S t e p S e v e n . p s m R o w s " : k e y = " r o w . i d " > < t h > < e l - i n p u t v - m o d e l = " r o w . r a n g e " p l a c e h o l d e r = " 填 写 价 格 区 间 " / > < / t h > < t d > < e l - i n p u t v - m o d e l = " r o w . t o o E x p e n s i v e " p l a c e h o l d e r = " 填 写 占 比 " / > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . t o o C h e a p " p l a c e h o l d e r = " 填 写 占 比 " / > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . a c c e p t a b l e " p l a c e h o l d e r = " 填 写 占 比 " / > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . b a r g a i n " p l a c e h o l d e r = " 填 写 占 比 " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < d i v v - i f = " p s m V i s u a l V i s i b l e " c l a s s = " p s m - v i s u a l i z a t i o n " a r i a - l i v e = " p o l i t e " > < d i v c l a s s = " p s m - l e g e n d " > < s p a n c l a s s = " p s m - l e g e n d - - e x p e n s i v e " > 太 贵 不 买 < / s p a n > < s p a n c l a s s = " p s m - l e g e n d - - c h e a p " > 太 便 宜 不 买 < / s p a n > < s p a n c l a s s = " p s m - l e g e n d - - a c c e p t a b l e " > 偏 高 可 接 受 < / s p a n > < s p a n c l a s s = " p s m - l e g e n d - - b a r g a i n " > 便 宜 划 算 < / s p a n > < / d i v > < a r t i c l e v - f o r = " r o w i n c o m p r e h e n s i v e S t e p S e v e n . p s m R o w s " : k e y = " ` v i s u a l - $ { r o w . i d } ` " c l a s s = " p s m - v i s u a l - r o w " > < s t r o n g > { { r o w . r a n g e | | " 未 填 写 价 格 区 间 " } } < / s t r o n g > < d i v c l a s s = " p s m - b a r s " > < s p a n c l a s s = " p s m - b a r p s m - b a r - - e x p e n s i v e " : s t y l e = " { w i d t h : ` $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . t o o E x p e n s i v e ) } % ` } " : t i t l e = " ` 太 贵 不 买 : $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . t o o E x p e n s i v e ) } % ` " > < / s p a n > < s p a n c l a s s = " p s m - b a r p s m - b a r - - c h e a p " : s t y l e = " { w i d t h : ` $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . t o o C h e a p ) } % ` } " : t i t l e = " ` 太 便 宜 不 买 : $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . t o o C h e a p ) } % ` " > < / s p a n > < s p a n c l a s s = " p s m - b a r p s m - b a r - - a c c e p t a b l e " : s t y l e = " { w i d t h : ` $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . a c c e p t a b l e ) } % ` } " : t i t l e = " ` 偏 高 可 接 受 : $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . a c c e p t a b l e ) } % ` " > < / s p a n > < s p a n c l a s s = " p s m - b a r p s m - b a r - - b a r g a i n " : s t y l e = " { w i d t h : ` $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . b a r g a i n ) } % ` } " : t i t l e = " ` 便 宜 划 算 : $ { c o m p r e h e n s i v e P s m P e r c e n t ( r o w . b a r g a i n ) } % ` " > < / s p a n > < / d i v > < / a r t i c l e > < / d i v > < / d i v >
< / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > ( 5 ) 汇总四种定价方法的测算结果 , 综合分析确定最终定价 , 并说明定价依据与利润空间 。 < / p > < div class = "pricing-summary-grid" > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table pricing-summary-table" > < thead > < tr > < th > 定价方法 < / th > < th > 测算结果 < / th > < th > 毛利润率 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepSeven.summaryRows" :key="row.method"><th>{{ row.method }}</th><td><el-input v-model="row.result" placeholder="填写测算结果" /></td><td><el-input v-model="row.margin" placeholder="填写毛利润率" /></td></tr></tbody></table></div><label class="final-pricing-analysis" > 最 终 量 产 定 价 分 析 : < el -input v -model = " comprehensiveStepSeven.finalPricingAnalysis " type = "textarea" : autosize = "{ minRows: 8, maxRows: 16 }" placeholder = "填写最终量产定价分析" / > < / label > < / div > < / div >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 8 " class = "comprehensive-step-card comprehensive-step-eight" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 1 、 完成产品开发与定价后 , 产品正式上线销售 。 店铺原有产品线一般包含多个品类 , 新上架的产品需要融入现有品类体系 , 明确在全店品类体系中的定位 , 进行针对性的运营资源配置 。 请根据案例资料提供的店铺各品类近12个月营收数据 , 对各品类按照销售额从大到小进行排序 , 并计算销售额占比和累计销售额占比 ; 累计占比前70 % 的品类划为A类核心品类 , 70 % ~ 90 % 区间的品类划为B类潜力品类 , 最后10 % 的品类划为C类长尾品类 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveAbcRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepEight.abcRows.length <= 1" @click ="removeComprehensiveAbcRow" > 删 除 一 行 < / el -button > < el -button class = "compact-action" @click ="exportComprehensiveAbc" > 导 出 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table abc-table" > < thead > < tr > < th > 品类名称 < / th > < th > 营收贡献 ( 万元 ) < / th > < th > 销售额占比 ( % ) < / th > < th > 累计占比 ( % ) < / th > < th > ABC分层判定 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepEight.abcRows" :key="row.id"><th><el-input v-model="row.name" placeholder="填写品类名称" /></th><td><el-input v-model="row.revenue" placeholder="填写营收贡献" /></td><td><el-input v-model="row.salesShare" placeholder="填写占比" /></td><td><el-input v-model="row.cumulativeShare" placeholder="填写累计占比" /></td><td><el-select v-model="row.classification" placeholder="请选择" > < el -option label = "A类核心品类" value = "A类核心品类" / > < el -option label = "B类潜力品类" value = "B类潜力品类" / > < el -option label = "C类长尾品类" value = "C类长尾品类" / > < / e l - s e l e c t > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < / d i v >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 2 、 作为战略新品 , 一般营收贡献较低 , 被归入C类长尾品类 。 但新品具有技术领先性和市场增长潜力 , 公司需对其进行流量扶持 , 需要进行新品分层优化调整 。 < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table new-product-table" > < thead > < tr > < th > 新品名称 < / th > < th > 当前分层 < / th > < th > 调整必要性分析 < / th > < th > 建议调整分层 < / th > < th > 流量扶持措施 < / th > < / tr > < / thead > < tbody > < tr > < td > < el -input v-model ="comprehensiveStepEight.newProduct.name" placeholder="填写新品名称" /></td><th>C类长尾品类</th><td><el-input v-model="comprehensiveStepEight.newProduct.rationale" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写调整必要性分析" /></td><td><el-select v-model="comprehensiveStepEight.newProduct.targetClass" placeholder="请选择" > < el -option label = "A类核心品类" value = "A类核心品类" / > < el -option label = "B类潜力品类" value = "B类潜力品类" / > < el -option label = "C类长尾品类" value = "C类长尾品类" / > < / e l - s e l e c t > < / t d > < t d > < e l - i n p u t v - m o d e l = " c o m p r e h e n s i v e S t e p E i g h t . n e w P r o d u c t . s u p p o r t " t y p e = " t e x t a r e a " : a u t o s i z e = " { m i n R o w s : 3 , m a x R o w s : 6 } " p l a c e h o l d e r = " 填 写 流 量 扶 持 措 施 " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < / d i v >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 3 、 根据ABC分层结果 , 针对A类核心品类 、 B类潜力品类 、 C类长尾品类和新品分别制定差异化的运营策略 、 库存策略和资源配置方案 。 结合案例资料提供的各品类SKU数 、 动销率 、 毛利率 、 库存周转率等经营数据 , 填写下表 : < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table abc-strategy-table" > < thead > < tr > < th > ABC分层 < / th > < th > 运营策略 < / th > < th > 库存策略 < / th > < th > 资源配置优先级 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepEight.strategyRows" :key="row.level" > < th > { { row . level } } < / th > < td > < el -input v -model = " row.operation " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写运营策略" / > < / td > < td > < el -input v -model = " row.inventory " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写库存策略" / > < / td > < td > < el -input v -model = " row.priority " placeholder = "填写资源优先级" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 4 、 完成品类结构优化后 , 新品已明确在全店品类体系中的定位 。 接下来需要选择合适的线上发布渠道 。 不同维度对新品渠道效果的影响不一样 , 请给以下几个核心评估维度设置权重 , 按照重要性分配权重 , 总权重合计100 % 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveChannelWeightRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepEight.channelWeightRows.length <= 1" @click ="removeComprehensiveChannelWeightRow" > 删 除 一 行 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table channel-weight-table" > < thead > < tr > < th > 评估维度 < / th > < th > 维度说明 < / th > < th > 权重占比 ( % ) < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepEight.channelWeightRows" :key="row.id"><th><el-input v-model="row.name" placeholder="填写评估维度" /></th><td><el-input v-model="row.description" placeholder="填写维度说明" /></td><td><el-input v-model="row.weight" placeholder="填写权重" /></td></tr><tr class="computed-row"><th colspan="2" > 总 权 重 < / th > < td > { { comprehensiveChannelWeightTotal } } < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < div class = "comprehensive-section-line" > < p > 5 、 根据案例资料提供的5大渠道近3个月数据 , 通过Min - Max归一法将各维度原始数据转换为0 - 10 分标准化得分并根据指标权重计算加权综合得分 : < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveChannelScoreRow" > 增 加 一 行 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepEight.channelScoreRows.length <= 1" @click ="removeComprehensiveChannelScoreRow" > 删 除 一 行 < / el -button > < el -button class = "compact-action" @click ="exportComprehensiveChannelScores" > 导 出 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table channel-score-table" > < thead > < tr > < th > 渠道 < / th > < th v-for ="dimension in comprehensiveStepEight.channelWeightRows" :key="dimension.id">{{ dimension.name || "评估维度" }}得分</th><th>加权得分</th></tr></thead><tbody><tr v-for="row in comprehensiveStepEight.channelScoreRows" :key="row.id"><th><el-input v-model="row.channel" placeholder="填写渠道" /></th><td v-for="dimension in comprehensiveStepEight.channelWeightRows" :key="dimension.id"><el-input v-model="row.scores[dimension.id]" placeholder="0-10" /></td><td class="rice-score" > {{ comprehensiveChannelReleaseScore ( row ) }} < / td > < / tr > < / tbody > < / table > < / div > < / div >
< label class = "comprehensive-conclusion" > 6 、 请根据以上计算结果 , 结合案例文档内容 , 分析所选核心渠道的优势势及运营建议 : < el -input v -model = " comprehensiveStepEight.channelConclusion " type = "textarea" : autosize = "{ minRows: 8, maxRows: 16 }" placeholder = "填写核心渠道优势及运营建议" / > < / label >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 9 " class = "comprehensive-step-card comprehensive-step-nine" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 1 、 完成发布渠道评估后 , 新品已选定核心发布渠道并进入投放运营阶段 。 新品投放不是 “ 一锤子买卖 ” , 需要在预算有限的前提下 , 先定预算 、 再定打法 , 然后根据真实投放数据核算效果 、 针对不达标渠道做出优化决策 。 < / p > < div class = "budget-total-input" > < label > 请设置新品投放试错预算 : < el -input v-model ="comprehensiveStepNine.totalBudget" placeholder="填写预算" /><span>元</span></label></div><p class="comprehensive-instruction">投放周期分为冷启动期、增长期、稳定期三个阶段。请将预算分配到三个渠道和三个时间段中。</p><div class="comprehensive-section-line"><span></span><div><el-button class="compact-action" @click="addComprehensiveBudgetRow">增加一行</el-button><el-button class="compact-action" :disabled="comprehensiveStepNine.budgetRows.length <= 1" @click="removeComprehensiveBudgetRow">删除一行</el-button></div></div><div class="comprehensive-table-wrap"><table class="comprehensive-table budget-table"><thead><tr><th>渠道</th><th>渠道定位</th><th>预算占比(%) </th><th>预算金额</th><th>冷启动期投放预算</th><th>增长期投放预算</th><th>稳定期投放预算</th></tr></thead><tbody><tr v-for="row in comprehensiveStepNine.budgetRows" :key="row.id"><td><el-input v-model="row.channel" placeholder="填写渠道" /></td><td><el-select v-model="row.positioning" placeholder="请选择"><el-option label="核心转化渠道" value="核心转化渠道" /><el-option label="内容种草渠道" value="内容种草渠道" /><el-option label="流量测试渠道" value="流量测试渠道" /></el-select></td><td><el-input v-model="row.share" placeholder="填写占比" /></td><td class="rice-score">{{ comprehensiveBudgetAmount(row) }}</td><td><el-input v-model="row.launchBudget" placeholder="填写预算" /></td><td><el-input v-model="row.growthBudget" placeholder="填写预算" /></td><td><el-input v-model="row.stableBudget" placeholder="填写预算" /></td></tr><tr class="computed-row"><th colspan="2">合计</th><td>{{ comprehensiveBudgetShareTotal }}</td><td>{{ comprehensiveBudgetAmountTotal }}</td><td colspan="3" > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 2 、 产品投放结束后 , 企业从各渠道后台导出投放数据 , 核算每个渠道的点击率和ROI , 判断是否达标 。 请从案例资料中获取各渠道的真实投放数据 , 填入下表并核算各渠道的点击率与ROI ( 投产比 ) 。 < / p > < div class = "roi-formula-grid" > < label > 产品毛利率 : < el -input v-model ="comprehensiveStepNine.grossMargin" placeholder="填写毛利率" /><span>%</span></label><label>保本ROI: <span class="calculated-field">{{ comprehensiveBreakEvenRoi }}</span><span>计算结果保留两位小数</span></label></div><p class="roi-note">保本ROI = 1 ÷ 毛利率;当投产比达到该数值时,推广花费刚好等于产品的毛利额,不会产生亏损,实现投放盈亏平衡。</p><p class="roi-note">企业投放目标为整体ROI不低于保本ROI。请依据案例数据核算结果, 判断各渠道投放是否达标; 对不达标渠道, 根据案例从选项池中选择最可能的问题诊断与优化动作。</p><div class="comprehensive-table-wrap"><table class="comprehensive-table roi-table"><thead><tr><th>渠道</th><th>点击率</th><th>ROI</th><th>是否达标</th><th>问题诊断</th><th>优化动作</th></tr></thead><tbody><tr v-for="row in comprehensiveStepNine.roiRows" :key="row.id"><td><el-input v-model="row.channel" placeholder="填写渠道" /></td><td><el-input v-model="row.clickRate" placeholder="填写点击率" /></td><td><el-input v-model="row.roi" placeholder="填写ROI" /></td><td><el-select v-model="row.qualified" placeholder="请选择" > < el -option label = "达标" value = "达标" / > < el -option label = "未达标" value = "未达标" / > < / e l - s e l e c t > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . d i a g n o s i s " t y p e = " t e x t a r e a " : a u t o s i z e = " { m i n R o w s : 2 , m a x R o w s : 5 } " p l a c e h o l d e r = " 填 写 问 题 诊 断 " / > < / t d > < t d > < e l - i n p u t v - m o d e l = " r o w . a c t i o n " t y p e = " t e x t a r e a " : a u t o s i z e = " { m i n R o w s : 2 , m a x R o w s : 5 } " p l a c e h o l d e r = " 填 写 优 化 动 作 " / > < / t d > < / t r > < / t b o d y > < / t a b l e > < / d i v > < / d i v >
< div class = "comprehensive-analysis-section" > < p class = "comprehensive-instruction" > 3 、 完成投放评估与优化后 , 新品已进入运营阶段 。 新品 “ 上架 ” 并不意味着结束 , 投放数据只能告诉我们 “ 卖得怎么样 ” , 却回答不了 “ 为什么卖不好 ” 。 产品问题诊断就是运营的听诊器 , 需要从数据漏斗 、 用户评论 、 对比测试三个层面找到问题根源 , 再输出可落地的产品迭代方案 。 < / p > < div class = "pricing-subsection" > < div class = "comprehensive-section-line" > < p > ( 1 ) 数据漏斗 。 从用户触达产品的全路径出发 , 搭建从曝光到复购的完整数据漏斗 , 逐层计算各环节转化率并对比行业均值 , 定位异常节点 。 < / p > < div > < el -button class = "compact-action" @click ="addComprehensiveFunnelChannel" > 增 加 一 列 < / el -button > < el -button class = "compact-action" : disabled = "comprehensiveStepNine.funnelChannels.length <= 1" @click ="removeComprehensiveFunnelChannel" > 删 除 一 列 < / el -button > < / div > < / div > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table funnel-table" > < thead > < tr > < th > 链路节点 < / th > < th v-for ="channel in comprehensiveStepNine.funnelChannels" :key="channel.id"><el-input v-model="channel.name" placeholder="填写渠道" /></th><th>行业均值</th></tr></thead><tbody><tr v-for="row in comprehensiveStepNine.funnelRows" :key="row.node"><th>{{ row.node }}</th><td v-for="channel in comprehensiveStepNine.funnelChannels" :key="channel.id"><el-input v-model="row.values[channel.id]" placeholder="填写转化率" /></td><td><el-input v-model="row.industryAverage" placeholder="填写行业均值" /></td></tr></tbody></table></div><div class="comprehensive-section-line funnel-diagnosis-heading"><p>对比案例行业均值数据,定位异常节点。</p><div><el-button class="compact-action" @click="addComprehensiveFunnelDiagnosis">增加一行</el-button><el-button class="compact-action" :disabled="comprehensiveStepNine.funnelDiagnoses.length <= 1" @click="removeComprehensiveFunnelDiagnosis">删除一行</el-button></div></div><div class="comprehensive-table-wrap"><table class="comprehensive-table funnel-diagnosis-table"><thead><tr><th>渠道</th><th>链路节点</th><th>店铺转化率</th><th>行业均值转化率</th><th>异常分析与判定</th></tr></thead><tbody><tr v-for="row in comprehensiveStepNine.funnelDiagnoses" :key="row.id" > < td > < el -input v -model = " row.channel " placeholder = "填写渠道" / > < / td > < td > < el -input v -model = " row.node " placeholder = "填写节点" / > < / td > < td > < el -input v -model = " row.storeRate " placeholder = "填写转化率" / > < / td > < td > < el -input v -model = " row.industryRate " placeholder = "填写行业均值" / > < / td > < td > < el -input v -model = " row.analysis " type = "textarea" : autosize = "{ minRows: 2, maxRows: 5 }" placeholder = "填写异常分析与判定" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< div class = "pricing-subsection review-analysis-section" > < p > ( 2 ) 用户评论分析 。 漏斗只能定位 “ 哪个环节出了问题 ” , 要回答 “ 为什么出问题 ” , 需要分析用户最真实的反馈 。 < / p > < div class = "review-analysis-grid" > < article class = "comment-upload-card" > < input ref = "stepNineCommentInput" class = "hidden-file-input" type = "file" accept = ".xls,.xlsx,.csv" @change ="handleStepNineCommentFileChange" / > < button type = "button" class = "upload-btn" : disabled = "stepNineUploading || isDemo" @click ="triggerStepNineCommentUpload" > {{ stepNineUploading ? " 导 入 中 ... " : " 导 入 评 论 数 据 生 成 词 云 " }} < / button > < p > 支持 XLS 、 XLSX 、 CSV , 上传后预览首个工作表 。 < / p > < div v-if ="comprehensiveStepNine.commentAttachment.name" class="comprehensive-file-preview"><div class="excel-preview"><strong>{{ comprehensiveStepNine.commentAttachment.name }}</strong><span>工作表:{{ comprehensiveStepNine.commentAttachment.preview?.sheetName || "首个工作表" }}</span><div v-if="comprehensiveStepNine.commentAttachment.preview?.headers?.length" class="excel-preview-table"><table><thead><tr><th v-for="header in comprehensiveStepNine.commentAttachment.preview.headers" :key="header">{{ header }}</th></tr></thead><tbody><tr v-for="(previewRow, index) in comprehensiveStepNine.commentAttachment.preview.rows" :key="index"><td v-for="(cell, cellIndex) in previewRow" :key ="cellIndex" > {{ cell }} < / td > < / tr > < / tbody > < / table > < / div > < / div > < div class = "file-preview-actions" > < a :href ="comprehensiveStepNine.commentAttachment.url" target = "_blank" rel = "noopener" > 打开原文件 < / a > < button type = "button" @click ="removeStepNineCommentAttachment" > 移 除 < / button > < / div > < / div > < div v-if ="comprehensiveCommentCloudTerms.length" class="comment-cloud" aria-label="评论词云"><span v-for="term in comprehensiveCommentCloudTerms" :key="term.text" :style="{ fontSize: `${term.size}px` }">{{ term.text }}</span></div></article><div class="top-issues"><p>请根据词云图填写词云中出现频次最高的TOP3问题。</p><label>TOP1: <el-input v-model="comprehensiveStepNine.topIssues.topOne" placeholder="填写问题" /></label><label>TOP2: <el-input v-model="comprehensiveStepNine.topIssues.topTwo" placeholder="填写问题" /></label><label>TOP3: <el-input v-model="comprehensiveStepNine.topIssues.topThree" placeholder="填写问题" /></label></div></div><div class="sentiment-section"><p>请点击情感倾向分析按钮,系统自动判断各情感类型评论数量,并计算各情感类型占比。</p><div><el-button class="compact-action" @click="analyzeComprehensiveSentiment">情感倾向分析</el-button><el-button class="compact-action" @click="stepNineSentimentVisible = !stepNineSentimentVisible">{{ stepNineSentimentVisible ? "收起可视化" : "情感倾向分析可视化" }}</el-button></div><div class="comprehensive-table-wrap"><table class="comprehensive-table sentiment-table"><thead><tr><th>情感类型</th><th>评论数量(条)</th><th>占比</th></tr></thead><tbody><tr v-for="row in comprehensiveStepNine.sentimentRows" :key="row.type"><th>{{ row.type }}</th><td>{{ row.count }}</td><td>{{ row.ratio }}</td></tr></tbody></table></div><div v-if="stepNineSentimentVisible" class="sentiment-visualization"><span v-for="row in comprehensiveStepNine.sentimentRows" :key="`visual-${row.type}`"><strong>{{ row.type }}</strong><i :style="{ width: `${comprehensiveSentimentPercent(row.ratio)}%` }" > < / i > < em > { { row . ratio } } < / em > < / span > < / div > < / div > < / div >
< div class = "pricing-subsection" > < p > ( 3 ) 找到根因后 , 还需验证 “ 改进方案是否真的有效 ” , 避免拍脑袋迭代 。 AB测试对照诊断法将用户随机分为对照组 ( 维持原方案 ) 和实验组 ( 应用改进方案 ) , 对比两组的关键指标差异 , 用数据判断改进方案是否值得落地 。 < / p > < p > 请从案例资料中获取AB测试两组数据 , 计算后填入下表并逐项判定 : < / p > < div class = "comprehensive-table-wrap" > < table class = "comprehensive-table ab-test-table" > < thead > < tr > < th > 测试指标 < / th > < th > 对照组数据 < / th > < th > 实验组数据 < / th > < th > 判定与分析 < / th > < / tr > < / thead > < tbody > < tr v-for ="row in comprehensiveStepNine.abRows" :key="row.metric" > < th > { { row . metric } } < / th > < td > < el -input v -model = " row.control " placeholder = "填写对照组数据" / > < / td > < td > < el -input v -model = " row.experiment " placeholder = "填写实验组数据" / > < / td > < td > < el -input v -model = " row.analysis " placeholder = "填写判定与分析" / > < / td > < / tr > < / tbody > < / table > < / div > < / div >
< / div >
< label class = "comprehensive-conclusion" > 4 、 结合全链路数据漏斗诊断 、 售后评论分析法及AB测试对照诊断 , 分析产品存在的问题 , 并提出具体的迭代优化建议 。 < el -input v -model = " comprehensiveStepNine.iterationConclusion " type = "textarea" : autosize = "{ minRows: 8, maxRows: 16 }" placeholder = "填写产品问题及迭代优化建议" / > < / label >
< / section >
< section v -else -if = " isComprehensiveCaseTraining & & currentStep = = = 10 " class = "comprehensive-step-card comprehensive-step-ten" >
< div class = "step-title-row" > < div > < p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p > < h3 > { { activeStepName } } < / h3 > < / div > < / div >
< div class = "comprehensive-section-line report-intro" > < p > 请根据前述实训步骤内容 , 撰写实训报告 , 也可以导出实训内容 , 借助AI大模型生成一份实训报告 , 实训报告框架如下所示 : < / p > < el -button class = "compact-action" @click ="exportOutcome" > 导 出 < / el -button > < / div >
< div class = "comprehensive-table-wrap" > < table class = "comprehensive-table report-framework-table" > < thead > < tr > < th > 模块 < / th > < th > 标题 < / th > < th > 内容要求 < / th > < / tr > < / thead > < tbody > < template v-for ="row in comprehensiveReportFramework" :key="row.module"><tr v-for="(section, index) in row.sections" :key="section.title"><th v-if="index === 0" :rowspan="row.sections.length" > {{ row.module }} < / th > < th > { { section . title } } < / th > < td > { { section . requirement } } < / td > < / tr > < / template > < / tbody > < / table > < / div >
< / section >
< section v -else -if = " ! processRows.length " class = "generic-step-card" >
< div v-if ="configuredSteps.length" class="step-title-row" >
< div >
< p class = "section-eyebrow" > 第 { { String ( currentStep ) . padStart ( 2 , "0" ) } } 步 < / p >
< h3 > { { activeStepName } } < / h3 >
< / div >
< / div >
< div v-if ="page.keyPoints?.length" class="key-point-grid" >
< article v-for ="item in page.keyPoints" :key="item" class="key-point-card" >
{ { item } }
< / article >
< / div >
< el -form class = "training-form" label -position = " top " >
< div class = "form-grid" >
< el -form -item v-for ="field in page.fields" :key="field.key" :label="field.label" >
< el -input
v - if = "field.type === 'textarea'"
v - model = "form[field.key]"
type = "textarea"
: autosize = "{ minRows: 4, maxRows: 8 }"
: placeholder = "field.placeholder"
/ >
< el -input v -else v -model = " form [ field.key ] " :placeholder ="field.placeholder" / >
< / e l - f o r m - i t e m >
< / div >
< / e l - f o r m >
< div v-if ="workRows.length" class="table-panel" >
< div class = "panel-title" >
< h4 > { { page . tableTitle || "实训工作表" } } < / h4 >
< p > 请结合自己的产品对象 , 在每一项后填写分析内容 , 形成可提交的实训成果 。 < / p >
< / div >
< el -table :data ="workRows" border class = "training-table" >
< el -table -column label = "项目" prop = "name" width = "190" / >
< el -table -column label = "说明" prop = "description" min -width = " 280 " / >
< el -table -column label = "学生填写" min -width = " 360 " >
< template # default = "{ row }" >
< el -input v -model = " row.answer " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写本项分析内容" / >
< / template >
< / e l - t a b l e - c o l u m n >
< / e l - t a b l e >
< / div >
< / section >
< footer v-if ="configuredSteps.length" class="step-navigation" aria-label="步骤操作" >
< el -button v-if ="!isFirstConfiguredStep" plain @click="goPreviousStep" > 上 一 步 < / el -button >
< el -button class = "btn-save" :loading ="saving" @click ="saveCurrentProgress" > 保 存 当 前 进 度 < / el -button >
< el -button v-if ="!isLastConfiguredStep" type="primary" @click="goNextStep" > 下 一 步 < / el -button >
< / footer >
< / section >
< / main >
< div class = "training-ai-tools" >
< TrainingAiSidebar
: task - key = "pageKey"
: has - saved - answer = "hasSavedAnswer"
: study - tip = "`当前任务建议:围绕“${page.title}”先明确产品对象,再按任务流程逐步补全分析内容。`"
: progress - items = "progressItems"
/ >
< / div >
< / div >
< / template >
< script setup >
import { Upload } from "@element-plus/icons-vue" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { getStudentTrainingAnswer , saveStudentTrainingAnswer , uploadStudentTrainingFile } from "@/api/studentTrainingAnswer" ;
import { getStudentTrainingPage } from "./studentTrainingPages" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue" ;
import { isStudentDemo } from "@/utils/studentDemo" ;
import { resolveProjectName , useProjectStudyTime } from "@/composables/useProjectStudyTime" ;
const route = useRoute ( ) ;
const { proxy } = getCurrentInstance ( ) ;
const props = defineProps ( {
previewTaskKey : { type : String , default : "" } ,
} ) ;
const pageKey = computed ( ( ) => props . previewTaskKey || route . meta ? . pageKey || route . params ? . pageKey ) ;
const isDemo = computed ( ( ) => isStudentDemo ( ) ) ;
const isProductDevelopmentProcess = computed ( ( ) => pageKey . value === "product-development-process" ) ;
const isComprehensiveCaseTraining = computed ( ( ) => pageKey . value === "comprehensive-case-training" ) ;
const remoteTask = ref ( null ) ;
const page = computed ( ( ) => mergeTrainingPage ( getStudentTrainingPage ( pageKey . value ) , remoteTask . value ) ) ;
const briefOpen = ref ( false ) ;
const taskBackground = computed ( ( ) => page . value . background || "请根据教师端配置的实训背景理解本次任务场景。" ) ;
const taskRequirement = computed ( ( ) => page . value . requirements || "请根据教师端配置的实训要求完成本次实训。" ) ;
const taskGoals = computed ( ( ) => {
const raw = page . value . goals ;
let list = [ ] ;
if ( Array . isArray ( raw ) ) {
list = raw ;
} else if ( typeof raw === "string" && raw . trim ( ) ) {
try {
const parsed = JSON . parse ( raw ) ;
list = Array . isArray ( parsed ) ? parsed : raw . split ( /[; ;|| ]/ ) ;
} catch ( error ) {
list = raw . split ( /[; ;|| ]/ ) ;
}
}
return list . map ( ( item ) => String ( item || "" ) . trim ( ) ) . filter ( Boolean ) ;
} ) ;
const projectName = computed ( ( ) => resolveProjectName ( pageKey . value , remoteTask . value ? . projectName , page . value ? . title ) ) ;
useProjectStudyTime ( projectName ) ;
const buildInitialForm = ( ) =>
Object . fromEntries ( ( page . value . fields || [ ] ) . map ( ( field ) => [ field . key , "" ] ) ) ;
const buildInitialRows = ( ) =>
( page . value . tableRows || [ ] ) . map ( ( [ name , description ] ) => ( {
name ,
description ,
answer : "" ,
} ) ) ;
const buildInitialProcessRows = ( ) =>
( page . value . processRows || [ ] ) . map ( ( row ) => ( {
... row ,
workContent : "" ,
} ) ) ;
const form = ref ( buildInitialForm ( ) ) ;
const workRows = ref ( buildInitialRows ( ) ) ;
const processRows = ref ( buildInitialProcessRows ( ) ) ;
const createComprehensiveStepOne = ( ) => ( { political : "" , economic : "" , social : "" , technology : "" , strengths : "" , weaknesses : "" , opportunities : "" , threats : "" , conclusion : "" } ) ;
const comprehensiveStepOne = ref ( createComprehensiveStepOne ( ) ) ;
const comprehensiveIndicators = [
{ key : "marketSize" , label : "市场规模" , suggestion : "建议25~35" , dimensions : [ { key : "searchVolume" , label : "搜索次数(万次)" , suggestion : "建议25~35" } , { key : "transactionAmount" , label : "成交金额(万元)" , suggestion : "建议65~75" } ] } ,
{ key : "marketPotential" , label : "市场潜力" , suggestion : "建议25~35" , dimensions : [ { key : "transactionGrowth" , label : "成交金额增速(%) " , suggestion : "建议55~65" } , { key : "categoryGrowth" , label : "类目成交增速(%) " , suggestion : "建议35~45" } ] } ,
{ key : "competition" , label : "竞争强度" , suggestion : "建议20~30" , dimensions : [ { key : "supplyDemandRatio" , label : "需供比" , suggestion : "建议25~35" } , { key : "onlineProductCount" , label : "在线商品数" , suggestion : "建议35~45" } , { key : "onlineMerchantCount" , label : "在线商家数" , suggestion : "建议25~35" } ] } ,
{ key : "operations" , label : "运营壁垒" , suggestion : "建议10~20" , dimensions : [ { key : "organicTrafficRatio" , label : "自然流量占比(%) " , suggestion : "建议55~65" } , { key : "returnRate" , label : "退货率(%) " , suggestion : "建议35~45" } ] } ,
] ;
const createComprehensiveCategory = ( ) => ( { id : ` category- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name : "" , score : "" } ) ;
const createComprehensiveStepTwo = ( ) => ( {
primaryWeights : { marketSize : "" , marketPotential : "" , competition : "" , operations : "" } ,
dimensionWeights : { searchVolume : "" , transactionAmount : "" , transactionGrowth : "" , categoryGrowth : "" , supplyDemandRatio : "" , onlineProductCount : "" , onlineMerchantCount : "" , organicTrafficRatio : "" , returnRate : "" } ,
totalWeights : { searchVolume : "" , transactionAmount : "" , transactionGrowth : "" , categoryGrowth : "" , supplyDemandRatio : "" , onlineProductCount : "" , onlineMerchantCount : "" , organicTrafficRatio : "" , returnRate : "" } ,
categoryScores : [ createComprehensiveCategory ( ) , createComprehensiveCategory ( ) ] ,
reviews : [ { id : "review-1" , favorableRate : "" , painPoints : "" , unmetNeeds : "" } , { id : "review-2" , favorableRate : "" , painPoints : "" , unmetNeeds : "" } , { id : "review-3" , favorableRate : "" , painPoints : "" , unmetNeeds : "" } ] ,
developmentConclusion : "" ,
} ) ;
const comprehensiveStepTwo = ref ( createComprehensiveStepTwo ( ) ) ;
const comprehensiveUploadDefinitions = [
{ key : "rfmResult" , label : "上传RFM用户分层统计结果表" , hint : "支持 .xls、.xlsx, 上传后预览首个工作表" , accept : ".xls,.xlsx" , kind : "excel" } ,
{ key : "segmentationChart" , label : "上传客户分层占比图" , hint : "支持 PNG、JPG、WEBP 图片预览" , accept : "image/png,image/jpeg,image/webp" , kind : "image" } ,
{ key : "comparisonChart" , label : "上传客户分层平均消费对比柱状图" , hint : "支持 PNG、JPG、WEBP 图片预览" , accept : "image/png,image/jpeg,image/webp" , kind : "image" } ,
{ key : "rfmHeatmap" , label : "上传RFM分布热力图" , hint : "支持 PNG、JPG、WEBP 图片预览" , accept : "image/png,image/jpeg,image/webp" , kind : "image" } ,
] ;
const comprehensiveRfmAnalysisRows = [
{ key : "highValueShare" , label : "高价值用户占比" , focus : "重要价值客户占比是多少?是否足以支撑产品开发?" } ,
{ key : "activity" , label : "用户活跃度" , focus : "R分数分布反映的用户活跃情况如何? " } ,
{ key : "spendingPower" , label : "消费能力" , focus : "M分数分布反映的用户消费能力如何? " } ,
{ key : "loyalty" , label : "用户忠诚度" , focus : "F分数分布反映的用户复购意愿如何? " } ,
{ key : "keyCustomer" , label : "重点客户群特征" , focus : "哪类客户最值得关注?为什么?" } ,
] ;
const comprehensiveKanoTypes = [ "基本型" , "期望型" , "兴奋型" , "无差异型" , "反向型" ] ;
const createComprehensiveAttachment = ( ) => ( { name : "" , originalFilename : "" , url : "" , preview : null } ) ;
const createComprehensiveProfile = ( ) => ( { id : ` profile- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , userInfo : "" , behavior : "" , painPoints : "" , consumerPsychology : "" } ) ;
const createComprehensiveKanoRow = ( ) => ( { id : ` kano- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , feature : "" , kanoType : "" , direction : "" } ) ;
const createComprehensiveStepThree = ( ) => ( {
attachments : Object . fromEntries ( comprehensiveUploadDefinitions . map ( ( item ) => [ item . key , createComprehensiveAttachment ( ) ] ) ) ,
rfmAnalyses : Object . fromEntries ( comprehensiveRfmAnalysisRows . map ( ( item ) => [ item . key , "" ] ) ) ,
profiles : [ createComprehensiveProfile ( ) , createComprehensiveProfile ( ) , createComprehensiveProfile ( ) ] ,
kanoRows : Array . from ( { length : 12 } , createComprehensiveKanoRow ) ,
questionnaire : "" ,
} ) ;
const comprehensiveStepThree = ref ( createComprehensiveStepThree ( ) ) ;
const comprehensiveUploadInputs = ref ( { } ) ;
const comprehensiveUploading = ref ( "" ) ;
const comprehensiveSentiments = [ "正面" , "中性" , "负面" ] ;
const comprehensiveJourneyStages = [
{ stage : "问题感知" , behaviorPlaceholder : "填写用户行为描述" , touchpointPlaceholder : "填写关键触点" , decisionPlaceholder : "填写关键决策因素" } ,
{ stage : "信息搜索" , behaviorPlaceholder : "填写用户行为描述" , touchpointPlaceholder : "填写关键触点" , decisionPlaceholder : "填写关键决策因素" } ,
{ stage : "评估决策" , behaviorPlaceholder : "填写用户行为描述" , touchpointPlaceholder : "填写关键触点" , decisionPlaceholder : "填写关键决策因素" } ,
{ stage : "购买转化" , behaviorPlaceholder : "填写用户行为描述" , touchpointPlaceholder : "填写关键触点" , decisionPlaceholder : "填写关键决策因素" } ,
{ stage : "购后行为" , behaviorPlaceholder : "填写用户行为描述" , touchpointPlaceholder : "填写关键触点" , decisionPlaceholder : "填写关键决策因素" } ,
] ;
const createJourneyRow = ( item ) => ( { ... item , behavior : "" , touchpoints : "" , sentiment : "" , decisionFactors : "" } ) ;
const createPainRow = ( stage ) => ( { stage , painPoint : "" , suggestion : "" } ) ;
const createComprehensiveRequirementRow = ( ) => ( { id : ` requirement- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , userVoice : "" , scenarioInsight : "" , featureDefinition : "" , featureDescription : "" , acceptanceCriteria : "" } ) ;
const createComprehensiveRiceRow = ( ) => ( { id : ` rice- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , featureName : "" , kanoType : "" , reach : "" , impact : "" , confidence : "" , effort : "" , priority : "" } ) ;
const createComprehensiveStepFour = ( ) => ( {
journeyRows : comprehensiveJourneyStages . map ( createJourneyRow ) ,
painRows : comprehensiveJourneyStages . map ( ( item ) => createPainRow ( item . stage ) ) ,
requirements : Array . from ( { length : 5 } , createComprehensiveRequirementRow ) ,
riceRows : Array . from ( { length : 6 } , createComprehensiveRiceRow ) ,
valueCanvas : { productService : "" , painRelievers : "" , gainCreators : "" , customerPains : "" , customerGains : "" , customerJobs : "" } ,
versions : { versionOne : "" , versionTwo : "" } ,
} ) ;
const comprehensiveStepFour = ref ( createComprehensiveStepFour ( ) ) ;
const createComprehensiveArchitectureRow = ( ) => ( { id : ` architecture- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , scene : "" , hardwareInput : "" , driverInput : "" , algorithm : "" , application : "" , driverOutput : "" , hardwareOutput : "" } ) ;
const comprehensiveFaultTypes = [
{ type : "操作/配网类问题" , scenePlaceholder : "例如: 配网失败、APP找不到设备、唤醒后连不上网" } ,
{ type : "云端连接类故障" , scenePlaceholder : "例如: 配网成功但经常断连、云端识别超时、APP远程控制失效" } ,
{ type : "本地硬件简单故障" , scenePlaceholder : "例如:语音识别正常但无法控制相关功能(质保内)" } ,
{ type : "严重故障" , scenePlaceholder : "例如:通电无反应、模组烧坏、进水损坏(质保内)" } ,
{ type : "超出质保期" , scenePlaceholder : "填写故障场景" } ,
] ;
const comprehensivePolicyDimensions = [ "退换货政策" , "质保政策" , "维修服务" , "响应时效" , "补偿方案" ] ;
const comprehensiveSummaryItems = [ "质保期" , "响应承诺" , "特色服务" ] ;
const createComprehensiveStepFive = ( ) => ( {
architectureRows : Array . from ( { length : 4 } , createComprehensiveArchitectureRow ) ,
faultRows : comprehensiveFaultTypes . map ( ( item ) => ( { ... item , scene : "" , process : "" , rule : "" } ) ) ,
policyRows : comprehensivePolicyDimensions . map ( ( dimension ) => ( { dimension , content : "" , conditions : "" } ) ) ,
summaryRows : comprehensiveSummaryItems . map ( ( item ) => ( { item , content : "" } ) ) ,
} ) ;
const comprehensiveStepFive = ref ( createComprehensiveStepFive ( ) ) ;
const comprehensiveProcurementDimensions = [
{ key : "cost" , label : "采购成本" , description : "元器件/模组的单台采购总成本,含运费、税费" } ,
{ key : "delivery" , label : "交货周期" , description : "从下单到收货的时间,影响试产/生产进度" } ,
{ key : "quality" , label : "品质可靠性" , description : "元器件是否为正品,有无翻新/故障,规格匹配度" } ,
{ key : "flexibility" , label : "采购灵活性" , description : "是否支持小批量拆卖、零散采购、退换货" } ,
{ key : "service" , label : "服务支持" , description : "是否提供技术文档、售后支持,退换货是否方便" } ,
] ;
const comprehensiveBomNames = [ "主控芯片" , "外壳组件" , "透光罩" , "底座" , "供电方案" , "电源管理IC" , "语音识别方案" , "WiFi联网模组" , "麦克风" , "扬声器" ] ;
const comprehensiveManufacturingNames = [ "设备折旧费" , "生产运维费" , "人员薪酬" , "场地租赁费" ] ;
const createComprehensiveBomRow = ( name = "" ) => ( { id : ` bom- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name , specification : "" , smallCost : "" , largeCost : "" } ) ;
const createComprehensiveManufacturingRow = ( name = "" ) => ( { id : ` manufacturing- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name , smallCost : "" , largeCost : "" } ) ;
const createComprehensiveChannelRow = ( ) => ( { id : ` channel- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , channel : "" , cost : "" , delivery : "" , quality : "" , flexibility : "" , service : "" } ) ;
const createComprehensiveStepSix = ( ) => ( {
smallBatchLimit : "" ,
largeBatchLimit : "" ,
bomRows : comprehensiveBomNames . map ( ( name ) => createComprehensiveBomRow ( name ) ) ,
manufacturingRows : comprehensiveManufacturingNames . map ( ( name ) => createComprehensiveManufacturingRow ( name ) ) ,
costOptimization : "" ,
weightRows : comprehensiveProcurementDimensions . map ( ( item ) => ( { ... item , trialWeight : "" , massWeight : "" } ) ) ,
channelRows : Array . from ( { length : 5 } , createComprehensiveChannelRow ) ,
procurementRecommendation : "" ,
} ) ;
const comprehensiveStepSix = ref ( createComprehensiveStepSix ( ) ) ;
const comprehensiveForecastMonths = [ "预测第1个月( t=13) " , "预测第2个月( t=14) " , "预测第3个月( t=15) " ] ;
const comprehensiveCostPricingChannels = [
{ channel : "厂家给经销商" , suggestion : "建议15%~25%" } ,
{ channel : "厂家直接电商零售" , suggestion : "建议40%~60%" } ,
{ channel : "高端定位品牌" , suggestion : "建议70%~80%" } ,
] ;
const createComprehensiveForecastRow = ( month ) => ( { month , forecastSales : "" } ) ;
const createComprehensiveCompetitorRow = ( ) => ( { id : ` competitor- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , type : "" , brand : "" , price : "" , advantage : "" } ) ;
const createComprehensiveValueRow = ( ) => ( { id : ` value- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , item : "" , value : "" } ) ;
const createComprehensivePsmRow = ( range = "" ) => ( { id : ` psm- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , range , tooExpensive : "" , tooCheap : "" , acceptable : "" , bargain : "" } ) ;
const comprehensivePricingSummaryMethods = [ "成本加成定价" , "竞争定价" , "价值定价" , "PSM价格敏感度" ] ;
const createComprehensiveStepSeven = ( ) => ( {
initialStock : "30" ,
forecastRows : comprehensiveForecastMonths . map ( createComprehensiveForecastRow ) ,
unitCost : "" ,
costPricingRows : comprehensiveCostPricingChannels . map ( ( item ) => ( { ... item , markup : "" } ) ) ,
competitorRows : Array . from ( { length : 4 } , createComprehensiveCompetitorRow ) ,
competitorDecision : { benchmark : "" , factoryCost : "" , price : "" , analysis : "" } ,
valueRows : [ createComprehensiveValueRow ( ) , createComprehensiveValueRow ( ) ] ,
valueDiscount : "" ,
psmRows : [ "≤100元" , "100~150元" , "150~200元" , "200~250元" , "250~300元" , "≥300元" ] . map ( createComprehensivePsmRow ) ,
summaryRows : comprehensivePricingSummaryMethods . map ( ( method ) => ( { method , result : "" , margin : "" } ) ) ,
finalPricingAnalysis : "" ,
} ) ;
const comprehensiveStepSeven = ref ( createComprehensiveStepSeven ( ) ) ;
const psmVisualVisible = ref ( false ) ;
const comprehensiveAbcProducts = [
[ "LED触控化妆镜" , "580" ] , [ "便携折叠化妆镜" , "352" ] , [ "壁挂式化妆镜" , "230" ] , [ "桌面放大镜" , "183" ] , [ "美妆补光灯" , "125" ] , [ "智能美妆镜Pro" , "89" ] , [ "镜子配件/替换件" , "56" ] , [ "联名定制款" , "32" ] , [ "智能化妆镜(新品)" , "18" ] ,
] ;
const comprehensiveChannelDimensions = [
[ "市场规模潜力" , "对应关键词日均总搜索量,代表渠道用户需求大盘空间" , "建议权重: 20~25" ] ,
[ "竞争激烈程度" , "供需比(在线商品数/日均搜索量),数值越低赛道竞争越宽松" , "建议权重: 15~20" ] ,
[ "流量点击效率" , "品类平均点击率,代表渠道用户对该品类的点击意愿强弱" , "建议权重: 12~18" ] ,
[ "流量获取成本" , "平均CPC, 数值越低获取精准用户的推广成本越低" , "建议权重: 15~20" ] ,
[ "流量转化效率" , "品类平均转化率,代表渠道用户的最终下单意愿" , "建议权重: 12~18" ] ,
[ "现有用户基础" , "店铺现有该品类相关访客数,代表品牌存量用户承接能力" , "建议权重: 5~10" ] ,
[ "匹配度" , "渠道属性与产品定位的适配度打分( 1-10分) " , "建议权重: 5~10" ] ,
] ;
const createComprehensiveAbcRow = ( name = "" , revenue = "" ) => ( { id : ` abc- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name , revenue , salesShare : "" , cumulativeShare : "" , classification : "" } ) ;
const createComprehensiveChannelWeightRow = ( item = null ) => ( { id : ` channel-weight- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name : item ? . [ 0 ] || "" , description : item ? . [ 1 ] || "" , suggestion : item ? . [ 2 ] || "" , weight : "" , preset : Boolean ( item ) } ) ;
const createComprehensiveChannelScoreRow = ( channel = "" , dimensions = [ ] ) => ( { id : ` channel-score- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , channel , scores : Object . fromEntries ( dimensions . map ( ( item ) => [ item . id , "" ] ) ) } ) ;
const createComprehensiveStepEight = ( ) => {
const channelWeightRows = comprehensiveChannelDimensions . map ( ( item ) => createComprehensiveChannelWeightRow ( item ) ) ;
return {
abcRows : comprehensiveAbcProducts . map ( ( [ name , revenue ] ) => createComprehensiveAbcRow ( name , revenue ) ) ,
newProduct : { name : "" , rationale : "" , targetClass : "" , support : "" } ,
strategyRows : [ "A类核心品类" , "B类潜力品类" , "C类长尾品类" , "新品" ] . map ( ( level ) => ( { level , operation : "" , inventory : "" , priority : "" } ) ) ,
channelWeightRows ,
channelScoreRows : [ "天猫" , "京东" , "抖音" , "拼多多" , "唯品会" ] . map ( ( channel ) => createComprehensiveChannelScoreRow ( channel , channelWeightRows ) ) ,
channelConclusion : "" ,
} ;
} ;
const comprehensiveStepEight = ref ( createComprehensiveStepEight ( ) ) ;
const comprehensiveChannelWeightTotal = computed ( ( ) => comprehensiveStepEight . value . channelWeightRows . reduce ( ( total , row ) => total + comprehensiveNumber ( row . weight ) , 0 ) . toFixed ( 2 ) ) ;
const createComprehensiveBudgetRow = ( ) => ( { id : ` budget- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , channel : "" , positioning : "" , share : "" , launchBudget : "" , growthBudget : "" , stableBudget : "" } ) ;
const createComprehensiveRoiRow = ( ) => ( { id : ` roi- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , channel : "" , clickRate : "" , roi : "" , qualified : "" , diagnosis : "" , action : "" } ) ;
const createComprehensiveFunnelChannel = ( name = "" ) => ( { id : ` funnel-channel- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , name } ) ;
const comprehensiveFunnelNodes = [ "曝光-点击" , "点击-加购" , "加购-下单" , "下单-好评" , "好评-复购" ] ;
const createComprehensiveFunnelRow = ( node , channels = [ ] ) => ( { node , values : Object . fromEntries ( channels . map ( ( channel ) => [ channel . id , "" ] ) ) , industryAverage : "" } ) ;
const createComprehensiveFunnelDiagnosis = ( ) => ( { id : ` funnel-diagnosis- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` , channel : "" , node : "" , storeRate : "" , industryRate : "" , analysis : "" } ) ;
const createComprehensiveAbRow = ( metric ) => ( { metric , control : "" , experiment : "" , analysis : "" } ) ;
const comprehensiveAbMetrics = [ "点击-加购转化率" , "加购-下单转化率" , "下单-好评转化率" , "详情页跳出率" , "退货率" ] ;
const createComprehensiveStepNine = ( ) => {
const funnelChannels = [ "天猫" , "京东" , "抖音" , "拼多多" ] . map ( createComprehensiveFunnelChannel ) ;
return {
totalBudget : "" ,
budgetRows : [ createComprehensiveBudgetRow ( ) , createComprehensiveBudgetRow ( ) , createComprehensiveBudgetRow ( ) ] ,
grossMargin : "" ,
roiRows : [ createComprehensiveRoiRow ( ) , createComprehensiveRoiRow ( ) ] ,
funnelChannels ,
funnelRows : comprehensiveFunnelNodes . map ( ( node ) => createComprehensiveFunnelRow ( node , funnelChannels ) ) ,
funnelDiagnoses : [ createComprehensiveFunnelDiagnosis ( ) ] ,
commentAttachment : createComprehensiveAttachment ( ) ,
topIssues : { topOne : "" , topTwo : "" , topThree : "" } ,
sentimentRows : [ "正面" , "中性" , "负面" ] . map ( ( type ) => ( { type , count : 0 , ratio : "0.00%" } ) ) ,
abRows : comprehensiveAbMetrics . map ( createComprehensiveAbRow ) ,
iterationConclusion : "" ,
} ;
} ;
const comprehensiveStepNine = ref ( createComprehensiveStepNine ( ) ) ;
const stepNineCommentInput = ref ( null ) ;
const stepNineUploading = ref ( false ) ;
const stepNineSentimentVisible = ref ( false ) ;
const comprehensiveReportFramework = [
{
module : "市场洞察" ,
sections : [
{ title : "行业需求及竞争环境分析" , requirement : "进行PEST分析和SWOT分析, 梳理目标赛道近1年市场规模增速, 盘点3-5家头部竞品的核心优势、定价区间与用户痛点, 总结行业现存的差异化机会点和风险点, 形成基础市场判断。" } ,
{ title : "数据化选品与市场机会识别" , requirement : "通过公开销量、用户评价数据量化竞品短板,拆解细分赛道空白区间,输出可落地的产品切入方向,避免同质化竞争。" } ,
] ,
} ,
{
module : "需求分析" ,
sections : [
{ title : "用户需求行为数据分析" , requirement : "根据用户行为数据进行RFM分析及客户分层, 抓取电商平台、社交平台的用户评论高频关键词, 进行重点用户画像, 锚定产品核心目标人群, 设计调查问卷, 定量分析用户需求痛点, 明确产品核心功能与差异化卖点, 对齐后续设计方向。" } ,
{ title : "消费场景解构及功能定位" , requirement : "基于用户旅程地图,场景化进一步洞察用户需求痛点,把用户需求转化为产品功能定义并进行优先级评估,剔除伪需求和非必要功能点,基于产品价值主张画布,明确产品概念定位及核心价值。" } ,
] ,
} ,
{ module : "产品规划与设计" , sections : [ { title : "产品架构与售后服务设计" , requirement : "梳理产品硬件/软件的分层功能架构,同步规划售后退换货周期、质保规则、故障响应流程,覆盖产品全生命周期的服务需求。" } ] } ,
{
module : "产品开发与测试验证" ,
sections : [
{ title : "BOM估算与供应渠道评估" , requirement : "核算单台产品元器件物料总成本, 从采购成本、交货周期、品质可靠性、采购灵活性、服务支持五个维度对比5个以上供应渠道, 筛选出最优供应链组合方案。" } ,
{ title : "需求预测及产品定价" , requirement : "进行需求预测, 结合成本、竞品定价、用户付费意愿及PSM价格敏感度测试完成定价测算, 通过小范围用户访谈验证需求匹配度, 最终确定产品上市指导价。" } ,
] ,
} ,
{
module : "产品上线与运营推广" ,
sections : [
{ title : "品类结构优化与发布渠道评估" , requirement : "进行品类结构优化,明确新品品牌定位与视觉体系,对比线上电商、线下渠道的获客成本与转化效率,选定首批核心发布渠道组合。" } ,
{ title : "投放效果评估与问题诊断" , requirement : "上线后跟踪曝光、转化、复购核心数据,定位投放环节的低效问题,输出迭代优化方案,完成首轮运营复盘。" } ,
] ,
} ,
] ;
const comprehensiveBreakEvenRoi = computed ( ( ) => {
const margin = comprehensiveNumber ( comprehensiveStepNine . value . grossMargin ) ;
return margin > 0 ? ( 100 / margin ) . toFixed ( 2 ) : "—" ;
} ) ;
const comprehensiveBudgetShareTotal = computed ( ( ) => comprehensiveStepNine . value . budgetRows . reduce ( ( total , row ) => total + comprehensiveNumber ( row . share ) , 0 ) . toFixed ( 2 ) ) ;
const comprehensiveBudgetAmountTotal = computed ( ( ) => comprehensiveStepNine . value . budgetRows . reduce ( ( total , row ) => total + comprehensiveNumber ( comprehensiveBudgetAmount ( row ) ) , 0 ) . toFixed ( 2 ) ) ;
const comprehensiveCommentCloudTerms = computed ( ( ) => buildComprehensiveCommentCloud ( comprehensiveStepNine . value . commentAttachment . preview ) ) ;
const comprehensiveCompetitorMargin = computed ( ( ) => {
const price = comprehensiveNumber ( comprehensiveStepSeven . value . competitorDecision . price ) ;
const cost = comprehensiveNumber ( comprehensiveStepSeven . value . competitorDecision . factoryCost ) ;
if ( ! price || ! hasMeaningfulAnswer ( comprehensiveStepSeven . value . competitorDecision . price ) || ! hasMeaningfulAnswer ( comprehensiveStepSeven . value . competitorDecision . factoryCost ) ) return "—" ;
return ( ( price - cost ) / price * 100 ) . toFixed ( 2 ) ;
} ) ;
const comprehensiveValueTotal = computed ( ( ) => comprehensiveStepSeven . value . valueRows . reduce ( ( total , row ) => total + comprehensiveNumber ( row . value ) , 0 ) . toFixed ( 2 ) ) ;
const comprehensiveValuePrice = computed ( ( ) => {
const discount = comprehensiveNumber ( comprehensiveStepSeven . value . valueDiscount ) ;
if ( ! hasMeaningfulAnswer ( comprehensiveStepSeven . value . valueDiscount ) ) return "—" ;
return ( Number ( comprehensiveValueTotal . value ) * discount / 100 ) . toFixed ( 2 ) ;
} ) ;
const hasSavedAnswer = ref ( false ) ;
const hasAnyAnswer = computed ( ( ) =>
Object . values ( form . value ) . some ( hasMeaningfulAnswer ) ||
workRows . value . some ( ( row ) => hasMeaningfulAnswer ( row . answer ) ) ||
processRows . value . some ( ( row ) => hasMeaningfulAnswer ( row . workContent ) ) ||
Object . values ( comprehensiveStepOne . value ) . some ( hasMeaningfulAnswer ) ||
Object . values ( comprehensiveStepTwo . value . primaryWeights ) . some ( hasMeaningfulAnswer ) ||
Object . values ( comprehensiveStepTwo . value . dimensionWeights ) . some ( hasMeaningfulAnswer ) ||
Object . values ( comprehensiveStepTwo . value . totalWeights ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepTwo . value . categoryScores . some ( ( item ) => hasMeaningfulAnswer ( item . name ) || hasMeaningfulAnswer ( item . score ) ) ||
comprehensiveStepTwo . value . reviews . some ( ( item ) => hasMeaningfulAnswer ( item . favorableRate ) || hasMeaningfulAnswer ( item . painPoints ) || hasMeaningfulAnswer ( item . unmetNeeds ) ) ||
hasMeaningfulAnswer ( comprehensiveStepTwo . value . developmentConclusion ) ||
Object . values ( comprehensiveStepThree . value . attachments ) . some ( ( item ) => hasMeaningfulAnswer ( item ? . url ) ) ||
Object . values ( comprehensiveStepThree . value . rfmAnalyses ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepThree . value . profiles . some ( ( item ) => [ item . userInfo , item . behavior , item . painPoints , item . consumerPsychology ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepThree . value . kanoRows . some ( ( item ) => [ item . feature , item . kanoType , item . direction ] . some ( hasMeaningfulAnswer ) ) ||
hasMeaningfulAnswer ( comprehensiveStepThree . value . questionnaire ) ||
comprehensiveStepFour . value . journeyRows . some ( ( item ) => [ item . behavior , item . touchpoints , item . sentiment , item . decisionFactors ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFour . value . painRows . some ( ( item ) => [ item . painPoint , item . suggestion ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFour . value . requirements . some ( ( item ) => [ item . userVoice , item . scenarioInsight , item . featureDefinition , item . featureDescription , item . acceptanceCriteria ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFour . value . riceRows . some ( ( item ) => [ item . featureName , item . kanoType , item . reach , item . impact , item . confidence , item . effort , item . priority ] . some ( hasMeaningfulAnswer ) ) ||
Object . values ( comprehensiveStepFour . value . valueCanvas ) . some ( hasMeaningfulAnswer ) ||
Object . values ( comprehensiveStepFour . value . versions ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepFive . value . architectureRows . some ( ( item ) => [ item . scene , item . hardwareInput , item . driverInput , item . algorithm , item . application , item . driverOutput , item . hardwareOutput ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFive . value . faultRows . some ( ( item ) => [ item . scene , item . process , item . rule ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFive . value . policyRows . some ( ( item ) => [ item . content , item . conditions ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepFive . value . summaryRows . some ( ( item ) => hasMeaningfulAnswer ( item . content ) ) ||
[ comprehensiveStepSix . value . smallBatchLimit , comprehensiveStepSix . value . largeBatchLimit , comprehensiveStepSix . value . costOptimization , comprehensiveStepSix . value . procurementRecommendation ] . some ( hasMeaningfulAnswer ) ||
comprehensiveStepSix . value . bomRows . some ( ( item ) => [ item . name , item . specification , item . smallCost , item . largeCost ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepSix . value . manufacturingRows . some ( ( item ) => [ item . name , item . smallCost , item . largeCost ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepSix . value . weightRows . some ( ( item ) => [ item . trialWeight , item . massWeight ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepSix . value . channelRows . some ( ( item ) => [ item . channel , item . cost , item . delivery , item . quality , item . flexibility , item . service ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepSeven . value . forecastRows . some ( ( item ) => hasMeaningfulAnswer ( item . forecastSales ) ) ||
hasMeaningfulAnswer ( comprehensiveStepSeven . value . unitCost ) ||
comprehensiveStepSeven . value . costPricingRows . some ( ( item ) => hasMeaningfulAnswer ( item . markup ) ) ||
comprehensiveStepSeven . value . competitorRows . some ( ( item ) => [ item . type , item . brand , item . price , item . advantage ] . some ( hasMeaningfulAnswer ) ) ||
Object . values ( comprehensiveStepSeven . value . competitorDecision ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepSeven . value . valueRows . some ( ( item ) => [ item . item , item . value ] . some ( hasMeaningfulAnswer ) ) ||
hasMeaningfulAnswer ( comprehensiveStepSeven . value . valueDiscount ) ||
comprehensiveStepSeven . value . psmRows . some ( ( item ) => [ item . tooExpensive , item . tooCheap , item . acceptable , item . bargain ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepSeven . value . summaryRows . some ( ( item ) => [ item . result , item . margin ] . some ( hasMeaningfulAnswer ) ) ||
hasMeaningfulAnswer ( comprehensiveStepSeven . value . finalPricingAnalysis ) ||
comprehensiveStepEight . value . abcRows . some ( ( item ) => [ item . salesShare , item . cumulativeShare , item . classification ] . some ( hasMeaningfulAnswer ) ) ||
Object . values ( comprehensiveStepEight . value . newProduct ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepEight . value . strategyRows . some ( ( item ) => [ item . operation , item . inventory , item . priority ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepEight . value . channelWeightRows . some ( ( item ) => hasMeaningfulAnswer ( item . weight ) || ( ! item . preset && [ item . name , item . description ] . some ( hasMeaningfulAnswer ) ) ) ||
comprehensiveStepEight . value . channelScoreRows . some ( ( item ) => hasMeaningfulAnswer ( item . channel ) || Object . values ( item . scores ) . some ( hasMeaningfulAnswer ) ) ||
hasMeaningfulAnswer ( comprehensiveStepEight . value . channelConclusion ) ||
[ comprehensiveStepNine . value . totalBudget , comprehensiveStepNine . value . grossMargin , comprehensiveStepNine . value . iterationConclusion ] . some ( hasMeaningfulAnswer ) ||
comprehensiveStepNine . value . budgetRows . some ( ( item ) => [ item . channel , item . positioning , item . share , item . launchBudget , item . growthBudget , item . stableBudget ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepNine . value . roiRows . some ( ( item ) => [ item . channel , item . clickRate , item . roi , item . qualified , item . diagnosis , item . action ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepNine . value . funnelRows . some ( ( item ) => [ item . industryAverage , ... Object . values ( item . values ) ] . some ( hasMeaningfulAnswer ) ) ||
comprehensiveStepNine . value . funnelDiagnoses . some ( ( item ) => [ item . channel , item . node , item . storeRate , item . industryRate , item . analysis ] . some ( hasMeaningfulAnswer ) ) ||
hasMeaningfulAnswer ( comprehensiveStepNine . value . commentAttachment . url ) ||
Object . values ( comprehensiveStepNine . value . topIssues ) . some ( hasMeaningfulAnswer ) ||
comprehensiveStepNine . value . abRows . some ( ( item ) => [ item . control , item . experiment , item . analysis ] . some ( hasMeaningfulAnswer ) )
) ;
const uploadInputRef = ref ( null ) ;
const saving = ref ( false ) ;
const uploading = ref ( false ) ;
const currentStep = ref ( 1 ) ;
const configuredSteps = computed ( ( ) => parseArray ( remoteTask . value ? . steps ) ) ;
const isFirstConfiguredStep = computed ( ( ) => currentStep . value <= 1 ) ;
const isLastConfiguredStep = computed ( ( ) => configuredSteps . value . length > 0 && currentStep . value >= configuredSteps . value . length ) ;
const indexedConfiguredSteps = computed ( ( ) => configuredSteps . value . map ( ( step , index ) => ( { step , index } ) ) ) ;
const comprehensiveStepNavRef = ref ( null ) ;
const comprehensiveNavDragging = ref ( false ) ;
const comprehensiveNavDrag = {
pointerId : null ,
startX : 0 ,
startScrollLeft : 0 ,
moved : false ,
} ;
const activeStepName = computed ( ( ) => configuredSteps . value [ currentStep . value - 1 ] || configuredSteps . value [ 0 ] || "" ) ;
const visibleProcessRows = computed ( ( ) => {
if ( ! configuredSteps . value . length || ! processRows . value . length ) {
return processRows . value ;
}
return processRows . value . filter ( ( _ , index ) => index === currentStep . value - 1 ) ;
} ) ;
const progressItems = computed ( ( ) => [
{ text : ` 进行中: ${ page . value . title } ` , status : "doing" } ,
{ text : "待完成:提交实训成果" , status : "" } ,
{ text : "可选:导出实训成果" , status : "" } ,
] ) ;
watch (
( ) => pageKey . value ,
async ( ) => {
await loadRemoteTask ( ) ;
form . value = buildInitialForm ( ) ;
workRows . value = buildInitialRows ( ) ;
processRows . value = buildInitialProcessRows ( ) ;
comprehensiveStepOne . value = createComprehensiveStepOne ( ) ;
comprehensiveStepTwo . value = createComprehensiveStepTwo ( ) ;
comprehensiveStepThree . value = createComprehensiveStepThree ( ) ;
comprehensiveStepFour . value = createComprehensiveStepFour ( ) ;
comprehensiveStepFive . value = createComprehensiveStepFive ( ) ;
comprehensiveStepSix . value = createComprehensiveStepSix ( ) ;
comprehensiveStepSeven . value = createComprehensiveStepSeven ( ) ;
comprehensiveStepEight . value = createComprehensiveStepEight ( ) ;
comprehensiveStepNine . value = createComprehensiveStepNine ( ) ;
stepNineSentimentVisible . value = false ;
hasSavedAnswer . value = false ;
await loadSavedAnswer ( ) ;
} ,
{ immediate : true }
) ;
watch ( configuredSteps , ( steps ) => {
if ( ! steps . length ) return ;
currentStep . value = Math . min ( Math . max ( currentStep . value , 1 ) , steps . length ) ;
} ) ;
function startComprehensiveNavDrag ( event ) {
if ( event . pointerType === "mouse" && event . button !== 0 ) return ;
const nav = comprehensiveStepNavRef . value ;
if ( ! nav ) return ;
comprehensiveNavDrag . pointerId = event . pointerId ;
comprehensiveNavDrag . startX = event . clientX ;
comprehensiveNavDrag . startScrollLeft = nav . scrollLeft ;
comprehensiveNavDrag . moved = false ;
comprehensiveNavDragging . value = true ;
nav . setPointerCapture ? . ( event . pointerId ) ;
}
function moveComprehensiveNavDrag ( event ) {
if ( ! comprehensiveNavDragging . value || event . pointerId !== comprehensiveNavDrag . pointerId ) return ;
const nav = comprehensiveStepNavRef . value ;
if ( ! nav ) return ;
const distance = event . clientX - comprehensiveNavDrag . startX ;
if ( Math . abs ( distance ) > 4 ) {
comprehensiveNavDrag . moved = true ;
event . preventDefault ( ) ;
}
nav . scrollLeft = comprehensiveNavDrag . startScrollLeft - distance ;
}
function endComprehensiveNavDrag ( event ) {
if ( event . pointerId !== comprehensiveNavDrag . pointerId ) return ;
const nav = comprehensiveStepNavRef . value ;
if ( nav ? . hasPointerCapture ? . ( event . pointerId ) ) {
nav . releasePointerCapture ( event . pointerId ) ;
}
comprehensiveNavDragging . value = false ;
comprehensiveNavDrag . pointerId = null ;
}
function selectComprehensiveStep ( step ) {
if ( comprehensiveNavDrag . moved ) {
comprehensiveNavDrag . moved = false ;
return ;
}
currentStep . value = step ;
}
async function loadRemoteTask ( ) {
remoteTask . value = null ;
if ( ! pageKey . value ) return ;
try {
const res = await getTrainingTaskByKey ( pageKey . value ) ;
remoteTask . value = res ? . data || null ;
} catch ( error ) {
remoteTask . value = null ;
}
}
function mergeTrainingPage ( basePage , task ) {
if ( ! task ) {
return {
... basePage ,
requirements : basePage . requirements || basePage . task ,
} ;
}
const goals = parseArray ( task . objectives ) ;
const flow = parseArray ( task . steps ) ;
const material = normalizeMaterial ( task . materialName , task . materialUrl ) ;
return {
... basePage ,
title : task . taskName || basePage . title ,
background : task . background || basePage . background ,
goals : goals . length ? goals : basePage . goals ,
requirements : task . requirements || basePage . task ,
task : basePage . processRows ? . length ? basePage . task : task . requirements || basePage . task ,
flow : flow . length ? flow : basePage . flow ,
material ,
} ;
}
function normalizeMaterial ( materialName , materialUrl ) {
const name = String ( materialName || "" ) . trim ( ) ;
const url = String ( materialUrl || "" ) . trim ( ) ;
if ( ! name || ! url || ( name === "案例资料.rar" && url === "/file/example.rar" ) ) {
return null ;
}
return { name , url : normalizeMaterialUrl ( materialUrl ) } ;
}
function normalizeMaterialUrl ( rawUrl ) {
const url = String ( rawUrl || "" ) . trim ( ) ;
if ( ! url ) {
return "" ;
}
try {
const parsed = new URL ( url ) ;
if (
[ "localhost" , "127.0.0.1" , "::1" ] . includes ( parsed . hostname ) &&
parsed . pathname . startsWith ( "/file/" )
) {
return ` ${ parsed . pathname } ${ parsed . search } ${ parsed . hash } ` ;
}
} catch {
// 相对地址无需转换。
}
return url ;
}
function parseArray ( value ) {
if ( Array . isArray ( value ) ) {
return value . map ( ( item ) => String ( item ? . name ? ? item ? ? "" ) . trim ( ) ) . filter ( Boolean ) ;
}
if ( ! value ) {
return [ ] ;
}
try {
const parsed = JSON . parse ( value ) ;
return Array . isArray ( parsed ) ? parsed . map ( ( item ) => String ( item ? . name ? ? item ? ? "" ) . trim ( ) ) . filter ( Boolean ) : [ ] ;
} catch ( error ) {
return String ( value )
. split ( /[; ;|| ]/ )
. map ( ( item ) => item . trim ( ) )
. filter ( Boolean ) ;
}
}
function hasMeaningfulAnswer ( value ) {
if ( typeof value === "string" ) return value . trim ( ) . length > 0 ;
return value !== null && value !== undefined && value !== "" ;
}
function addComprehensiveCategory ( ) {
comprehensiveStepTwo . value . categoryScores . push ( createComprehensiveCategory ( ) ) ;
}
function removeComprehensiveCategory ( ) {
if ( comprehensiveStepTwo . value . categoryScores . length > 2 ) {
comprehensiveStepTwo . value . categoryScores . pop ( ) ;
}
}
function comprehensiveReviewCategory ( index ) {
const fallback = [ "智能健身镜" , "智能卫浴镜" , "智能美妆镜" ] ;
return String ( comprehensiveStepTwo . value . categoryScores [ index ] ? . name || "" ) . trim ( ) || fallback [ index ] || ` 产品品类 ${ index + 1 } ` ;
}
function setComprehensiveUploadInput ( key , element ) {
if ( element ) {
comprehensiveUploadInputs . value [ key ] = element ;
}
}
function triggerComprehensiveUpload ( key ) {
if ( isDemo . value ) {
proxy ? . $modal ? . msgWarning ? . ( "演示模式不上传文件" ) ;
return ;
}
comprehensiveUploadInputs . value [ key ] ? . click ( ) ;
}
async function parseComprehensiveExcelPreview ( file ) {
const XLSX = await import ( "xlsx" ) ;
const workbook = XLSX . read ( await file . arrayBuffer ( ) , { type : "array" } ) ;
const sheetName = workbook . SheetNames ? . [ 0 ] || "首个工作表" ;
const worksheet = workbook . Sheets ? . [ sheetName ] ;
const values = worksheet ? XLSX . utils . sheet _to _json ( worksheet , { header : 1 , defval : "" , blankrows : false } ) : [ ] ;
const width = Math . min ( 8 , Math . max ( 0 , ... ( values . slice ( 0 , 6 ) . map ( ( row ) => Array . isArray ( row ) ? row . length : 0 ) ) ) ) ;
return {
sheetName ,
headers : ( values [ 0 ] || [ ] ) . slice ( 0 , width ) . map ( ( item , index ) => String ( item || ` 列 ${ index + 1 } ` ) ) ,
rows : values . slice ( 1 , 6 ) . map ( ( row ) => Array . from ( { length : width } , ( _ , index ) => String ( row ? . [ index ] ? ? "" ) ) ) ,
} ;
}
async function handleComprehensiveFileChange ( key , event ) {
const file = event . target ? . files ? . [ 0 ] ;
const definition = comprehensiveUploadDefinitions . find ( ( item ) => item . key === key ) ;
if ( ! file || ! definition ) return ;
const valid = definition . kind === "excel" ? /\.xlsx?$/i . test ( file . name ) : /^image\/(png|jpeg|webp)$/ . test ( file . type ) ;
if ( ! valid || file . size > 10 * 1024 * 1024 ) {
proxy ? . $modal ? . msgError ? . ( definition . kind === "excel" ? "请上传不超过 10MB 的 XLS 或 XLSX 文件" : "请上传不超过 10MB 的 PNG、JPG 或 WEBP 图片" ) ;
event . target . value = "" ;
return ;
}
comprehensiveUploading . value = key ;
try {
const preview = definition . kind === "excel" ? await parseComprehensiveExcelPreview ( file ) : null ;
const formData = new FormData ( ) ;
formData . append ( "file" , file ) ;
const res = await uploadStudentTrainingFile ( formData ) ;
comprehensiveStepThree . value . attachments [ key ] = {
name : res ? . originalFilename || file . name ,
originalFilename : res ? . originalFilename || file . name ,
url : res ? . url || res ? . fileName || "" ,
preview ,
} ;
proxy ? . $modal ? . msgSuccess ? . ( "文件已上传并生成预览" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( definition . kind === "excel" ? "Excel 文件解析或上传失败" : "图片上传失败" ) ;
} finally {
comprehensiveUploading . value = "" ;
event . target . value = "" ;
}
}
function removeComprehensiveAttachment ( key ) {
comprehensiveStepThree . value . attachments [ key ] = createComprehensiveAttachment ( ) ;
}
function addComprehensiveKanoRow ( ) {
comprehensiveStepThree . value . kanoRows . push ( createComprehensiveKanoRow ( ) ) ;
}
function removeComprehensiveKanoRow ( ) {
if ( comprehensiveStepThree . value . kanoRows . length > 1 ) {
comprehensiveStepThree . value . kanoRows . pop ( ) ;
}
}
function addComprehensiveRequirementRow ( ) {
comprehensiveStepFour . value . requirements . push ( createComprehensiveRequirementRow ( ) ) ;
}
function removeComprehensiveRequirementRow ( ) {
if ( comprehensiveStepFour . value . requirements . length > 1 ) {
comprehensiveStepFour . value . requirements . pop ( ) ;
}
}
function comprehensiveRiceScore ( row ) {
const reach = Number ( row ? . reach ) ;
const impact = Number ( row ? . impact ) ;
const confidenceText = String ( row ? . confidence || "" ) . trim ( ) . replace ( "%" , "" ) ;
const confidence = Number ( confidenceText ) ;
const effort = Number ( row ? . effort ) ;
if ( ! [ reach , impact , confidence , effort ] . every ( Number . isFinite ) || effort <= 0 ) return "—" ;
const normalizedConfidence = confidence > 1 ? confidence / 100 : confidence ;
return ( reach * impact * normalizedConfidence / effort ) . toFixed ( 1 ) ;
}
function addComprehensiveArchitectureRow ( ) {
comprehensiveStepFive . value . architectureRows . push ( createComprehensiveArchitectureRow ( ) ) ;
}
function removeComprehensiveArchitectureRow ( ) {
if ( comprehensiveStepFive . value . architectureRows . length > 1 ) {
comprehensiveStepFive . value . architectureRows . pop ( ) ;
}
}
function addComprehensiveBomRow ( ) {
comprehensiveStepSix . value . bomRows . push ( createComprehensiveBomRow ( ) ) ;
}
function removeComprehensiveBomRow ( ) {
if ( comprehensiveStepSix . value . bomRows . length > 1 ) {
comprehensiveStepSix . value . bomRows . pop ( ) ;
}
}
function addComprehensiveManufacturingRow ( ) {
comprehensiveStepSix . value . manufacturingRows . push ( createComprehensiveManufacturingRow ( ) ) ;
}
function removeComprehensiveManufacturingRow ( ) {
if ( comprehensiveStepSix . value . manufacturingRows . length > 1 ) {
comprehensiveStepSix . value . manufacturingRows . pop ( ) ;
}
}
function comprehensiveNumber ( value ) {
const number = Number ( String ( value ? ? "" ) . trim ( ) . replace ( "%" , "" ) ) ;
return Number . isFinite ( number ) ? number : 0 ;
}
function comprehensiveCostTotal ( rows , key ) {
return rows . reduce ( ( sum , row ) => sum + comprehensiveNumber ( row ? . [ key ] ) , 0 ) ;
}
function comprehensiveBomTotal ( batch ) {
const key = batch === "large" ? "largeCost" : "smallCost" ;
return comprehensiveCostTotal ( comprehensiveStepSix . value . bomRows , key ) . toFixed ( 2 ) ;
}
function comprehensiveManufacturingTotal ( batch ) {
const key = batch === "large" ? "largeCost" : "smallCost" ;
return comprehensiveCostTotal ( comprehensiveStepSix . value . manufacturingRows , key ) . toFixed ( 2 ) ;
}
function comprehensiveUnitCost ( batch ) {
const key = batch === "large" ? "largeCost" : "smallCost" ;
return ( comprehensiveCostTotal ( comprehensiveStepSix . value . bomRows , key ) + comprehensiveCostTotal ( comprehensiveStepSix . value . manufacturingRows , key ) ) . toFixed ( 2 ) ;
}
function comprehensiveChannelScore ( row , scenario ) {
const weightKey = scenario === "mass" ? "massWeight" : "trialWeight" ;
const hasScore = comprehensiveProcurementDimensions . some ( ( item ) => hasMeaningfulAnswer ( row ? . [ item . key ] ) ) ;
const hasWeight = comprehensiveStepSix . value . weightRows . some ( ( item ) => hasMeaningfulAnswer ( item [ weightKey ] ) ) ;
if ( ! hasScore || ! hasWeight ) return "—" ;
const total = comprehensiveProcurementDimensions . reduce ( ( sum , dimension ) => {
const weight = comprehensiveStepSix . value . weightRows . find ( ( item ) => item . key === dimension . key ) ? . [ weightKey ] ;
return sum + comprehensiveNumber ( row ? . [ dimension . key ] ) * comprehensiveNumber ( weight ) / 100 ;
} , 0 ) ;
return total . toFixed ( 2 ) ;
}
function comprehensiveSafetyStock ( row ) {
if ( ! hasMeaningfulAnswer ( row ? . forecastSales ) ) return "—" ;
return String ( Math . round ( comprehensiveNumber ( row . forecastSales ) * 0.1 ) ) ;
}
function comprehensiveBeginningStock ( index ) {
if ( index === 0 ) return hasMeaningfulAnswer ( comprehensiveStepSeven . value . initialStock ) ? String ( Math . round ( comprehensiveNumber ( comprehensiveStepSeven . value . initialStock ) ) ) : "—" ;
const previous = comprehensiveStepSeven . value . forecastRows [ index - 1 ] ;
return comprehensiveSafetyStock ( previous ) ;
}
function comprehensivePurchaseQuantity ( row , index ) {
if ( ! hasMeaningfulAnswer ( row ? . forecastSales ) ) return "—" ;
const beginning = comprehensiveBeginningStock ( index ) ;
if ( beginning === "—" ) return "—" ;
return String ( Math . round ( comprehensiveNumber ( row . forecastSales ) + comprehensiveNumber ( comprehensiveSafetyStock ( row ) ) - comprehensiveNumber ( beginning ) ) ) ;
}
function comprehensiveCostBasedPrice ( row ) {
if ( ! hasMeaningfulAnswer ( comprehensiveStepSeven . value . unitCost ) || ! hasMeaningfulAnswer ( row ? . markup ) ) return "—" ;
return ( comprehensiveNumber ( comprehensiveStepSeven . value . unitCost ) * ( 1 + comprehensiveNumber ( row . markup ) / 100 ) ) . toFixed ( 2 ) ;
}
function addComprehensiveValueRow ( ) {
comprehensiveStepSeven . value . valueRows . push ( createComprehensiveValueRow ( ) ) ;
}
function removeComprehensiveValueRow ( ) {
if ( comprehensiveStepSeven . value . valueRows . length > 1 ) {
comprehensiveStepSeven . value . valueRows . pop ( ) ;
}
}
function addComprehensivePsmRow ( ) {
comprehensiveStepSeven . value . psmRows . push ( createComprehensivePsmRow ( ) ) ;
}
function removeComprehensivePsmRow ( ) {
if ( comprehensiveStepSeven . value . psmRows . length > 1 ) {
comprehensiveStepSeven . value . psmRows . pop ( ) ;
}
}
function comprehensivePsmPercent ( value ) {
return Math . min ( 100 , Math . max ( 0 , comprehensiveNumber ( value ) ) ) ;
}
function exportComprehensivePsm ( ) {
const headers = [ "价格(元)" , "太贵不买累计占比(%)" , "太便宜不买累计占比(%)" , "偏高可接受累计占比(%)" , "便宜划算累计占比(%)" ] ;
const escapeCsv = ( value ) => ` " ${ String ( value ? ? "" ) . replaceAll ( '"' , '""' ) } " ` ;
const rows = comprehensiveStepSeven . value . psmRows . map ( ( row ) => [ row . range , row . tooExpensive , row . tooCheap , row . acceptable , row . bargain ] ) ;
const blob = new Blob ( [ [ headers , ... rows ] . map ( ( row ) => row . map ( escapeCsv ) . join ( "," ) ) . join ( "\n" ) ] , { type : "text/csv;charset=utf-8" } ) ;
const url = URL . createObjectURL ( blob ) ;
const link = document . createElement ( "a" ) ;
link . href = url ;
link . download = "PSM价格敏感度数据.csv" ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
URL . revokeObjectURL ( url ) ;
}
function addComprehensiveAbcRow ( ) {
comprehensiveStepEight . value . abcRows . push ( createComprehensiveAbcRow ( ) ) ;
}
function removeComprehensiveAbcRow ( ) {
if ( comprehensiveStepEight . value . abcRows . length > 1 ) {
comprehensiveStepEight . value . abcRows . pop ( ) ;
}
}
function addComprehensiveChannelWeightRow ( ) {
const row = createComprehensiveChannelWeightRow ( ) ;
comprehensiveStepEight . value . channelWeightRows . push ( row ) ;
comprehensiveStepEight . value . channelScoreRows . forEach ( ( scoreRow ) => {
scoreRow . scores [ row . id ] = "" ;
} ) ;
}
function removeComprehensiveChannelWeightRow ( ) {
if ( comprehensiveStepEight . value . channelWeightRows . length <= 1 ) return ;
const removed = comprehensiveStepEight . value . channelWeightRows . pop ( ) ;
comprehensiveStepEight . value . channelScoreRows . forEach ( ( scoreRow ) => {
delete scoreRow . scores [ removed . id ] ;
} ) ;
}
function addComprehensiveChannelScoreRow ( ) {
comprehensiveStepEight . value . channelScoreRows . push ( createComprehensiveChannelScoreRow ( "" , comprehensiveStepEight . value . channelWeightRows ) ) ;
}
function removeComprehensiveChannelScoreRow ( ) {
if ( comprehensiveStepEight . value . channelScoreRows . length > 1 ) {
comprehensiveStepEight . value . channelScoreRows . pop ( ) ;
}
}
function comprehensiveChannelReleaseScore ( row ) {
const dimensions = comprehensiveStepEight . value . channelWeightRows ;
const hasWeight = dimensions . some ( ( item ) => hasMeaningfulAnswer ( item . weight ) ) ;
const hasScore = dimensions . some ( ( item ) => hasMeaningfulAnswer ( row ? . scores ? . [ item . id ] ) ) ;
if ( ! hasWeight || ! hasScore ) return "—" ;
return dimensions . reduce ( ( total , item ) => total + comprehensiveNumber ( row ? . scores ? . [ item . id ] ) * comprehensiveNumber ( item . weight ) / 100 , 0 ) . toFixed ( 2 ) ;
}
function downloadComprehensiveCsv ( headers , rows , filename ) {
const escapeCsv = ( value ) => ` " ${ String ( value ? ? "" ) . replace ( /"/g , '""' ) } " ` ;
const blob = new Blob ( [ [ headers , ... rows ] . map ( ( row ) => row . map ( escapeCsv ) . join ( "," ) ) . join ( "\n" ) ] , { type : "text/csv;charset=utf-8" } ) ;
const url = URL . createObjectURL ( blob ) ;
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 exportComprehensiveAbc ( ) {
const headers = [ "品类名称" , "营收贡献(万元)" , "销售额占比(%) " , "累计占比(%) " , "ABC分层判定" ] ;
const rows = comprehensiveStepEight . value . abcRows . map ( ( row ) => [ row . name , row . revenue , row . salesShare , row . cumulativeShare , row . classification ] ) ;
downloadComprehensiveCsv ( headers , rows , "ABC品类分层数据.csv" ) ;
}
function exportComprehensiveChannelScores ( ) {
const dimensions = comprehensiveStepEight . value . channelWeightRows ;
const headers = [ "渠道" , ... dimensions . map ( ( item ) => ` ${ item . name || "评估维度" } 得分 ` ) , "加权得分" ] ;
const rows = comprehensiveStepEight . value . channelScoreRows . map ( ( row ) => [ row . channel , ... dimensions . map ( ( item ) => row . scores ? . [ item . id ] || "" ) , comprehensiveChannelReleaseScore ( row ) ] ) ;
downloadComprehensiveCsv ( headers , rows , "新品发布渠道评分.csv" ) ;
}
function addComprehensiveBudgetRow ( ) {
comprehensiveStepNine . value . budgetRows . push ( createComprehensiveBudgetRow ( ) ) ;
}
function removeComprehensiveBudgetRow ( ) {
if ( comprehensiveStepNine . value . budgetRows . length > 1 ) {
comprehensiveStepNine . value . budgetRows . pop ( ) ;
}
}
function comprehensiveBudgetAmount ( row ) {
if ( ! hasMeaningfulAnswer ( comprehensiveStepNine . value . totalBudget ) || ! hasMeaningfulAnswer ( row ? . share ) ) return "—" ;
return ( comprehensiveNumber ( comprehensiveStepNine . value . totalBudget ) * comprehensiveNumber ( row . share ) / 100 ) . toFixed ( 2 ) ;
}
function addComprehensiveFunnelChannel ( ) {
const channel = createComprehensiveFunnelChannel ( ) ;
comprehensiveStepNine . value . funnelChannels . push ( channel ) ;
comprehensiveStepNine . value . funnelRows . forEach ( ( row ) => {
row . values [ channel . id ] = "" ;
} ) ;
}
function removeComprehensiveFunnelChannel ( ) {
if ( comprehensiveStepNine . value . funnelChannels . length <= 1 ) return ;
const channel = comprehensiveStepNine . value . funnelChannels . pop ( ) ;
comprehensiveStepNine . value . funnelRows . forEach ( ( row ) => {
delete row . values [ channel . id ] ;
} ) ;
}
function addComprehensiveFunnelDiagnosis ( ) {
comprehensiveStepNine . value . funnelDiagnoses . push ( createComprehensiveFunnelDiagnosis ( ) ) ;
}
function removeComprehensiveFunnelDiagnosis ( ) {
if ( comprehensiveStepNine . value . funnelDiagnoses . length > 1 ) {
comprehensiveStepNine . value . funnelDiagnoses . pop ( ) ;
}
}
function triggerStepNineCommentUpload ( ) {
if ( isDemo . value ) {
proxy ? . $modal ? . msgWarning ? . ( "演示模式不上传文件" ) ;
return ;
}
stepNineCommentInput . value ? . click ( ) ;
}
async function handleStepNineCommentFileChange ( event ) {
const file = event . target ? . files ? . [ 0 ] ;
if ( ! file ) return ;
if ( ! /\.(xlsx?|csv)$/i . test ( file . name ) || file . size > 10 * 1024 * 1024 ) {
proxy ? . $modal ? . msgError ? . ( "请上传不超过 10MB 的 XLS、XLSX 或 CSV 评论数据文件" ) ;
event . target . value = "" ;
return ;
}
stepNineUploading . value = true ;
try {
const preview = await parseComprehensiveExcelPreview ( file ) ;
const formData = new FormData ( ) ;
formData . append ( "file" , file ) ;
const res = await uploadStudentTrainingFile ( formData ) ;
comprehensiveStepNine . value . commentAttachment = {
name : res ? . originalFilename || file . name ,
originalFilename : res ? . originalFilename || file . name ,
url : res ? . url || res ? . fileName || "" ,
preview ,
} ;
proxy ? . $modal ? . msgSuccess ? . ( "评论数据已导入并生成预览" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( "评论数据解析或上传失败" ) ;
} finally {
stepNineUploading . value = false ;
event . target . value = "" ;
}
}
function removeStepNineCommentAttachment ( ) {
comprehensiveStepNine . value . commentAttachment = createComprehensiveAttachment ( ) ;
comprehensiveStepNine . value . sentimentRows = [ "正面" , "中性" , "负面" ] . map ( ( type ) => ( { type , count : 0 , ratio : "0.00%" } ) ) ;
}
function commentPreviewTexts ( preview ) {
return ( preview ? . rows || [ ] ) . map ( ( row ) => row . map ( ( cell ) => String ( cell || "" ) ) . join ( " " ) ) . filter ( Boolean ) ;
}
function buildComprehensiveCommentCloud ( preview ) {
const stopWords = new Set ( [ "产品" , "使用" , "感觉" , "就是" , "这个" , "一个" , "还是" , "可以" , "非常" , "真的" , "没有" , "我们" , "你们" , "东西" , "自己" ] ) ;
const counts = new Map ( ) ;
commentPreviewTexts ( preview ) . join ( " " ) . match ( /[\u4e00-\u9fff]{2,}|[A-Za-z]{3,}/g ) ? . forEach ( ( segment ) => {
const terms = /^[\u4e00-\u9fff]+$/ . test ( segment ) ? Array . from ( { length : Math . max ( 0 , segment . length - 1 ) } , ( _ , index ) => segment . slice ( index , index + 2 ) ) : [ segment . toLowerCase ( ) ] ;
terms . forEach ( ( term ) => {
if ( ! stopWords . has ( term ) ) counts . set ( term , ( counts . get ( term ) || 0 ) + 1 ) ;
} ) ;
} ) ;
const top = [ ... counts . entries ( ) ] . sort ( ( left , right ) => right [ 1 ] - left [ 1 ] ) . slice ( 0 , 20 ) ;
const max = top [ 0 ] ? . [ 1 ] || 1 ;
return top . map ( ( [ text , count ] ) => ( { text , size : 14 + Math . round ( count / max * 18 ) } ) ) ;
}
function analyzeComprehensiveSentiment ( ) {
const positiveWords = [ "满意" , "喜欢" , "好用" , "推荐" , "不错" , "优秀" , "方便" , "清晰" , "稳定" , "好看" , "值得" ] ;
const negativeWords = [ "失望" , "问题" , "故障" , "不好" , "不行" , "退货" , "差" , "慢" , "贵" , "卡顿" , "损坏" , "噪音" ] ;
const counts = { 正面 : 0 , 中性 : 0 , 负面 : 0 } ;
commentPreviewTexts ( comprehensiveStepNine . value . commentAttachment . preview ) . forEach ( ( text ) => {
const positive = positiveWords . reduce ( ( total , word ) => total + ( text . includes ( word ) ? 1 : 0 ) , 0 ) ;
const negative = negativeWords . reduce ( ( total , word ) => total + ( text . includes ( word ) ? 1 : 0 ) , 0 ) ;
counts [ positive > negative ? "正面" : negative > positive ? "负面" : "中性" ] += 1 ;
} ) ;
const total = Object . values ( counts ) . reduce ( ( sum , count ) => sum + count , 0 ) ;
comprehensiveStepNine . value . sentimentRows = [ "正面" , "中性" , "负面" ] . map ( ( type ) => ( { type , count : counts [ type ] , ratio : total ? ` ${ ( counts [ type ] / total * 100 ) . toFixed ( 2 ) } % ` : "0.00%" } ) ) ;
if ( ! total ) {
proxy ? . $modal ? . msgWarning ? . ( "请先导入评论数据" ) ;
}
}
function comprehensiveSentimentPercent ( value ) {
return Math . min ( 100 , Math . max ( 0 , comprehensiveNumber ( String ( value || "" ) . replace ( "%" , "" ) ) ) ) ;
}
function buildOutcome ( ) {
return {
title : page . value . title ,
form : form . value ,
rows : workRows . value ,
processRows : processRows . value ,
comprehensiveStepOne : comprehensiveStepOne . value ,
comprehensiveStepTwo : comprehensiveStepTwo . value ,
comprehensiveStepThree : comprehensiveStepThree . value ,
comprehensiveStepFour : comprehensiveStepFour . value ,
comprehensiveStepFive : comprehensiveStepFive . value ,
comprehensiveStepSix : comprehensiveStepSix . value ,
comprehensiveStepSeven : comprehensiveStepSeven . value ,
comprehensiveStepEight : comprehensiveStepEight . value ,
comprehensiveStepNine : comprehensiveStepNine . value ,
file : form . value . ganttFileUrl
? {
name : form . value . ganttFileName || "" ,
url : form . value . ganttFileUrl ,
originalFilename : form . value . ganttOriginalFilename || form . value . ganttFileName || "" ,
}
: null ,
} ;
}
function applySavedAnswer ( value ) {
if ( ! value ) return ;
try {
const parsed = JSON . parse ( value ) ;
if ( parsed ? . currentStep && Number ( parsed . currentStep ) > 0 ) {
currentStep . value = Number ( parsed . currentStep ) ;
}
if ( parsed ? . form ) {
form . value = {
... form . value ,
... parsed . form ,
} ;
}
if ( parsed ? . comprehensiveStepOne && typeof parsed . comprehensiveStepOne === "object" ) {
comprehensiveStepOne . value = { ... createComprehensiveStepOne ( ) , ... parsed . comprehensiveStepOne } ;
}
if ( parsed ? . comprehensiveStepTwo && typeof parsed . comprehensiveStepTwo === "object" ) {
const defaults = createComprehensiveStepTwo ( ) ;
comprehensiveStepTwo . value = {
... defaults ,
... parsed . comprehensiveStepTwo ,
primaryWeights : { ... defaults . primaryWeights , ... parsed . comprehensiveStepTwo . primaryWeights } ,
dimensionWeights : { ... defaults . dimensionWeights , ... parsed . comprehensiveStepTwo . dimensionWeights } ,
totalWeights : { ... defaults . totalWeights , ... parsed . comprehensiveStepTwo . totalWeights } ,
categoryScores : Array . isArray ( parsed . comprehensiveStepTwo . categoryScores ) && parsed . comprehensiveStepTwo . categoryScores . length ? parsed . comprehensiveStepTwo . categoryScores : defaults . categoryScores ,
reviews : Array . isArray ( parsed . comprehensiveStepTwo . reviews ) && parsed . comprehensiveStepTwo . reviews . length ? defaults . reviews . map ( ( row , index ) => ( { ... row , ... parsed . comprehensiveStepTwo . reviews [ index ] } ) ) : defaults . reviews ,
} ;
}
if ( parsed ? . comprehensiveStepThree && typeof parsed . comprehensiveStepThree === "object" ) {
const defaults = createComprehensiveStepThree ( ) ;
const saved = parsed . comprehensiveStepThree ;
comprehensiveStepThree . value = {
... defaults ,
... saved ,
attachments : Object . fromEntries ( comprehensiveUploadDefinitions . map ( ( item ) => [ item . key , { ... createComprehensiveAttachment ( ) , ... ( saved . attachments ? . [ item . key ] || { } ) } ] ) ) ,
rfmAnalyses : { ... defaults . rfmAnalyses , ... ( saved . rfmAnalyses || { } ) } ,
profiles : Array . isArray ( saved . profiles ) && saved . profiles . length ? defaults . profiles . map ( ( row , index ) => ( { ... row , ... saved . profiles [ index ] } ) ) : defaults . profiles ,
kanoRows : Array . isArray ( saved . kanoRows ) && saved . kanoRows . length ? saved . kanoRows : defaults . kanoRows ,
} ;
}
if ( parsed ? . comprehensiveStepFour && typeof parsed . comprehensiveStepFour === "object" ) {
const defaults = createComprehensiveStepFour ( ) ;
const saved = parsed . comprehensiveStepFour ;
comprehensiveStepFour . value = {
... defaults ,
... saved ,
journeyRows : Array . isArray ( saved . journeyRows ) && saved . journeyRows . length ? defaults . journeyRows . map ( ( row , index ) => ( { ... row , ... saved . journeyRows [ index ] } ) ) : defaults . journeyRows ,
painRows : Array . isArray ( saved . painRows ) && saved . painRows . length ? defaults . painRows . map ( ( row , index ) => ( { ... row , ... saved . painRows [ index ] } ) ) : defaults . painRows ,
requirements : Array . isArray ( saved . requirements ) && saved . requirements . length ? saved . requirements : defaults . requirements ,
riceRows : Array . isArray ( saved . riceRows ) && saved . riceRows . length ? saved . riceRows : defaults . riceRows ,
valueCanvas : { ... defaults . valueCanvas , ... ( saved . valueCanvas || { } ) } ,
versions : { ... defaults . versions , ... ( saved . versions || { } ) } ,
} ;
}
if ( parsed ? . comprehensiveStepFive && typeof parsed . comprehensiveStepFive === "object" ) {
const defaults = createComprehensiveStepFive ( ) ;
const saved = parsed . comprehensiveStepFive ;
comprehensiveStepFive . value = {
... defaults ,
... saved ,
architectureRows : Array . isArray ( saved . architectureRows ) && saved . architectureRows . length ? saved . architectureRows : defaults . architectureRows ,
faultRows : Array . isArray ( saved . faultRows ) && saved . faultRows . length ? defaults . faultRows . map ( ( row , index ) => ( { ... row , ... saved . faultRows [ index ] } ) ) : defaults . faultRows ,
policyRows : Array . isArray ( saved . policyRows ) && saved . policyRows . length ? defaults . policyRows . map ( ( row , index ) => ( { ... row , ... saved . policyRows [ index ] } ) ) : defaults . policyRows ,
summaryRows : Array . isArray ( saved . summaryRows ) && saved . summaryRows . length ? defaults . summaryRows . map ( ( row , index ) => ( { ... row , ... saved . summaryRows [ index ] } ) ) : defaults . summaryRows ,
} ;
}
if ( parsed ? . comprehensiveStepSix && typeof parsed . comprehensiveStepSix === "object" ) {
const defaults = createComprehensiveStepSix ( ) ;
const saved = parsed . comprehensiveStepSix ;
comprehensiveStepSix . value = {
... defaults ,
... saved ,
bomRows : Array . isArray ( saved . bomRows ) && saved . bomRows . length ? saved . bomRows : defaults . bomRows ,
manufacturingRows : Array . isArray ( saved . manufacturingRows ) && saved . manufacturingRows . length ? saved . manufacturingRows : defaults . manufacturingRows ,
weightRows : Array . isArray ( saved . weightRows ) && saved . weightRows . length ? defaults . weightRows . map ( ( row , index ) => ( { ... row , ... saved . weightRows [ index ] } ) ) : defaults . weightRows ,
channelRows : Array . isArray ( saved . channelRows ) && saved . channelRows . length ? saved . channelRows : defaults . channelRows ,
} ;
}
if ( parsed ? . comprehensiveStepSeven && typeof parsed . comprehensiveStepSeven === "object" ) {
const defaults = createComprehensiveStepSeven ( ) ;
const saved = parsed . comprehensiveStepSeven ;
comprehensiveStepSeven . value = {
... defaults ,
... saved ,
forecastRows : Array . isArray ( saved . forecastRows ) && saved . forecastRows . length ? defaults . forecastRows . map ( ( row , index ) => ( { ... row , ... saved . forecastRows [ index ] } ) ) : defaults . forecastRows ,
costPricingRows : Array . isArray ( saved . costPricingRows ) && saved . costPricingRows . length ? defaults . costPricingRows . map ( ( row , index ) => ( { ... row , ... saved . costPricingRows [ index ] } ) ) : defaults . costPricingRows ,
competitorRows : Array . isArray ( saved . competitorRows ) && saved . competitorRows . length ? saved . competitorRows : defaults . competitorRows ,
competitorDecision : { ... defaults . competitorDecision , ... ( saved . competitorDecision || { } ) } ,
valueRows : Array . isArray ( saved . valueRows ) && saved . valueRows . length ? saved . valueRows : defaults . valueRows ,
psmRows : Array . isArray ( saved . psmRows ) && saved . psmRows . length ? saved . psmRows : defaults . psmRows ,
summaryRows : Array . isArray ( saved . summaryRows ) && saved . summaryRows . length ? defaults . summaryRows . map ( ( row , index ) => ( { ... row , ... saved . summaryRows [ index ] } ) ) : defaults . summaryRows ,
} ;
}
if ( parsed ? . comprehensiveStepEight && typeof parsed . comprehensiveStepEight === "object" ) {
const defaults = createComprehensiveStepEight ( ) ;
const saved = parsed . comprehensiveStepEight ;
const channelWeightRows = Array . isArray ( saved . channelWeightRows ) && saved . channelWeightRows . length ? saved . channelWeightRows : defaults . channelWeightRows ;
comprehensiveStepEight . value = {
... defaults ,
... saved ,
abcRows : Array . isArray ( saved . abcRows ) && saved . abcRows . length ? saved . abcRows : defaults . abcRows ,
newProduct : { ... defaults . newProduct , ... ( saved . newProduct || { } ) } ,
strategyRows : Array . isArray ( saved . strategyRows ) && saved . strategyRows . length ? defaults . strategyRows . map ( ( row , index ) => ( { ... row , ... saved . strategyRows [ index ] } ) ) : defaults . strategyRows ,
channelWeightRows ,
channelScoreRows : Array . isArray ( saved . channelScoreRows ) && saved . channelScoreRows . length ? saved . channelScoreRows . map ( ( row ) => ( { ... row , scores : Object . fromEntries ( channelWeightRows . map ( ( dimension ) => [ dimension . id , row . scores ? . [ dimension . id ] || "" ] ) ) } ) ) : defaults . channelScoreRows ,
} ;
}
if ( parsed ? . comprehensiveStepNine && typeof parsed . comprehensiveStepNine === "object" ) {
const defaults = createComprehensiveStepNine ( ) ;
const saved = parsed . comprehensiveStepNine ;
const funnelChannels = Array . isArray ( saved . funnelChannels ) && saved . funnelChannels . length ? saved . funnelChannels : defaults . funnelChannels ;
comprehensiveStepNine . value = {
... defaults ,
... saved ,
budgetRows : Array . isArray ( saved . budgetRows ) && saved . budgetRows . length ? saved . budgetRows : defaults . budgetRows ,
roiRows : Array . isArray ( saved . roiRows ) && saved . roiRows . length ? saved . roiRows : defaults . roiRows ,
funnelChannels ,
funnelRows : Array . isArray ( saved . funnelRows ) && saved . funnelRows . length ? defaults . funnelRows . map ( ( row , index ) => ( { ... row , ... saved . funnelRows [ index ] , values : Object . fromEntries ( funnelChannels . map ( ( channel ) => [ channel . id , saved . funnelRows [ index ] ? . values ? . [ channel . id ] || "" ] ) ) } ) ) : defaults . funnelRows ,
funnelDiagnoses : Array . isArray ( saved . funnelDiagnoses ) && saved . funnelDiagnoses . length ? saved . funnelDiagnoses : defaults . funnelDiagnoses ,
commentAttachment : { ... createComprehensiveAttachment ( ) , ... ( saved . commentAttachment || { } ) } ,
topIssues : { ... defaults . topIssues , ... ( saved . topIssues || { } ) } ,
sentimentRows : Array . isArray ( saved . sentimentRows ) && saved . sentimentRows . length ? defaults . sentimentRows . map ( ( row , index ) => ( { ... row , ... saved . sentimentRows [ index ] } ) ) : defaults . sentimentRows ,
abRows : Array . isArray ( saved . abRows ) && saved . abRows . length ? defaults . abRows . map ( ( row , index ) => ( { ... row , ... saved . abRows [ index ] } ) ) : defaults . abRows ,
} ;
}
if ( Array . isArray ( parsed ? . rows ) && parsed . rows . length ) {
workRows . value = buildInitialRows ( ) . map ( ( row , index ) => ( {
... row ,
answer : parsed . rows [ index ] ? . answer || "" ,
} ) ) ;
}
if ( Array . isArray ( parsed ? . processRows ) && parsed . processRows . length ) {
const savedByNo = parsed . processRows . reduce ( ( map , row ) => {
if ( row ? . no != null ) {
map [ row . no ] = row ;
}
return map ;
} , { } ) ;
processRows . value = buildInitialProcessRows ( ) . map ( ( row , index ) => ( {
... row ,
workContent : savedByNo [ row . no ] ? . workContent || parsed . processRows [ index ] ? . workContent || "" ,
} ) ) ;
}
} catch ( error ) {
// Ignore malformed historical answers.
}
}
async function loadSavedAnswer ( ) {
if ( ! pageKey . value ) return ;
try {
const res = await getStudentTrainingAnswer ( pageKey . value ) ;
const answer = res ? . data ;
hasSavedAnswer . value = Boolean (
answer && [ answer . step1Answer , answer . step2Answer , answer . step3Answer , answer . step4Answer , answer . dynamicStepAnswers ] . some ( hasMeaningfulAnswer )
) ;
if ( answer ? . currentStep && Number ( answer . currentStep ) > 0 ) {
currentStep . value = Number ( answer . currentStep ) ;
}
if ( answer ? . step1Answer ) {
applySavedAnswer ( answer . step1Answer ) ;
}
} catch ( error ) {
// Keep the page usable when the answer endpoint is unavailable.
}
}
function buildAnswerPayload ( saveAction = "SAVE" ) {
return {
step1Answer : JSON . stringify ( buildOutcome ( ) ) ,
currentStep : currentStep . value ,
submitted : saveAction === "SUBMIT" ,
saveAction ,
} ;
}
async function saveAnswer ( saveAction = "SAVE" ) {
if ( ! pageKey . value || saving . value ) return false ;
saving . value = true ;
try {
await saveStudentTrainingAnswer ( pageKey . value , buildAnswerPayload ( saveAction ) ) ;
hasSavedAnswer . value = hasAnyAnswer . value ;
proxy ? . $modal ? . msgSuccess ( saveAction === "SUBMIT" ? "提交成功" : "已保存" ) ;
return true ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( saveAction === "SUBMIT" ? "提交失败" : "保存失败" ) ;
return false ;
} finally {
saving . value = false ;
}
}
function saveCurrentProgress ( ) {
return saveAnswer ( "SAVE" ) ;
}
function goPreviousStep ( ) {
if ( currentStep . value > 1 ) {
currentStep . value -= 1 ;
}
}
async function goNextStep ( ) {
if ( currentStep . value >= configuredSteps . value . length ) return ;
const saved = await saveCurrentProgress ( ) ;
if ( saved ) {
currentStep . value += 1 ;
}
}
function submitTask ( ) {
return saveAnswer ( "SUBMIT" ) ;
}
async function resetTraining ( ) {
form . value = buildInitialForm ( ) ;
workRows . value = buildInitialRows ( ) ;
processRows . value = buildInitialProcessRows ( ) ;
comprehensiveStepOne . value = createComprehensiveStepOne ( ) ;
comprehensiveStepTwo . value = createComprehensiveStepTwo ( ) ;
comprehensiveStepThree . value = createComprehensiveStepThree ( ) ;
comprehensiveStepFour . value = createComprehensiveStepFour ( ) ;
comprehensiveStepFive . value = createComprehensiveStepFive ( ) ;
comprehensiveStepSix . value = createComprehensiveStepSix ( ) ;
comprehensiveStepSeven . value = createComprehensiveStepSeven ( ) ;
comprehensiveStepEight . value = createComprehensiveStepEight ( ) ;
comprehensiveStepNine . value = createComprehensiveStepNine ( ) ;
stepNineSentimentVisible . value = false ;
try {
await saveAnswer ( "RESET" ) ;
} catch ( error ) {
// saveAnswer already reports failures.
}
}
function triggerUpload ( ) {
if ( isDemo . value ) { proxy ? . $modal ? . msgWarning ? . ( "演示模式不上传文件" ) ; return ; }
uploadInputRef . value ? . click ( ) ;
}
async function handleProcessFileChange ( event ) {
const file = event . target . files ? . [ 0 ] ;
if ( ! file ) return ;
const formData = new FormData ( ) ;
formData . append ( "file" , file ) ;
uploading . value = true ;
try {
const res = await uploadStudentTrainingFile ( formData ) ;
form . value = {
... form . value ,
ganttFileName : res ? . originalFilename || file . name ,
ganttOriginalFilename : res ? . originalFilename || file . name ,
ganttFileUrl : res ? . url || res ? . fileName || "" ,
} ;
proxy ? . $modal ? . msgSuccess ( "文件已上传" ) ;
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( "文件上传失败" ) ;
} finally {
uploading . value = false ;
if ( event . target ) {
event . target . value = "" ;
}
}
}
function exportOutcome ( ) {
const blob = new Blob ( [ JSON . stringify ( buildOutcome ( ) , null , 2 ) ] , { type : "application/json;charset=utf-8" } ) ;
const url = URL . createObjectURL ( blob ) ;
const link = document . createElement ( "a" ) ;
link . href = url ;
link . download = ` ${ page . value . title } -实训成果.json ` ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
URL . revokeObjectURL ( url ) ;
proxy ? . $modal ? . msgSuccess ( "导出成功" ) ;
}
< / script >
< style lang = "scss" scoped >
. student - training - page {
/* 紧凑字号令牌:全局 var(--student-fs-*) 规则在本页自动取紧凑值 */
-- student - fs - display : 17 px ;
-- student - fs - h1 : 15 px ;
-- student - fs - h2 : 14 px ;
-- student - fs - h3 : 13 px ;
-- student - fs - body : 13 px ;
-- student - fs - body - sm : 12 px ;
-- student - fs - caption : 11 px ;
position : relative ;
z - index : 2 ;
display : grid ;
grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ;
gap : 24 px ;
width : 100 % ;
max - width : 100 % ;
min - height : calc ( 100 vh - 106 px ) ;
margin : 0 ;
/* padding 由架构标签 .app-main 提供,子页面根标签不再叠加 */
// padding: 24px 24px 32px;
color : # 1 f2937 ;
background : # ffffff ;
}
. training - core ,
. ai - card {
border : 1 px solid # e5e7eb ;
background : # ffffff ;
box - shadow : none ;
}
. training - ai - tools {
display : flex ;
flex - direction : column ;
gap : 22 px ;
min - width : 0 ;
}
. training - core {
padding : 20 px ;
border - radius : 16 px ;
}
. task - header {
margin - bottom : 16 px ;
}
. task - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
min - width : 0 ;
}
. task - title {
margin : 0 ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - display ) ;
line - height : 1.35 ;
font - weight : 700 ;
}
. task - brief {
margin : 0 ;
color : # 6 b7280 ;
font - size : 12 px ;
line - height : 1.6 ;
}
. task - brief - text {
display : - webkit - box ;
overflow : hidden ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 1 ;
}
. task - brief - toggle {
padding : 0 ;
border : 0 ;
color : # 2563 eb ;
background : transparent ;
font : inherit ;
font - weight : 700 ;
cursor : pointer ;
& : hover {
text - decoration : underline ;
}
}
. task - brief - more {
display : grid ;
gap : 6 px ;
margin - top : 8 px ;
padding - top : 8 px ;
border - top : 1 px solid # e5e7eb ;
b {
color : # 1 f2937 ;
font - size : 12 px ;
}
ul {
display : grid ;
gap : 4 px ;
margin : 0 ;
padding - left : 18 px ;
}
p {
margin : 0 ;
}
}
. goal - bg {
margin - bottom : 28 px ;
padding : 14 px 18 px ;
border : 1 px solid # e5e7eb ;
border - radius : 16 px ;
background : # ffffff ;
p ,
li {
color : # 4 b5563 ;
font - size : 13 px ;
line - height : 1.6 ;
}
p ,
ol {
margin : 0 ;
}
ol {
padding - left : 18 px ;
}
}
. subtitle - icon {
display : flex ;
align - items : center ;
gap : 8 px ;
margin - bottom : 8 px ;
color : # 6 b7280 ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.12 em ;
}
. subtitle - icon -- spaced {
margin - top : 12 px ;
}
. task - card ,
. generic - step - card ,
. process - step - card {
padding : 18 px 22 px ;
border : 1 px solid # e5e7eb ;
border - radius : 14 px ;
background : # ffffff ;
}
. task - card {
margin - top : 0 ;
}
. generic - step - card ,
. process - step - card {
margin - top : 32 px ;
}
. training - form ,
. table - panel {
margin : 0 ;
padding : 0 ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
}
. workbench - head {
display : flex ;
justify - content : space - between ;
align - items : flex - start ;
gap : 18 px ;
margin - bottom : 18 px ;
padding - bottom : 14 px ;
border - bottom : 1 px solid # e5e7eb ;
h2 ,
h3 {
margin : 4 px 0 6 px ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h1 ) ! important ;
font - weight : 700 ! important ;
line - height : 1.35 ! important ;
}
p {
margin : 0 ;
color : # 6 b7280 ;
font - size : var ( -- student - fs - body ) ;
font - weight : 400 ;
line - height : 1.6 ;
}
}
. workbench - eyebrow {
margin : 0 0 6 px ! important ;
color : # 6 b7280 ! important ;
font - size : var ( -- student - fs - caption ) ! important ;
font - weight : 700 ! important ;
letter - spacing : 0.12 em ;
line - height : 1.4 ! important ;
}
. comprehensive - training - toolbar {
flex : 0 0 auto ;
max - width : 100 % ;
justify - content : flex - end ;
}
. workbench - actions {
display : flex ;
flex : 0 0 auto ;
flex - wrap : wrap ;
margin - left : auto ;
justify - content : flex - end ;
gap : 10 px ;
}
. step - navigation {
display : flex ;
justify - content : center ;
align - items : center ;
gap : 14 px ;
margin - top : 22 px ;
padding - top : 20 px ;
border - top : 1 px solid # e5e7eb ;
: deep ( . el - button ) {
min - width : 112 px ;
height : 40 px ;
margin : 0 ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 10 px ;
color : # 2563 eb ;
background : # ffffff ;
font - weight : 700 ;
}
: deep ( . el - button -- primary ) {
border - color : # 2563 eb ;
color : # ffffff ;
background : # 2563 eb ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.16 ) ;
}
: deep ( . el - button . is - plain ) {
border - color : rgba ( 37 , 99 , 235 , 0.35 ) ;
color : # 2563 eb ;
background : # ffffff ;
}
: deep ( . el - button : hover : not ( : disabled ) ) {
border - color : # 2563 eb ;
color : # 2563 eb ;
background : # eff6ff ;
}
: deep ( . el - button -- primary : hover : not ( : disabled ) ) {
border - color : # 1 d4ed8 ;
color : # ffffff ;
background : # 1 d4ed8 ;
}
. btn - save {
height : 36 px ;
min - height : 36 px ;
padding : 0 14 px ;
border - color : # e5e7eb ;
color : # 374151 ;
background : # f3f4f6 ;
border - radius : 8 px ;
}
. btn - save : hover : not ( : disabled ) {
border - color : # d1d5db ;
color : # 1 f2937 ;
background : # f3f4f6 ;
}
}
. material - download {
display : inline - flex ;
align - items : center ;
gap : 6 px ;
max - width : 260 px ;
min - height : 40 px ;
padding : 0 18 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 10 px ;
color : # 2563 eb ;
background : # ffffff ;
font - size : 13 px ;
font - weight : 700 ;
line - height : 1.2 ;
text - decoration : none ;
& : hover {
color : # 1 d4ed8 ;
border - color : # 2563 eb ;
background : # eff6ff ;
}
}
. section - eyebrow {
margin : 0 ;
color : # 6 b7280 ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.12 em ;
line - height : 1.4 ;
}
. step - indicator {
position : relative ;
display : flex ;
align - items : center ;
gap : 0 ;
width : calc ( 100 % + 24 px ) ;
margin : 0 - 12 px 32 px ;
padding : 18 px 22 px ;
border : 1 px solid # e5e7eb ;
border - radius : 14 px ;
background : # ffffff ;
box - shadow : none ;
overflow - x : auto ;
scrollbar - width : thin ;
}
. step - tab {
display : flex ;
align - items : center ;
flex : 1 0 145 px ;
min - width : 0 ;
min - height : 42 px ;
padding : 8 px 14 px ;
border : 1 px solid # e5e7eb ;
border - radius : 8 px ;
color : # 6 b7280 ;
background : # ffffff ;
box - shadow : none ;
font - family : inherit ;
cursor : pointer ;
text - align : center ;
transition : color 0.2 s ease , background 0.2 s ease , border - color 0.2 s ease , box - shadow 0.2 s ease , transform 0.2 s ease ;
& : hover {
border - color : # 2563 eb ;
color : # 1 d4ed8 ;
background : # eff6ff ;
box - shadow : 0 3 px 10 px rgba ( 37 , 99 , 235 , 0.12 ) ;
transform : translateY ( - 1 px ) ;
. step - action {
color : # 2563 eb ;
}
}
& : focus - visible {
outline : 2 px solid # 2563 eb ;
outline - offset : 4 px ;
}
& . active {
border - color : # 1 d4ed8 ;
color : # ffffff ;
background : # 2563 eb ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.2 ) ;
. step - marker {
border - color : # ffffff ;
color : # 1 d4ed8 ;
background : # ffffff ;
box - shadow : none ;
}
}
& . complete {
border - color : # dbeafe ;
color : # 1 d4ed8 ;
. step - marker {
border - color : transparent ;
color : # 1 d4ed8 ;
background : rgba ( 37 , 99 , 235 , 0.1 ) ;
box - shadow : none ;
}
}
}
. step - marker {
display : inline - flex ;
flex : 0 0 22 px ;
align - items : center ;
justify - content : center ;
width : 22 px ;
height : 22 px ;
border : 1 px solid # e5e7eb ;
border - radius : 6 px ;
color : # 6 b7280 ;
background : # f3f4f6 ;
box - shadow : none ;
font - family : ui - monospace , "SF Mono" , Consolas , monospace ;
font - size : 12 px ;
font - weight : 700 ;
line - height : 1 ;
letter - spacing : 0 ;
transition : border - color 0.2 s ease , color 0.2 s ease , background 0.2 s ease , box - shadow 0.2 s ease ;
}
. step - copy {
display : grid ;
gap : 4 px ;
min - width : 0 ;
margin - left : 10 px ;
}
. step - name {
display : - webkit - box ;
overflow : hidden ;
color : inherit ;
font - size : 14 px ;
font - weight : 800 ;
line - height : 1.25 ;
text - overflow : ellipsis ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 2 ;
}
. step - action {
color : # 6 b7280 ;
font - size : 10 px ;
font - weight : 800 ;
letter - spacing : 0.06 em ;
line - height : 1 ;
transition : color 0.2 s ease ;
}
. step - connector {
position : relative ;
flex : 0 1 40 px ;
min - width : 18 px ;
height : 2 px ;
margin : 0 8 px ;
background : # e5e7eb ;
}
. step - connector : : after {
position : absolute ;
top : 50 % ;
right : - 1 px ;
width : 6 px ;
height : 6 px ;
border - top : 1 px solid # 6 b7280 ;
border - right : 1 px solid # 6 b7280 ;
content : "" ;
transform : translateY ( - 50 % ) rotate ( 45 deg ) ;
}
. step - tab . complete + . step - connector {
background : # 93 c5fd ;
box - shadow : none ;
}
. step - tab . complete + . step - connector : : after {
border - color : # 93 c5fd ;
}
. comprehensive - step - nav {
position : relative ;
width : calc ( 100 % + 24 px ) ;
margin : 0 - 12 px 32 px ;
padding : 18 px 22 px ;
overflow - x : auto ;
overflow - y : hidden ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
cursor : grab ;
overscroll - behavior - x : contain ;
scroll - behavior : smooth ;
scrollbar - width : thin ;
scrollbar - color : # d1d5db transparent ;
touch - action : pan - y ;
user - select : none ;
}
. comprehensive - step - nav . is - dragging {
cursor : grabbing ;
scroll - behavior : auto ;
}
. comprehensive - step - strip {
display : flex ;
align - items : center ;
width : max - content ;
min - width : 100 % ;
}
. comprehensive - step - strip . step - tab {
flex : 0 0 190 px ;
width : 190 px ;
min - width : 190 px ;
}
. comprehensive - step - strip . step - name {
display : block ;
white - space : nowrap ;
font - size : 13 px ;
- webkit - line - clamp : unset ;
}
. comprehensive - step - strip . step - marker {
flex - basis : 22 px ;
width : 22 px ;
height : 22 px ;
font - size : 11 px ;
}
. comprehensive - step - strip . step - copy {
margin - left : 8 px ;
}
. comprehensive - step - strip . step - connector {
flex : 0 0 30 px ;
width : 30 px ;
min - width : 30 px ;
margin : 0 5 px ;
}
. step - title - row {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 18 px ;
margin - bottom : 14 px ;
h3 {
margin : 4 px 0 0 ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h1 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
}
. flow - rail {
display : grid ;
grid - template - columns : repeat ( auto - fit , minmax ( 160 px , 1 fr ) ) ;
gap : 12 px ;
margin - top : 18 px ;
}
. process - training {
margin - top : 4 px ;
}
. process - intro ,
. upload - tip {
margin : 0 0 18 px ;
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. process - table - wrap {
overflow - x : auto ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. process - table {
width : 100 % ;
min - width : 1080 px ;
border - collapse : collapse ;
table - layout : fixed ;
th {
padding : 14 px 16 px ;
border - right : 1 px solid # e5e7eb ;
color : # 1 f2937 ;
background : # ffffff ;
font - size : 13 px ;
font - weight : 700 ;
text - align : center ;
}
td {
padding : 16 px ;
border - top : 1 px solid # e5e7eb ;
border - right : 1 px solid # e5e7eb ;
vertical - align : middle ;
}
th : last - child ,
td : last - child {
border - right : 0 ;
}
}
. serial - col {
width : 72 px ;
}
. name - col {
width : 190 px ;
}
. desc - col {
width : 280 px ;
}
. serial - cell ,
. task - name - cell {
color : # 1 f2937 ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 700 ;
text - align : center ;
}
. desc - cell {
color : # 4 b5563 ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.6 ;
text - align : center ;
}
. process - textarea {
width : 100 % ;
min - height : 112 px ;
padding : 12 px 14 px ;
border : 1 px solid # d1d5db ;
border - radius : 10 px ;
outline : none ;
resize : vertical ;
color : # 1 f2937 ;
background : # ffffff ;
line - height : 1.6 ;
transition : border - color 0.16 s ease , box - shadow 0.16 s ease , background 0.16 s ease ;
& : : placeholder {
color : # 6 b7280 ;
}
& : focus {
border - color : # 2563 eb ;
background : # eff6ff ;
box - shadow : 0 0 0 3 px rgba ( 37 , 99 , 235 , 0.14 ) ;
}
}
. upload - tip {
margin - top : 26 px ;
margin - bottom : 22 px ;
}
. upload - action {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
justify - content : center ;
gap : 14 px ;
}
. hidden - file - input {
display : none ;
}
. upload - btn {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 8 px ;
min - height : 40 px ;
padding : 0 18 px ;
border : 1 px solid # 2563 eb ;
border - radius : 10 px ;
color : # ffffff ;
background : # 2563 eb ;
box - shadow : none ;
font - size : var ( -- student - fs - body ) ;
font - weight : 700 ;
cursor : pointer ;
}
. upload - file - name {
color : # 1 f2937 ;
font - size : 13 px ;
font - weight : 700 ;
}
. flow - step ,
. key - point - card {
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
box - shadow : none ;
}
. flow - step {
min - height : 86 px ;
padding : 14 px 16 px ;
span {
display : block ;
margin - bottom : 6 px ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
}
strong {
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
font - weight : 400 ;
line - height : 1.6 ;
}
}
. key - point - grid {
display : grid ;
grid - template - columns : repeat ( auto - fit , minmax ( 160 px , 1 fr ) ) ;
gap : 12 px ;
margin : 0 0 20 px ;
}
. key - point - card {
padding : 14 px 16 px ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
color : # 4 b5563 ;
background : # ffffff ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 700 ;
line - height : 1.6 ;
}
. form - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 14 px ;
}
. panel - title {
margin : 22 px 0 16 px ;
h4 {
margin : 0 0 8 px ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
}
p {
margin : 0 ;
color : # 6 b7280 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
}
: deep ( . el - form - item _ _label ) {
color : # 4 b5563 ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 600 ;
line - height : 1.6 ;
}
: deep ( . el - input _ _wrapper ) ,
: deep ( . el - textarea _ _inner ) {
border : 1 px solid # d1d5db ;
border - radius : 8 px ;
box - shadow : none ;
background : # ffffff ;
}
: deep ( . el - textarea _ _inner ) {
padding : 12 px 16 px ;
}
: deep ( . el - input _ _inner ) ,
: deep ( . el - textarea _ _inner ) {
color : # 1 f2937 ;
}
: deep ( . el - input _ _inner : : placeholder ) ,
: deep ( . el - textarea _ _inner : : placeholder ) {
color : # 6 b7280 ;
}
. training - table {
-- el - table - border - color : # e5e7eb ;
-- el - table - header - bg - color : # ffffff ;
-- el - table - header - text - color : # 1 f2937 ;
-- el - table - row - hover - bg - color : # f9fafb ;
-- el - table - bg - color : # ffffff ;
-- el - table - tr - bg - color : # ffffff ;
-- el - table - text - color : # 4 b5563 ;
border - radius : 12 px ;
overflow : hidden ;
}
. training - actions {
display : flex ;
flex - wrap : wrap ;
justify - content : center ;
gap : 14 px ;
margin - top : 24 px ;
}
. training - actions : deep ( . training - action ) {
min - width : 112 px ;
height : 36 px ;
min - height : 36 px ;
padding : 0 14 px ;
border - radius : 8 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 10 px ;
color : # 2563 eb ;
background : # ffffff ;
box - shadow : none ;
font - size : var ( -- student - fs - body ) ;
font - weight : 700 ;
line - height : 1 ;
letter - spacing : 0 ;
text - shadow : none ;
}
. training - actions : deep ( . training - action span ) {
display : inline - flex ;
align - items : center ;
color : inherit ;
font - size : inherit ;
line - height : 1 ;
}
. training - actions : deep ( . training - action -- submit ) {
border - color : # 2563 eb ;
color : # ffffff ;
background : # 2563 eb ;
}
. training - actions : deep ( . training - action -- reset ) {
border - color : rgba ( 37 , 99 , 235 , 0.35 ) ;
color : # 2563 eb ;
background : # ffffff ;
}
. training - actions : deep ( . training - action -- submit : hover : not ( . is - disabled ) ) {
border - color : # 1 d4ed8 ;
color : # ffffff ;
background : # 1 d4ed8 ;
}
. training - actions : deep ( . training - action -- reset : hover : not ( . is - disabled ) ) ,
. training - actions : deep ( . training - action : hover : not ( . is - disabled ) ) {
border - color : # 2563 eb ;
color : # 1 d4ed8 ;
background : # eff6ff ;
}
. product - development - process - page {
. goal - bg {
margin - bottom : 28 px ;
padding : 14 px 18 px ;
border : 1 px solid # e5e7eb ;
border - radius : 16 px ;
background : # ffffff ;
}
. task - card {
margin - top : 0 ;
padding : 18 px 22 px ;
border : 1 px solid # e5e7eb ;
border - radius : 14 px ;
background : # ffffff ;
}
. workbench - head h3 {
font - size : var ( -- student - fs - h1 ) ! important ;
font - weight : 700 ! important ;
line - height : 1.35 ! important ;
}
. workbench - head . section - eyebrow {
color : # 6 b7280 ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.12 em ;
line - height : 1.4 ;
}
. workbench - description {
color : # 6 b7280 ! important ;
font - size : var ( -- student - fs - body ) ! important ;
font - weight : 400 ;
line - height : 1.6 ! important ;
}
. workbench - actions : deep ( . material - download ) ,
. workbench - actions : deep ( . outline - action ) {
min - width : 0 ! important ;
min - height : 34 px ! important ;
height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
font - size : 13 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
}
. process - step - card {
padding : 18 px 22 px ;
border : 1 px solid # e5e7eb ;
border - radius : 14 px ;
background : # ffffff ;
}
. step - title - row {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 18 px ;
margin - bottom : 14 px ;
h3 {
margin : 4 px 0 0 ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h1 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
}
. process - intro ,
. upload - tip {
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. process - table - wrap {
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. process - table {
th {
padding : 12 px ;
border - right - color : # e5e7eb ;
background : # ffffff ;
font - size : 13 px ;
font - weight : 700 ;
}
td {
padding : 12 px ;
border - top - color : # e5e7eb ;
border - right - color : # e5e7eb ;
background : # ffffff ;
vertical - align : top ;
}
}
. serial - cell ,
. task - name - cell ,
. desc - cell {
font - size : 13 px ;
}
. process - textarea {
min - height : 110 px ;
padding : 12 px 14 px ;
border : 1 px solid # d1d5db ;
border - radius : 10 px ;
color : # 1 f2937 ;
background : # ffffff ;
}
. upload - action {
justify - content : flex - start ;
}
. upload - btn {
min - width : 0 ! important ;
min - height : 34 px ! important ;
height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid # 2563 eb ! important ;
border - radius : 8 px ! important ;
background : # 2563 eb ! important ;
box - shadow : none ! important ;
font - size : 13 px ! important ;
font - weight : 700 ! important ;
}
. upload - file - name {
color : # 1 f2937 ;
font - size : 13 px ;
font - weight : 700 ;
}
}
@ media ( max - width : 1100 px ) {
. student - training - page {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 900 px ) {
. student - training - page {
margin : 0 ;
/* padding 由架构标签 .app-main 提供,子页面根标签不再叠加 */
// padding: 14px 14px 18px;
}
. training - core {
padding : 18 px ;
border - radius : 16 px ;
}
. workbench - head ,
. workbench - actions ,
. step - title - row {
flex - direction : column ;
align - items : stretch ;
}
. workbench - actions : deep ( . material - download ) ,
. workbench - actions : deep ( . outline - action ) {
width : 100 % ;
}
. workbench - head {
flex - direction : column ;
}
. comprehensive - training - toolbar {
width : 100 % ;
justify - content : flex - start ;
}
. step - navigation {
flex - wrap : wrap ;
: deep ( . el - button ) {
flex : 1 1 132 px ;
}
}
. form - grid {
grid - template - columns : 1 fr ;
}
. step - indicator {
width : 100 % ;
margin : 0 0 32 px ;
padding : 16 px ;
overflow : visible ;
flex - direction : column ;
align - items : stretch ;
border - radius : 20 px ;
}
. step - tab {
flex : 0 0 auto ;
width : 100 % ;
min - width : 0 ;
}
. step - connector {
flex : 0 0 20 px ;
width : 2 px ;
min - width : 0 ;
height : 20 px ;
align - self : flex - start ;
margin : 4 px 0 4 px 18 px ;
}
. step - connector : : after {
top : auto ;
right : auto ;
bottom : - 1 px ;
left : 50 % ;
transform : translateX ( - 50 % ) rotate ( 135 deg ) ;
}
. comprehensive - step - nav {
width : 100 % ;
margin : 0 0 32 px ;
padding : 16 px ;
overflow - x : auto ;
}
. comprehensive - step - strip . step - tab {
flex : 0 0 172 px ;
width : 172 px ;
min - width : 172 px ;
}
. comprehensive - step - strip . step - connector {
flex : 0 0 24 px ;
width : 24 px ;
min - width : 24 px ;
height : 2 px ;
align - self : center ;
margin : 0 5 px ;
}
. comprehensive - step - strip . step - connector : : after {
top : 50 % ;
right : - 1 px ;
bottom : auto ;
left : auto ;
transform : translateY ( - 50 % ) rotate ( 45 deg ) ;
}
. product - development - process - page {
. workbench - actions ,
. step - title - row ,
. upload - action {
flex - direction : column ;
align - items : stretch ;
}
. upload - btn {
width : 100 % ;
}
}
. training - actions {
flex - direction : column ;
align - items : stretch ;
gap : 14 px ;
}
. training - actions : deep ( . training - action ) {
width : 100 % ;
}
}
. comprehensive - step - card {
margin - top : 32 px ;
padding : 20 px ;
border : 1 px solid # e5e7eb ;
border - radius : 14 px ;
background : # ffffff ;
}
. comprehensive - analysis - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 34 px ;
}
. analysis - panel h4 {
margin : 0 0 12 px ;
color : # 1 f2937 ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
. analysis - panel > p ,
. comprehensive - conclusion {
margin : 0 ;
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.65 ;
}
. analysis - field - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 26 px 22 px ;
margin - top : 14 px ;
}
. analysis - field - grid label ,
. comprehensive - conclusion {
display : grid ;
gap : 9 px ;
color : # 4 b5563 ;
}
. analysis - field - grid : deep ( . el - textarea _ _inner ) ,
. comprehensive - conclusion : deep ( . el - textarea _ _inner ) {
min - height : 150 px ! important ;
color : # 1 f2937 ;
background : # ffffff ;
box - shadow : 0 0 0 1 px # e5e7eb inset ;
}
. comprehensive - conclusion {
margin - top : 32 px ;
}
. comprehensive - conclusion : deep ( . el - textarea _ _inner ) {
min - height : 170 px ! important ;
}
. comprehensive - step - two {
display : grid ;
gap : 24 px ;
}
. comprehensive - step - two . step - title - row {
margin - bottom : 0 ;
}
. comprehensive - instruction {
margin : 0 ;
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.65 ;
}
. comprehensive - table - wrap {
overflow - x : auto ;
border : 1 px solid # e5e7eb ;
border - radius : 10 px ;
}
. comprehensive - table {
width : 100 % ;
min - width : 850 px ;
border - collapse : collapse ;
}
. comprehensive - table th ,
. comprehensive - table td {
padding : 9 px 10 px ;
border - right : 1 px solid # e5e7eb ;
border - bottom : 1 px solid # e5e7eb ;
color : # 4 b5563 ;
text - align : center ;
vertical - align : middle ;
}
. comprehensive - table th : last - child ,
. comprehensive - table td : last - child {
border - right : 0 ;
}
. comprehensive - table tbody tr : last - child th ,
. comprehensive - table tbody tr : last - child td {
border - bottom : 0 ;
}
. comprehensive - table thead th {
color : # 1 f2937 ;
background : # ffffff ;
font - weight : 700 ;
white - space : nowrap ;
}
. comprehensive - table tbody th {
background : # ffffff ;
font - weight : 700 ;
}
. comprehensive - table : deep ( . el - input _ _wrapper ) {
background : # ffffff ;
box - shadow : 0 0 0 1 px # e5e7eb inset ;
}
. comprehensive - table : deep ( . el - input _ _inner ) ,
. comprehensive - table : deep ( . el - textarea _ _inner ) {
color : # 1 f2937 ;
}
. comprehensive - score - section ,
. comprehensive - review - section {
display : grid ;
gap : 14 px ;
}
. comprehensive - section - line {
display : flex ;
align - items : center ;
flex - wrap : wrap ;
gap : 14 px ;
}
. comprehensive - section - line p {
margin : 0 ;
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.65 ;
}
. comprehensive - section - line > div {
display : flex ;
gap : 8 px ;
}
. compact - action {
min - height : 32 px ;
margin : 0 ! important ;
border - color : # d1d5db ! important ;
color : # 374151 ! important ;
background : # ffffff ! important ;
font - weight : 700 ;
}
. comprehensive - score - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 18 px 28 px ;
}
. comprehensive - score - grid label {
display : grid ;
grid - template - columns : auto minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) ;
align - items : center ;
gap : 8 px ;
color : # 4 b5563 ;
white - space : nowrap ;
}
. comprehensive - review - table td {
height : 76 px ;
}
. comprehensive - review - table th : first - child {
width : 16 % ;
}
. comprehensive - review - table td : nth - child ( 2 ) {
width : 12 % ;
}
. comprehensive - step - three {
display : grid ;
gap : 26 px ;
}
. rfm - upload - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 18 px ;
}
. rfm - upload - card {
display : grid ;
align - content : start ;
gap : 10 px ;
min - height : 146 px ;
padding : 16 px ;
border : 1 px dashed # d1d5db ;
border - radius : 12 px ;
background : # ffffff ;
}
. rfm - upload - card . upload - btn {
justify - self : center ;
}
. rfm - upload - card > p {
margin : 0 ;
color : # 6 b7280 ;
font - size : 12 px ;
line - height : 1.5 ;
text - align : center ;
}
. comprehensive - file - preview {
display : grid ;
gap : 9 px ;
min - width : 0 ;
padding - top : 10 px ;
border - top : 1 px solid # e5e7eb ;
}
. comprehensive - file - preview : deep ( . el - image ) {
width : 100 % ;
height : 155 px ;
border : 1 px solid # e5e7eb ;
border - radius : 8 px ;
}
. excel - preview {
display : grid ;
gap : 5 px ;
color : # 4 b5563 ;
font - size : 12 px ;
}
. excel - preview > span {
color : # 6 b7280 ;
}
. excel - preview - table {
overflow : auto ;
max - height : 150 px ;
border : 1 px solid # e5e7eb ;
}
. excel - preview - table table {
width : max - content ;
min - width : 100 % ;
border - collapse : collapse ;
}
. excel - preview - table th ,
. excel - preview - table td {
max - width : 140 px ;
padding : 6 px 8 px ;
overflow : hidden ;
border - right : 1 px solid # e5e7eb ;
border - bottom : 1 px solid # e5e7eb ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. excel - preview - table th {
background : # f3f4f6 ;
}
. file - preview - actions {
display : flex ;
justify - content : space - between ;
gap : 12 px ;
}
. file - preview - actions a ,
. file - preview - actions button {
padding : 0 ;
border : 0 ;
color : # 1 f2937 ;
background : transparent ;
font : inherit ;
font - size : 12 px ;
cursor : pointer ;
}
. file - preview - actions button {
color : # d9554c ;
}
. comprehensive - analysis - section {
display : grid ;
gap : 14 px ;
}
. rfm - analysis - table th : first - child {
width : 22 % ;
}
. rfm - analysis - table td : nth - child ( 2 ) {
width : 32 % ;
text - align : left ;
}
. rfm - profile - table td {
height : 74 px ;
}
. kano - table th : first - child {
width : 9 % ;
}
. kano - table td : nth - child ( 2 ) {
width : 40 % ;
}
. kano - table td : nth - child ( 3 ) {
width : 24 % ;
}
. kano - table : deep ( . el - select ) {
width : 100 % ;
}
. comprehensive - step - four {
display : grid ;
gap : 26 px ;
}
. journey - table ,
. requirements - table ,
. rice - table {
min - width : 1120 px ;
}
. journey - table th : nth - child ( 1 ) {
width : 5 % ;
}
. journey - table th : nth - child ( 2 ) {
width : 10 % ;
}
. journey - table td : nth - child ( 3 ) ,
. journey - table td : nth - child ( 4 ) ,
. journey - table td : nth - child ( 6 ) {
width : 24 % ;
}
. journey - table td : nth - child ( 5 ) {
width : 12 % ;
}
. journey - table : deep ( . el - select ) ,
. rice - table : deep ( . el - select ) {
width : 100 % ;
}
. journey - pain - table {
min - width : 780 px ;
}
. journey - pain - table th : first - child {
width : 19 % ;
}
. requirements - table th : first - child {
width : 6 % ;
}
. requirements - table td : nth - child ( 2 ) ,
. requirements - table td : nth - child ( 3 ) ,
. requirements - table td : nth - child ( 4 ) ,
. requirements - table td : nth - child ( 5 ) ,
. requirements - table td : nth - child ( 6 ) {
width : 18.8 % ;
}
. rice - table th : first - child {
width : 10 % ;
}
. rice - table td {
min - width : 115 px ;
}
. rice - score {
color : # 1 f2937 ;
font - weight : 900 ;
}
. value - canvas {
overflow - x : auto ;
padding : 16 px 4 px ;
}
. value - map {
display : grid ;
grid - template - columns : minmax ( 280 px , 1 fr ) 64 px minmax ( 360 px , 1.1 fr ) ;
align - items : center ;
min - width : 760 px ;
gap : 18 px ;
}
. value - map - left ,
. value - map - right {
display : grid ;
gap : 12 px ;
padding : 20 px ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. value - map - right {
border - radius : 50 % ;
aspect - ratio : 1.15 / 1 ;
padding : 34 px 72 px ;
}
. value - map label {
display : grid ;
gap : 7 px ;
color : # 4 b5563 ;
font - weight : 800 ;
}
. value - map : deep ( . el - textarea _ _inner ) {
color : # 1 f2937 ;
background : # ffffff ;
box - shadow : 0 0 0 1 px # e5e7eb inset ;
}
. value - fit {
display : grid ;
width : 58 px ;
height : 58 px ;
place - items : center ;
border : 1 px solid # 2563 eb ;
border - radius : 50 % ;
color : # ffffff ;
background : # 2563 eb ;
box - shadow : none ;
font - size : 15 px ;
font - weight : 700 ;
}
. version - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 28 px ;
}
. version - grid label {
display : grid ;
gap : 9 px ;
color : # 4 b5563 ;
}
. version - grid : deep ( . el - textarea _ _inner ) {
min - height : 175 px ! important ;
color : # 1 f2937 ;
background : # ffffff ;
box - shadow : 0 0 0 1 px # e5e7eb inset ;
}
. comprehensive - step - five {
display : grid ;
gap : 26 px ;
}
. architecture - table {
min - width : 1420 px ;
}
. architecture - table th : first - child {
width : 7 % ;
}
. architecture - table td {
min - width : 150 px ;
}
. fault - table {
min - width : 980 px ;
}
. fault - table th : first - child {
width : 24 % ;
}
. fault - table td {
width : 25.3 % ;
}
. policy - table {
min - width : 920 px ;
}
. policy - table th : first - child {
width : 23 % ;
}
. policy - table td {
width : 38.5 % ;
}
. after - sales - summary - table {
min - width : 740 px ;
}
. after - sales - summary - table th : first - child {
width : 20 % ;
}
. comprehensive - step - six {
display : grid ;
gap : 26 px ;
}
. cost - constraint - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 20 px 28 px ;
}
. cost - constraint - grid label {
display : grid ;
grid - template - columns : auto minmax ( 110 px , 1 fr ) auto ;
align - items : center ;
gap : 8 px ;
color : # 4 b5563 ;
font - weight : 700 ;
}
. cost - constraint - grid : deep ( . el - input _ _wrapper ) {
background : # ffffff ;
box - shadow : 0 0 0 1 px # e5e7eb inset ;
}
. bom - table {
min - width : 880 px ;
}
. manufacturing - table {
min - width : 820 px ;
}
. procurement - weight - table {
min - width : 1060 px ;
}
. procurement - score - table {
min - width : 1140 px ;
}
. computed - row th ,
. computed - row td {
color : # 1 f2937 ;
background : # f9fafb ;
font - weight : 700 ;
}
. comprehensive - step - seven {
display : grid ;
gap : 26 px ;
}
. forecast - rule - list {
display : grid ;
gap : 4 px ;
margin : 0 0 16 px ;
color : # 4 b5563 ;
line - height : 1.75 ;
}
. demand - forecast - table ,
. cost - pricing - table {
min - width : 860 px ;
}
. pricing - subsection {
display : grid ;
gap : 12 px ;
}
. pricing - subsection > p {
margin : 0 ;
color : # 1 f2937 ;
line - height : 1.7 ;
}
. pricing - formula {
color : # 6 b7280 ! important ;
}
. cost - price - input {
max - width : 400 px ;
}
. cost - price - input label ,
. competitor - decision > label ,
. value - pricing - result label {
display : grid ;
grid - template - columns : auto minmax ( 120 px , 1 fr ) auto ;
align - items : center ;
gap : 8 px ;
color : # 1 f2937 ;
font - weight : 700 ;
}
. competitor - pricing - section {
margin - top : 24 px ;
}
. competitor - pricing - grid ,
. value - pricing - grid ,
. pricing - summary - grid {
display : grid ;
grid - template - columns : minmax ( 0 , 1.8 fr ) minmax ( 290 px , 0.8 fr ) ;
align - items : start ;
gap : 24 px ;
}
. competitor - table {
min - width : 960 px ;
}
. competitor - decision ,
. value - pricing - result ,
. final - pricing - analysis {
display : grid ;
gap : 14 px ;
padding : 18 px ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. competitor - decision . textarea - label {
grid - template - columns : 1 fr ;
align - items : stretch ;
}
. calculated - field ,
. value - pricing - result strong {
color : # 1 f2937 ;
font - size : 17 px ;
font - weight : 900 ;
}
. value - pricing - table {
min - width : 720 px ;
}
. value - pricing - result p {
margin : 0 ;
color : # 6 b7280 ;
line - height : 1.7 ;
}
. psm - table {
min - width : 1060 px ;
}
. psm - visualization {
display : grid ;
gap : 14 px ;
padding : 18 px ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. psm - legend {
display : flex ;
flex - wrap : wrap ;
gap : 12 px 18 px ;
color : # 4 b5563 ;
font - size : 13 px ;
}
. psm - legend span : : before {
display : inline - block ;
width : 8 px ;
height : 8 px ;
margin - right : 6 px ;
border - radius : 50 % ;
content : "" ;
}
. psm - legend -- expensive : : before ,
. psm - bar -- expensive { background : # 374151 ; }
. psm - legend -- cheap : : before ,
. psm - bar -- cheap { background : # 6 b7280 ; }
. psm - legend -- acceptable : : before ,
. psm - bar -- acceptable { background : # 9 ca3af ; }
. psm - legend -- bargain : : before ,
. psm - bar -- bargain { background : # d1d5db ; }
. psm - visual - row {
display : grid ;
grid - template - columns : 120 px minmax ( 0 , 1 fr ) ;
align - items : center ;
gap : 14 px ;
color : # 4 b5563 ;
}
. psm - bars {
display : grid ;
grid - template - rows : repeat ( 4 , 6 px ) ;
gap : 5 px ;
}
. psm - bar {
display : block ;
min - width : 2 px ;
border - radius : 999 px ;
box - shadow : none ;
}
. pricing - summary - table {
min - width : 700 px ;
}
. final - pricing - analysis {
color : # 1 f2937 ;
font - weight : 800 ;
}
. comprehensive - step - eight {
display : grid ;
gap : 26 px ;
}
. abc - table {
min - width : 980 px ;
}
. new - product - table {
min - width : 1080 px ;
}
. abc - strategy - table {
min - width : 1040 px ;
}
. channel - weight - table {
min - width : 980 px ;
}
. channel - score - table {
min - width : 1180 px ;
}
. channel - score - table th ,
. channel - score - table td {
min - width : 132 px ;
}
. comprehensive - step - nine {
display : grid ;
gap : 26 px ;
}
. budget - total - input {
max - width : 430 px ;
margin : 12 px 0 18 px ;
}
. budget - total - input label ,
. roi - formula - grid label {
display : grid ;
grid - template - columns : auto minmax ( 120 px , 1 fr ) auto ;
align - items : center ;
gap : 8 px ;
color : # 1 f2937 ;
font - weight : 700 ;
}
. budget - table {
min - width : 1280 px ;
}
. roi - formula - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 20 px 34 px ;
max - width : 760 px ;
margin : 20 px 0 ;
}
. roi - formula - grid label : last - child {
grid - template - columns : auto minmax ( 65 px , auto ) 1 fr ;
}
. roi - note {
margin : 8 px 0 ;
color : # 4 b5563 ;
line - height : 1.75 ;
}
. roi - table {
min - width : 1220 px ;
}
. funnel - table {
min - width : 1140 px ;
}
. funnel - diagnosis - heading {
margin - top : 18 px ;
}
. funnel - diagnosis - table {
min - width : 1040 px ;
}
. review - analysis - section {
margin - top : 24 px ;
}
. review - analysis - grid {
display : grid ;
grid - template - columns : minmax ( 0 , 1.15 fr ) minmax ( 300 px , 0.85 fr ) ;
align - items : start ;
gap : 28 px ;
}
. comment - upload - card ,
. top - issues ,
. sentiment - section {
display : grid ;
gap : 14 px ;
padding : 18 px ;
border : 1 px solid # e5e7eb ;
border - radius : 12 px ;
background : # ffffff ;
}
. comment - upload - card > p ,
. top - issues > p ,
. sentiment - section > p {
margin : 0 ;
color : # 6 b7280 ;
line - height : 1.65 ;
}
. top - issues label {
display : grid ;
grid - template - columns : 52 px minmax ( 0 , 1 fr ) ;
align - items : center ;
gap : 8 px ;
color : # 1 f2937 ;
font - weight : 700 ;
}
. comment - cloud {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
gap : 8 px 14 px ;
min - height : 90 px ;
padding : 14 px ;
border : 1 px dashed # d1d5db ;
border - radius : 12 px ;
background : # ffffff ;
}
. comment - cloud span {
color : # 1 f2937 ;
font - weight : 800 ;
line - height : 1 ;
}
. sentiment - section {
grid - template - columns : 1 fr auto ;
align - items : center ;
margin - top : 20 px ;
}
. sentiment - section > p ,
. sentiment - section > . comprehensive - table - wrap ,
. sentiment - visualization {
grid - column : 1 / - 1 ;
}
. sentiment - table {
min - width : 680 px ;
}
. sentiment - visualization {
display : grid ;
gap : 10 px ;
}
. sentiment - visualization span {
display : grid ;
grid - template - columns : 46 px minmax ( 0 , 1 fr ) 54 px ;
align - items : center ;
gap : 10 px ;
color : # 4 b5563 ;
}
. sentiment - visualization i {
height : 9 px ;
border - radius : 999 px ;
background : # 2563 eb ;
box - shadow : none ;
}
. sentiment - visualization em {
color : # 1 f2937 ;
font - style : normal ;
font - weight : 800 ;
}
. ab - test - table {
min - width : 1020 px ;
}
. comprehensive - step - ten {
display : grid ;
gap : 24 px ;
}
. report - intro {
align - items : center ;
}
. report - framework - table {
min - width : 1040 px ;
}
. report - framework - table th : first - child {
width : 14 % ;
}
. report - framework - table th : nth - child ( 2 ) {
width : 25 % ;
}
. report - framework - table td {
color : # 4 b5563 ;
line - height : 1.75 ;
text - align : left ;
}
@ media ( max - width : 900 px ) {
. comprehensive - score - grid {
grid - template - columns : 1 fr ;
}
. rfm - upload - grid {
grid - template - columns : 1 fr ;
}
. version - grid {
grid - template - columns : 1 fr ;
}
. cost - constraint - grid {
grid - template - columns : 1 fr ;
}
. competitor - pricing - grid ,
. value - pricing - grid ,
. pricing - summary - grid {
grid - template - columns : 1 fr ;
}
. psm - visual - row {
grid - template - columns : 1 fr ;
gap : 7 px ;
}
. roi - formula - grid ,
. review - analysis - grid {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 640 px ) {
. comprehensive - section - line ,
. comprehensive - section - line > div {
align - items : stretch ;
flex - direction : column ;
}
. comprehensive - score - grid label {
grid - template - columns : 88 px minmax ( 0 , 1 fr ) ;
}
. comprehensive - score - grid label span {
grid - column : 1 ;
}
. comprehensive - score - grid label : deep ( . el - input ) : last - child {
grid - column : 2 ;
}
. cost - constraint - grid label {
grid - template - columns : 1 fr ;
align - items : stretch ;
}
. cost - price - input label ,
. competitor - decision > label ,
. value - pricing - result label ,
. budget - total - input label ,
. roi - formula - grid label ,
. roi - formula - grid label : last - child {
grid - template - columns : 1 fr ;
align - items : stretch ;
}
. sentiment - section {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 1100 px ) {
. comprehensive - analysis - grid {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 640 px ) {
. comprehensive - step - card {
padding : 16 px ;
border - radius : 14 px ;
}
. analysis - field - grid {
grid - template - columns : 1 fr ;
gap : 18 px ;
}
}
< / style >
< style lang = "scss" >
< / style >