|
|
<template>
|
|
|
<div class="diagnosis-page student-training-shell">
|
|
|
<main class="diagnosis-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<span class="task-badge"><el-icon><DataAnalysis /></el-icon> 核心实训 · 产品上线与运营推广</span>
|
|
|
<h1 class="task-title">{{ taskTitle }}</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>
|
|
|
<ul><li v-for="goal in taskGoals" :key="goal">{{ goal }}</li></ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
<TrainingTaskToolbar class="diagnosis-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportTrainingOutcome" />
|
|
|
</section>
|
|
|
<section class="workbench">
|
|
|
<nav class="step-indicator" aria-label="产品问题诊断步骤"><button type="button" :class="['step-tab', { active: currentStep === 1, complete: currentStep > 1 }]" :aria-current="currentStep === 1 ? 'step' : undefined" @click="currentStep = 1"><span class="step-marker">01</span><span class="step-copy"><b class="step-name">全链路漏斗诊断</b><small class="step-action">{{ currentStep === 1 ? '当前步骤 →' : '点击切换 →' }}</small></span></button><span class="step-connector" aria-hidden="true" /><button type="button" :class="['step-tab', { active: currentStep === 2, complete: currentStep > 2 }]" :aria-current="currentStep === 2 ? 'step' : undefined" @click="currentStep = 2"><span class="step-marker">02</span><span class="step-copy"><b class="step-name">用户评论根因分析</b><small class="step-action">{{ currentStep === 2 ? '当前步骤 →' : '点击切换 →' }}</small></span></button><span class="step-connector" aria-hidden="true" /><button type="button" :class="['step-tab', { active: currentStep === 3 }]" :aria-current="currentStep === 3 ? 'step' : undefined" @click="currentStep = 3"><span class="step-marker">03</span><span class="step-copy"><b class="step-name">AB测试验证</b><small class="step-action">{{ currentStep === 3 ? '当前步骤 →' : '点击切换 →' }}</small></span></button></nav>
|
|
|
|
|
|
<template v-if="currentStep === 1">
|
|
|
<p class="lead">从用户触达产品的全路径出发,搭建从曝光到复购的完整数据漏斗,逐层计算各环节转化率并对比行业均值,定位产品在哪个环节出现了明显异常流失,精准锁定问题所在的运营环节。</p>
|
|
|
<div class="diagnosis-materials" aria-label="第 1 步参考资料">
|
|
|
<div class="diagnosis-materials__item"><strong>1.下载案例数据表,使用Excel计算各节点的转化率,参考《实训操作说明》。</strong><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadNodeGuide">下载实训操作说明</button></div></div>
|
|
|
<div class="diagnosis-materials__item"><strong>2.下载Excel模版,填写关键信息,自动计算各环节转化率。提供自动化模版。</strong><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadNodeTemplate">下载Excel模板</button></div></div>
|
|
|
<p class="diagnosis-materials__ai">3.导出数据表上传到通用AI大模型,通过AI大模型辅助完成计算,参考步骤及提示词如下:请根据表格数据,计算从曝光到复购链路节点转化率。</p>
|
|
|
</div>
|
|
|
<section class="section-block">
|
|
|
<div class="section-heading"><span>一</span><div><h3>计算各节点转化率</h3><p>根据案例数据计算各个渠道链路节点转化率,参考方法及工具如下:</p></div></div>
|
|
|
<div class="table-toolbar funnel-channel-toolbar">
|
|
|
<span>链路节点和行业均值固定,中间渠道可按案例数据增加或删除。</span>
|
|
|
<button type="button" class="table-button" @click="addFunnelChannel">增加渠道</button>
|
|
|
</div>
|
|
|
<div class="table-wrap">
|
|
|
<table class="funnel-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th class="fixed-column">链路节点</th>
|
|
|
<th v-for="channel in channels" :key="channel.id" class="channel-column">
|
|
|
<div class="channel-heading">
|
|
|
<el-input v-model="channel.name" maxlength="20" placeholder="填写渠道名称" />
|
|
|
<button type="button" class="channel-remove" :aria-label="`删除${channel.name || '未命名'}渠道`" @click="removeFunnelChannel(channel.id)">×</button>
|
|
|
</div>
|
|
|
</th>
|
|
|
<th class="fixed-column">行业均值</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in conversionRates" :key="row.linkNode">
|
|
|
<th>{{ row.linkNode }}</th>
|
|
|
<td v-for="(channel, channelIndex) in channels" :key="channel.id">
|
|
|
<div class="rate-input">
|
|
|
<el-input-number v-model="row.channelRates[channel.id]" :min="0" :max="100" :precision="2" :controls="false" :placeholder="row.linkNode === '曝光-点击' && channelIndex === 0 ? '示例:11.5' : '请输入'" />
|
|
|
<span class="unit">%</span>
|
|
|
</div>
|
|
|
</td>
|
|
|
<td><div class="rate-input"><el-input-number v-model="row.industryAverage" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></div></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
<section class="section-block visual-section"><div class="section-heading"><span>二</span><div><h3>全链路转化漏斗可视化</h3><p>根据节点转化率计算结果,绘制各渠道转化率与行业均值的转化漏斗对比图,可视化呈现多环节情况,参考方法及工具如下:</p></div></div><div class="diagnosis-materials" aria-label="第 2 步参考资料"><div class="diagnosis-materials__item"><strong>1.使用Excel绘图功能,自主完成转化漏斗对比图,可参考《实训操作说明》。</strong><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadFunnelGuide">下载实训操作说明</button></div></div><div class="diagnosis-materials__item"><strong>2.下载Excel模版,填写关键信息,自动计算各环节转化率并生成转化漏斗对比图。</strong><div class="diagnosis-materials__actions"><button type="button" class="secondary-button" @click="downloadFunnelTemplate">下载Excel模板</button></div></div><p class="diagnosis-materials__ai">3.导出数据表上传到通用AI大模型,通过AI大模型辅助完成,参考步骤及提示词如下:请根据表格数据绘制漏斗图,并与行业均值漏斗图进行对比。</p></div><input ref="chartInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleChartUpload" /><div class="chart-upload-area"><template v-if="funnelChart.url"><div class="chart-preview"><el-image :src="funnelChart.url" :preview-src-list="[funnelChart.url]" fit="cover" preview-teleported /><div><strong>{{ funnelChart.name || '节点转化漏斗对比图' }}</strong><span>点击缩略图可放大预览</span><button type="button" @click="clearChart">移除图片</button></div></div><button type="button" class="secondary-button" :disabled="uploading" @click="chartInput?.click()">{{ uploading ? '上传中…' : '更换对比图' }}</button></template><template v-else><button type="button" class="upload-button" :disabled="uploading" @click="chartInput?.click()">{{ uploading ? '上传中…' : '上传节点转化漏斗对比图' }}</button><p>支持 PNG、JPG、WEBP 格式,图片不超过 5MB;上传后可点击预览。</p></template></div></section>
|
|
|
<section class="section-block diagnosis-section"><div class="section-heading"><span>三</span><div><h3>定位异常节点</h3><p>对比案例行业均值数据,定位异常节点。</p></div></div><div class="table-toolbar"><span>对比案例行业均值数据,定位异常节点。</span><div><button type="button" class="table-button" :disabled="anomalyRows.length >= 5" @click="addAnomalyRow">增加一行</button><button type="button" class="table-button ghost" :disabled="anomalyRows.length <= 1" @click="removeAnomalyRow">删除一行</button></div></div><div class="table-wrap"><table class="diagnosis-table"><thead><tr><th>渠道</th><th>链路节点</th><th>店铺转化率</th><th>行业均值转化率</th><th>异常分析与判定</th></tr></thead><tbody><tr v-for="(row, index) in anomalyRows" :key="index"><td><el-select v-model="row.channelId" placeholder="请选择" @change="syncAnomalyChannel(row)"><el-option v-for="channel in availableChannels" :key="channel.id" :label="channel.name" :value="channel.id" /></el-select></td><td><el-select v-model="row.linkNode" placeholder="请选择"><el-option v-for="node in nodes" :key="node" :label="node" :value="node" /></el-select></td><td><div class="rate-input"><el-input-number v-model="row.storeConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></div></td><td><div class="rate-input"><el-input-number v-model="row.industryAverageConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></div></td><td><el-input v-model="row.anomalyAnalysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="请填写异常原因或正常判定" /></td></tr></tbody></table></div><p class="diagnosis-step-one-notice">说明:这个表格默认展示一行可填写,最少一行,可以增加行,最多5行,这里填写的渠道需要来源于第一个表的渠道。</p></section>
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="currentStep === 2">
|
|
|
<p class="lead">漏斗只能定位“哪个环节出了问题”,要回答为什么出问题,需要分析用户最真实的反馈。本步骤通过词云分析和情感倾向分析,从用户评价中提取高频问题,判断整体口碑倾向,验证漏斗异常背后的真实根因。</p>
|
|
|
<section class="section-block"><div class="section-heading"><span>一</span><div><h3>词云分析</h3><p>导入数据,生成词云,基于案例评论数据自动生成高频问题词云。</p></div></div><input ref="commentInput" class="visually-hidden" type="file" accept=".csv,.txt,.xls,.xlsx,text/csv,text/plain,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @change="handleCommentImport" /><div class="comment-import"><button type="button" class="upload-button" :disabled="analyzing" @click="commentInput?.click()">{{ analyzing ? '正在生成词云…' : '导入评论数据生成词云' }}</button><p>支持 CSV、TXT、XLS、XLSX,文件不超过 5MB。</p><div v-if="commentPreview.fileName" class="data-preview"><div><strong>{{ commentPreview.fileName }}</strong><span>已解析 {{ commentPreview.totalComments }} 条评论 · 数据预览</span></div><ul><li v-for="(comment, index) in commentPreview.comments" :key="index">{{ comment }}</li></ul></div></div><div v-if="wordCloud.length" class="word-cloud-card"><div class="word-cloud-title"><span>高频问题词云</span><small>基于导入评论自动生成</small></div><div ref="wordCloudRef" class="word-cloud" aria-label="评论高频问题词云"></div></div><div class="top-issues"><p>请根据词云图填写词云中出现频次最高的TOP3问题。</p><div><label v-for="(_, index) in topIssues" :key="index">TOP{{ index + 1 }}:<el-input v-model="topIssues[index]" :placeholder="`填写第 ${index + 1} 个高频问题`" /></label></div></div></section>
|
|
|
<section class="section-block"><div class="section-heading"><span>二</span><div><h3>情感倾向分析</h3></div></div><div class="sentiment-actions"><span>点击情感倾向分析后,系统会自动计算正面、中性、负面评论数量及占比,并按校验所需精度保留两位小数。</span><button type="button" class="upload-button" :disabled="!analysisResult" @click="runSentimentAnalysis">情感倾向分析</button></div><div class="table-wrap"><table class="sentiment-table"><thead><tr><th>情感类型</th><th>评论数量(条)</th><th>占比</th></tr></thead><tbody><tr v-for="row in sentiments" :key="row.type"><th>{{ row.type }}</th><td><span class="auto-count">{{ row.count ?? '—' }}</span></td><td><div class="rate-input"><el-input-number v-model="row.percentage" :min="0" :max="100" :precision="2" :controls="false" disabled placeholder="系统自动计算" /><span class="unit">%</span></div></td></tr></tbody></table></div></section>
|
|
|
<section class="section-block root-cause"><p>请结合词云TOP3与情感分析结果,说明本案例的核心问题根本原因;说明高频问题与情感数据是如何验证步骤一漏斗异常结论的</p><el-input v-model="rootCauseAnalysis" type="textarea" :rows="6" maxlength="1000" show-word-limit placeholder="请填写根因分析与对步骤一异常结论的验证说明" /></section>
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
<p class="lead">找到根因后,还需验证“改进方案是否真的有效”,避免拍脑袋迭代。AB测试对照诊断法将用户随机分为对照组(维持原方案)和实验组(应用改进方案),对比两组的关键指标差异,用数据判断改进方案是否值得落地。</p>
|
|
|
<section class="section-block"><div class="section-heading"><span>(1)</span><div><h3>确定测试方案</h3><p>请基于案例资料,针对步骤一和步骤二分析结果,设计AB测试方案</p></div></div><el-input v-model="abTestPlan" type="textarea" :rows="6" maxlength="1000" show-word-limit placeholder="填写对照组与实验组设置、改进方案、随机分流方式、观察周期和成功判定标准" /></section>
|
|
|
<section class="section-block"><div class="section-heading"><span>(2)</span><div><h3>AB测试数据填报</h3><p>请从案例资料中获取AB测试两组数据,计算后填入下表并逐项判定:</p></div></div><div class="table-wrap"><table class="ab-table"><thead><tr><th>测试指标</th><th>对照组数据</th><th>实验组数据</th><th>判定与分析</th></tr></thead><tbody><tr v-for="row in abRows" :key="row.metric"><th>{{ row.metric }}</th><td><div class="rate-input"><el-input-number v-model="row.controlValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="示例:56%" /><span class="unit">%</span></div></td><td><div class="rate-input"><el-input-number v-model="row.experimentValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="示例:64%" /><span class="unit">%</span></div></td><td><el-input v-model="row.analysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="示例:提升8pp,显著提升,采纳" /></td></tr></tbody></table></div></section>
|
|
|
<section class="section-block"><div class="section-heading"><span>(3)</span><div><h3>可视化呈现</h3><p>绘制AB测试结果对比图,参考方法及工具如下:</p></div></div><input ref="abChartInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleAbChartUpload" /><div class="chart-upload-area"><template v-if="abChart.url"><div class="chart-preview"><el-image :src="abChart.url" :preview-src-list="[abChart.url]" fit="cover" preview-teleported /><div><strong>{{ abChart.name || 'AB 测试结果对比图' }}</strong><span>点击缩略图可放大预览</span><button type="button" @click="clearAbChart">移除图片</button></div></div><button type="button" class="secondary-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '上传中…' : '更换对比图' }}</button></template><template v-else><button type="button" class="upload-button" :disabled="uploading" @click="abChartInput?.click()">{{ uploading ? '上传中…' : '上传AB测试结果对比图' }}</button><p>支持 PNG、JPG、WEBP 格式,图片不超过 5MB;上传后可点击预览。</p></template></div><div class="ab-materials"><div><ol><li>1.使用Excel/WPS绘图功能,自主完成AB测试结果对比图,参考《实训操作说明》。</li><li>2.下载Excel模版,填写关键信息,自动计算AB测试数据,并生成测试结果对比图。</li><li>3.导出数据表上传到通用AI大模型,通过AI大模型辅助完成,参考步骤及提示词如下:请根据表格数据,进行对照组和实验组数据对比,采用组合柱状图生成可视化图形。</li></ol></div><div class="ab-materials__actions"><button type="button" class="secondary-button" @click="downloadAbGuide">下载实训操作说明</button><button type="button" class="secondary-button" @click="downloadAbTemplate">下载Excel模板</button></div></div></section>
|
|
|
<section class="section-block"><div class="section-heading"><span>(4)</span><div><h3>诊断结论与迭代优化建议</h3><p>结合全链路数据漏斗诊断、售后评论分析法及AB测试对照诊断,分析产品存在的问题,并提出具体的迭代优化建议。</p></div></div><el-input v-model="iterationRecommendation" type="textarea" :rows="6" maxlength="1200" show-word-limit placeholder="请填写产品问题诊断结论及具体迭代优化建议" /></section>
|
|
|
</template>
|
|
|
<div class="actions step-navigation"><button v-if="currentStep > 1" type="button" class="secondary-button" :disabled="saving" @click="currentStep -= 1">上一步</button><button type="button" class="submit-button" :disabled="saving || uploading || analyzing" @click="saveCurrentProgress">保存当前进度</button><button v-if="currentStep < 3" type="button" class="submit-button" :disabled="saving || uploading || analyzing" @click="nextStep">下一步</button></div>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar :study-tip="currentStep === 1 ? '先从案例资料提取各渠道漏斗数据,计算转化率并与行业均值逐层对比。' : currentStep === 2 ? '先导入案例评论数据生成词云,再点击情感倾向分析,并用高频问题与情感占比解释漏斗异常。' : '基于前两步结论设计随机分流的 AB 测试,用五项指标的组间差异验证改进方案。'" :progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
|
|
import { DataAnalysis } from "@element-plus/icons-vue";
|
|
|
import * as echarts from "echarts";
|
|
|
import "echarts-wordcloud";
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
import { analyzeProductDiagnosisIterationStepTwo, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile, validateProductDiagnosisIterationStepOne, validateProductDiagnosisIterationStepTwo, validateProductDiagnosisIterationStepThree } from "@/api/studentTrainingAnswer";
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
import { isStudentDemo } from "@/utils/studentDemo";
|
|
|
|
|
|
const NODE_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E1.docx";
|
|
|
const NODE_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%881.xlsx";
|
|
|
const FUNNEL_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E2.docx";
|
|
|
const FUNNEL_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%882.xlsx";
|
|
|
const AB_TEST_GUIDE_URL = "/file/training-materials/product-diagnosis-iteration/23.%E5%AE%9E%E8%AE%AD%E6%93%8D%E4%BD%9C%E8%AF%B4%E6%98%8E3.docx";
|
|
|
const AB_TEST_TEMPLATE_URL = "/file/training-materials/product-diagnosis-iteration/23.Excel%E6%A8%A1%E7%89%883.xlsx";
|
|
|
function downloadBuiltInFile(url) { window.open(url, "_blank", "noopener"); }
|
|
|
function downloadNodeGuide() { downloadBuiltInFile(NODE_GUIDE_URL); }
|
|
|
function downloadNodeTemplate() { downloadBuiltInFile(NODE_TEMPLATE_URL); }
|
|
|
function downloadFunnelGuide() { downloadBuiltInFile(FUNNEL_GUIDE_URL); }
|
|
|
function downloadFunnelTemplate() { downloadBuiltInFile(FUNNEL_TEMPLATE_URL); }
|
|
|
function downloadAbGuide() { downloadBuiltInFile(AB_TEST_GUIDE_URL); }
|
|
|
function downloadAbTemplate() { downloadBuiltInFile(AB_TEST_TEMPLATE_URL); }
|
|
|
const briefOpen = ref(false);
|
|
|
const TASK_KEY = "product-diagnosis-iteration";
|
|
|
const nodes = ["曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"];
|
|
|
const defaultChannels = [{ id: "tmall", name: "天猫" }, { id: "jd", name: "京东" }, { id: "douyin", name: "抖音" }, { id: "pinduoduo", name: "拼多多" }];
|
|
|
let channelSerial = 0;
|
|
|
const { proxy } = getCurrentInstance(); const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const uploading = ref(false); const analyzing = ref(false); const chartInput = ref(null); const commentInput = ref(null); const abChartInput = ref(null); const wordCloudRef = ref(null); let wordCloudChart = null;
|
|
|
const channels = ref(createDefaultChannels());
|
|
|
const conversionRates = ref(createConversionRates()); const anomalyRows = ref([createAnomalyRow()]); const funnelChart = ref({ url: "", name: "" }); const analysisResult = ref(null); const commentPreview = ref({ fileName: "", totalComments: 0, comments: [] }); const wordCloud = ref([]); const topIssues = ref(["", "", ""]); const sentiments = ref(createSentiments()); const rootCauseAnalysis = ref(""); const abTestPlan = ref(""); const abRows = ref(createAbRows()); const abChart = ref({ url: "", name: "" }); const iterationRecommendation = ref("");
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "产品问题诊断与迭代"); const taskBackground = computed(() => taskConfig.value?.background || "从曝光到复购的完整数据漏斗与用户评价出发,定位异常流失并形成迭代方向。"); const taskRequirement = computed(() => taskConfig.value?.requirements || "完成转化漏斗诊断、用户评论词云和情感根因分析。"); const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["掌握全链路转化率计算方法", "能够将店铺数据与行业均值进行对比", "从用户评价中定位问题根因"]));
|
|
|
const availableChannels = computed(() => channels.value.filter((channel) => channel.name.trim()));
|
|
|
const funnelRatesComplete = computed(() => {
|
|
|
const names = availableChannels.value.map((channel) => channel.name.trim());
|
|
|
if (!channels.value.length || names.length !== channels.value.length || new Set(names).size !== names.length) return false;
|
|
|
return conversionRates.value.every((row) => isRate(row.industryAverage) && channels.value.every((channel) => isRate(row.channelRates[channel.id])));
|
|
|
});
|
|
|
const progressItems = computed(() => currentStep.value === 1 ? [{ text: "填写五个节点的渠道转化率", status: funnelRatesComplete.value ? "done" : "doing" }, { text: "上传节点转化漏斗对比图", status: funnelChart.value.url ? "done" : "" }, { text: "完成异常节点分析", status: anomalyRows.value.every((row) => row.anomalyAnalysis?.trim()) ? "done" : "" }] : currentStep.value === 2 ? [{ text: "导入评论数据并生成词云", status: analysisResult.value ? "done" : "doing" }, { text: "完成情感倾向分析", status: sentiments.value.every((row) => row.count !== null) ? "done" : "" }, { text: "填写根因分析", status: rootCauseAnalysis.value.trim() ? "done" : "" }] : [{ text: "设计 AB 测试方案", status: abTestPlan.value.trim() ? "done" : "doing" }, { text: "填写五项对照数据", status: abRows.value.every((row) => row.controlValue !== null && row.experimentValue !== null && row.analysis?.trim()) ? "done" : "" }, { text: "上传 AB 对比图", status: abChart.value.url ? "done" : "" }, { text: "提交迭代优化建议", status: iterationRecommendation.value.trim() ? "done" : "" }]);
|
|
|
function createDefaultChannels() { return defaultChannels.map((channel) => ({ ...channel })); }
|
|
|
function createConversionRates(channelList = channels.value) { return nodes.map((linkNode) => ({ linkNode, channelRates: Object.fromEntries(channelList.map((channel) => [channel.id, null])), industryAverage: null })); }
|
|
|
function createAnomalyRow() { return { channelId: "", channel: "", linkNode: "", storeConversionRate: null, industryAverageConversionRate: null, anomalyAnalysis: "" }; }
|
|
|
function createSentiments() { return ["正面", "中性", "负面"].map((type) => ({ type, count: null, percentage: null })); }
|
|
|
function createAbRows() { return ["点击-加购转化率", "加购-下单转化率", "下单-好评转化率", "详情页跳出率", "退货率"].map((metric) => ({ metric, controlValue: null, experimentValue: null, analysis: "" })); }
|
|
|
function isRate(value) { return value !== null && value !== "" && Number.isFinite(Number(value)); }
|
|
|
function diagnosisErrorMessage(error, fallback) {
|
|
|
const serverMessage = String(error?.response?.data?.msg || "").trim();
|
|
|
const message = serverMessage || String(error?.message || "").trim();
|
|
|
return /Request failed with status code|系统接口\d{3}异常/i.test(message) ? fallback : message || fallback;
|
|
|
}
|
|
|
function buildStepOne() {
|
|
|
const channelSnapshot = channels.value.map((channel) => ({ id: channel.id, name: channel.name.trim() }));
|
|
|
return {
|
|
|
channels: channelSnapshot,
|
|
|
conversionRates: conversionRates.value.map((row) => ({ linkNode: row.linkNode, channelRates: { ...row.channelRates }, industryAverage: row.industryAverage })),
|
|
|
funnelChartUrl: funnelChart.value.url,
|
|
|
funnelChartName: funnelChart.value.name,
|
|
|
anomalyRows: anomalyRows.value.map((row) => ({ ...row, channel: channelSnapshot.find((channel) => channel.id === row.channelId)?.name || row.channel || "" })),
|
|
|
};
|
|
|
}
|
|
|
function buildStepTwo() {
|
|
|
return {
|
|
|
sourceFileName: commentPreview.value.fileName,
|
|
|
totalComments: commentPreview.value.totalComments,
|
|
|
topIssues: [...topIssues.value],
|
|
|
sentiments: sentiments.value.map((row) => ({ ...row })),
|
|
|
rootCauseAnalysis: rootCauseAnalysis.value,
|
|
|
wordCloud: wordCloud.value,
|
|
|
previewComments: commentPreview.value.comments,
|
|
|
analysisResult: analysisResult.value ? { ...analysisResult.value } : null,
|
|
|
};
|
|
|
}
|
|
|
function buildStepTwoValidationPayload(stepTwo = buildStepTwo()) {
|
|
|
return {
|
|
|
sourceFileName: stepTwo.sourceFileName,
|
|
|
topIssues: stepTwo.topIssues,
|
|
|
sentiments: stepTwo.sentiments,
|
|
|
rootCauseAnalysis: stepTwo.rootCauseAnalysis,
|
|
|
};
|
|
|
}
|
|
|
function buildStepThree() { return { testPlan: abTestPlan.value, rows: abRows.value.map((row) => ({ ...row })), comparisonChartUrl: abChart.value.url, comparisonChartName: abChart.value.name, iterationRecommendation: iterationRecommendation.value }; }
|
|
|
function addFunnelChannel() {
|
|
|
channelSerial += 1;
|
|
|
const channel = { id: `channel-${Date.now()}-${channelSerial}`, name: "" };
|
|
|
channels.value.push(channel);
|
|
|
conversionRates.value.forEach((row) => { row.channelRates[channel.id] = null; });
|
|
|
}
|
|
|
function removeFunnelChannel(channelId) {
|
|
|
channels.value = channels.value.filter((channel) => channel.id !== channelId);
|
|
|
conversionRates.value.forEach((row) => { delete row.channelRates[channelId]; });
|
|
|
anomalyRows.value.forEach((row) => {
|
|
|
if (row.channelId === channelId) {
|
|
|
row.channelId = "";
|
|
|
row.channel = "";
|
|
|
row.storeConversionRate = null;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
function syncAnomalyChannel(row) { row.channel = channels.value.find((channel) => channel.id === row.channelId)?.name.trim() || ""; }
|
|
|
function addAnomalyRow() { if (anomalyRows.value.length < 5) anomalyRows.value.push(createAnomalyRow()); }
|
|
|
function removeAnomalyRow() { if (anomalyRows.value.length > 1) anomalyRows.value.pop(); }
|
|
|
function clearChart() { if (funnelChart.value.url.startsWith("blob:")) URL.revokeObjectURL(funnelChart.value.url); funnelChart.value = { url: "", name: "" }; }
|
|
|
async function handleChartUpload(event) { const file = event.target?.files?.[0]; if (!file) return; const accepted = ["image/jpeg", "image/png", "image/webp"]; if (!accepted.includes(file.type) || file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("请上传不超过 5MB 的 PNG、JPG 或 WEBP 图片"); event.target.value = ""; return; } uploading.value = true; try { let url = ""; if (isStudentDemo()) url = URL.createObjectURL(file); else { const data = new FormData(); data.append("file", file); const response = await uploadStudentTrainingFile(data); url = response?.url || response?.data?.url || ""; } if (!url) throw new Error("上传成功但未返回图片地址"); clearChart(); funnelChart.value = { url, name: file.name }; proxy?.$modal?.msgSuccess?.("漏斗对比图上传成功,可点击图片预览"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败,请重试"); } finally { uploading.value = false; event.target.value = ""; } }
|
|
|
async function saveStepOneDraft() { await persistStepOne(false); } async function validateStepOne() { if (saving.value) return; saving.value = true; try { const data = buildStepOne(); await validateProductDiagnosisIterationStepOne(data); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(data), currentStep: 2, submitted: false, saveAction: "SAVE" }); currentStep.value = 2; proxy?.$modal?.msgSuccess?.("第 1 步校验通过,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请检查第 1 步填写内容"); } finally { saving.value = false; } } async function persistStepOne(showMessage) { if (saving.value) return; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOne()), currentStep: 1, submitted: false, saveAction: "SAVE" }); if (showMessage !== false) proxy?.$modal?.msgSuccess?.("草稿已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请重试"); } finally { saving.value = false; } }
|
|
|
async function handleCommentImport(event) { const file = event.target?.files?.[0]; if (!file) return; if (!/\.(csv|txt|xls|xlsx)$/i.test(file.name) || file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("请上传不超过 5MB 的 CSV、TXT、XLS 或 XLSX 评论文件"); event.target.value = ""; return; } analyzing.value = true; try { const result = (await analyzeProductDiagnosisIterationStepTwo(file))?.data; if (!result) throw new Error("未获得评论分析结果"); analysisResult.value = result; commentPreview.value = { fileName: result.sourceFileName || file.name, totalComments: result.totalComments || 0, comments: result.previewComments || [] }; wordCloud.value = result.wordCloud || []; topIssues.value = [0, 1, 2].map((index) => result.topIssues?.[index] || ""); sentiments.value = createSentiments(); await nextTick(); renderWordCloud(); proxy?.$modal?.msgSuccess?.("评论数据已导入,词云已生成"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "评论数据分析失败,请检查文件格式"); } finally { analyzing.value = false; event.target.value = ""; } }
|
|
|
function renderWordCloud() { if (!wordCloudRef.value || !wordCloud.value.length) return; wordCloudChart?.dispose(); wordCloudChart = echarts.init(wordCloudRef.value); wordCloudChart.setOption({ backgroundColor: "transparent", tooltip: { show: true }, series: [{ type: "wordCloud", shape: "circle", gridSize: 5, sizeRange: [16, 58], rotationRange: [-35, 35], textStyle: { color: () => ["#53e2ff", "#6ae1bb", "#ffd06a", "#91b8ff", "#ff9e88"][Math.floor(Math.random() * 5)] }, data: wordCloud.value }] }); }
|
|
|
function runSentimentAnalysis() {
|
|
|
if (!analysisResult.value) return;
|
|
|
const counts = [analysisResult.value.positiveCount, analysisResult.value.neutralCount, analysisResult.value.negativeCount].map((value) => Number(value) || 0);
|
|
|
const total = counts.reduce((sum, value) => sum + value, 0);
|
|
|
sentiments.value = ["正面", "中性", "负面"].map((type, index) => ({
|
|
|
type,
|
|
|
count: counts[index],
|
|
|
percentage: total > 0 ? Number((counts[index] * 100 / total).toFixed(2)) : null,
|
|
|
}));
|
|
|
proxy?.$modal?.msgSuccess?.("情感评论数量与占比已自动计算");
|
|
|
}
|
|
|
async function saveStepTwoDraft() { if (saving.value) return false; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwo()), currentStep: 2, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("当前步骤已保存"); return true; } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请重试"); return false; } finally { saving.value = false; } } async function validateStepTwo() { if (saving.value) return false; saving.value = true; try { const data = buildStepTwo(); await validateProductDiagnosisIterationStepTwo(buildStepTwoValidationPayload(data)); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(data), currentStep: 2, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("第 2 步校验通过,已保存"); return true; } catch (error) { proxy?.$modal?.msgError?.(diagnosisErrorMessage(error, "请检查词云、情感占比和根因分析")); return false; } finally { saving.value = false; } }
|
|
|
function clearAbChart() { if (abChart.value.url.startsWith("blob:")) URL.revokeObjectURL(abChart.value.url); abChart.value = { url: "", name: "" }; }
|
|
|
async function handleAbChartUpload(event) { const file = event.target?.files?.[0]; if (!file) return; const accepted = ["image/jpeg", "image/png", "image/webp"]; if (!accepted.includes(file.type) || file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("请上传不超过 5MB 的 PNG、JPG 或 WEBP 图片"); event.target.value = ""; return; } uploading.value = true; try { let url = ""; if (isStudentDemo()) url = URL.createObjectURL(file); else { const data = new FormData(); data.append("file", file); const response = await uploadStudentTrainingFile(data); url = response?.url || response?.data?.url || ""; } if (!url) throw new Error("上传成功但未返回图片地址"); clearAbChart(); abChart.value = { url, name: file.name }; proxy?.$modal?.msgSuccess?.("AB 测试结果对比图上传成功,可点击图片预览"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败,请重试"); } finally { uploading.value = false; event.target.value = ""; } }
|
|
|
async function saveStepThreeDraft() { if (saving.value) return; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThree()), currentStep: 3, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("草稿已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请重试"); } finally { saving.value = false; } }
|
|
|
async function saveCurrentProgress() {
|
|
|
if (currentStep.value === 1) return persistStepOne(true);
|
|
|
if (currentStep.value === 2) return saveStepTwoDraft();
|
|
|
return saveStepThreeDraft();
|
|
|
}
|
|
|
async function nextStep() {
|
|
|
if (currentStep.value === 1) return validateStepOne();
|
|
|
if (currentStep.value === 2 && await validateStepTwo()) currentStep.value = 3;
|
|
|
}
|
|
|
async function validateStepThree() { if (saving.value) return; saving.value = true; try { const data = buildStepThree(); await validateProductDiagnosisIterationStepThree(data); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(data), currentStep: 3, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("第 3 步校验通过,已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请检查 AB 测试方案、指标数据和对比图"); } finally { saving.value = false; } }
|
|
|
async function submitTask() {
|
|
|
if (saving.value || uploading.value || analyzing.value) return false;
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
const stepOne = buildStepOne();
|
|
|
await validateProductDiagnosisIterationStepOne(stepOne);
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(stepOne), currentStep: 1, submitted: false, saveAction: "SAVE" });
|
|
|
const stepTwo = buildStepTwo();
|
|
|
await validateProductDiagnosisIterationStepTwo(buildStepTwoValidationPayload(stepTwo));
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(stepTwo), currentStep: 2, submitted: false, saveAction: "SAVE" });
|
|
|
const stepThree = buildStepThree();
|
|
|
await validateProductDiagnosisIterationStepThree(stepThree);
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(stepThree), currentStep: 3, submitted: true, saveAction: "SUBMIT" });
|
|
|
currentStep.value = 3;
|
|
|
proxy?.$modal?.msgSuccess?.("实训任务已提交");
|
|
|
return true;
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(diagnosisErrorMessage(error, "提交失败,请检查三个步骤的填写内容"));
|
|
|
return false;
|
|
|
} finally {
|
|
|
saving.value = false;
|
|
|
}
|
|
|
}
|
|
|
async function resetTraining() {
|
|
|
if (saving.value || uploading.value || analyzing.value) return false;
|
|
|
try {
|
|
|
await deleteStudentTrainingAnswer(TASK_KEY);
|
|
|
clearChart(); clearAbChart(); wordCloudChart?.dispose(); wordCloudChart = null;
|
|
|
channels.value = createDefaultChannels(); conversionRates.value = createConversionRates(); anomalyRows.value = [createAnomalyRow()]; analysisResult.value = null;
|
|
|
commentPreview.value = { fileName: "", totalComments: 0, comments: [] }; wordCloud.value = []; topIssues.value = ["", "", ""]; sentiments.value = createSentiments(); rootCauseAnalysis.value = "";
|
|
|
abTestPlan.value = ""; abRows.value = createAbRows(); iterationRecommendation.value = ""; currentStep.value = 1;
|
|
|
proxy?.$modal?.msgSuccess?.("已清空全部实训步骤,可以重新开始");
|
|
|
return true;
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError?.(error?.message || "重新实训失败,请稍后重试");
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
function exportTrainingOutcome() {
|
|
|
const outcome = { taskName: taskTitle.value, exportedAt: new Date().toISOString(), step1: buildStepOne(), step2: buildStepTwo(), step3: buildStepThree() };
|
|
|
const url = URL.createObjectURL(new Blob([JSON.stringify(outcome, null, 2)], { type: "application/json;charset=utf-8" }));
|
|
|
const link = document.createElement("a"); link.href = url; link.download = "产品问题诊断与迭代-实训成果.json"; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
|
|
|
proxy?.$modal?.msgSuccess?.("实训成果已导出");
|
|
|
}
|
|
|
function restoreStepOne(stepOne) {
|
|
|
const hasSavedChannels = Array.isArray(stepOne.channels);
|
|
|
const seenIds = new Set();
|
|
|
const restoredChannels = hasSavedChannels
|
|
|
? stepOne.channels.map((channel) => ({ id: String(channel?.id || "").trim(), name: String(channel?.name || "").trim() })).filter((channel) => {
|
|
|
if (!channel.id || seenIds.has(channel.id)) return false;
|
|
|
seenIds.add(channel.id);
|
|
|
return true;
|
|
|
})
|
|
|
: createDefaultChannels();
|
|
|
channels.value = restoredChannels;
|
|
|
if (Array.isArray(stepOne.conversionRates) && stepOne.conversionRates.length === nodes.length) {
|
|
|
conversionRates.value = nodes.map((linkNode) => {
|
|
|
const saved = stepOne.conversionRates.find((row) => row?.linkNode === linkNode) || {};
|
|
|
return {
|
|
|
linkNode,
|
|
|
channelRates: Object.fromEntries(restoredChannels.map((channel) => [channel.id, saved.channelRates?.[channel.id] ?? saved[channel.id] ?? null])),
|
|
|
industryAverage: saved.industryAverage ?? null,
|
|
|
};
|
|
|
});
|
|
|
} else {
|
|
|
conversionRates.value = createConversionRates(restoredChannels);
|
|
|
}
|
|
|
if (Array.isArray(stepOne.anomalyRows) && stepOne.anomalyRows.length) {
|
|
|
anomalyRows.value = stepOne.anomalyRows.slice(0, 5).map((row) => {
|
|
|
const channelId = String(row?.channelId || restoredChannels.find((channel) => channel.name === row?.channel)?.id || "");
|
|
|
return { ...createAnomalyRow(), ...row, channelId };
|
|
|
});
|
|
|
}
|
|
|
if (stepOne.funnelChartUrl) funnelChart.value = { url: stepOne.funnelChartUrl, name: stepOne.funnelChartName || "节点转化漏斗对比图" };
|
|
|
}
|
|
|
function parseSavedStep(value) {
|
|
|
if (!value) return {};
|
|
|
if (typeof value === "object") return value;
|
|
|
try { return JSON.parse(value); } catch (error) { return {}; }
|
|
|
}
|
|
|
function restoreStepTwo(stepTwo) {
|
|
|
if (!stepTwo || typeof stepTwo !== "object" || !Object.keys(stepTwo).length) return;
|
|
|
const savedAnalysis = stepTwo.analysisResult && typeof stepTwo.analysisResult === "object" ? stepTwo.analysisResult : {};
|
|
|
const restoredWordCloud = Array.isArray(stepTwo.wordCloud) ? stepTwo.wordCloud : Array.isArray(savedAnalysis.wordCloud) ? savedAnalysis.wordCloud : [];
|
|
|
const restoredComments = Array.isArray(stepTwo.previewComments) ? stepTwo.previewComments : Array.isArray(savedAnalysis.previewComments) ? savedAnalysis.previewComments : [];
|
|
|
const restoredTopIssues = [0, 1, 2].map((index) => stepTwo.topIssues?.[index] || savedAnalysis.topIssues?.[index] || "");
|
|
|
const savedSentiments = Array.isArray(stepTwo.sentiments) ? stepTwo.sentiments : [];
|
|
|
sentiments.value = createSentiments().map((row) => ({ ...row, ...(savedSentiments.find((item) => item?.type === row.type) || {}) }));
|
|
|
const count = (type, analysisKey) => {
|
|
|
const analysisCount = Number(savedAnalysis[analysisKey]);
|
|
|
if (Number.isFinite(analysisCount)) return analysisCount;
|
|
|
const sentimentCount = Number(sentiments.value.find((row) => row.type === type)?.count);
|
|
|
return Number.isFinite(sentimentCount) ? sentimentCount : 0;
|
|
|
};
|
|
|
const positiveCount = count("正面", "positiveCount");
|
|
|
const neutralCount = count("中性", "neutralCount");
|
|
|
const negativeCount = count("负面", "negativeCount");
|
|
|
const sourceFileName = String(stepTwo.sourceFileName || savedAnalysis.sourceFileName || "");
|
|
|
const inferredTotal = positiveCount + neutralCount + negativeCount;
|
|
|
const savedTotal = Number(stepTwo.totalComments ?? savedAnalysis.totalComments);
|
|
|
const totalComments = Number.isFinite(savedTotal) ? savedTotal : inferredTotal;
|
|
|
|
|
|
commentPreview.value = { fileName: sourceFileName, totalComments, comments: restoredComments };
|
|
|
wordCloud.value = restoredWordCloud;
|
|
|
topIssues.value = restoredTopIssues;
|
|
|
rootCauseAnalysis.value = stepTwo.rootCauseAnalysis || "";
|
|
|
if (sourceFileName || restoredWordCloud.length || restoredComments.length || restoredTopIssues.some(Boolean)) {
|
|
|
analysisResult.value = { sourceFileName, totalComments, positiveCount, neutralCount, negativeCount, topIssues: restoredTopIssues, wordCloud: restoredWordCloud, previewComments: restoredComments };
|
|
|
}
|
|
|
nextTick(renderWordCloud);
|
|
|
}
|
|
|
function restore(answer) {
|
|
|
const stepOne = parseSavedStep(answer?.step1Answer);
|
|
|
restoreStepOne(stepOne);
|
|
|
restoreStepTwo(parseSavedStep(answer?.step2Answer));
|
|
|
const stepThree = parseSavedStep(answer?.step3Answer);
|
|
|
if (stepThree.testPlan || stepThree.comparisonChartUrl || stepThree.iterationRecommendation) { abTestPlan.value = stepThree.testPlan || ""; if (Array.isArray(stepThree.rows) && stepThree.rows.length === createAbRows().length) abRows.value = createAbRows().map((row) => ({ ...row, ...(stepThree.rows.find((item) => item?.metric === row.metric) || {}) })); if (stepThree.comparisonChartUrl) abChart.value = { url: stepThree.comparisonChartUrl, name: stepThree.comparisonChartName || "AB 测试结果对比图" }; iterationRecommendation.value = stepThree.iterationRecommendation || ""; }
|
|
|
currentStep.value = [1, 2, 3].includes(Number(answer?.currentStep)) ? Number(answer.currentStep) : 1;
|
|
|
}
|
|
|
onMounted(async () => { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } try { restore((await getStudentTrainingAnswer(TASK_KEY))?.data); } catch (error) {} }); onBeforeUnmount(() => { if (funnelChart.value.url.startsWith("blob:")) URL.revokeObjectURL(funnelChart.value.url); if (abChart.value.url.startsWith("blob:")) URL.revokeObjectURL(abChart.value.url); wordCloudChart?.dispose(); });
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
/* ===== 页面级令牌(墨黑极简主题) ===== */
|
|
|
.diagnosis-page {
|
|
|
--student-accent: #1f2937;
|
|
|
--student-accent-deep: #111827;
|
|
|
--student-accent-light: #4b5563;
|
|
|
--student-accent-soft: #f3f4f6;
|
|
|
--student-danger: #d9554c;
|
|
|
--student-success: #4a9d6e;
|
|
|
--student-layer-1: #374151;
|
|
|
--student-layer-2: #43a294;
|
|
|
--student-layer-3: #8b6fc0;
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
--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;
|
|
|
--student-fs-button: 13px;
|
|
|
--student-fs-button-lg: 13px;
|
|
|
--student-fs-data: 14px;
|
|
|
|
|
|
/* 不设 grid-template-columns,由全局 .student-training-shell 单列布局接管 */
|
|
|
min-width: 0;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
padding: 24px;
|
|
|
}
|
|
|
|
|
|
/* 对齐 foundation 的 .training-core:只设 gap,无边框/背景/阴影 */
|
|
|
.diagnosis-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* 任务头:独立卡片 */
|
|
|
.task-header {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin: 0;
|
|
|
padding: 16px;
|
|
|
border: 1px solid #e7e9ed;
|
|
|
border-radius: 16px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
|
|
|
.task-badge {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 7px;
|
|
|
padding: 4px 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 999px;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: 12px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
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: var(--student-accent-deep);
|
|
|
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 var(--student-panel-border);
|
|
|
|
|
|
b { color: var(--student-text); font-size: 12px; }
|
|
|
ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; }
|
|
|
p { margin: 0; }
|
|
|
}
|
|
|
|
|
|
.diagnosis-toolbar {
|
|
|
flex: 0 0 auto;
|
|
|
justify-content: flex-end;
|
|
|
max-width: 100%;
|
|
|
align-self: center;
|
|
|
}
|
|
|
|
|
|
/* workbench:透明容器,去掉旧的 border/bg */
|
|
|
.workbench {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
margin-top: 0;
|
|
|
}
|
|
|
|
|
|
/* lead 描述 */
|
|
|
.lead {
|
|
|
margin: 0 0 14px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* section-block:透明化 */
|
|
|
.section-block {
|
|
|
margin-top: 14px;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.section-heading {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
> span {
|
|
|
display: grid;
|
|
|
flex: 0 0 28px;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
place-items: center;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-size: 13px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
h3 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 按钮(墨黑极简) ===== */
|
|
|
.secondary-button,
|
|
|
.table-button,
|
|
|
.upload-button,
|
|
|
.submit-button {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 40px;
|
|
|
padding: 0 18px;
|
|
|
border: 1px solid rgba(31, 41, 55, 0.4);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
font: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
text-shadow: none;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.submit-button {
|
|
|
border-color: var(--student-accent);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-weight: 700;
|
|
|
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.28);
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
color: #ffffff;
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 5px 14px rgba(17, 24, 39, 0.32);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.table-button.ghost {
|
|
|
border-color: rgba(31, 41, 55, 0.3);
|
|
|
color: var(--student-text-secondary);
|
|
|
background: #ffffff;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
button:disabled,
|
|
|
button.is-disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
}
|
|
|
|
|
|
.funnel-table,
|
|
|
.diagnosis-table,
|
|
|
.sentiment-table,
|
|
|
.ab-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.funnel-table { min-width: 900px; }
|
|
|
.diagnosis-table { min-width: 1060px; }
|
|
|
.sentiment-table { min-width: 640px; }
|
|
|
.ab-table { min-width: 860px; }
|
|
|
|
|
|
.funnel-table th, .funnel-table td,
|
|
|
.diagnosis-table th, .diagnosis-table td,
|
|
|
.sentiment-table th, .sentiment-table td,
|
|
|
.ab-table th, .ab-table td {
|
|
|
padding: 10px 12px;
|
|
|
border-right: 1px solid var(--student-panel-border);
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
vertical-align: middle;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.funnel-table thead th,
|
|
|
.diagnosis-table thead th,
|
|
|
.sentiment-table thead th,
|
|
|
.ab-table thead th {
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.funnel-table tbody th,
|
|
|
.sentiment-table tbody th,
|
|
|
.ab-table tbody th {
|
|
|
color: var(--student-text);
|
|
|
background: transparent;
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.funnel-table td,
|
|
|
.diagnosis-table td,
|
|
|
.sentiment-table td,
|
|
|
.ab-table td {
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.ab-table thead th { padding: 12px 10px; font-size: var(--student-fs-h2); }
|
|
|
.ab-table tbody th { width: 25%; padding: 12px 10px; }
|
|
|
.ab-table td { padding: 10px 11px; }
|
|
|
.ab-table td:nth-child(2), .ab-table td:nth-child(3) { width: 18%; }
|
|
|
.ab-table td:last-child { width: 39%; }
|
|
|
|
|
|
/* 表格内组件 */
|
|
|
.funnel-table :deep(.el-input-number),
|
|
|
.diagnosis-table :deep(.el-input-number),
|
|
|
.sentiment-table :deep(.el-input-number),
|
|
|
.diagnosis-table :deep(.el-select) { width: 100%; }
|
|
|
|
|
|
.unit {
|
|
|
margin-left: 5px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 12px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.auto-count {
|
|
|
color: var(--student-accent);
|
|
|
font-size: 16px;
|
|
|
font-weight: 800;
|
|
|
}
|
|
|
|
|
|
.funnel-channel-toolbar > span { display: block; }
|
|
|
|
|
|
.funnel-table .fixed-column,
|
|
|
.funnel-table tbody th { min-width: 145px; }
|
|
|
.funnel-table .channel-column,
|
|
|
.funnel-table tbody td { min-width: 180px; }
|
|
|
|
|
|
.channel-heading {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 7px;
|
|
|
|
|
|
:deep(.el-input) { min-width: 120px; }
|
|
|
:deep(.el-input__wrapper) {
|
|
|
background: #ffffff;
|
|
|
box-shadow: 0 0 0 1px var(--student-panel-border) inset;
|
|
|
}
|
|
|
:deep(.el-input__inner) {
|
|
|
color: var(--student-text);
|
|
|
text-align: center;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.channel-remove {
|
|
|
display: inline-grid;
|
|
|
flex: 0 0 24px;
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
place-items: center;
|
|
|
padding: 0;
|
|
|
border: 1px solid rgba(217, 85, 76, 0.4);
|
|
|
border-radius: 6px;
|
|
|
color: var(--student-danger);
|
|
|
background: rgba(217, 85, 76, 0.08);
|
|
|
font: inherit;
|
|
|
font-size: 17px;
|
|
|
line-height: 1;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover, &:focus-visible {
|
|
|
border-color: var(--student-danger);
|
|
|
color: #ffffff;
|
|
|
background: var(--student-danger);
|
|
|
outline: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.rate-input {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
width: 100%;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
:deep(.el-input-number) {
|
|
|
flex: 1 1 auto;
|
|
|
width: calc(100% - 22px);
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.unit {
|
|
|
display: inline-flex;
|
|
|
flex: 0 0 auto;
|
|
|
align-items: center;
|
|
|
height: 32px;
|
|
|
margin-left: 0;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.table-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 14px;
|
|
|
margin: 0 0 12px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
|
|
|
> div,
|
|
|
.actions,
|
|
|
.diagnosis-materials__actions,
|
|
|
.ab-materials__actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.actions {
|
|
|
justify-content: center;
|
|
|
margin-top: 20px;
|
|
|
|
|
|
.secondary-button,
|
|
|
.submit-button {
|
|
|
min-width: 142px;
|
|
|
min-height: 40px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 参考资料区 ===== */
|
|
|
.diagnosis-materials {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
margin: 14px 0;
|
|
|
|
|
|
&__item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 20px;
|
|
|
padding: 12px 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: var(--student-accent-soft);
|
|
|
|
|
|
strong, span { display: block; }
|
|
|
strong {
|
|
|
color: var(--student-text);
|
|
|
font-size: 13px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
span {
|
|
|
margin-top: 4px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&__ai {
|
|
|
margin: 0;
|
|
|
padding: 10px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 13px;
|
|
|
line-height: 1.7;
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
}
|
|
|
|
|
|
&__actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.diagnosis-section .table-toolbar > span { display: none; }
|
|
|
.diagnosis-step-one-notice { display: none; }
|
|
|
|
|
|
@media (max-width: 860px) {
|
|
|
.diagnosis-materials__item {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
.diagnosis-materials__actions { justify-content: flex-start; }
|
|
|
}
|
|
|
|
|
|
/* ===== AB 测试资料区 ===== */
|
|
|
.ab-materials {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 20px;
|
|
|
margin-top: 14px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
|
|
|
ol {
|
|
|
margin: 0;
|
|
|
padding-left: 20px;
|
|
|
line-height: 1.75;
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
&__actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
flex: 0 0 auto;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 860px) {
|
|
|
.ab-materials {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 上传区 ===== */
|
|
|
.chart-upload-area,
|
|
|
.comment-import {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
gap: 10px;
|
|
|
min-height: 180px;
|
|
|
padding: 22px;
|
|
|
border: 1px dashed var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-panel-bg);
|
|
|
text-align: center;
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.upload-button {
|
|
|
min-height: 42px;
|
|
|
padding: 0 18px;
|
|
|
}
|
|
|
|
|
|
.chart-preview {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 16px;
|
|
|
width: 100%;
|
|
|
text-align: left;
|
|
|
|
|
|
:deep(.el-image) {
|
|
|
width: 180px;
|
|
|
height: 110px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
> div {
|
|
|
display: grid;
|
|
|
gap: 7px;
|
|
|
}
|
|
|
|
|
|
strong { color: var(--student-text); }
|
|
|
span { color: var(--student-text-secondary); font-size: var(--student-fs-body-sm); }
|
|
|
|
|
|
button {
|
|
|
width: max-content;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-danger);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover { text-decoration: underline; }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 词云 / 数据预览 ===== */
|
|
|
.data-preview,
|
|
|
.word-cloud-card,
|
|
|
.top-issues,
|
|
|
.root-cause {
|
|
|
margin-top: 14px;
|
|
|
padding: 14px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.data-preview > div,
|
|
|
.word-cloud-title {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 600;
|
|
|
|
|
|
span, small {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 400;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.data-preview ul {
|
|
|
max-height: 130px;
|
|
|
margin: 12px 0 0;
|
|
|
padding-left: 20px;
|
|
|
overflow: auto;
|
|
|
color: var(--student-text);
|
|
|
line-height: 1.7;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.word-cloud { height: 280px; }
|
|
|
|
|
|
.top-issues > p,
|
|
|
.root-cause > p {
|
|
|
margin: 0 0 10px;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.top-issues > div {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.top-issues label {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
color: var(--student-text);
|
|
|
font-weight: 600;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.sentiment-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 14px;
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
> span {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.visual-section,
|
|
|
.diagnosis-section { margin-top: 14px; }
|
|
|
|
|
|
/* ===== Element Plus 输入框换肤 ===== */
|
|
|
.diagnosis-page {
|
|
|
:deep(.el-input__wrapper),
|
|
|
:deep(.el-textarea__inner) {
|
|
|
background: transparent;
|
|
|
box-shadow: 0 0 0 1px var(--student-panel-border) inset;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input__inner),
|
|
|
:deep(.el-textarea__inner) {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
:deep(.el-textarea__inner) {
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input__wrapper.is-focus),
|
|
|
:deep(.el-textarea__inner:focus) {
|
|
|
box-shadow: 0 0 0 1px var(--student-accent) inset, 0 0 0 2px rgba(31, 41, 55, 0.15) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.visually-hidden {
|
|
|
position: absolute;
|
|
|
width: 1px;
|
|
|
height: 1px;
|
|
|
overflow: hidden;
|
|
|
clip: rect(0 0 0 0);
|
|
|
white-space: nowrap;
|
|
|
clip-path: inset(50%);
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 900px) {
|
|
|
.diagnosis-page { padding: 12px; }
|
|
|
.top-issues > div { grid-template-columns: 1fr; }
|
|
|
.chart-preview {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
.chart-preview :deep(.el-image) { width: 100%; height: 170px; }
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
.task-header { flex-direction: column; align-items: flex-start; }
|
|
|
.diagnosis-toolbar { width: 100%; justify-content: flex-start; }
|
|
|
.table-toolbar,
|
|
|
.sentiment-actions {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
.actions {
|
|
|
flex-direction: column;
|
|
|
|
|
|
.secondary-button,
|
|
|
.submit-button { width: 100%; }
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== 步骤导航:按钮切换风 ===== */
|
|
|
#app .studentLayout section.app-main:has(.diagnosis-page) .step-indicator {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 10px !important;
|
|
|
width: 100% !important;
|
|
|
margin: 4px 0 16px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-connector { display: none !important; }
|
|
|
|
|
|
.step-tab {
|
|
|
flex: 0 0 auto !important;
|
|
|
min-width: auto !important;
|
|
|
max-width: none !important;
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
height: auto !important;
|
|
|
min-height: 42px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-radius: 8px !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
white-space: nowrap !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease !important;
|
|
|
|
|
|
.step-marker {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
min-width: 22px !important;
|
|
|
width: auto !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border: none !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: 12px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: background-color 0.16s ease, color 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
.step-copy {
|
|
|
display: flex !important;
|
|
|
flex-direction: column !important;
|
|
|
gap: 0 !important;
|
|
|
min-width: 0 !important;
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: normal !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
}
|
|
|
|
|
|
.step-action { display: none !important; }
|
|
|
|
|
|
/* 未到达项 hover */
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #1f2937 !important;
|
|
|
background: #f9fafb !important;
|
|
|
color: #1f2937 !important;
|
|
|
.step-marker { background: #1f2937 !important; color: #ffffff !important; }
|
|
|
}
|
|
|
|
|
|
/* 已完成 */
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #dce9e2 !important;
|
|
|
color: #374151 !important;
|
|
|
.step-marker { background: rgba(74, 157, 110, 0.12) !important; color: #3f8a5f !important; }
|
|
|
&:hover {
|
|
|
border-color: #4a9d6e !important;
|
|
|
background: #f9fafb !important;
|
|
|
.step-marker { background: #4a9d6e !important; color: #ffffff !important; }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 当前步骤 */
|
|
|
&.active {
|
|
|
border-color: #1f2937 !important;
|
|
|
background: #1f2937 !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
.step-marker { background: #ffffff !important; color: #1f2937 !important; }
|
|
|
&:hover {
|
|
|
background: #111827 !important;
|
|
|
border-color: #111827 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 窄屏 ===== */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.diagnosis-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
gap: 8px !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
#app .studentLayout section.app-main:has(.diagnosis-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 auto !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 顶部任务工具栏换肤 ===== */
|
|
|
.diagnosis-page .training-task-toolbar { gap: 8px; }
|
|
|
|
|
|
.diagnosis-page .task-toolbar-button.el-button {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none;
|
|
|
text-shadow: none;
|
|
|
font-size: 13px;
|
|
|
font-weight: 600;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 14px;
|
|
|
color: inherit;
|
|
|
}
|
|
|
|
|
|
&:hover, &:focus {
|
|
|
border-color: #1f2937;
|
|
|
color: #1f2937;
|
|
|
background: #f9fafb;
|
|
|
box-shadow: 0 2px 6px rgba(17, 24, 39, 0.1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.diagnosis-page .task-toolbar-button--submit.el-button {
|
|
|
border-color: #1f2937;
|
|
|
color: #ffffff;
|
|
|
background: #1f2937;
|
|
|
box-shadow: 0 2px 8px rgba(17, 24, 39, 0.22);
|
|
|
|
|
|
&:hover, &:focus {
|
|
|
border-color: #111827;
|
|
|
color: #ffffff;
|
|
|
background: #111827;
|
|
|
box-shadow: 0 4px 12px rgba(17, 24, 39, 0.28);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.diagnosis-page .task-toolbar-button.el-button.is-loading {
|
|
|
border-color: #d1d5db;
|
|
|
color: #374151;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
</style>
|