feat: add target user rfm insights

dev-QQq
chenyuan 3 weeks ago
parent 037f9cc33c
commit 8d17352139

@ -375,3 +375,22 @@ export function checkTargetUserProfileStepTwo(payload) {
headers: { repeatSubmit: false },
});
}
export function checkTargetUserProfileStepThree(payload) {
const insights = Array.isArray(payload?.insights) ? payload.insights : [];
const expectedDimensions = ["高价值用户占比", "用户活跃度", "消费能力", "用户忠诚度", "重点客户群特征"];
if (isStudentDemo()) {
const dimensions = new Set(insights.map((item) => item?.dimension));
const incomplete = insights.find((item) => !String(item?.analysis || "").trim());
if (insights.length !== expectedDimensions.length || expectedDimensions.some((dimension) => !dimensions.has(dimension)) || incomplete) {
return Promise.reject(new Error("请完成五个 RFM 用户群体特点分析维度"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/target-user-profile/step-3/validate",
method: "post",
data: { insights },
headers: { repeatSubmit: false },
});
}

@ -131,32 +131,27 @@
</section>
<section v-show="currentStep === 3" class="step-panel">
<div class="segmentation-intro">
<p>用户细分与市场选择结合上述分析进一步细分市场寻找产品开发机会填写下表</p>
<p>1确定细分变量和细分市场往往采用多变量组合进行市场细分</p>
<p>2评估每个细分市场的吸引力可以不用细分用户的行为和需求痛点分析市场的吸引力</p>
<p>3选择目标细分市场每个目标细分市场定位</p>
</div>
<p class="task-desc task-desc--compact">请根据数据可视化结果分析用户群体特点</p>
<div class="segmentation-table-wrap">
<table class="segmentation-table">
<div class="rfm-insight-table-wrap">
<table class="rfm-insight-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.segmentationRows" :key="index">
<td>
<input v-model="row.market" class="segmentation-market-input" />
</td>
<td>
<textarea v-model="row.customerAnalysis" class="segmentation-cell-input" />
</td>
<tr v-for="insight in form.rfmInsights" :key="insight.dimension">
<th scope="row">{{ insight.dimension }}</th>
<td>{{ insight.keyPoint }}</td>
<td>
<textarea v-model="row.productOpportunity" class="segmentation-cell-input" />
<textarea
v-model="insight.analysis"
class="rfm-insight-input"
:placeholder="`请结合${insight.dimension}填写分析结论`"
/>
</td>
</tr>
</tbody>
@ -241,7 +236,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, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import productReferenceImage from "@/assets/images/target-user-profile-u1344.png";
const TASK_KEY = "target-user-profile";
@ -250,6 +245,13 @@ const RFM_CHARTS = [
{ id: "average-spend", index: 2, title: "客户分层平均消费对比柱状图", buttonText: "上传客户分层平均消费对比柱状图" },
{ id: "score-heatmap", index: 3, title: "RFM 分数分布热力图", buttonText: "上传 RFM 分布热力图" },
];
const RFM_INSIGHT_ROWS = [
{ dimension: "高价值用户占比", keyPoint: "重要价值客户占比是多少?是否足以支撑产品开发?" },
{ dimension: "用户活跃度", keyPoint: "R 分数分布反映的用户活跃情况如何?" },
{ dimension: "消费能力", keyPoint: "M 分数分布反映的用户消费能力如何?" },
{ dimension: "用户忠诚度", keyPoint: "F 分数分布反映的用户复购意愿如何?" },
{ dimension: "重点客户群特征", keyPoint: "哪些客户最值得关注?为什么?" },
];
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
@ -405,6 +407,7 @@ function createForm() {
profileFileUrl: "",
profilePreview: createEmptyPreview(),
rfmCharts: createEmptyRfmCharts(),
rfmInsights: createEmptyRfmInsights(),
profileAnalysis: "",
keyUserProfile: "",
keyUserRecords: createKeyUserRecords(),
@ -424,6 +427,10 @@ function createEmptyRfmCharts() {
}, {});
}
function createEmptyRfmInsights() {
return RFM_INSIGHT_ROWS.map((item) => ({ ...item, analysis: "" }));
}
function createEmptyPreview() {
return {
sheetName: "",
@ -541,6 +548,7 @@ function applySavedAnswer(value) {
profileFileUrl: parsed.profileFileUrl || parsed.file?.url || "",
profilePreview: normalizeExcelPreview(parsed.profilePreview),
rfmCharts: normalizeRfmCharts(parsed.rfmCharts),
rfmInsights: normalizeRfmInsights(parsed.rfmInsights),
profileAnalysis: parsed.profileAnalysis || "",
keyUserProfile: parsed.keyUserProfile || "",
keyUserRecords: normalizeKeyUserRecords(parsed.keyUserRecords, parsed.keyUserProfile),
@ -572,6 +580,14 @@ function normalizeRfmCharts(charts) {
}, defaults);
}
function normalizeRfmInsights(insights) {
const savedInsights = Array.isArray(insights) ? insights : [];
return RFM_INSIGHT_ROWS.map((item) => ({
...item,
analysis: String(savedInsights.find((saved) => saved?.dimension === item.dimension)?.analysis || ""),
}));
}
function normalizeExcelPreview(preview) {
if (!preview || typeof preview !== "object" || !Array.isArray(preview.columns) || !Array.isArray(preview.rows)) {
return createEmptyPreview();
@ -719,6 +735,7 @@ function buildAnswerPayload(status) {
profileFileUrl: form.value.profileFileUrl,
profilePreview: form.value.profilePreview,
rfmCharts: form.value.rfmCharts,
rfmInsights: form.value.rfmInsights,
file: {
name: form.value.profileFileName,
url: form.value.profileFileUrl,
@ -756,6 +773,9 @@ async function persist(status = "IN_PROGRESS") {
})),
});
}
if (status !== "RESET" && (currentStep.value === 3 || status === "SUBMITTED")) {
await checkTargetUserProfileStepThree({ insights: form.value.rfmInsights });
}
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
@ -768,7 +788,7 @@ function saveCurrentProgress() {
}
async function goNext() {
if (currentStep.value === 1 || currentStep.value === 2) {
if (currentStep.value === 1 || currentStep.value === 2 || currentStep.value === 3) {
await persist("IN_PROGRESS");
}
currentStep.value += 1;
@ -794,8 +814,10 @@ function exportOutcome() {
const segmentationText = form.value.segmentationRows
.map((row) => `${row.market || ""}\n客户分析\n${row.customerAnalysis || ""}\n产品机会\n${row.productOpportunity || ""}`)
.join("\n\n");
const rfmInsightsText = form.value.rfmInsights.map((item) => `${item.dimension}\n${item.analysis || ""}`).join("\n\n");
const coreFunctionsText = form.value.coreFunctions.map((item, index) => `核心功能${index + 1}${item || ""}`).join("\n");
const content = `目标用户画像\n\n任务流程${flowSteps.value.join(" > ")}\n\n上传文件${form.value.profileFileName || "未上传"}\n文件地址${form.value.profileFileUrl || ""}\n\n客户群画像分析\n${form.value.profileAnalysis || ""}\n\n重点用户画像\n${keyUserRecordsText}\n\n用户细分与市场选择\n${segmentationText}\n\n计划设计的核心功能\n${coreFunctionsText}\n\n预计产品价格区间${form.value.priceMin || ""} - ${form.value.priceMax || ""}\n\n分析结论\n${form.value.conclusion || ""}`;
const rfmChartsText = RFM_CHARTS.map((chart) => `${chart.title}${form.value.rfmCharts[chart.id]?.url || "未上传"}`).join("\n");
const content = `目标用户画像\n\n任务流程${flowSteps.value.join(" > ")}\n\nRFM 分层分析 Excel${form.value.profileFileName || "未上传"}\n文件地址${form.value.profileFileUrl || ""}\n\nRFM 可视化图表:\n${rfmChartsText}\n\nRFM 用户群体特点分析:\n${rfmInsightsText}\n\n重点用户画像\n${keyUserRecordsText}\n\n用户细分与市场选择\n${segmentationText}\n\n计划设计的核心功能\n${coreFunctionsText}\n\n预计产品价格区间${form.value.priceMin || ""} - ${form.value.priceMax || ""}\n\n分析结论\n${form.value.conclusion || ""}`;
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
@ -1421,6 +1443,89 @@ function exportOutcome() {
line-height: 1.7;
}
.rfm-insight-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.rfm-insight-table {
width: 100%;
min-width: 850px;
border-collapse: collapse;
table-layout: fixed;
th,
td {
padding: 18px;
border-right: 1px solid rgba(67, 125, 153, 0.52);
border-bottom: 1px solid rgba(67, 125, 153, 0.52);
color: #d7ecff;
text-align: left;
vertical-align: middle;
line-height: 1.7;
}
thead th {
color: #effcff;
background: rgba(13, 129, 184, 0.78);
text-align: center;
font-size: 15px;
font-weight: 800;
}
tbody th {
width: 22%;
color: #bcefff;
background: rgba(7, 48, 72, 0.48);
text-align: center;
font-weight: 800;
}
tbody td:nth-child(2) {
width: 34%;
color: #c4d9e6;
}
tbody td:last-child {
width: 44%;
}
tr:last-child th,
tr:last-child td {
border-bottom: 0;
}
th:last-child,
td:last-child {
border-right: 0;
}
}
.rfm-insight-input {
width: 100%;
min-height: 92px;
padding: 12px 13px;
border: 1px solid rgba(58, 126, 157, 0.8);
border-radius: 10px;
outline: none;
resize: vertical;
color: #eefaff;
background: rgba(5, 27, 41, 0.84);
font: inherit;
line-height: 1.65;
&::placeholder {
color: #7896a8;
}
&:focus {
border-color: #5ee8ff;
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.12);
}
}
.product-plan-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);

Loading…
Cancel
Save