|
|
|
@ -131,32 +131,27 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="currentStep === 3" class="step-panel">
|
|
|
|
<section v-show="currentStep === 3" class="step-panel">
|
|
|
|
<div class="segmentation-intro">
|
|
|
|
<p class="task-desc task-desc--compact">请根据数据可视化结果,分析用户群体特点:</p>
|
|
|
|
<p>用户细分与市场选择。结合上述分析,进一步细分市场,寻找产品开发机会,填写下表。</p>
|
|
|
|
|
|
|
|
<p>(1)确定细分变量和细分市场,往往采用多变量组合进行市场细分。</p>
|
|
|
|
|
|
|
|
<p>(2)评估每个细分市场的吸引力,可以不用细分用户的行为和需求痛点分析市场的吸引力。</p>
|
|
|
|
|
|
|
|
<p>(3)选择目标细分市场,每个目标细分市场定位。</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="segmentation-table-wrap">
|
|
|
|
<div class="rfm-insight-table-wrap">
|
|
|
|
<table class="segmentation-table">
|
|
|
|
<table class="rfm-insight-table">
|
|
|
|
<thead>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<tr>
|
|
|
|
<th>细分市场</th>
|
|
|
|
<th>分析维度</th>
|
|
|
|
<th>客户分析</th>
|
|
|
|
<th>分析要点</th>
|
|
|
|
<th>产品机会</th>
|
|
|
|
<th>你的分析</th>
|
|
|
|
</tr>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
<tbody>
|
|
|
|
<tr v-for="(row, index) in form.segmentationRows" :key="index">
|
|
|
|
<tr v-for="insight in form.rfmInsights" :key="insight.dimension">
|
|
|
|
<td>
|
|
|
|
<th scope="row">{{ insight.dimension }}</th>
|
|
|
|
<input v-model="row.market" class="segmentation-market-input" />
|
|
|
|
<td>{{ insight.keyPoint }}</td>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
|
|
|
<textarea v-model="row.customerAnalysis" class="segmentation-cell-input" />
|
|
|
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td>
|
|
|
|
<td>
|
|
|
|
<textarea v-model="row.productOpportunity" class="segmentation-cell-input" />
|
|
|
|
<textarea
|
|
|
|
|
|
|
|
v-model="insight.analysis"
|
|
|
|
|
|
|
|
class="rfm-insight-input"
|
|
|
|
|
|
|
|
:placeholder="`请结合${insight.dimension}填写分析结论`"
|
|
|
|
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</tbody>
|
|
|
|
@ -241,7 +236,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
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";
|
|
|
|
import productReferenceImage from "@/assets/images/target-user-profile-u1344.png";
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_KEY = "target-user-profile";
|
|
|
|
const TASK_KEY = "target-user-profile";
|
|
|
|
@ -250,6 +245,13 @@ const RFM_CHARTS = [
|
|
|
|
{ id: "average-spend", index: 2, title: "客户分层平均消费对比柱状图", buttonText: "上传客户分层平均消费对比柱状图" },
|
|
|
|
{ id: "average-spend", index: 2, title: "客户分层平均消费对比柱状图", buttonText: "上传客户分层平均消费对比柱状图" },
|
|
|
|
{ id: "score-heatmap", index: 3, title: "RFM 分数分布热力图", buttonText: "上传 RFM 分布热力图" },
|
|
|
|
{ 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 { proxy } = getCurrentInstance();
|
|
|
|
|
|
|
|
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
@ -405,6 +407,7 @@ function createForm() {
|
|
|
|
profileFileUrl: "",
|
|
|
|
profileFileUrl: "",
|
|
|
|
profilePreview: createEmptyPreview(),
|
|
|
|
profilePreview: createEmptyPreview(),
|
|
|
|
rfmCharts: createEmptyRfmCharts(),
|
|
|
|
rfmCharts: createEmptyRfmCharts(),
|
|
|
|
|
|
|
|
rfmInsights: createEmptyRfmInsights(),
|
|
|
|
profileAnalysis: "",
|
|
|
|
profileAnalysis: "",
|
|
|
|
keyUserProfile: "",
|
|
|
|
keyUserProfile: "",
|
|
|
|
keyUserRecords: createKeyUserRecords(),
|
|
|
|
keyUserRecords: createKeyUserRecords(),
|
|
|
|
@ -424,6 +427,10 @@ function createEmptyRfmCharts() {
|
|
|
|
}, {});
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createEmptyRfmInsights() {
|
|
|
|
|
|
|
|
return RFM_INSIGHT_ROWS.map((item) => ({ ...item, analysis: "" }));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createEmptyPreview() {
|
|
|
|
function createEmptyPreview() {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
sheetName: "",
|
|
|
|
sheetName: "",
|
|
|
|
@ -541,6 +548,7 @@ function applySavedAnswer(value) {
|
|
|
|
profileFileUrl: parsed.profileFileUrl || parsed.file?.url || "",
|
|
|
|
profileFileUrl: parsed.profileFileUrl || parsed.file?.url || "",
|
|
|
|
profilePreview: normalizeExcelPreview(parsed.profilePreview),
|
|
|
|
profilePreview: normalizeExcelPreview(parsed.profilePreview),
|
|
|
|
rfmCharts: normalizeRfmCharts(parsed.rfmCharts),
|
|
|
|
rfmCharts: normalizeRfmCharts(parsed.rfmCharts),
|
|
|
|
|
|
|
|
rfmInsights: normalizeRfmInsights(parsed.rfmInsights),
|
|
|
|
profileAnalysis: parsed.profileAnalysis || "",
|
|
|
|
profileAnalysis: parsed.profileAnalysis || "",
|
|
|
|
keyUserProfile: parsed.keyUserProfile || "",
|
|
|
|
keyUserProfile: parsed.keyUserProfile || "",
|
|
|
|
keyUserRecords: normalizeKeyUserRecords(parsed.keyUserRecords, parsed.keyUserProfile),
|
|
|
|
keyUserRecords: normalizeKeyUserRecords(parsed.keyUserRecords, parsed.keyUserProfile),
|
|
|
|
@ -572,6 +580,14 @@ function normalizeRfmCharts(charts) {
|
|
|
|
}, defaults);
|
|
|
|
}, 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) {
|
|
|
|
function normalizeExcelPreview(preview) {
|
|
|
|
if (!preview || typeof preview !== "object" || !Array.isArray(preview.columns) || !Array.isArray(preview.rows)) {
|
|
|
|
if (!preview || typeof preview !== "object" || !Array.isArray(preview.columns) || !Array.isArray(preview.rows)) {
|
|
|
|
return createEmptyPreview();
|
|
|
|
return createEmptyPreview();
|
|
|
|
@ -719,6 +735,7 @@ function buildAnswerPayload(status) {
|
|
|
|
profileFileUrl: form.value.profileFileUrl,
|
|
|
|
profileFileUrl: form.value.profileFileUrl,
|
|
|
|
profilePreview: form.value.profilePreview,
|
|
|
|
profilePreview: form.value.profilePreview,
|
|
|
|
rfmCharts: form.value.rfmCharts,
|
|
|
|
rfmCharts: form.value.rfmCharts,
|
|
|
|
|
|
|
|
rfmInsights: form.value.rfmInsights,
|
|
|
|
file: {
|
|
|
|
file: {
|
|
|
|
name: form.value.profileFileName,
|
|
|
|
name: form.value.profileFileName,
|
|
|
|
url: form.value.profileFileUrl,
|
|
|
|
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));
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
|
|
|
|
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
|
|
|
|
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
@ -768,7 +788,7 @@ function saveCurrentProgress() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function goNext() {
|
|
|
|
async function goNext() {
|
|
|
|
if (currentStep.value === 1 || currentStep.value === 2) {
|
|
|
|
if (currentStep.value === 1 || currentStep.value === 2 || currentStep.value === 3) {
|
|
|
|
await persist("IN_PROGRESS");
|
|
|
|
await persist("IN_PROGRESS");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
currentStep.value += 1;
|
|
|
|
currentStep.value += 1;
|
|
|
|
@ -794,8 +814,10 @@ function exportOutcome() {
|
|
|
|
const segmentationText = form.value.segmentationRows
|
|
|
|
const segmentationText = form.value.segmentationRows
|
|
|
|
.map((row) => `${row.market || ""}\n客户分析:\n${row.customerAnalysis || ""}\n产品机会:\n${row.productOpportunity || ""}`)
|
|
|
|
.map((row) => `${row.market || ""}\n客户分析:\n${row.customerAnalysis || ""}\n产品机会:\n${row.productOpportunity || ""}`)
|
|
|
|
.join("\n\n");
|
|
|
|
.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 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 blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement("a");
|
|
|
|
const link = document.createElement("a");
|
|
|
|
@ -1421,6 +1443,89 @@ function exportOutcome() {
|
|
|
|
line-height: 1.7;
|
|
|
|
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 {
|
|
|
|
.product-plan-layout {
|
|
|
|
display: grid;
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
|
|
|
|
grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
|
|
|
|
|