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

77 lines
29 KiB
Vue

This file contains ambiguous Unicode characters!

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

<template>
<div class="diagnosis-page student-training-shell">
<main class="diagnosis-core">
<section class="task-header"><span>核心实训 · 产品上线与运营推广</span><h1>{{ taskTitle }}</h1></section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="workbench">
<div class="workbench-top"><div><p>产品问题诊断与迭代</p><h2>{{ currentStep === 1 ? '全链路转化漏斗诊断' : currentStep === 2 ? '用户评论根因分析' : 'AB 测试验证方案' }}</h2></div><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /></div>
<nav class="step-tabs" aria-label=""><button type="button" :class="{ active: currentStep === 1 }" @click="currentStep = 1"><b>01</b><span>全链路漏斗诊断<small>转化率与异常节点</small></span></button><i></i><button type="button" :class="{ active: currentStep === 2 }" @click="currentStep = 2"><b>02</b><span>用户评论根因分析<small>词云与情感倾向</small></span></button><i></i><button type="button" :class="{ active: currentStep === 3 }" @click="currentStep = 3"><b>03</b><span>AB 测试验证<small>改进方案有效性</small></span></button></nav>
<template v-if="currentStep === 1">
<p class="lead">从曝光到复购逐层计算渠道转化率,并与行业均值比较,定位产品运营环节中的异常流失。</p>
<section class="section-block"><div class="section-heading"><span>一</span><div><h3>计算各节点转化率</h3><p>根据案例数据填写各渠道链路节点转化率,数值以百分比(%)填写。</p></div></div><div class="table-wrap"><table class="funnel-table"><thead><tr><th>链路节点</th><th>天猫</th><th>京东</th><th>抖音</th><th>拼多多</th><th>行业均值</th></tr></thead><tbody><tr v-for="row in conversionRates" :key="row.linkNode"><th>{{ row.linkNode }}</th><td v-for="field in rateFields" :key="field"><el-input-number v-model="row[field]" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></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><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>默认 1 行,最多 5 行;渠道必须来源于上表。</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.channel" placeholder="请选择"><el-option v-for="channel in channels" :key="channel.key" :label="channel.label" :value="channel.label" /></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><el-input-number v-model="row.storeConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input-number v-model="row.industryAverageConversionRate" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input v-model="row.anomalyAnalysis" type="textarea" :rows="2" maxlength="300" show-word-limit placeholder="请填写异常原因或正常判定" /></td></tr></tbody></table></div></section>
<div class="actions"><button type="button" class="secondary-button" :disabled="saving" @click="saveStepOneDraft">保存草稿</button><button type="button" class="submit-button" :disabled="saving || uploading" @click="validateStepOne">{{ saving ? '校验中…' : '校验并进入第 2 步' }}</button></div>
</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><p>点击分析后,系统自动填充评论数量;请自行计算占比,保留两位小数。</p></div></div><div class="sentiment-actions"><button type="button" class="upload-button" :disabled="!analysisResult" @click="runSentimentAnalysis">情感倾向分析</button><span>评论数量由系统填充;系统仅校验学生填写的占比。</span></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><el-input-number v-model="row.percentage" :min="0" :max="100" :precision="2" :controls="false" :disabled="row.count === null" placeholder="请输入" /><span class="unit">%</span></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>
<div class="actions"><button type="button" class="secondary-button" :disabled="saving" @click="saveStepTwoDraft">保存草稿</button><button type="button" class="submit-button" :disabled="saving || analyzing" @click="validateStepTwo">{{ saving ? '校验中…' : '校验并保存第 2 步' }}</button></div>
</template>
<template v-else>
<p class="lead">找到根因后,还需要通过 AB 测试验证改进方案是否真正有效:将用户随机分为维持原方案的对照组和应用改进方案的实验组,用关键指标差异判断方案是否值得落地。</p>
<section class="section-block"><div class="section-heading"><span>一</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>二</span><div><h3>AB 测试数据填报</h3><p>填写案例资料中的对照组与实验组数据,并逐项给出判定与分析。</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><el-input-number v-model="row.controlValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></td><td><el-input-number v-model="row.experimentValue" :min="0" :max="100" :precision="2" :controls="false" placeholder="请输入" /><span class="unit">%</span></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>三</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></section>
<section class="section-block"><div class="section-heading"><span>四</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>
<div class="actions"><button type="button" class="secondary-button" :disabled="saving" @click="saveStepThreeDraft">保存草稿</button><button type="button" class="submit-button" :disabled="saving || uploading" @click="validateStepThree">{{ saving ? '校验中…' : '校验并保存第 3 步' }}</button></div>
</template>
</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 * as echarts from "echarts";
import "echarts-wordcloud";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { analyzeProductDiagnosisIterationStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile, validateProductDiagnosisIterationStepOne, validateProductDiagnosisIterationStepTwo, validateProductDiagnosisIterationStepThree } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
const TASK_KEY = "product-diagnosis-iteration"; const nodes = ["曝光-点击", "点击-加购", "加购-下单", "下单-好评", "好评-复购"]; const channels = [{ key: "tmall", label: "天猫" }, { key: "jd", label: "京东" }, { key: "douyin", label: "抖音" }, { key: "pinduoduo", label: "拼多多" }]; const rateFields = [...channels.map((item) => item.key), "industryAverage"];
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 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 progressItems = computed(() => currentStep.value === 1 ? [{ text: "填写五个节点的渠道转化率", status: conversionRates.value.every((row) => rateFields.every((field) => row[field] !== null && row[field] !== "" && Number.isFinite(Number(row[field])))) ? "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 createConversionRates() { return nodes.map((linkNode) => ({ linkNode, tmall: null, jd: null, douyin: null, pinduoduo: null, industryAverage: null })); } function createAnomalyRow() { return { 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 buildStepOne() { return { conversionRates: conversionRates.value.map((row) => ({ ...row })), funnelChartUrl: funnelChart.value.url, funnelChartName: funnelChart.value.name, anomalyRows: anomalyRows.value.map((row) => ({ ...row })) }; } function buildStepTwo() { return { sourceFileName: commentPreview.value.fileName, topIssues: [...topIssues.value], sentiments: sentiments.value.map((row) => ({ ...row })), rootCauseAnalysis: rootCauseAnalysis.value, wordCloud: wordCloud.value, previewComments: commentPreview.value.comments }; } function buildStepThree() { return { testPlan: abTestPlan.value, rows: abRows.value.map((row) => ({ ...row })), comparisonChartUrl: abChart.value.url, comparisonChartName: abChart.value.name, iterationRecommendation: iterationRecommendation.value }; }
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; sentiments.value = [{ type: "正面", count: analysisResult.value.positiveCount, percentage: null }, { type: "中性", count: analysisResult.value.neutralCount, percentage: null }, { type: "负面", count: analysisResult.value.negativeCount, percentage: null }]; proxy?.$modal?.msgSuccess?.("评论数量已自动填充,请计算并填写各类情感占比"); }
async function saveStepTwoDraft() { if (saving.value) return; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwo()), currentStep: 2, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("草稿已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "保存失败,请重试"); } finally { saving.value = false; } } async function validateStepTwo() { if (saving.value) return; saving.value = true; try { const data = buildStepTwo(); await validateProductDiagnosisIterationStepTwo(data); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(data), currentStep: 2, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess?.("第 2 步校验通过,已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请检查词云、情感占比和根因分析"); } 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 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; } }
function restore(answer) { try { const stepOne = JSON.parse(answer?.step1Answer || "{}"); if (Array.isArray(stepOne.conversionRates) && stepOne.conversionRates.length === nodes.length) conversionRates.value = nodes.map((node) => ({ ...createConversionRates().find((row) => row.linkNode === node), ...(stepOne.conversionRates.find((row) => row?.linkNode === node) || {}) })); if (Array.isArray(stepOne.anomalyRows) && stepOne.anomalyRows.length) anomalyRows.value = stepOne.anomalyRows.slice(0, 5).map((row) => ({ ...createAnomalyRow(), ...row })); if (stepOne.funnelChartUrl) funnelChart.value = { url: stepOne.funnelChartUrl, name: stepOne.funnelChartName || "节点转化漏斗对比图" }; const stepTwo = JSON.parse(answer?.step2Answer || "{}"); if (stepTwo.sourceFileName) { commentPreview.value = { fileName: stepTwo.sourceFileName, totalComments: 0, comments: stepTwo.previewComments || [] }; wordCloud.value = stepTwo.wordCloud || []; topIssues.value = [0, 1, 2].map((index) => stepTwo.topIssues?.[index] || ""); sentiments.value = (stepTwo.sentiments || createSentiments()).map((row) => ({ ...createSentiments().find((item) => item.type === row.type), ...row })); rootCauseAnalysis.value = stepTwo.rootCauseAnalysis || ""; nextTick(renderWordCloud); } const stepThree = JSON.parse(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; } catch (error) {} }
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">
.ab-table{width:100%;min-width:860px;border-collapse:collapse}.ab-table th,.ab-table td{border:1px solid rgba(139,213,241,.45)}.ab-table thead th{padding:14px 10px;color:#fff;background:#079ed9;font-size:16px}.ab-table tbody th{width:25%;padding:14px 10px;color:#dff8ff;background:rgba(6,67,94,.34);text-align:center}.ab-table td{padding:9px 11px;text-align:center}.ab-table td:nth-child(2),.ab-table td:nth-child(3){width:18%}.ab-table td:last-child{width:39%}
</style>