feat: add product factors step three analysis

dev-QQq
chenyuan 3 weeks ago
parent 493e7ded86
commit 331f35797a

@ -251,3 +251,27 @@ export function checkProductDevelopmentFactorsStepTwo(payload) {
if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepTwoDemo(payload) }); } catch (error) { return Promise.reject(error); } } if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepTwoDemo(payload) }); } catch (error) { return Promise.reject(error); } }
return request({ url: "/api/student-training-answers/product-development-factors/step-2/validate", method: "post", data: payload, headers: { repeatSubmit: false } }); return request({ url: "/api/student-training-answers/product-development-factors/step-2/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
} }
const productDevelopmentFactorsStepThreeDimensions = ["供应链与成本控制", "商业模式", "市场反馈"];
function validateProductDevelopmentFactorsStepThreeDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (factors.length !== productDevelopmentFactorsStepThreeDimensions.length
|| new Set(dimensions).size !== productDevelopmentFactorsStepThreeDimensions.length
|| productDevelopmentFactorsStepThreeDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("请完成供应链与成本控制、商业模式和市场反馈分析");
}
const incomplete = factors.find((factor) => !String(factor?.successAnalysis || "").trim()
|| !String(factor?.failedAnalysis || "").trim());
if (incomplete) throw new Error(`请完成${incomplete.dimension || "该项"}的成功产品和失败产品分析`);
return { valid: true, message: "校验通过" };
}
export function checkProductDevelopmentFactorsStepThree(payload) {
if (isStudentDemo()) {
try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepThreeDemo(payload) }); }
catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/product-development-factors/step-3/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}

