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

3916 lines
180 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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">
<span class="task-badge">核心实训 · 产品开发基础</span>
<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-badge {
display: inline-flex;
align-items: center;
margin-bottom: 0;
padding: 4px 12px;
border: 1px solid #e5e7eb;
border-radius: 999px;
color: #111827;
background: #f3f4f6;
font-size: 12px;
font-weight: 700;
}
.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: #111827;
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(31, 41, 55, 0.4);
border-radius: 10px;
color: #1f2937;
background: #ffffff;
font-weight: 700;
}
:deep(.el-button--primary) {
border-color: #1f2937;
color: #ffffff;
background: #1f2937;
box-shadow: 0 2px 6px rgba(31, 41, 55, 0.16);
}
:deep(.el-button.is-plain) {
border-color: rgba(31, 41, 55, 0.4);
color: #1f2937;
background: #ffffff;
}
:deep(.el-button:hover:not(:disabled)) {
border-color: #1f2937;
color: #1f2937;
background: #f3f4f6;
}
:deep(.el-button--primary:hover:not(:disabled)) {
border-color: #111827;
color: #ffffff;
background: #111827;
}
.btn-save {
border-color: #e5e7eb;
color: #374151;
background: #f3f4f6;
}
.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 #e5e7eb;
border-radius: 10px;
color: #1f2937;
background: #ffffff;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
text-decoration: none;
&:hover {
color: #1f2937;
border-color: #1f2937;
background: #f9fafb;
}
}
.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: #1f2937;
color: #1f2937;
background: #f9fafb;
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12);
transform: translateY(-1px);
.step-action {
color: #1f2937;
}
}
&:focus-visible {
outline: 2px solid #1f2937;
outline-offset: 4px;
}
&.active {
border-color: #1f2937;
color: #ffffff;
background: #1f2937;
box-shadow: 0 2px 6px rgba(31, 41, 55, 0.16);
.step-marker {
border-color: #ffffff;
color: #1f2937;
background: #ffffff;
box-shadow: none;
}
}
&.complete {
border-color: #dce9e2;
color: #374151;
.step-marker {
border-color: transparent;
color: #3f8a5f;
background: rgba(74, 157, 110, 0.12);
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: #4a9d6e;
box-shadow: none;
}
.step-tab.complete + .step-connector::after {
border-color: #4a9d6e;
}
.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: #1f2937;
background: #f9fafb;
box-shadow: 0 0 0 3px rgba(31, 41, 55, 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 #1f2937;
border-radius: 10px;
color: #ffffff;
background: #1f2937;
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: 40px;
padding: 0 18px;
border: 1px solid #e5e7eb;
border-radius: 10px;
color: #1f2937;
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: #1f2937;
color: #ffffff;
background: #1f2937;
}
.training-actions :deep(.training-action--reset) {
border-color: #e5e7eb;
color: #1f2937;
background: #ffffff;
}
.training-actions :deep(.training-action--submit:hover:not(.is-disabled)) {
border-color: #111827;
color: #ffffff;
background: #111827;
}
.training-actions :deep(.training-action--reset:hover:not(.is-disabled)),
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #1f2937;
color: #1f2937;
background: #f9fafb;
}
.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: 40px !important;
height: 40px !important;
padding: 0 18px !important;
border: 1px solid #e5e7eb !important;
border-radius: 10px !important;
color: #1f2937 !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: 40px !important;
height: 40px !important;
padding: 0 18px !important;
border: 1px solid #1f2937 !important;
border-radius: 10px !important;
background: #1f2937 !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 #1f2937;
border-radius: 50%;
color: #ffffff;
background: #1f2937;
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: #1f2937;
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>