feat: add target user portrait step

fix/non-blocking-route-navigation
chenyuan 2 months ago
parent 8d17352139
commit 8837a0e573

@ -394,3 +394,22 @@ export function checkTargetUserProfileStepThree(payload) {
headers: { repeatSubmit: false },
});
}
export function checkTargetUserProfileStepFour(payload) {
const records = Array.isArray(payload?.records) ? payload.records : [];
const expectedCodes = ["U01", "U02", "U03"];
if (isStudentDemo()) {
const codes = new Set(records.map((record) => record?.code));
const incomplete = records.find((record) => ["user", "basicAttributes", "tags", "behavior", "painPoints", "psychology"].some((field) => !String(record?.[field] || "").trim()));
if (records.length !== expectedCodes.length || expectedCodes.some((code) => !codes.has(code)) || incomplete) {
return Promise.reject(new Error("请完成三位重点用户的完整画像"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/target-user-profile/step-4/validate",
method: "post",
data: { records },
headers: { repeatSubmit: false },
});
}

@ -160,35 +160,70 @@
</section>
<section v-show="currentStep === 4" class="step-panel">
<div class="product-plan-layout">
<div class="product-plan-form">
<p class="product-plan-desc">结合上述分析,请说明如果你来开发相关的产品,计划设计那些核心功能,预计产品的价格区间是多少。</p>
<div class="product-plan-label">计划设计的核心功能:</div>
<textarea
v-for="(_, index) in form.coreFunctions"
:key="index"
v-model="form.coreFunctions[index]"
class="core-function-input"
:placeholder="`核心功能${index + 1}`"
/>
<p class="task-desc task-desc--compact">
开发团队在用户分层后,已访谈了 3 位购买过相关产品的重点用户,并撰写了访谈记录。请根据访谈记录,从行为特征、需求痛点、消费心理三个维度构建重点用户画像。
</p>
<div class="product-plan-label product-plan-label--price">预计产品价格区间(元):</div>
<div class="price-range-row">
<input v-model="form.priceMin" class="price-input" />
<span></span>
<input v-model="form.priceMax" class="price-input" />
</div>
<div class="interview-tabs" aria-label="重点用户访谈记录">
<button
v-for="(record, index) in form.keyUserRecords"
:key="record.code"
type="button"
:class="['interview-tab', { active: activeInterviewIndex === index }]"
@click="handleInterviewClick(index)"
>
{{ record.title }}({{ record.code }})
</button>
</div>
<label class="analysis-conclusion-block">
<span>分析结论:</span>
<textarea v-model="form.conclusion" class="analysis-conclusion-input" />
</label>
<el-dialog v-model="interviewDialogVisible" :title="`${currentInterviewTranscript.code} 访谈记录`" width="min(820px, 92vw)" append-to-body>
<div class="interview-dialog-tabs" aria-label="切换访谈记录">
<button
v-for="(record, index) in form.keyUserRecords"
:key="record.code"
type="button"
:class="{ active: activeInterviewIndex === index }"
@click="activeInterviewIndex = index"
>
{{ record.code }}
</button>
</div>
<div class="product-reference">
<img :src="productReferenceImage" alt="小米有品三雄极光天域声光伴眠吸顶灯参考图" />
<div class="interview-dialog-body interview-dialog-body--panel">
<p class="interview-line">基本属性:{{ currentInterviewTranscript.basicAttributes }}</p>
<p class="interview-title">访谈内容:</p>
<div v-for="item in currentInterviewTranscript.content" :key="item.question" class="interview-qa">
<p>Q:{{ item.question }}</p>
<p>A:{{ item.answer }}</p>
</div>
</div>
</el-dialog>
<div class="portrait-table-wrap">
<section v-for="record in form.keyUserRecords" :key="record.code" class="portrait-record">
<div class="portrait-user portrait-user--form">
<label>
<span>用户:</span>
<input v-model="record.user" class="portrait-input" :placeholder="`例如:${record.code}`" />
</label>
<label>
<span>基础属性:</span>
<input v-model="record.basicAttributes" class="portrait-input portrait-input--wide" placeholder="年龄、性别、职业、收入、城市等" />
</label>
<label>
<span>标签:</span>
<input v-model="record.tags" class="portrait-input portrait-input--wide" placeholder="例如:高价值用户、品质偏好者" />
</label>
</div>
<div class="portrait-grid">
<div class="portrait-head">行为特征</div>
<div class="portrait-head">需求痛点</div>
<div class="portrait-head">消费心理</div>
<textarea v-model="record.behavior" class="portrait-cell" placeholder="请结合访谈记录归纳行为特征" />
<textarea v-model="record.painPoints" class="portrait-cell" placeholder="请结合访谈记录归纳需求痛点" />
<textarea v-model="record.psychology" class="portrait-cell" placeholder="请结合访谈记录归纳消费心理" />
</div>
</section>
</div>
</section>
@ -236,8 +271,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 { checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import productReferenceImage from "@/assets/images/target-user-profile-u1344.png";
import { checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "target-user-profile";
const RFM_CHARTS = [
@ -363,12 +397,12 @@ function createKeyUserRecords() {
{
title: "访谈记录一",
code: "U01",
user: "U01",
basicAttributes: "29岁,男性,职业为程序员,月收入2.8万元,居住于一线城市(上海)",
tags: "高压力职场人,高压力、高收入",
behavior: "1.工作侵占生活,作息不规律:\n2.积极寻求科技解决方案:\n3.碎片化与无缝衔接的需求:\n4.数据驱动的自我量化倾向。",
painPoints: "1.精神紧张与生理不适:2.现有解决方案有缺陷,药物担心依赖性,软件App体验被干扰,设备(手机)本身与睡眠环境冲突;\n3.对“智能化”有高阶需求;\n4.对专业性与有效性的渴求。",
psychology: "1.功效优先的理性决策者:\n2.为“效率”与“便捷”付费的意愿强烈:3.“科技感”与“品牌调性”是重要加分项:\n4.投资健康的长期主义者。",
user: "",
basicAttributes: "",
tags: "",
behavior: "",
painPoints: "",
psychology: "",
},
{
title: "访谈记录二",
@ -776,6 +810,9 @@ async function persist(status = "IN_PROGRESS") {
if (status !== "RESET" && (currentStep.value === 3 || status === "SUBMITTED")) {
await checkTargetUserProfileStepThree({ insights: form.value.rfmInsights });
}
if (status !== "RESET" && (currentStep.value === 4 || status === "SUBMITTED")) {
await checkTargetUserProfileStepFour({ records: form.value.keyUserRecords });
}
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
@ -1503,6 +1540,36 @@ function exportOutcome() {
}
}
.interview-dialog-tabs {
display: flex;
gap: 8px;
padding: 0 20px 14px;
border-bottom: 1px solid #e2ebf0;
button {
min-width: 58px;
height: 30px;
border: 1px solid #bdd6e2;
border-radius: 6px;
color: #466477;
background: #f7fbfd;
cursor: pointer;
font-weight: 700;
&.active {
border-color: #138ac0;
color: #ffffff;
background: #138ac0;
}
}
}
.interview-dialog-body--panel {
max-height: 58vh;
padding: 20px;
color: #34495a;
}
.rfm-insight-input {
width: 100%;
min-height: 92px;

Loading…
Cancel
Save