|
|
<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>
|