|
|
|
|
@ -95,18 +95,48 @@
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(2)" class="scenario-step-section">
|
|
|
|
|
<p class="requirement">
|
|
|
|
|
需求说明:上述几项内容让学生拖动到相应框中进行排序,完成用户旅程地图填充。
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<div class="scenario-list">
|
|
|
|
|
<label v-for="item in scenarioStatements" :key="item.key" class="scenario-row">
|
|
|
|
|
<span>{{ item.text }}</span>
|
|
|
|
|
<select v-model="form.classifications[item.key]" class="phase-select">
|
|
|
|
|
<option v-for="phase in phases" :key="phase.key" :value="phase.key">{{ phase.label }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="journey-analysis-intro">
|
|
|
|
|
<p class="journey-sort-kicker">Journey evidence map</p>
|
|
|
|
|
<h4>分析购买旅程中的行为与体验</h4>
|
|
|
|
|
<p>请根据访谈记录,分析用户在购买旅程各阶段的行为表现,识别关键触点,并评估用户的情感体验水平。</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="journey-analysis-wrap">
|
|
|
|
|
<table class="journey-analysis-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>序号</th>
|
|
|
|
|
<th>旅程阶段</th>
|
|
|
|
|
<th>用户行为描述</th>
|
|
|
|
|
<th>关键触点</th>
|
|
|
|
|
<th>情感体验</th>
|
|
|
|
|
<th>关键决策因素</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="(row, index) in form.journeyAnalysis" :key="row.stageId">
|
|
|
|
|
<td class="journey-analysis-index">{{ index + 1 }}</td>
|
|
|
|
|
<th class="journey-analysis-stage">{{ stageTitle(row.stageId) }}</th>
|
|
|
|
|
<td>
|
|
|
|
|
<textarea v-model="row.behaviorDescription" placeholder="描述用户在此阶段的行为表现"></textarea>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<textarea v-model="row.keyTouchpoints" placeholder="填写平台、内容、服务或产品触点"></textarea>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<select v-model="row.emotionExperience" aria-label="选择情感体验">
|
|
|
|
|
<option value="" disabled>请选择</option>
|
|
|
|
|
<option v-for="emotion in emotionOptions" :key="emotion" :value="emotion">{{ emotion }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<textarea v-model="row.decisionFactors" placeholder="填写推动或阻碍决策的关键因素"></textarea>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-if="journeyAnalysisMessage" class="journey-check-message" role="status">{{ journeyAnalysisMessage }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(3)" class="scenario-step-section">
|
|
|
|
|
@ -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));
|
|
|
|
|
|