diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index b0dd82b..6569c16 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -452,3 +452,31 @@ export function checkConsumerScenarioStepOne(payload) { headers: { repeatSubmit: false }, }); } + +export function checkConsumerScenarioStepTwo(payload) { + const rows = Array.isArray(payload?.rows) ? payload.rows : []; + if (isStudentDemo()) { + const requiredStageIds = consumerScenarioJourneyOrder; + const incomplete = rows.find((row) => !String(row?.behaviorDescription || "").trim() + || !String(row?.keyTouchpoints || "").trim() + || !["负面", "中性", "正面"].includes(row?.emotionExperience) + || !String(row?.decisionFactors || "").trim()); + const valid = rows.length === requiredStageIds.length + && rows.every((row) => requiredStageIds.includes(row?.stageId)) + && new Set(rows.map((row) => row.stageId)).size === requiredStageIds.length + && !incomplete; + return Promise.resolve({ + code: 200, + data: { + valid, + message: valid ? "旅程分析填写完整。" : "请完整填写五个阶段的用户行为、关键触点、情感体验和决策因素。", + }, + }); + } + return request({ + url: "/api/student-training-answers/consumer-scenario/step-2/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 38858ce..b2302e3 100644 --- a/src/views/demand/consumer-scenario.vue +++ b/src/views/demand/consumer-scenario.vue @@ -95,18 +95,48 @@
-

- 需求说明:上述几项内容让学生拖动到相应框中进行排序,完成用户旅程地图填充。 -

- -
- -
+
+

Journey evidence map

+

分析购买旅程中的行为与体验

+

请根据访谈记录,分析用户在购买旅程各阶段的行为表现,识别关键触点,并评估用户的情感体验水平。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
序号旅程阶段用户行为描述关键触点情感体验关键决策因素
{{ index + 1 }}{{ stageTitle(row.stageId) }} + + + + + + + +
+
+

{{ journeyAnalysisMessage }}

@@ -235,7 +265,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, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; +import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; const TASK_KEY = "consumer-scenario"; const { proxy } = getCurrentInstance(); @@ -278,6 +308,8 @@ const journeyStages = [ }, ]; +const emotionOptions = ["负面", "中性", "正面"]; + const phases = [ { key: "problem", label: "问题感知(需求触发)" }, { key: "search", label: "信息搜索(产品探索)" }, @@ -376,9 +408,11 @@ const emotionPoints = [ const taskConfig = ref(null); const saving = ref(false); const checkingJourney = ref(false); +const checkingJourneyAnalysis = ref(false); const draggedStageId = ref(""); const journeyCheckMessage = ref(""); const journeyCheckedCorrectly = ref(false); +const journeyAnalysisMessage = ref(""); function createMatrix() { return journeyRows.reduce((matrix, row) => { @@ -397,10 +431,21 @@ function createClassifications() { }, {}); } +function createJourneyAnalysis() { + return journeyStages.map((stage) => ({ + stageId: stage.id, + behaviorDescription: "", + keyTouchpoints: "", + emotionExperience: "", + decisionFactors: "", + })); +} + function createForm() { return { activeStep: 1, journeyOrder: createShuffledJourneyOrder(), + journeyAnalysis: createJourneyAnalysis(), classifications: createClassifications(), matrix: createMatrix(), }; @@ -423,7 +468,7 @@ const orderedJourneyStages = computed(() => const progressItems = computed(() => [ { text: journeyCheckedCorrectly.value ? "已完成:购买旅程排序" : "进行中:购买旅程排序", status: journeyCheckedCorrectly.value ? "done" : "doing" }, - { text: hasMatrixContent.value ? "进行中:用户旅程地图填写" : "待完成:用户旅程地图填写", status: hasMatrixContent.value ? "doing" : "" }, + { text: hasJourneyAnalysisContent.value ? "进行中:用户旅程分析" : "待完成:用户旅程分析", status: hasJourneyAnalysisContent.value ? "doing" : "" }, { text: "待完成:提交实训成果", status: "" }, ]); @@ -449,6 +494,13 @@ const hasMatrixContent = computed(() => ) ); +const hasJourneyAnalysisContent = computed(() => + form.value.journeyAnalysis.some((row) => + [row.behaviorDescription, row.keyTouchpoints, row.emotionExperience, row.decisionFactors] + .some((value) => String(value || "").trim()) + ) +); + onMounted(async () => { await loadTaskConfig(); await loadSavedAnswer(); @@ -497,6 +549,7 @@ function applySavedAnswer(value) { form.value = { activeStep: normalizeActiveStep(parsed.currentStep), journeyOrder: normalizeJourneyOrder(parsed.journeyOrder), + journeyAnalysis: normalizeJourneyAnalysis(parsed.journeyAnalysis), classifications: { ...createClassifications(), ...(parsed.classifications || {}), @@ -510,6 +563,20 @@ function applySavedAnswer(value) { } } +function normalizeJourneyAnalysis(value) { + const savedRows = Array.isArray(value) ? value : []; + return createJourneyAnalysis().map((row) => { + const savedRow = savedRows.find((item) => item?.stageId === row.stageId) || {}; + return { + ...row, + behaviorDescription: savedRow.behaviorDescription || "", + keyTouchpoints: savedRow.keyTouchpoints || "", + emotionExperience: emotionOptions.includes(savedRow.emotionExperience) ? savedRow.emotionExperience : "", + decisionFactors: savedRow.decisionFactors || "", + }; + }); +} + function normalizeJourneyOrder(value) { if (!Array.isArray(value) || value.length !== journeyStages.length) return createShuffledJourneyOrder(); const uniqueOrder = [...new Set(value)]; @@ -540,7 +607,7 @@ function normalizeActiveStep(value) { async function setCurrentStep(step) { if (!hasConfiguredSteps.value) return; const targetStep = normalizeActiveStep(step); - if (targetStep > form.value.activeStep && form.value.activeStep === 1 && !(await persist("IN_PROGRESS"))) return; + if (targetStep > form.value.activeStep && [1, 2].includes(form.value.activeStep) && !(await persist("IN_PROGRESS"))) return; form.value.activeStep = targetStep; } @@ -615,6 +682,26 @@ async function ensureJourneyOrderValid(showSuccess = false) { } } +function stageTitle(stageId) { + return journeyStages.find((stage) => stage.id === stageId)?.title || "未知阶段"; +} + +async function ensureJourneyAnalysisValid() { + if (checkingJourneyAnalysis.value) return false; + checkingJourneyAnalysis.value = true; + try { + const response = await checkConsumerScenarioStepTwo({ rows: form.value.journeyAnalysis }); + const result = response?.data || {}; + journeyAnalysisMessage.value = result.message || (result.valid ? "旅程分析填写完整。" : "请完整填写五个阶段的旅程分析。"); + return result.valid === true; + } catch (error) { + journeyAnalysisMessage.value = error?.message || "旅程分析校验失败,请稍后重试。"; + return false; + } finally { + checkingJourneyAnalysis.value = false; + } +} + function buildOutcome() { return { taskKey: TASK_KEY, @@ -623,6 +710,7 @@ function buildOutcome() { currentStep: form.value.activeStep, journeyOrder: form.value.journeyOrder, journeyCheckedCorrectly: journeyCheckedCorrectly.value, + journeyAnalysis: form.value.journeyAnalysis, phases, classifications: form.value.classifications, matrix: form.value.matrix, @@ -642,6 +730,7 @@ function buildAnswerPayload(status) { async function persist(status = "IN_PROGRESS") { if (saving.value) return; if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureJourneyOrderValid())) return false; + if (status !== "RESET" && form.value.activeStep === 2 && !(await ensureJourneyAnalysisValid())) return false; saving.value = true; try { await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status)); @@ -1208,6 +1297,125 @@ function exportOutcome() { } } +.journey-analysis-intro { + margin-bottom: 18px; + padding: 20px; + border-left: 3px solid #3bd8ff; + border-radius: 0 16px 16px 0; + background: linear-gradient(90deg, rgba(7, 95, 137, 0.28), rgba(4, 31, 49, 0.18)); + + h4 { + margin: 4px 0 8px; + color: #ffffff; + font-size: 21px; + } + + p { + margin: 0; + color: #c3dfec; + font-size: 14px; + line-height: 1.8; + } +} + +.journey-analysis-wrap { + overflow-x: auto; + border: 1px solid rgba(72, 182, 228, 0.46); + border-radius: 16px; + background: rgba(2, 20, 33, 0.72); +} + +.journey-analysis-table { + width: 100%; + min-width: 1120px; + border-collapse: collapse; + table-layout: fixed; + + th, + td { + border: 1px solid rgba(77, 167, 220, 0.28); + } + + thead th { + padding: 13px 10px; + color: #effcff; + 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; + } + + tbody tr { + background: rgba(2, 17, 29, 0.48); + + &:nth-child(even) { + background: rgba(9, 47, 67, 0.34); + } + } + + td, + tbody th { + padding: 10px; + vertical-align: top; + } + + th:nth-child(1) { width: 54px; } + th:nth-child(2) { width: 132px; } + th:nth-child(3), th:nth-child(4), th:nth-child(6) { width: 27%; } + th:nth-child(5) { width: 118px; } + + textarea, + select { + width: 100%; + border: 1px solid transparent; + border-radius: 8px; + color: #eaf9ff; + background: transparent; + font: inherit; + font-size: 13px; + line-height: 1.65; + outline: none; + box-sizing: border-box; + + &:focus { + border-color: rgba(92, 224, 255, 0.76); + background: rgba(8, 64, 92, 0.42); + box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.1); + } + } + + textarea { + min-height: 112px; + padding: 8px; + resize: vertical; + } + + textarea::placeholder { + color: #7897a8; + } + + select { + min-height: 38px; + margin-top: 30px; + padding: 0 9px; + border-color: rgba(99, 177, 211, 0.6); + background: rgba(6, 38, 58, 0.78); + } +} + +.journey-analysis-index, +.journey-analysis-stage { + color: #dff7ff; + font-weight: 800; + text-align: center; + vertical-align: middle !important; +} + +.journey-analysis-stage { + color: #9defff; + line-height: 1.65; +} + .journey-flow { display: grid; grid-template-columns: repeat(5, minmax(120px, 1fr));