From aa66b87a5eddff17fd437bfebad5ff8b6e128706 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 11 Aug 2026 22:51:32 +0800 Subject: [PATCH] feat: add consumer scenario optimization analysis --- src/api/studentTrainingAnswer.js | 24 +++ src/views/demand/consumer-scenario.vue | 222 +++++++++++++++++++------ 2 files changed, 193 insertions(+), 53 deletions(-) diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index 2074d5a..b970ec0 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -500,3 +500,27 @@ export function checkConsumerScenarioStepThree(payload) { headers: { repeatSubmit: false }, }); } + +export function checkConsumerScenarioStepFour(payload) { + const rows = Array.isArray(payload?.rows) ? payload.rows : []; + if (isStudentDemo()) { + const valid = rows.length === consumerScenarioJourneyOrder.length + && rows.every((row) => consumerScenarioJourneyOrder.includes(row?.stageId) + && String(row?.painPoint || "").trim() + && String(row?.optimizationSuggestion || "").trim()) + && new Set(rows.map((row) => row.stageId)).size === consumerScenarioJourneyOrder.length; + return Promise.resolve({ + code: 200, + data: { + valid, + message: valid ? "五个阶段的优化分析填写完整。" : "请完整填写五个阶段的痛点和优化建议。", + }, + }); + } + return request({ + url: "/api/student-training-answers/consumer-scenario/step-4/validate", + method: "post", + data: { rows }, + headers: { repeatSubmit: false }, + }); +} diff --git a/src/views/demand/consumer-scenario.vue b/src/views/demand/consumer-scenario.vue index 1d899d7..7fe7541 100644 --- a/src/views/demand/consumer-scenario.vue +++ b/src/views/demand/consumer-scenario.vue @@ -191,60 +191,30 @@
-
-
示例:APP 用户体验地图
-
-
阶段
-
{{ phase.name }}
+
+ Step 04 +

识别旅程痛点,形成优化建议

+

结合用户购买旅程图,分析各阶段存在的痛点及未满足的需求,提出可落地的产品优化建议。

-
-
用户目标
-
-

{{ goal }}

-
-
-
-
用户行为
-
-
- {{ item }} -
-
-
-
-
接触点
-
-

{{ phase.touchpoint }}

-
-
-
-
情绪曲线
-
- - - - - - - - - - -
- - {{ point.icon }} - {{ point.label }} - -
-
-
-
-
痛点/机会点
-
-

{{ phase.pain }}

-
+
+ + + + + + + + + + + + + + + +
旅程阶段识别的痛点优化建议
{{ stageShortTitle(row.stageId) }}
-
+

{{ journeyOptimizationMessage }}

