You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/training/GenericTrainingPage.vue

3909 lines
180 KiB
Vue

<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 /></el-icon>
{{ 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" /></el-select></td><td><el-input v-model="row.direction" placeholder="填写功能方向" /></td></tr></tbody></table></div>
</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" /></el-select></td><td><el-input v-model="row.decisionFactors" type="textarea" :autosize="{ minRows: 3, maxRows: 7 }" :placeholder="row.decisionPlaceholder" /></td></tr></tbody></table></div></div>
<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" /></el-select></td></tr></tbody></table></div></div>
<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 ? "收起可视化" : "可视化" }}</el-button></div></div><div class="comprehensive-table-wrap"><table class="comprehensive-table psm-table"><thead><tr><th>价格(元)</th><th>太贵不买累计占比(%)</th><th>太便宜不买累计占比(%)</th><th>偏高可接受累计占比(%)</th><th>便宜划算累计占比(%)</th></tr></thead><tbody><tr v-for="row in comprehensiveStepSeven.psmRows" :key="row.id"><th><el-input v-model="row.range" placeholder="填写价格区间" /></th><td><el-input v-model="row.tooExpensive" placeholder="填写占比" /></td><td><el-input v-model="row.tooCheap" placeholder="填写占比" /></td><td><el-input v-model="row.acceptable" placeholder="填写占比" /></td><td><el-input v-model="row.bargain" placeholder="填写占比" /></td></tr></tbody></table></div><div v-if="psmVisualVisible" class="psm-visualization" aria-live="polite"><div class="psm-legend"><span class="psm-legend--expensive">太贵不买</span><span class="psm-legend--cheap">太便宜不买</span><span class="psm-legend--acceptable">偏高可接受</span><span class="psm-legend--bargain">便宜划算</span></div><article v-for="row in comprehensiveStepSeven.psmRows" :key="`visual-${row.id}`" class="psm-visual-row"><strong>{{ row.range || "未填写价格区间" }}</strong><div class="psm-bars"><span class="psm-bar psm-bar--expensive" :style="{ width: `${comprehensivePsmPercent(row.tooExpensive)}%` }" :title="`太贵不买:${comprehensivePsmPercent(row.tooExpensive)}%`"></span><span class="psm-bar psm-bar--cheap" :style="{ width: `${comprehensivePsmPercent(row.tooCheap)}%` }" :title="`太便宜不买:${comprehensivePsmPercent(row.tooCheap)}%`"></span><span class="psm-bar psm-bar--acceptable" :style="{ width: `${comprehensivePsmPercent(row.acceptable)}%` }" :title="`偏高可接受:${comprehensivePsmPercent(row.acceptable)}%`"></span><span class="psm-bar psm-bar--bargain" :style="{ width: `${comprehensivePsmPercent(row.bargain)}%` }" :title="`便宜划算:${comprehensivePsmPercent(row.bargain)}%`"></span></div></article></div></div>
</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类长尾品类" /></el-select></td></tr></tbody></table></div></div>
<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类长尾品类" /></el-select></td><td><el-input v-model="comprehensiveStepEight.newProduct.support" type="textarea" :autosize="{ minRows: 3, maxRows: 6 }" placeholder="填写流量扶持措施" /></td></tr></tbody></table></div></div>
<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="未达标" /></el-select></td><td><el-input v-model="row.diagnosis" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" placeholder="填写问题诊断" /></td><td><el-input v-model="row.action" 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><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" />
</el-form-item>
</div>
</el-form>
<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>
</el-table-column>
</el-table>
</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: 17px;
--student-fs-h1: 15px;
--student-fs-h2: 14px;
--student-fs-h3: 13px;
--student-fs-body: 13px;
--student-fs-body-sm: 12px;
--student-fs-caption: 11px;
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
width: 100%;
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
/* padding 由架构标签 .app-main 提供,子页面根标签不再叠加 */
// padding: 24px 24px 32px;
color: #1f2937;
background: #ffffff;
}
.training-core,
.ai-card {
border: 1px solid #e5e7eb;
background: #ffffff;
box-shadow: none;
}
.training-ai-tools {
display: flex;
flex-direction: column;
gap: 22px;
min-width: 0;
}
.training-core {
padding: 20px;
border-radius: 16px;
}
.task-header {
margin-bottom: 16px;
}
.task-head-main {
display: grid;
justify-items: start;
gap: 8px;
min-width: 0;
}
.task-title {
margin: 0;
color: #1f2937;
font-size: var(--student-fs-display);
line-height: 1.35;
font-weight: 700;
}
.task-brief {
margin: 0;
color: #6b7280;
font-size: 12px;
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: #2563eb;
background: transparent;
font: inherit;
font-weight: 700;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.task-brief-more {
display: grid;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #e5e7eb;
b {
color: #1f2937;
font-size: 12px;
}
ul {
display: grid;
gap: 4px;
margin: 0;
padding-left: 18px;
}
p {
margin: 0;
}
}
.goal-bg {
margin-bottom: 28px;
padding: 14px 18px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #ffffff;
p,
li {
color: #4b5563;
font-size: 13px;
line-height: 1.6;
}
p,
ol {
margin: 0;
}
ol {
padding-left: 18px;
}
}
.subtitle-icon {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #6b7280;
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.12em;
}
.subtitle-icon--spaced {
margin-top: 12px;
}
.task-card,
.generic-step-card,
.process-step-card {
padding: 18px 22px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #ffffff;
}
.task-card {
margin-top: 0;
}
.generic-step-card,
.process-step-card {
margin-top: 32px;
}
.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: 18px;
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 1px solid #e5e7eb;
h2,
h3 {
margin: 4px 0 6px;
color: #1f2937;
font-size: var(--student-fs-h1) !important;
font-weight: 700 !important;
line-height: 1.35 !important;
}
p {
margin: 0;
color: #6b7280;
font-size: var(--student-fs-body);
font-weight: 400;
line-height: 1.6;
}
}
.workbench-eyebrow {
margin: 0 0 6px !important;
color: #6b7280 !important;
font-size: var(--student-fs-caption) !important;
font-weight: 700 !important;
letter-spacing: 0.12em;
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: 10px;
}
.step-navigation {
display: flex;
justify-content: center;
align-items: center;
gap: 14px;
margin-top: 22px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
:deep(.el-button) {
min-width: 112px;
height: 40px;
margin: 0;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 10px;
color: #2563eb;
background: #ffffff;
font-weight: 700;
}
:deep(.el-button--primary) {
border-color: #2563eb;
color: #ffffff;
background: #2563eb;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.16);
}
:deep(.el-button.is-plain) {
border-color: rgba(37, 99, 235, 0.35);
color: #2563eb;
background: #ffffff;
}
:deep(.el-button:hover:not(:disabled)) {
border-color: #2563eb;
color: #2563eb;
background: #eff6ff;
}
:deep(.el-button--primary:hover:not(:disabled)) {
border-color: #1d4ed8;
color: #ffffff;
background: #1d4ed8;
}
.btn-save {
height: 36px;
min-height: 36px;
padding: 0 14px;
border-color: #e5e7eb;
color: #374151;
background: #f3f4f6;
border-radius: 8px;
}
.btn-save:hover:not(:disabled) {
border-color: #d1d5db;
color: #1f2937;
background: #f3f4f6;
}
}
.material-download {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 260px;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 10px;
color: #2563eb;
background: #ffffff;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
text-decoration: none;
&:hover {
color: #1d4ed8;
border-color: #2563eb;
background: #eff6ff;
}
}
.section-eyebrow {
margin: 0;
color: #6b7280;
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.12em;
line-height: 1.4;
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #ffffff;
box-shadow: none;
overflow-x: auto;
scrollbar-width: thin;
}
.step-tab {
display: flex;
align-items: center;
flex: 1 0 145px;
min-width: 0;
min-height: 42px;
padding: 8px 14px;
border: 1px solid #e5e7eb;
border-radius: 8px;
color: #6b7280;
background: #ffffff;
box-shadow: none;
font-family: inherit;
cursor: pointer;
text-align: center;
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
&:hover {
border-color: #2563eb;
color: #1d4ed8;
background: #eff6ff;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
transform: translateY(-1px);
.step-action {
color: #2563eb;
}
}
&:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 4px;
}
&.active {
border-color: #1d4ed8;
color: #ffffff;
background: #2563eb;
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2);
.step-marker {
border-color: #ffffff;
color: #1d4ed8;
background: #ffffff;
box-shadow: none;
}
}
&.complete {
border-color: #dbeafe;
color: #1d4ed8;
.step-marker {
border-color: transparent;
color: #1d4ed8;
background: rgba(37, 99, 235, 0.1);
box-shadow: none;
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 22px;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border: 1px solid #e5e7eb;
border-radius: 6px;
color: #6b7280;
background: #f3f4f6;
box-shadow: none;
font-family: ui-monospace, "SF Mono", Consolas, monospace;
font-size: 12px;
font-weight: 700;
line-height: 1;
letter-spacing: 0;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
gap: 4px;
min-width: 0;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: 14px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #6b7280;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
transition: color 0.2s ease;
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
background: #e5e7eb;
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #6b7280;
border-right: 1px solid #6b7280;
content: "";
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete + .step-connector {
background: #93c5fd;
box-shadow: none;
}
.step-tab.complete + .step-connector::after {
border-color: #93c5fd;
}
.comprehensive-step-nav {
position: relative;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
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 190px;
width: 190px;
min-width: 190px;
}
.comprehensive-step-strip .step-name {
display: block;
white-space: nowrap;
font-size: 13px;
-webkit-line-clamp: unset;
}
.comprehensive-step-strip .step-marker {
flex-basis: 22px;
width: 22px;
height: 22px;
font-size: 11px;
}
.comprehensive-step-strip .step-copy {
margin-left: 8px;
}
.comprehensive-step-strip .step-connector {
flex: 0 0 30px;
width: 30px;
min-width: 30px;
margin: 0 5px;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #1f2937;
font-size: var(--student-fs-h1);
font-weight: 700;
line-height: 1.35;
}
}
.flow-rail {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-top: 18px;
}
.process-training {
margin-top: 4px;
}
.process-intro,
.upload-tip {
margin: 0 0 18px;
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.6;
}
.process-table-wrap {
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.process-table {
width: 100%;
min-width: 1080px;
border-collapse: collapse;
table-layout: fixed;
th {
padding: 14px 16px;
border-right: 1px solid #e5e7eb;
color: #1f2937;
background: #ffffff;
font-size: 13px;
font-weight: 700;
text-align: center;
}
td {
padding: 16px;
border-top: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
vertical-align: middle;
}
th:last-child,
td:last-child {
border-right: 0;
}
}
.serial-col {
width: 72px;
}
.name-col {
width: 190px;
}
.desc-col {
width: 280px;
}
.serial-cell,
.task-name-cell {
color: #1f2937;
font-size: var(--student-fs-body-sm);
font-weight: 700;
text-align: center;
}
.desc-cell {
color: #4b5563;
font-size: var(--student-fs-body-sm);
line-height: 1.6;
text-align: center;
}
.process-textarea {
width: 100%;
min-height: 112px;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 10px;
outline: none;
resize: vertical;
color: #1f2937;
background: #ffffff;
line-height: 1.6;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
&::placeholder {
color: #6b7280;
}
&:focus {
border-color: #2563eb;
background: #eff6ff;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
}
}
.upload-tip {
margin-top: 26px;
margin-bottom: 22px;
}
.upload-action {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px;
}
.hidden-file-input {
display: none;
}
.upload-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding: 0 18px;
border: 1px solid #2563eb;
border-radius: 10px;
color: #ffffff;
background: #2563eb;
box-shadow: none;
font-size: var(--student-fs-body);
font-weight: 700;
cursor: pointer;
}
.upload-file-name {
color: #1f2937;
font-size: 13px;
font-weight: 700;
}
.flow-step,
.key-point-card {
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
box-shadow: none;
}
.flow-step {
min-height: 86px;
padding: 14px 16px;
span {
display: block;
margin-bottom: 6px;
color: #1f2937;
font-size: var(--student-fs-h2);
font-weight: 700;
}
strong {
color: #4b5563;
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(160px, 1fr));
gap: 12px;
margin: 0 0 20px;
}
.key-point-card {
padding: 14px 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
color: #4b5563;
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, 1fr));
gap: 14px;
}
.panel-title {
margin: 22px 0 16px;
h4 {
margin: 0 0 8px;
color: #1f2937;
font-size: var(--student-fs-h2);
font-weight: 700;
}
p {
margin: 0;
color: #6b7280;
font-size: var(--student-fs-body);
line-height: 1.6;
}
}
:deep(.el-form-item__label) {
color: #4b5563;
font-size: var(--student-fs-body-sm);
font-weight: 600;
line-height: 1.6;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
border: 1px solid #d1d5db;
border-radius: 8px;
box-shadow: none;
background: #ffffff;
}
:deep(.el-textarea__inner) {
padding: 12px 16px;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
color: #1f2937;
}
:deep(.el-input__inner::placeholder),
:deep(.el-textarea__inner::placeholder) {
color: #6b7280;
}
.training-table {
--el-table-border-color: #e5e7eb;
--el-table-header-bg-color: #ffffff;
--el-table-header-text-color: #1f2937;
--el-table-row-hover-bg-color: #f9fafb;
--el-table-bg-color: #ffffff;
--el-table-tr-bg-color: #ffffff;
--el-table-text-color: #4b5563;
border-radius: 12px;
overflow: hidden;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 24px;
}
.training-actions :deep(.training-action) {
min-width: 112px;
height: 36px;
min-height: 36px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 10px;
color: #2563eb;
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: #2563eb;
color: #ffffff;
background: #2563eb;
}
.training-actions :deep(.training-action--reset) {
border-color: rgba(37, 99, 235, 0.35);
color: #2563eb;
background: #ffffff;
}
.training-actions :deep(.training-action--submit:hover:not(.is-disabled)) {
border-color: #1d4ed8;
color: #ffffff;
background: #1d4ed8;
}
.training-actions :deep(.training-action--reset:hover:not(.is-disabled)),
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #2563eb;
color: #1d4ed8;
background: #eff6ff;
}
.product-development-process-page {
.goal-bg {
margin-bottom: 28px;
padding: 14px 18px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #ffffff;
}
.task-card {
margin-top: 0;
padding: 18px 22px;
border: 1px solid #e5e7eb;
border-radius: 14px;
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: #6b7280;
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.12em;
line-height: 1.4;
}
.workbench-description {
color: #6b7280 !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: 34px !important;
height: 34px !important;
padding: 0 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
box-shadow: none !important;
font-size: 13px !important;
font-weight: 700 !important;
line-height: 1 !important;
}
.process-step-card {
padding: 18px 22px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #ffffff;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #1f2937;
font-size: var(--student-fs-h1);
font-weight: 700;
line-height: 1.35;
}
}
.process-intro,
.upload-tip {
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.6;
}
.process-table-wrap {
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.process-table {
th {
padding: 12px;
border-right-color: #e5e7eb;
background: #ffffff;
font-size: 13px;
font-weight: 700;
}
td {
padding: 12px;
border-top-color: #e5e7eb;
border-right-color: #e5e7eb;
background: #ffffff;
vertical-align: top;
}
}
.serial-cell,
.task-name-cell,
.desc-cell {
font-size: 13px;
}
.process-textarea {
min-height: 110px;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 10px;
color: #1f2937;
background: #ffffff;
}
.upload-action {
justify-content: flex-start;
}
.upload-btn {
min-width: 0 !important;
min-height: 34px !important;
height: 34px !important;
padding: 0 14px !important;
border: 1px solid #2563eb !important;
border-radius: 8px !important;
background: #2563eb !important;
box-shadow: none !important;
font-size: 13px !important;
font-weight: 700 !important;
}
.upload-file-name {
color: #1f2937;
font-size: 13px;
font-weight: 700;
}
}
@media (max-width: 1100px) {
.student-training-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.student-training-page {
margin: 0;
/* padding 由架构标签 .app-main 提供,子页面根标签不再叠加 */
// padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 16px;
}
.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 132px;
}
}
.form-grid {
grid-template-columns: 1fr;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow: visible;
flex-direction: column;
align-items: stretch;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
min-width: 0;
}
.step-connector {
flex: 0 0 20px;
width: 2px;
min-width: 0;
height: 20px;
align-self: flex-start;
margin: 4px 0 4px 18px;
}
.step-connector::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
.comprehensive-step-nav {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow-x: auto;
}
.comprehensive-step-strip .step-tab {
flex: 0 0 172px;
width: 172px;
min-width: 172px;
}
.comprehensive-step-strip .step-connector {
flex: 0 0 24px;
width: 24px;
min-width: 24px;
height: 2px;
align-self: center;
margin: 0 5px;
}
.comprehensive-step-strip .step-connector::after {
top: 50%;
right: -1px;
bottom: auto;
left: auto;
transform: translateY(-50%) rotate(45deg);
}
.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: 14px;
}
.training-actions :deep(.training-action) {
width: 100%;
}
}
.comprehensive-step-card {
margin-top: 32px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #ffffff;
}
.comprehensive-analysis-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 34px;
}
.analysis-panel h4 {
margin: 0 0 12px;
color: #1f2937;
font-size: var(--student-fs-h2);
font-weight: 700;
line-height: 1.35;
}
.analysis-panel > p,
.comprehensive-conclusion {
margin: 0;
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.65;
}
.analysis-field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 26px 22px;
margin-top: 14px;
}
.analysis-field-grid label,
.comprehensive-conclusion {
display: grid;
gap: 9px;
color: #4b5563;
}
.analysis-field-grid :deep(.el-textarea__inner),
.comprehensive-conclusion :deep(.el-textarea__inner) {
min-height: 150px !important;
color: #1f2937;
background: #ffffff;
box-shadow: 0 0 0 1px #e5e7eb inset;
}
.comprehensive-conclusion {
margin-top: 32px;
}
.comprehensive-conclusion :deep(.el-textarea__inner) {
min-height: 170px !important;
}
.comprehensive-step-two {
display: grid;
gap: 24px;
}
.comprehensive-step-two .step-title-row {
margin-bottom: 0;
}
.comprehensive-instruction {
margin: 0;
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.65;
}
.comprehensive-table-wrap {
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 10px;
}
.comprehensive-table {
width: 100%;
min-width: 850px;
border-collapse: collapse;
}
.comprehensive-table th,
.comprehensive-table td {
padding: 9px 10px;
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
color: #4b5563;
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: #1f2937;
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 1px #e5e7eb inset;
}
.comprehensive-table :deep(.el-input__inner),
.comprehensive-table :deep(.el-textarea__inner) {
color: #1f2937;
}
.comprehensive-score-section,
.comprehensive-review-section {
display: grid;
gap: 14px;
}
.comprehensive-section-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
}
.comprehensive-section-line p {
margin: 0;
color: #4b5563;
font-size: var(--student-fs-body);
line-height: 1.65;
}
.comprehensive-section-line > div {
display: flex;
gap: 8px;
}
.compact-action {
min-height: 32px;
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, 1fr));
gap: 18px 28px;
}
.comprehensive-score-grid label {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 8px;
color: #4b5563;
white-space: nowrap;
}
.comprehensive-review-table td {
height: 76px;
}
.comprehensive-review-table th:first-child {
width: 16%;
}
.comprehensive-review-table td:nth-child(2) {
width: 12%;
}
.comprehensive-step-three {
display: grid;
gap: 26px;
}
.rfm-upload-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.rfm-upload-card {
display: grid;
align-content: start;
gap: 10px;
min-height: 146px;
padding: 16px;
border: 1px dashed #d1d5db;
border-radius: 12px;
background: #ffffff;
}
.rfm-upload-card .upload-btn {
justify-self: center;
}
.rfm-upload-card > p {
margin: 0;
color: #6b7280;
font-size: 12px;
line-height: 1.5;
text-align: center;
}
.comprehensive-file-preview {
display: grid;
gap: 9px;
min-width: 0;
padding-top: 10px;
border-top: 1px solid #e5e7eb;
}
.comprehensive-file-preview :deep(.el-image) {
width: 100%;
height: 155px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.excel-preview {
display: grid;
gap: 5px;
color: #4b5563;
font-size: 12px;
}
.excel-preview > span {
color: #6b7280;
}
.excel-preview-table {
overflow: auto;
max-height: 150px;
border: 1px 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: 140px;
padding: 6px 8px;
overflow: hidden;
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
text-overflow: ellipsis;
white-space: nowrap;
}
.excel-preview-table th {
background: #f3f4f6;
}
.file-preview-actions {
display: flex;
justify-content: space-between;
gap: 12px;
}
.file-preview-actions a,
.file-preview-actions button {
padding: 0;
border: 0;
color: #1f2937;
background: transparent;
font: inherit;
font-size: 12px;
cursor: pointer;
}
.file-preview-actions button {
color: #d9554c;
}
.comprehensive-analysis-section {
display: grid;
gap: 14px;
}
.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: 74px;
}
.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: 26px;
}
.journey-table,
.requirements-table,
.rice-table {
min-width: 1120px;
}
.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: 780px;
}
.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: 115px;
}
.rice-score {
color: #1f2937;
font-weight: 900;
}
.value-canvas {
overflow-x: auto;
padding: 16px 4px;
}
.value-map {
display: grid;
grid-template-columns: minmax(280px, 1fr) 64px minmax(360px, 1.1fr);
align-items: center;
min-width: 760px;
gap: 18px;
}
.value-map-left,
.value-map-right {
display: grid;
gap: 12px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.value-map-right {
border-radius: 50%;
aspect-ratio: 1.15 / 1;
padding: 34px 72px;
}
.value-map label {
display: grid;
gap: 7px;
color: #4b5563;
font-weight: 800;
}
.value-map :deep(.el-textarea__inner) {
color: #1f2937;
background: #ffffff;
box-shadow: 0 0 0 1px #e5e7eb inset;
}
.value-fit {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border: 1px solid #2563eb;
border-radius: 50%;
color: #ffffff;
background: #2563eb;
box-shadow: none;
font-size: 15px;
font-weight: 700;
}
.version-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 28px;
}
.version-grid label {
display: grid;
gap: 9px;
color: #4b5563;
}
.version-grid :deep(.el-textarea__inner) {
min-height: 175px !important;
color: #1f2937;
background: #ffffff;
box-shadow: 0 0 0 1px #e5e7eb inset;
}
.comprehensive-step-five {
display: grid;
gap: 26px;
}
.architecture-table {
min-width: 1420px;
}
.architecture-table th:first-child {
width: 7%;
}
.architecture-table td {
min-width: 150px;
}
.fault-table {
min-width: 980px;
}
.fault-table th:first-child {
width: 24%;
}
.fault-table td {
width: 25.3%;
}
.policy-table {
min-width: 920px;
}
.policy-table th:first-child {
width: 23%;
}
.policy-table td {
width: 38.5%;
}
.after-sales-summary-table {
min-width: 740px;
}
.after-sales-summary-table th:first-child {
width: 20%;
}
.comprehensive-step-six {
display: grid;
gap: 26px;
}
.cost-constraint-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px 28px;
}
.cost-constraint-grid label {
display: grid;
grid-template-columns: auto minmax(110px, 1fr) auto;
align-items: center;
gap: 8px;
color: #4b5563;
font-weight: 700;
}
.cost-constraint-grid :deep(.el-input__wrapper) {
background: #ffffff;
box-shadow: 0 0 0 1px #e5e7eb inset;
}
.bom-table {
min-width: 880px;
}
.manufacturing-table {
min-width: 820px;
}
.procurement-weight-table {
min-width: 1060px;
}
.procurement-score-table {
min-width: 1140px;
}
.computed-row th,
.computed-row td {
color: #1f2937;
background: #f9fafb;
font-weight: 700;
}
.comprehensive-step-seven {
display: grid;
gap: 26px;
}
.forecast-rule-list {
display: grid;
gap: 4px;
margin: 0 0 16px;
color: #4b5563;
line-height: 1.75;
}
.demand-forecast-table,
.cost-pricing-table {
min-width: 860px;
}
.pricing-subsection {
display: grid;
gap: 12px;
}
.pricing-subsection > p {
margin: 0;
color: #1f2937;
line-height: 1.7;
}
.pricing-formula {
color: #6b7280 !important;
}
.cost-price-input {
max-width: 400px;
}
.cost-price-input label,
.competitor-decision > label,
.value-pricing-result label {
display: grid;
grid-template-columns: auto minmax(120px, 1fr) auto;
align-items: center;
gap: 8px;
color: #1f2937;
font-weight: 700;
}
.competitor-pricing-section {
margin-top: 24px;
}
.competitor-pricing-grid,
.value-pricing-grid,
.pricing-summary-grid {
display: grid;
grid-template-columns: minmax(0, 1.8fr) minmax(290px, 0.8fr);
align-items: start;
gap: 24px;
}
.competitor-table {
min-width: 960px;
}
.competitor-decision,
.value-pricing-result,
.final-pricing-analysis {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.competitor-decision .textarea-label {
grid-template-columns: 1fr;
align-items: stretch;
}
.calculated-field,
.value-pricing-result strong {
color: #1f2937;
font-size: 17px;
font-weight: 900;
}
.value-pricing-table {
min-width: 720px;
}
.value-pricing-result p {
margin: 0;
color: #6b7280;
line-height: 1.7;
}
.psm-table {
min-width: 1060px;
}
.psm-visualization {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.psm-legend {
display: flex;
flex-wrap: wrap;
gap: 12px 18px;
color: #4b5563;
font-size: 13px;
}
.psm-legend span::before {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
content: "";
}
.psm-legend--expensive::before,
.psm-bar--expensive { background: #374151; }
.psm-legend--cheap::before,
.psm-bar--cheap { background: #6b7280; }
.psm-legend--acceptable::before,
.psm-bar--acceptable { background: #9ca3af; }
.psm-legend--bargain::before,
.psm-bar--bargain { background: #d1d5db; }
.psm-visual-row {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
align-items: center;
gap: 14px;
color: #4b5563;
}
.psm-bars {
display: grid;
grid-template-rows: repeat(4, 6px);
gap: 5px;
}
.psm-bar {
display: block;
min-width: 2px;
border-radius: 999px;
box-shadow: none;
}
.pricing-summary-table {
min-width: 700px;
}
.final-pricing-analysis {
color: #1f2937;
font-weight: 800;
}
.comprehensive-step-eight {
display: grid;
gap: 26px;
}
.abc-table {
min-width: 980px;
}
.new-product-table {
min-width: 1080px;
}
.abc-strategy-table {
min-width: 1040px;
}
.channel-weight-table {
min-width: 980px;
}
.channel-score-table {
min-width: 1180px;
}
.channel-score-table th,
.channel-score-table td {
min-width: 132px;
}
.comprehensive-step-nine {
display: grid;
gap: 26px;
}
.budget-total-input {
max-width: 430px;
margin: 12px 0 18px;
}
.budget-total-input label,
.roi-formula-grid label {
display: grid;
grid-template-columns: auto minmax(120px, 1fr) auto;
align-items: center;
gap: 8px;
color: #1f2937;
font-weight: 700;
}
.budget-table {
min-width: 1280px;
}
.roi-formula-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px 34px;
max-width: 760px;
margin: 20px 0;
}
.roi-formula-grid label:last-child {
grid-template-columns: auto minmax(65px, auto) 1fr;
}
.roi-note {
margin: 8px 0;
color: #4b5563;
line-height: 1.75;
}
.roi-table {
min-width: 1220px;
}
.funnel-table {
min-width: 1140px;
}
.funnel-diagnosis-heading {
margin-top: 18px;
}
.funnel-diagnosis-table {
min-width: 1040px;
}
.review-analysis-section {
margin-top: 24px;
}
.review-analysis-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
align-items: start;
gap: 28px;
}
.comment-upload-card,
.top-issues,
.sentiment-section {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.comment-upload-card > p,
.top-issues > p,
.sentiment-section > p {
margin: 0;
color: #6b7280;
line-height: 1.65;
}
.top-issues label {
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
align-items: center;
gap: 8px;
color: #1f2937;
font-weight: 700;
}
.comment-cloud {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
min-height: 90px;
padding: 14px;
border: 1px dashed #d1d5db;
border-radius: 12px;
background: #ffffff;
}
.comment-cloud span {
color: #1f2937;
font-weight: 800;
line-height: 1;
}
.sentiment-section {
grid-template-columns: 1fr auto;
align-items: center;
margin-top: 20px;
}
.sentiment-section > p,
.sentiment-section > .comprehensive-table-wrap,
.sentiment-visualization {
grid-column: 1 / -1;
}
.sentiment-table {
min-width: 680px;
}
.sentiment-visualization {
display: grid;
gap: 10px;
}
.sentiment-visualization span {
display: grid;
grid-template-columns: 46px minmax(0, 1fr) 54px;
align-items: center;
gap: 10px;
color: #4b5563;
}
.sentiment-visualization i {
height: 9px;
border-radius: 999px;
background: #2563eb;
box-shadow: none;
}
.sentiment-visualization em {
color: #1f2937;
font-style: normal;
font-weight: 800;
}
.ab-test-table {
min-width: 1020px;
}
.comprehensive-step-ten {
display: grid;
gap: 24px;
}
.report-intro {
align-items: center;
}
.report-framework-table {
min-width: 1040px;
}
.report-framework-table th:first-child {
width: 14%;
}
.report-framework-table th:nth-child(2) {
width: 25%;
}
.report-framework-table td {
color: #4b5563;
line-height: 1.75;
text-align: left;
}
@media (max-width: 900px) {
.comprehensive-score-grid {
grid-template-columns: 1fr;
}
.rfm-upload-grid {
grid-template-columns: 1fr;
}
.version-grid {
grid-template-columns: 1fr;
}
.cost-constraint-grid {
grid-template-columns: 1fr;
}
.competitor-pricing-grid,
.value-pricing-grid,
.pricing-summary-grid {
grid-template-columns: 1fr;
}
.psm-visual-row {
grid-template-columns: 1fr;
gap: 7px;
}
.roi-formula-grid,
.review-analysis-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.comprehensive-section-line,
.comprehensive-section-line > div {
align-items: stretch;
flex-direction: column;
}
.comprehensive-score-grid label {
grid-template-columns: 88px minmax(0, 1fr);
}
.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: 1fr;
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: 1fr;
align-items: stretch;
}
.sentiment-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 1100px) {
.comprehensive-analysis-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.comprehensive-step-card {
padding: 16px;
border-radius: 14px;
}
.analysis-field-grid {
grid-template-columns: 1fr;
gap: 18px;
}
}
</style>
<style lang="scss">
</style>