diff --git a/src/views/assessment/diagnosis.vue b/src/views/assessment/diagnosis.vue index a4f1f1e..6001883 100644 --- a/src/views/assessment/diagnosis.vue +++ b/src/views/assessment/diagnosis.vue @@ -135,7 +135,26 @@ function buildStepOne() { anomalyRows: anomalyRows.value.map((row) => ({ ...row, channel: channelSnapshot.find((channel) => channel.id === row.channelId)?.name || row.channel || "" })), }; } -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 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; @@ -173,7 +192,7 @@ function runSentimentAnalysis() { })); 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(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; } } +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; } } @@ -195,7 +214,7 @@ async function submitTask() { await validateProductDiagnosisIterationStepOne(stepOne); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(stepOne), currentStep: 1, submitted: false, saveAction: "SAVE" }); const stepTwo = buildStepTwo(); - await validateProductDiagnosisIterationStepTwo(stepTwo); + await validateProductDiagnosisIterationStepTwo(buildStepTwoValidationPayload(stepTwo)); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(stepTwo), currentStep: 2, submitted: false, saveAction: "SAVE" }); const stepThree = buildStepThree(); await validateProductDiagnosisIterationStepThree(stepThree); @@ -262,16 +281,49 @@ function restoreStepOne(stepOne) { } 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) { - try { - const stepOne = JSON.parse(answer?.step1Answer || "{}"); - restoreStepOne(stepOne); - 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) {} + 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(); }); diff --git a/tests/product-diagnosis-dynamic-channels.static.test.cjs b/tests/product-diagnosis-dynamic-channels.static.test.cjs index f52ae92..95574bf 100644 --- a/tests/product-diagnosis-dynamic-channels.static.test.cjs +++ b/tests/product-diagnosis-dynamic-channels.static.test.cjs @@ -46,3 +46,13 @@ test("三个步骤的后端校验错误均交由页面显示单条中文提示", assert.match(page, /function diagnosisErrorMessage[\s\S]*?Request failed with status code/); assert.match(page, /validateStepTwo[\s\S]*?diagnosisErrorMessage\(error/); }); + +test("第二步草稿保存完整分析状态并在重新进入页面后恢复", () => { + assert.match(page, /function buildStepTwo\(\)[\s\S]*?totalComments: commentPreview\.value\.totalComments[\s\S]*?analysisResult: analysisResult\.value/); + assert.match(page, /function buildStepTwoValidationPayload/); + assert.match(page, /validateProductDiagnosisIterationStepTwo\(buildStepTwoValidationPayload\(data\)\)/); + assert.match(page, /function restoreStepTwo\(stepTwo\)[\s\S]*?rootCauseAnalysis\.value = stepTwo\.rootCauseAnalysis/); + assert.match(page, /analysisResult\.value = \{ sourceFileName, totalComments, positiveCount, neutralCount, negativeCount/); + assert.match(page, /restoreStepTwo\(parseSavedStep\(answer\?\.step2Answer\)\)/); + assert.doesNotMatch(page, /if \(stepTwo\.sourceFileName\)/); +});