diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js
index c66568a..c575349 100644
--- a/src/api/studentTrainingAnswer.js
+++ b/src/api/studentTrainingAnswer.js
@@ -413,3 +413,22 @@ export function checkTargetUserProfileStepFour(payload) {
headers: { repeatSubmit: false },
});
}
+
+export function checkTargetUserProfileStepFive(payload) {
+ const value = payload || {};
+ const segments = Array.isArray(value.segments) ? value.segments : [];
+ const requiredFields = ["market", "attractiveness", "painPoints", "behaviorCharacteristics", "productOpportunity"];
+ if (isStudentDemo()) {
+ const incomplete = segments.find((segment) => requiredFields.some((field) => !String(segment?.[field] || "").trim()));
+ if (!segments.length || segments.length > 6 || incomplete || !String(value.marketSelection || "").trim()) {
+ 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-5/validate",
+ method: "post",
+ data: { segments, marketSelection: String(value.marketSelection || "") },
+ headers: { repeatSubmit: false },
+ });
+}
diff --git a/src/views/demand/people.vue b/src/views/demand/people.vue
index d05759f..9fa9253 100644
--- a/src/views/demand/people.vue
+++ b/src/views/demand/people.vue
@@ -227,6 +227,65 @@
+
+
+ 结合 RFM 用户群画像和重点用户画像的分析结果,进一步细分市场,寻找产品开发机会。请评估各细分市场的吸引力,并从不同细分用户的行为特征和需求痛点中提炼产品机会。
+
+
+
+ 可直接编辑表格内容,支持行扩展,最多 {{ MAX_MARKET_SEGMENTS }} 行。
+
+
+
+
+
市场选择
+
结合前面的数据分析,请说明团队应该针对哪些用户,开发什么样的产品。
+
+
+
+
@@ -271,7 +330,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 { checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
+import { checkTargetUserProfileStepFive, checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "target-user-profile";
const RFM_CHARTS = [
@@ -286,6 +345,8 @@ const RFM_INSIGHT_ROWS = [
{ dimension: "用户忠诚度", keyPoint: "F 分数分布反映的用户复购意愿如何?" },
{ dimension: "重点客户群特征", keyPoint: "哪些客户最值得关注?为什么?" },
];
+const DEFAULT_FLOW_STEPS = ["RFM 用户分层分析", "RFM 数据可视化", "用户群体特点分析", "重点用户画像", "用户细分与市场选择"];
+const MAX_MARKET_SEGMENTS = 6;
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
@@ -427,12 +488,12 @@ function createKeyUserRecords() {
];
}
-function createSegmentationRows() {
- return ["新锐白领", "精致妈妈", "都市银发", "继续补充", "继续补充"].map((market) => ({
- market,
- customerAnalysis: "",
- productOpportunity: "",
- }));
+function createMarketSegments() {
+ return ["职场白领(25-35岁)", "精致妈妈(28-38岁)", "银发人群(50-65岁)", ""].map((market) => createEmptyMarketSegment(market));
+}
+
+function createEmptyMarketSegment(market = "") {
+ return { market, attractiveness: "", painPoints: "", behaviorCharacteristics: "", productOpportunity: "" };
}
function createForm() {
@@ -445,12 +506,8 @@ function createForm() {
profileAnalysis: "",
keyUserProfile: "",
keyUserRecords: createKeyUserRecords(),
- segmentationStrategy: "",
- segmentationRows: createSegmentationRows(),
- coreFunctions: ["", "", ""],
- priceMin: "",
- priceMax: "",
- conclusion: "",
+ marketSegments: createMarketSegments(),
+ marketSelection: "",
};
}
@@ -481,7 +538,10 @@ const taskTitle = computed(() => taskConfig.value?.taskName || "目标用户画
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
-const flowSteps = computed(() => parseArray(taskConfig.value?.steps, []));
+const flowSteps = computed(() => {
+ const configuredSteps = parseArray(taskConfig.value?.steps, DEFAULT_FLOW_STEPS);
+ return configuredSteps.length >= DEFAULT_FLOW_STEPS.length ? configuredSteps : DEFAULT_FLOW_STEPS;
+});
const steps = computed(() =>
flowSteps.value.map((name, index) => ({
no: index + 1,
@@ -558,16 +618,20 @@ function normalizeKeyUserRecords(records, legacyText = "") {
return defaults;
}
-function normalizeSegmentationRows(rows, legacyText = "") {
- const defaults = createSegmentationRows();
- if (Array.isArray(rows)) {
- return defaults.map((item, index) => ({
- ...item,
- ...(rows[index] || {}),
+function normalizeMarketSegments(rows, legacyRows = []) {
+ const defaults = createMarketSegments();
+ if (Array.isArray(rows) && rows.length) {
+ return rows.slice(0, MAX_MARKET_SEGMENTS).map((item, index) => ({
+ ...(defaults[index] || createEmptyMarketSegment()),
+ ...(item || {}),
}));
}
- if (legacyText) {
- defaults[0].customerAnalysis = legacyText;
+ if (Array.isArray(legacyRows) && legacyRows.length) {
+ return legacyRows.slice(0, MAX_MARKET_SEGMENTS).map((item, index) => ({
+ ...(defaults[index] || createEmptyMarketSegment()),
+ market: String(item?.market || defaults[index]?.market || ""),
+ productOpportunity: String(item?.productOpportunity || ""),
+ }));
}
return defaults;
}
@@ -586,14 +650,10 @@ function applySavedAnswer(value) {
profileAnalysis: parsed.profileAnalysis || "",
keyUserProfile: parsed.keyUserProfile || "",
keyUserRecords: normalizeKeyUserRecords(parsed.keyUserRecords, parsed.keyUserProfile),
- segmentationStrategy: parsed.segmentationStrategy || "",
- segmentationRows: normalizeSegmentationRows(parsed.segmentationRows, parsed.segmentationStrategy),
- coreFunctions: Array.isArray(parsed.coreFunctions) ? ["", "", ""].map((item, index) => parsed.coreFunctions[index] || item) : ["", "", ""],
- priceMin: parsed.priceMin || "",
- priceMax: parsed.priceMax || "",
- conclusion: parsed.conclusion || "",
+ marketSegments: normalizeMarketSegments(parsed.marketSegments, parsed.segmentationRows),
+ marketSelection: parsed.marketSelection || parsed.segmentationStrategy || "",
};
- currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
+ currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), steps.value.length || DEFAULT_FLOW_STEPS.length);
} catch (error) {
// 忽略历史异常数据。
}
@@ -697,6 +757,16 @@ async function handleFileChange(event) {
}
}
+function addMarketSegment() {
+ if (form.value.marketSegments.length >= MAX_MARKET_SEGMENTS) return;
+ form.value.marketSegments.push(createEmptyMarketSegment());
+}
+
+function removeMarketSegment() {
+ if (form.value.marketSegments.length <= 1) return;
+ form.value.marketSegments.pop();
+}
+
async function handleChartFileChange(chart, event) {
const file = event.target.files?.[0];
if (!file) return;
@@ -777,12 +847,8 @@ function buildAnswerPayload(status) {
profileAnalysis: form.value.profileAnalysis,
keyUserProfile: form.value.keyUserProfile,
keyUserRecords: form.value.keyUserRecords,
- segmentationStrategy: form.value.segmentationStrategy,
- segmentationRows: form.value.segmentationRows,
- coreFunctions: form.value.coreFunctions,
- priceMin: form.value.priceMin,
- priceMax: form.value.priceMax,
- conclusion: form.value.conclusion,
+ marketSegments: form.value.marketSegments,
+ marketSelection: form.value.marketSelection,
}),
};
}
@@ -813,6 +879,9 @@ async function persist(status = "IN_PROGRESS") {
if (status !== "RESET" && (currentStep.value === 4 || status === "SUBMITTED")) {
await checkTargetUserProfileStepFour({ records: form.value.keyUserRecords });
}
+ if (status !== "RESET" && (currentStep.value === 5 || status === "SUBMITTED")) {
+ await checkTargetUserProfileStepFive({ segments: form.value.marketSegments, marketSelection: form.value.marketSelection });
+ }
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
@@ -825,9 +894,7 @@ function saveCurrentProgress() {
}
async function goNext() {
- if (currentStep.value === 1 || currentStep.value === 2 || currentStep.value === 3) {
- await persist("IN_PROGRESS");
- }
+ await persist("IN_PROGRESS");
currentStep.value += 1;
}
@@ -848,13 +915,12 @@ function exportOutcome() {
`${record.title}(${record.code})\n用户:${record.user || ""}\n基础属性:${record.basicAttributes || ""}\n标签:${record.tags || ""}\n行为特征:\n${record.behavior || ""}\n需求痛点:\n${record.painPoints || ""}\n消费心理:\n${record.psychology || ""}`,
)
.join("\n\n");
- const segmentationText = form.value.segmentationRows
- .map((row) => `${row.market || ""}\n客户分析:\n${row.customerAnalysis || ""}\n产品机会:\n${row.productOpportunity || ""}`)
+ const segmentationText = form.value.marketSegments
+ .map((row) => `${row.market || ""}\n市场吸引力:${row.attractiveness || ""}\n需求痛点:\n${row.painPoints || ""}\n行为特征:\n${row.behaviorCharacteristics || ""}\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 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 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${form.value.marketSelection || ""}`;
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
@@ -2086,6 +2152,153 @@ function exportOutcome() {
background: rgba(18, 85, 116, 0.78);
}
+.market-segment-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ margin-top: 26px;
+
+ .section-eyebrow {
+ margin-bottom: 5px;
+ }
+
+ h4 {
+ margin: 0;
+ color: #f0fbff;
+ font-size: 18px;
+ }
+}
+
+.market-segment-toolbar__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.market-segment-toolbar__actions :deep(.segment-action) {
+ min-width: 104px;
+ height: 40px;
+ border: 1px solid rgba(92, 224, 255, 0.7);
+ border-radius: 10px;
+ color: #ffffff;
+ background: linear-gradient(95deg, #0b84bd, #096491);
+ font-weight: 800;
+}
+
+.market-segment-toolbar__actions :deep(.segment-action--muted) {
+ border-color: rgba(118, 176, 202, 0.55);
+ color: #cce7f5;
+ background: rgba(14, 73, 101, 0.76);
+}
+
+.market-segment-tip {
+ margin: 10px 0 16px;
+ color: #6ee3ff;
+ font-size: 13px;
+}
+
+.market-segment-table-wrap {
+ overflow-x: auto;
+ border: 1px solid rgba(0, 180, 255, 0.25);
+ border-radius: 18px;
+ background: rgba(1, 14, 26, 0.72);
+}
+
+.market-segment-table {
+ width: 100%;
+ min-width: 1120px;
+ border-collapse: collapse;
+ table-layout: fixed;
+
+ th,
+ td {
+ padding: 12px;
+ border-right: 1px solid rgba(77, 167, 220, 0.24);
+ border-bottom: 1px solid rgba(77, 167, 220, 0.24);
+ vertical-align: middle;
+ }
+
+ th {
+ color: #ecfbff;
+ background: linear-gradient(100deg, #158fc4, #08739f);
+ font-size: 15px;
+ font-weight: 800;
+ }
+
+ td:nth-child(1) { width: 18%; }
+ td:nth-child(2) { width: 15%; }
+ td:nth-child(n + 3) { width: 22.33%; }
+
+ tr:last-child td { border-bottom: 0; }
+ th:last-child,
+ td:last-child { border-right: 0; }
+}
+
+.market-segment-input,
+.market-segment-textarea,
+.market-selection-textarea {
+ box-sizing: border-box;
+ width: 100%;
+ border: 1px solid rgba(67, 134, 166, 0.8);
+ outline: none;
+ color: #eaf7ff;
+ background: rgba(7, 29, 43, 0.92);
+ font-family: inherit;
+ line-height: 1.6;
+
+ &::placeholder { color: #7896a5; }
+ &:focus {
+ border-color: #5fe7ff;
+ box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
+ }
+}
+
+.market-segment-input {
+ height: 40px;
+ padding: 0 10px;
+ border-radius: 10px;
+}
+
+.market-segment-textarea {
+ min-height: 108px;
+ padding: 10px;
+ border-radius: 10px;
+ resize: vertical;
+}
+
+.market-attractiveness {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+}
+
+.market-attractiveness :deep(.el-radio) {
+ margin-right: 0;
+ color: #d9f2ff;
+}
+
+.market-selection-block {
+ margin-top: 34px;
+
+ .section-eyebrow { margin-bottom: 8px; }
+
+ h4 {
+ margin: 0 0 14px;
+ color: #dcefff;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 1.7;
+ }
+}
+
+.market-selection-textarea {
+ min-height: 180px;
+ padding: 14px;
+ border-radius: 14px;
+ resize: vertical;
+}
+
@media (max-width: 1100px) {
.target-profile-page {
grid-template-columns: 1fr;
@@ -2102,6 +2315,11 @@ function exportOutcome() {
.product-reference {
justify-content: flex-start;
}
+
+ .market-segment-toolbar {
+ align-items: flex-start;
+ flex-direction: column;
+ }
}
@media (max-width: 900px) {
@@ -2166,6 +2384,10 @@ function exportOutcome() {
grid-template-columns: 1fr;
}
+ .market-segment-toolbar__actions {
+ width: 100%;
+ }
+
.interview-tabs {
flex-direction: column;
gap: 12px;