@@ -291,7 +261,7 @@ 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 { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; +import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, checkConsumerScenarioStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"; import { isStudentDemo } from "@/utils/studentDemo"; const TASK_KEY = "consumer-scenario"; @@ -436,12 +406,14 @@ const taskConfig = ref(null); const saving = ref(false); const checkingJourney = ref(false); const checkingJourneyAnalysis = ref(false); +const checkingJourneyOptimization = ref(false); const journeyMapInput = ref(null); const uploadingJourneyMap = ref(false); const draggedStageId = ref(""); const journeyCheckMessage = ref(""); const journeyCheckedCorrectly = ref(false); const journeyAnalysisMessage = ref(""); +const journeyOptimizationMessage = ref(""); function createMatrix() { return journeyRows.reduce((matrix, row) => { @@ -470,12 +442,21 @@ function createJourneyAnalysis() { })); } +function createJourneyOptimizations() { + return journeyStages.map((stage) => ({ + stageId: stage.id, + painPoint: "", + optimizationSuggestion: "", + })); +} + function createForm() { return { activeStep: 1, journeyOrder: createShuffledJourneyOrder(), journeyAnalysis: createJourneyAnalysis(), journeyMap: { url: "", name: "" }, + journeyOptimizations: createJourneyOptimizations(), classifications: createClassifications(), matrix: createMatrix(), }; @@ -581,6 +562,7 @@ function applySavedAnswer(value) { journeyOrder: normalizeJourneyOrder(parsed.journeyOrder), journeyAnalysis: normalizeJourneyAnalysis(parsed.journeyAnalysis), journeyMap: normalizeJourneyMap(parsed.journeyMap), + journeyOptimizations: normalizeJourneyOptimizations(parsed.journeyOptimizations), classifications: { ...createClassifications(), ...(parsed.classifications || {}), @@ -615,6 +597,18 @@ function normalizeJourneyAnalysis(value) { }); } +function normalizeJourneyOptimizations(value) { + const savedRows = Array.isArray(value) ? value : []; + return createJourneyOptimizations().map((row) => { + const savedRow = savedRows.find((item) => item?.stageId === row.stageId) || {}; + return { + ...row, + painPoint: savedRow.painPoint || "", + optimizationSuggestion: savedRow.optimizationSuggestion || "", + }; + }); +} + function normalizeJourneyOrder(value) { if (!Array.isArray(value) || value.length !== journeyStages.length) return createShuffledJourneyOrder(); const uniqueOrder = [...new Set(value)]; @@ -724,6 +718,10 @@ function stageTitle(stageId) { return journeyStages.find((stage) => stage.id === stageId)?.title || "未知阶段"; } +function stageShortTitle(stageId) { + return stageTitle(stageId).split("(")[0]; +} + async function ensureJourneyAnalysisValid() { if (checkingJourneyAnalysis.value) return false; checkingJourneyAnalysis.value = true; @@ -802,6 +800,22 @@ async function ensureJourneyMapValid() { } } +async function ensureJourneyOptimizationValid() { + if (checkingJourneyOptimization.value) return false; + checkingJourneyOptimization.value = true; + try { + const response = await checkConsumerScenarioStepFour({ rows: form.value.journeyOptimizations }); + const result = response?.data || {}; + journeyOptimizationMessage.value = result.message || (result.valid ? "五个阶段的优化分析填写完整。" : "请完整填写五个阶段的痛点和优化建议。"); + return result.valid === true; + } catch (error) { + journeyOptimizationMessage.value = error?.message || "优化建议校验失败,请稍后重试。"; + return false; + } finally { + checkingJourneyOptimization.value = false; + } +} + function downloadJourneyMapTemplate() { const escapeCsvValue = (value) => `"${String(value || "").replaceAll('"', '""')}"`; const header = ["序号", "旅程阶段", "用户行为描述", "关键触点", "情感体验", "关键决策因素", "痛点标记", "机会点标记"]; @@ -834,6 +848,7 @@ function buildOutcome() { journeyCheckedCorrectly: journeyCheckedCorrectly.value, journeyAnalysis: form.value.journeyAnalysis, journeyMap: form.value.journeyMap, + journeyOptimizations: form.value.journeyOptimizations, phases, classifications: form.value.classifications, matrix: form.value.matrix, @@ -855,6 +870,7 @@ async function persist(status = "IN_PROGRESS") { if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureJourneyOrderValid())) return false; if (status !== "RESET" && form.value.activeStep === 2 && !(await ensureJourneyAnalysisValid())) return false; if (status !== "RESET" && form.value.activeStep === 3 && !(await ensureJourneyMapValid())) return false; + if (status !== "RESET" && form.value.activeStep === 4 && !(await ensureJourneyOptimizationValid())) return false; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status)); @@ -1680,6 +1696,106 @@ function exportOutcome() { } } +.journey-optimization-intro { + margin-bottom: 18px; + padding: 20px 22px; + border-left: 3px solid #41d9ff; + border-radius: 0 16px 16px 0; + background: linear-gradient(90deg, rgba(7, 98, 143, 0.3), rgba(3, 27, 42, 0.12)); + + span { + color: #51d8ff; + font-size: 13px; + font-weight: 900; + letter-spacing: 0.08em; + } + + h4 { + margin: 6px 0 8px; + color: #ffffff; + font-size: 21px; + } + + p { + margin: 0; + color: #c4dfe9; + font-size: 14px; + line-height: 1.8; + } +} + +.journey-optimization-wrap { + overflow-x: auto; + border: 1px solid rgba(72, 182, 228, 0.46); + border-radius: 16px; + background: rgba(2, 20, 33, 0.72); +} + +.journey-optimization-table { + width: 100%; + min-width: 900px; + border-collapse: collapse; + table-layout: fixed; + + th, + td { + border: 1px solid rgba(77, 167, 220, 0.28); + } + + thead th { + padding: 14px 12px; + color: #f0fcff; + background: linear-gradient(90deg, rgba(8, 128, 178, 0.94), rgba(5, 94, 143, 0.9)); + font-size: 14px; + font-weight: 900; + text-align: center; + } + + thead th:first-child { + width: 170px; + } + + tbody th { + padding: 18px 14px; + color: #a6ecff; + background: rgba(3, 36, 55, 0.58); + font-size: 14px; + font-weight: 800; + text-align: center; + } + + td { + padding: 10px; + background: rgba(1, 17, 29, 0.45); + } + + textarea { + width: 100%; + min-height: 92px; + padding: 10px 12px; + border: 1px solid transparent; + border-radius: 9px; + outline: none; + resize: vertical; + color: #ebfaff; + background: transparent; + font: inherit; + font-size: 13px; + line-height: 1.65; + box-sizing: border-box; + + &:focus { + border-color: rgba(92, 224, 255, 0.72); + background: rgba(8, 64, 92, 0.42); + box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.1); + } + } + + textarea::placeholder { + color: #7696a8; + } +} + .journey-flow { display: grid; grid-template-columns: repeat(5, minmax(120px, 1fr));