diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js
index 422d013..05a30c0 100644
--- a/src/api/studentTrainingAnswer.js
+++ b/src/api/studentTrainingAnswer.js
@@ -423,3 +423,27 @@ export function checkConsumerBehaviorStepFour(payload) {
headers: { repeatSubmit: false },
});
}
+
+export function checkProductValuePositioningStepOne(payload) {
+ const rows = Array.isArray(payload?.rows) ? payload.rows : [];
+ const types = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
+ if (isStudentDemo()) {
+ const valid = rows.length === types.length && rows.every((row, index) => row?.id === index + 1
+ && row?.type === types[index]
+ && Boolean(String(row?.description || "").trim())
+ && Boolean(String(row?.scene || "").trim()));
+ return Promise.resolve({
+ code: 200,
+ data: {
+ valid,
+ message: valid ? "核心痛点分类填写完整。" : "请完成 3 个核心痛点、弱需求和伪需求的描述与场景。",
+ },
+ });
+ }
+ return request({
+ url: "/api/student-training-answers/product-value-positioning/step-1/validate",
+ method: "post",
+ data: { rows },
+ headers: { repeatSubmit: false },
+ });
+}
diff --git a/src/views/product/differentiation.vue b/src/views/product/differentiation.vue
index 1d5e0d7..5d1b690 100644
--- a/src/views/product/differentiation.vue
+++ b/src/views/product/differentiation.vue
@@ -48,20 +48,31 @@
-
-
-
+
+
价值主张的起点不是产品功能,而是用户痛点。不要试图满足所有人,只抓占 80% 的核心用户最痛的 3 个问题。
+
参考实训 11 的需求优先级评估结果,整理出 Top3 核心痛点,同时识别弱需求和伪需求——它们不作为核心价值主张,只作为加分项。
+
+
+
+
+
+ | 序号 |
+ 需求类型 |
+ 需求描述 |
+ 场景描述 |
+
+
+
+
+ | {{ index + 1 }} |
+ {{ row.type }} |
+ |
+ |
+
+
+
+
{{ corePainValidationMessage }}
@@ -202,14 +213,18 @@ 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 { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
+import { checkProductValuePositioningStepOne, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const TASK_KEY = "product-value-positioning";
const { proxy } = getCurrentInstance();
const saving = ref(false);
+const checkingCorePain = ref(false);
+const corePainValidationMessage = ref("");
const taskConfig = ref(null);
+const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
+
const defaultRequirement =
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。";
const defaultBackground =
@@ -224,6 +239,7 @@ const defaultGoals = [
function createForm() {
return {
activeStep: 1,
+ corePainRows: corePainTypes.map((type, index) => ({ id: index + 1, type, description: "", scene: "" })),
research: { marketAnalysis: "", userResearch: "", persona: "" },
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
@@ -277,20 +293,22 @@ function normalizeActiveStep(value) {
return Math.min(Math.max(step, 1), configuredSteps.value.length);
}
-function setCurrentStep(step) {
- form.value.activeStep = normalizeActiveStep(step);
+async function setCurrentStep(step) {
+ const targetStep = normalizeActiveStep(step);
+ if (targetStep > form.value.activeStep && form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
+ form.value.activeStep = targetStep;
}
function isSectionVisible(section) {
return !hasConfiguredSteps.value || form.value.activeStep === section;
}
-function goPreviousStep() {
- setCurrentStep(form.value.activeStep - 1);
+async function goPreviousStep() {
+ await setCurrentStep(form.value.activeStep - 1);
}
-function goNextStep() {
- setCurrentStep(form.value.activeStep + 1);
+async function goNextStep() {
+ await setCurrentStep(form.value.activeStep + 1);
}
function applySavedAnswer(value) {
@@ -309,6 +327,7 @@ function normalizeForm(value = {}) {
...base,
...value,
activeStep: normalizeActiveStep(value.activeStep),
+ corePainRows: normalizeCorePainRows(value.corePainRows),
research: { ...base.research, ...(value.research || {}) },
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
@@ -316,6 +335,32 @@ function normalizeForm(value = {}) {
};
}
+function normalizeCorePainRows(rows = []) {
+ const savedRows = Array.isArray(rows) ? rows : [];
+ return corePainTypes.map((type, index) => ({
+ id: index + 1,
+ type,
+ description: String(savedRows[index]?.description || ""),
+ scene: String(savedRows[index]?.scene || ""),
+ }));
+}
+
+async function ensureCorePainValid() {
+ if (checkingCorePain.value) return false;
+ checkingCorePain.value = true;
+ try {
+ const response = await checkProductValuePositioningStepOne({ rows: form.value.corePainRows });
+ const result = response?.data || {};
+ corePainValidationMessage.value = result.message || (result.valid ? "核心痛点分类填写完整。" : "请完成 3 个核心痛点、弱需求和伪需求的描述与场景。");
+ return result.valid === true;
+ } catch (error) {
+ corePainValidationMessage.value = error?.message || "核心痛点校验失败,请稍后重试。";
+ return false;
+ } finally {
+ checkingCorePain.value = false;
+ }
+}
+
function buildOutcome() {
return {
taskKey: TASK_KEY,
@@ -323,6 +368,7 @@ function buildOutcome() {
taskName: taskTitle.value,
currentStep: form.value.activeStep,
configuredSteps: configuredSteps.value,
+ corePainRows: form.value.corePainRows,
research: form.value.research,
positionCanvas: form.value.positionCanvas,
valueCanvas: form.value.valueCanvas,
@@ -341,10 +387,12 @@ function buildAnswerPayload(status) {
async function persist(status = "IN_PROGRESS") {
if (saving.value) return;
+ if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureCorePainValid())) return false;
saving.value = true;
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
+ return true;
} finally {
saving.value = false;
}
@@ -723,6 +771,97 @@ function exportOutcome() {
.value-grid .field-block { margin: 0; }
+.core-pain-intro {
+ margin-bottom: 18px;
+ padding: 16px 18px;
+ border-left: 3px solid #55ddff;
+ border-radius: 0 14px 14px 0;
+ background: linear-gradient(90deg, rgba(7, 92, 132, 0.28), rgba(5, 34, 51, 0.12));
+
+ p {
+ margin: 0;
+ color: #cae7f2;
+ font-size: 14px;
+ line-height: 1.8;
+ }
+
+ p + p { margin-top: 6px; }
+}
+
+.core-pain-table-wrap { overflow-x: auto; }
+
+.core-pain-table {
+ width: 100%;
+ min-width: 820px;
+ border-collapse: collapse;
+ table-layout: fixed;
+
+ th,
+ td {
+ border: 1px solid rgba(77, 167, 220, 0.28);
+ text-align: center;
+ vertical-align: middle;
+ }
+
+ th {
+ padding: 11px 10px;
+ color: #f5fcff;
+ background: linear-gradient(100deg, #0787bd, #075276);
+ font-size: 14px;
+ font-weight: 800;
+ }
+
+ td {
+ height: 84px;
+ padding: 9px;
+ color: #d7f1ff;
+ background: rgba(1, 14, 26, 0.48);
+ }
+
+ th:nth-child(1),
+ td:nth-child(1) { width: 7%; }
+ th:nth-child(2),
+ td:nth-child(2) { width: 15%; }
+ th:nth-child(3),
+ td:nth-child(3) { width: 32%; }
+
+ .pain-type-cell {
+ color: #b9e7f6;
+ font-weight: 800;
+ background: rgba(7, 62, 88, 0.46);
+ }
+
+ textarea {
+ display: block;
+ width: 100%;
+ height: 76px;
+ min-height: 52px;
+ padding: 10px;
+ border: 1px solid rgba(45, 95, 126, 0.95);
+ border-radius: 10px;
+ outline: none;
+ color: #eef5ff;
+ background: rgba(4, 24, 38, 0.72);
+ box-sizing: border-box;
+ font: inherit;
+ font-size: 13px;
+ line-height: 1.55;
+ resize: vertical;
+
+ &:focus {
+ border-color: rgba(92, 224, 255, 0.7);
+ box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13);
+ }
+ }
+}
+
+.core-pain-validation-message {
+ margin: 12px 0 0;
+ color: #ffab9d;
+ font-size: 13px;
+ font-weight: 800;
+}
+
.step-navigation {
display: flex;
justify-content: center;