@ -104,6 +104,14 @@
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepThree"> 3 </button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon></button></div> <div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepThree"> 3 </button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon></button></div>
</template> </template>
<template v-else-if="currentStep === 2">
<p class="factor-intro">商业化及市场反馈角度分析填写下表</p>
<div class="training-table-wrap"><table class="factor-table"><thead><tr><th class="dimension-col">维度</th><th class="criteria-col">评估项</th><th>成功产品</th><th>失败产品</th></tr></thead><tbody>
<tr v-for="row in stepThreeRows" :key="row.dimension"><td class="dimension-cell">{{ row.dimension }}</td><td class="criteria-cell">{{ row.criteria }}</td><td><textarea v-model="row.successAnalysis" class="table-textarea" placeholder="填写成功产品在该维度的分析" /></td><td><textarea v-model="row.failedAnalysis" class="table-textarea" placeholder="填写失败产品在该维度的分析" /></td></tr>
</tbody></table></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepFour"> 4 </button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon></button></div>
</template>
<div v-else class="step-placeholder"> <div v-else class="step-placeholder">
<p> {{ currentStep + 1 }} 步内容待配置</p> <p> {{ currentStep + 1 }} 步内容待配置</p>
<span>已保存的分析可从上方步骤栏返回查看</span> <span>已保存的分析可从上方步骤栏返回查看</span>
@ -125,7 +133,7 @@
<script setup> <script setup>
import { Download, Operation, RefreshLeft } from "@element-plus/icons-vue"; import { Download, Operation, RefreshLeft } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, checkProductDevelopmentFactorsStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user"; import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
@ -182,6 +190,11 @@ const STEP_TWO_FACTOR_ROWS = [
{ dimension: "技术应用", criteria: "核心技术是否已成熟、可量产?避免使用实验室级原型(如未验证的 AI 模型)" }, { dimension: "技术应用", criteria: "核心技术是否已成熟、可量产?避免使用实验室级原型(如未验证的 AI 模型)" },
{ dimension: "合规与认证", criteria: "是否通过目标市场强制认证目标市场要求的产品资质、检测报告等中国CCC美国FCC欧盟CE出口需符合 RoHS、REACH。是否规避专利与知识产权风险" }, { dimension: "合规与认证", criteria: "是否通过目标市场强制认证目标市场要求的产品资质、检测报告等中国CCC美国FCC欧盟CE出口需符合 RoHS、REACH。是否规避专利与知识产权风险" },
]; ];
const STEP_THREE_FACTOR_ROWS = [
{ dimension: "供应链与成本控制", criteria: "产品成本是否可控是否支持规模化是否具备备选供应链关键元器件如芯片、传感器有≥2家可替换供应商" },
{ dimension: "商业模式", criteria: "盈利模式是否清晰硬件销售毛利率≥50%或有订阅服务如云存储、AI 功能包)。是否构建“数据 - 反馈 - 迭代”闭环?产品使用数据可回传,驱动功能优化与新版本开发" },
{ dimension: "市场反馈", criteria: "是否处于需求上升期?功能、设计、服务、品牌是否有差异化壁垒?用户是否高留存、高互动、低退货?" },
];
const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({ const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
...row, ...row,
@ -189,8 +202,10 @@ const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
failedAnalysis: "", failedAnalysis: "",
})); }));
const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" })); const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const createStepThreeRows = () => STEP_THREE_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const stepOneRows = ref(createStepOneRows()); const stepOneRows = ref(createStepOneRows());
const stepTwoRows = ref(createStepTwoRows()); const stepTwoRows = ref(createStepTwoRows());
const stepThreeRows = ref(createStepThreeRows());
const progressItems = [ const progressItems = [
{ text: "进行中:成功/失败产品对比", status: "doing" }, { text: "进行中:成功/失败产品对比", status: "doing" },
@ -230,6 +245,7 @@ async function loadSavedAnswer() {
clearDraft(); clearDraft();
} }
if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows); if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows);
if (answer?.step3Answer) stepThreeRows.value = restoreRows(answer.step3Answer, createStepThreeRows);
restoreCurrentStep(answer?.currentStep); restoreCurrentStep(answer?.currentStep);
} catch (error) { } catch (error) {
// Keep local draft when the backend answer cannot be loaded. // Keep local draft when the backend answer cannot be loaded.
@ -310,6 +326,7 @@ function loadDraft() {
if (draft?.stepOne) stepOneRows.value = restoreRows(draft.stepOne, createStepOneRows); if (draft?.stepOne) stepOneRows.value = restoreRows(draft.stepOne, createStepOneRows);
else stepOneRows.value = restoreRows(raw, createStepOneRows); else stepOneRows.value = restoreRows(raw, createStepOneRows);
if (draft?.stepTwo) stepTwoRows.value = restoreRows(draft.stepTwo, createStepTwoRows); if (draft?.stepTwo) stepTwoRows.value = restoreRows(draft.stepTwo, createStepTwoRows);
if (draft?.stepThree) stepThreeRows.value = restoreRows(draft.stepThree, createStepThreeRows);
} }
} catch (error) { } catch (error) {
clearDraft(); clearDraft();
@ -336,7 +353,8 @@ function buildStepOneOutcome() {
}; };
} }
function buildStepTwoOutcome() { return { title: taskTitle.value, factors: stepTwoRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; } function buildStepTwoOutcome() { return { title: taskTitle.value, factors: stepTwoRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildDraft() { return { stepOne: buildStepOneOutcome(), stepTwo: buildStepTwoOutcome(), currentStep: currentStep.value + 1 }; } function buildStepThreeOutcome() { return { title: taskTitle.value, factors: stepThreeRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildDraft() { return { stepOne: buildStepOneOutcome(), stepTwo: buildStepTwoOutcome(), stepThree: buildStepThreeOutcome(), currentStep: currentStep.value + 1 }; }
function buildStepOneValidationPayload() { function buildStepOneValidationPayload() {
return { return {
@ -358,6 +376,8 @@ function buildStepOneAnswerPayload(saveAction = "SAVE", persistedStep = currentS
} }
function buildStepTwoValidationPayload() { return { factors: stepTwoRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; } function buildStepTwoValidationPayload() { return { factors: stepTwoRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepTwoAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: persistedStep, submitted: false, saveAction }; } function buildStepTwoAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
function buildStepThreeValidationPayload() { return { factors: stepThreeRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepThreeAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
async function saveAndGoToStepTwo() { async function saveAndGoToStepTwo() {
if (saving.value) return; if (saving.value) return;
@ -392,9 +412,23 @@ async function saveAndGoToStepThree() {
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成技术应用和合规与认证分析"); } finally { saving.value = false; } } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成技术应用和合规与认证分析"); } finally { saving.value = false; }
} }
async function saveAndGoToStepFour() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepThree(buildStepThreeValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepThreeAnswerPayload("SAVE", 4));
clearDraft();
if (trainingSteps.value.length < 4) { proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 4 步"); return; }
currentStep.value = 3;
proxy?.$modal?.msgSuccess("已保存,已进入第 4 步");
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成供应链与成本控制、商业模式和市场反馈分析"); } finally { saving.value = false; }
}
async function resetTraining() { async function resetTraining() {
stepOneRows.value = createStepOneRows(); stepOneRows.value = createStepOneRows();
stepTwoRows.value = createStepTwoRows(); stepTwoRows.value = createStepTwoRows();
stepThreeRows.value = createStepThreeRows();
currentStep.value = 0; currentStep.value = 0;
clearDraft(); clearDraft();
try { try {

@ -29,5 +29,16 @@ assert.match(page, /buildStepTwoAnswerPayload\("SAVE", 3\)/, "step two must save
assert.doesNotMatch(page, /buildStepTwoAnswerPayload\("SUBMIT"/, "step two must not submit the task"); assert.doesNotMatch(page, /buildStepTwoAnswerPayload\("SUBMIT"/, "step two must not submit the task");
assert.match(api, /export function checkProductDevelopmentFactorsStepTwo\(/, "the student API must expose the step-two validator"); assert.match(api, /export function checkProductDevelopmentFactorsStepTwo\(/, "the student API must expose the step-two validator");
assert.match(api, /product-development-factors\/step-2\/validate/, "the student API must call the step-two validation route"); assert.match(api, /product-development-factors\/step-2\/validate/, "the student API must call the step-two validation route");
assert.match(page, /const STEP_THREE_FACTOR_ROWS = \[/, "step three must define its fixed rows");
assert.match(page, /供应链与成本控制/, "step three must include supply-chain and cost control");
assert.match(page, /商业模式/, "step three must include business model");
assert.match(page, /市场反馈/, "step three must include market feedback");
assert.match(page, /v-else-if="currentStep === 2"/, "the third table must render only on step three");
assert.match(page, /await checkProductDevelopmentFactorsStepThree\(buildStepThreeValidationPayload\(\)\)/, "step three must validate before saving");
assert.match(page, /step3Answer: JSON\.stringify\(buildStepThreeOutcome\(\)\)/, "step three must save its own answer field");
assert.match(page, /buildStepThreeAnswerPayload\("SAVE", 4\)/, "step three must save progress for step four");
assert.doesNotMatch(page, /buildStepThreeAnswerPayload\("SUBMIT"/, "step three must not submit the task");
assert.match(api, /export function checkProductDevelopmentFactorsStepThree\(/, "the student API must expose the step-three validator");
assert.match(api, /product-development-factors\/step-3\/validate/, "the student API must call the step-three validation route");
console.log("product development factors step-one contract passed"); console.log("product development factors step-one contract passed");

Loading…
Cancel
Save