diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js
index 311f1ed..d0fce08 100644
--- a/src/api/studentTrainingAnswer.js
+++ b/src/api/studentTrainingAnswer.js
@@ -134,3 +134,39 @@ export function checkNewProductSurveyStepTwo(form) {
headers: { repeatSubmit: false },
});
}
+
+function validateNewProductSurveyStepThreeDemo(payload) {
+ const expectedDimensions = ["核心层", "有形层", "延伸层"];
+ const rows = Array.isArray(payload?.rows) ? payload.rows : [];
+ if (rows.length !== expectedDimensions.length) {
+ throw new Error("请完成核心层、有形层、延伸层分析");
+ }
+ const dimensions = rows.map((row) => String(row?.dimension || "").trim());
+ if (new Set(dimensions).size !== expectedDimensions.length || expectedDimensions.some((dimension) => !dimensions.includes(dimension))) {
+ throw new Error("分析维度不完整或重复");
+ }
+ const invalidRow = rows.find((row) => {
+ const points = Array.isArray(row?.points) ? row.points : [];
+ return points.map((point) => String(point || "").trim()).filter(Boolean).length < 2;
+ });
+ if (invalidRow) {
+ throw new Error(`${invalidRow.dimension || "该层"}至少填写 2 条分析要点`);
+ }
+ return { valid: true, message: "校验通过" };
+}
+
+export function checkNewProductSurveyStepThree(payload) {
+ if (isStudentDemo()) {
+ try {
+ return Promise.resolve({ code: 200, data: validateNewProductSurveyStepThreeDemo(payload) });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ }
+ return request({
+ url: "/api/student-training-answers/new-product-survey/step-3/validate",
+ method: "post",
+ data: payload,
+ headers: { repeatSubmit: false },
+ });
+}
diff --git a/src/views/foundation/new-product-survey.vue b/src/views/foundation/new-product-survey.vue
index aa52aa4..774d2b0 100644
--- a/src/views/foundation/new-product-survey.vue
+++ b/src/views/foundation/new-product-survey.vue
@@ -154,10 +154,10 @@
- 根据查找的产品,从核心层、有形层、延伸层开展三层次分析,对比不同产品如何创造用户价值。
+ 基于上一步采集的真实数据,运用“三层次理论”进行深度分析,参考示例填写下表;每层至少填写 2 条要点并按换行分条。
@@ -165,21 +165,22 @@
| 分析维度 |
- 内容要求 |
实例:AI学习机 |
- 产品1 |
- 产品2 |
- 产品3 |
+ {{ step2Form.productName || '所选产品' }}分析 |
- | {{ row.dimension }} |
- {{ row.requirement }} |
- {{ row.example }} |
- |
- |
- |
+ {{ row.dimension }}{{ row.definition }} |
+
+
+ - {{ point }}
+
+ |
+
+
+ 已填写 {{ countStepThreePoints(row.analysis) }} / 至少 2 条
+ |
@@ -269,7 +270,7 @@ import {
TrendCharts,
} from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
-import { checkNewProductSurveyStepOne, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
+import { checkNewProductSurveyStepOne, checkNewProductSurveyStepThree, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import { extractTrainingStepNames } from "@/views/training/taskKeyMap";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
@@ -357,33 +358,14 @@ const STEP_ONE_SELLING_POINTS = [
const createStep2Form = () => ({ platform: "", scene: "", customScene: "", keyword: "", productName: "", images: [] });
-const createStep3Rows = () => [
- {
- dimension: "核心层",
- requirement: "说明产品解决的核心痛点、用户需求和核心利益。",
- example: "解决学生学习效率低、家长辅导时间不足等痛点,提供个性化学习支持。",
- product1: "",
- product2: "",
- product3: "",
- },
- {
- dimension: "有形层",
- requirement: "记录功能、设计、品牌、参数、包装、价格等可见要素。",
- example: "护眼屏、AI语音互动、拍照搜题、错题本、课程资源、学习报告。",
- product1: "",
- product2: "",
- product3: "",
- },
- {
- dimension: "延伸层",
- requirement: "分析售后、会员、内容更新、物流、生态兼容和数据安全等附加服务。",
- example: "课程持续更新、家长端管理、云端错题同步、售后保修、会员内容权益。",
- product1: "",
- product2: "",
- product3: "",
- },
+const STEP_THREE_REFERENCE_ROWS = [
+ { dimension: "核心层", definition: "AI技术解决的核心需求/功能", referencePoints: ["大模型作业批改", "自适应学习路径规划"] },
+ { dimension: "有形层", definition: "外观/材质/参数/价格策略", referencePoints: ["护眼屏+铝合金机身", "搭载NPU芯片", "全平台统一定价2999元,某网红直播间专属价2699元"] },
+ { dimension: "延伸层", definition: "软件升级/专属服务/社群运营", referencePoints: ["终身免费题库更新", "VIP学科规划师1对1", "家长互助社群及资料共享微信群"] },
];
+const createStep3Rows = () => STEP_THREE_REFERENCE_ROWS.map((row) => ({ ...row, analysis: "" }));
+
const createStep4Form = () => ({
coreValue: "",
tangibleValue: "",
@@ -406,7 +388,7 @@ const currentStudyTip = computed(() => {
const tips = {
1: "当前步骤建议:先从案例或电商详情页中拆出核心层、有形层、延伸层,不要把功能和用户利益混在一起。",
2: "当前步骤建议:选择一个主流电商平台与真实 AI 产品,用“核心词 + 属性词 + 场景词”搜索并保留主图、详情页截图。",
- 3: "当前步骤建议:同一维度横向比较 3 个产品,重点找出 AI 技术在哪一层创造了差异化。",
+ 3: "当前步骤建议:将上一步选定产品的卖点按核心层、有形层、延伸层拆分,每层至少记录两条可核对的分析要点。",
4: "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。",
};
return tips[currentStep.value];
@@ -623,7 +605,19 @@ function applyStep3Answer(value) {
const parsed = safeParse(value);
const rows = Array.isArray(parsed) ? parsed : parsed?.step3Rows || parsed?.rows || parsed?.layerRows;
if (!Array.isArray(rows) || !rows.length) return false;
- step3Rows.value = normalizeRows(rows, createStep3Rows(), "dimension");
+ const savedByDimension = rows.reduce((result, row) => {
+ if (row?.dimension) result[row.dimension] = row;
+ return result;
+ }, {});
+ step3Rows.value = createStep3Rows().map((row, index) => {
+ const saved = savedByDimension[row.dimension] || rows[index] || {};
+ const points = Array.isArray(saved.points) ? saved.points : [];
+ const legacyValue = [saved.analysis, saved.product1, saved.product2, saved.product3].find((item) => String(item || "").trim());
+ return {
+ ...row,
+ analysis: points.length ? points.map((item) => String(item || "").trim()).filter(Boolean).join("\n") : String(legacyValue || ""),
+ };
+ });
return true;
}
@@ -667,6 +661,26 @@ function normalizeRows(rows, fallback, matchKey = "") {
}));
}
+function splitStepThreePoints(value) {
+ return String(value || "")
+ .split(/\r?\n/)
+ .map((item) => item.trim())
+ .filter(Boolean);
+}
+
+function countStepThreePoints(value) {
+ return splitStepThreePoints(value).length;
+}
+
+function buildStepThreeValidationPayload() {
+ return {
+ rows: step3Rows.value.map((row) => ({
+ dimension: row.dimension,
+ points: splitStepThreePoints(row.analysis),
+ })),
+ };
+}
+
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
@@ -674,7 +688,7 @@ function buildAnswerPayload(status) {
currentStep: currentStep.value,
step1Answer: JSON.stringify({ classifications: classifications.value, checkResult: checkResult.value }),
step2Answer: JSON.stringify(step2Form.value),
- step3Answer: JSON.stringify(step3Rows.value),
+ step3Answer: JSON.stringify(buildStepThreeValidationPayload()),
step4Answer: JSON.stringify(step4Form.value),
};
}
@@ -714,6 +728,20 @@ async function nextStep() {
}
return;
}
+ if (currentStep.value === 3) {
+ checking.value = true;
+ try {
+ await checkNewProductSurveyStepThree(buildStepThreeValidationPayload());
+ await saveAnswer("IN_PROGRESS", true);
+ proxy?.$modal?.msgSuccess("第三步分析已校验并保存");
+ advanceStep();
+ } catch (error) {
+ proxy?.$modal?.msgWarning(error?.message || "请确保每个层次至少填写 2 条分析要点");
+ } finally {
+ checking.value = false;
+ }
+ return;
+ }
if (currentStep.value !== 1) {
advanceStep();
return;
@@ -1513,9 +1541,26 @@ function exportOutcome() {
}
.dimension-cell {
+ min-width: 170px;
color: #eafaff;
- font-weight: 900;
- text-align: center;
+ text-align: left;
+
+ strong,
+ span {
+ display: block;
+ }
+
+ strong {
+ margin-bottom: 7px;
+ font-weight: 900;
+ }
+
+ span {
+ color: #9fc6da;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.55;
+ }
}
.requirement-cell,
@@ -1529,6 +1574,28 @@ function exportOutcome() {
color: #f5d99d;
}
+.reference-points {
+ display: grid;
+ gap: 8px;
+ margin: 0;
+ padding-left: 20px;
+}
+
+.analysis-input-cell {
+ min-width: 280px;
+}
+
+.analysis-count {
+ margin: 8px 2px 0;
+ color: #ffb1b1;
+ font-size: 12px;
+ font-weight: 700;
+
+ &.is-ready {
+ color: #86e7bd;
+ }
+}
+
.example-hint {
display: flex;
gap: 8px;
diff --git a/tests/new-product-survey-step-one.static.test.cjs b/tests/new-product-survey-step-one.static.test.cjs
index e593bc5..b2440cf 100644
--- a/tests/new-product-survey-step-one.static.test.cjs
+++ b/tests/new-product-survey-step-one.static.test.cjs
@@ -24,6 +24,11 @@ assert.match(page, /@click="previewStepTwoImage\(image\.url\)"/, "uploaded produ
assert.match(page, /step2Answer: JSON\.stringify\(step2Form\.value\)/, "step two must persist the survey form instead of table rows");
assert.match(page, /checkNewProductSurveyStepTwo\(step2Form\.value\)/, "step two must request server-side validation before advancing");
assert.match(api, /new-product-survey\/step-2\/validate/, "the student API must call the step-two validation endpoint");
+assert.match(page, /const STEP_THREE_REFERENCE_ROWS = \[/, "step three must define the fixed AI learning-machine reference rows");
+assert.match(page, /step2Form\.productName \|\| '所选产品'/, "step three must use the surveyed product name as its analysis-column heading");
+assert.match(page, /countStepThreePoints\(row\.analysis\)/, "step three must show the number of entered analysis points");
+assert.match(page, /checkNewProductSurveyStepThree\(buildStepThreeValidationPayload\(\)\)/, "step three must validate its rows before advancing");
+assert.match(api, /new-product-survey\/step-3\/validate/, "the student API must call the step-three validation endpoint");
assert.match(materialButton, /下载案例资料/, "the download button must use a fixed student-facing label");
assert.doesNotMatch(materialButton, /\{\{ material\?\.name \|\| "案例文档" \}\}/, "the upload file name must not become button text");