feat: add product factors step two analysis

dev-QQq
chenyuan 3 weeks ago
parent fade9af35b
commit 9394595e03

@ -237,3 +237,17 @@ export function checkProductDevelopmentFactorsStepOne(payload) {
headers: { repeatSubmit: false }, headers: { repeatSubmit: false },
}); });
} }
const productDevelopmentFactorsStepTwoDimensions = ["技术应用", "合规与认证"];
function validateProductDevelopmentFactorsStepTwoDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (factors.length !== 2 || new Set(dimensions).size !== 2 || productDevelopmentFactorsStepTwoDimensions.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 checkProductDevelopmentFactorsStepTwo(payload) {
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 } });
}

@ -63,7 +63,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="row in factorRows" :key="row.dimension"> <tr v-for="row in stepOneRows" :key="row.dimension">
<td class="dimension-cell">{{ row.dimension }}</td> <td class="dimension-cell">{{ row.dimension }}</td>
<td class="criteria-cell">{{ row.criteria }}</td> <td class="criteria-cell">{{ row.criteria }}</td>
<td> <td>
@ -96,9 +96,17 @@
</div> </div>
</template> </template>
<template v-else-if="currentStep === 1">
<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 stepTwoRows" :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="saveAndGoToStepThree"> 3 </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> 1 步分析已保存可从上方步骤栏返回查看</span> <span>已保存的分析可从上方步骤栏返回查看</span>
</div> </div>
</section> </section>
</section> </section>
@ -117,7 +125,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, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, 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";
@ -170,14 +178,19 @@ const STEP_ONE_FACTOR_ROWS = [
"是否能接入主流平台是否支持iOS/Android、HomeKit、米家、Alexa等生态是否支持第三方开发者扩展提供API或SDK鼓励生态共建", "是否能接入主流平台是否支持iOS/Android、HomeKit、米家、Alexa等生态是否支持第三方开发者扩展提供API或SDK鼓励生态共建",
}, },
]; ];
const STEP_TWO_FACTOR_ROWS = [
{ dimension: "技术应用", criteria: "核心技术是否已成熟、可量产?避免使用实验室级原型(如未验证的 AI 模型)" },
{ dimension: "合规与认证", criteria: "是否通过目标市场强制认证目标市场要求的产品资质、检测报告等中国CCC美国FCC欧盟CE出口需符合 RoHS、REACH。是否规避专利与知识产权风险" },
];
const createFactorRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({ const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
...row, ...row,
successAnalysis: "", successAnalysis: "",
failedAnalysis: "", failedAnalysis: "",
})); }));
const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const factorRows = ref(createFactorRows()); const stepOneRows = ref(createStepOneRows());
const stepTwoRows = ref(createStepTwoRows());
const progressItems = [ const progressItems = [
{ text: "进行中:成功/失败产品对比", status: "doing" }, { text: "进行中:成功/失败产品对比", status: "doing" },
@ -193,7 +206,7 @@ onMounted(async () => {
}); });
watch( watch(
() => JSON.stringify(buildOutcome()), () => JSON.stringify(buildDraft()),
() => persistDraft() () => persistDraft()
); );
@ -213,9 +226,10 @@ async function loadSavedAnswer() {
const res = await getStudentTrainingAnswer(TASK_KEY); const res = await getStudentTrainingAnswer(TASK_KEY);
const answer = res?.data; const answer = res?.data;
if (answer?.step1Answer) { if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer); stepOneRows.value = restoreRows(answer.step1Answer, createStepOneRows);
clearDraft(); clearDraft();
} }
if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows);
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.
@ -250,10 +264,10 @@ function parseSavedAnswer(value) {
} }
} }
function applySavedAnswer(value) { function restoreRows(value, createRows) {
const parsed = parseSavedAnswer(value); const parsed = parseSavedAnswer(value);
if (!parsed || typeof parsed !== "object") { if (!parsed || typeof parsed !== "object") {
return; return createRows();
} }
if (Array.isArray(parsed.factors)) { if (Array.isArray(parsed.factors)) {
const savedByDimension = parsed.factors.reduce((map, row) => { const savedByDimension = parsed.factors.reduce((map, row) => {
@ -262,12 +276,13 @@ function applySavedAnswer(value) {
} }
return map; return map;
}, {}); }, {});
factorRows.value = createFactorRows().map((row, index) => ({ return createRows().map((row, index) => ({
...row, ...row,
successAnalysis: savedByDimension[row.dimension]?.successAnalysis || parsed.factors[index]?.successAnalysis || "", successAnalysis: savedByDimension[row.dimension]?.successAnalysis || parsed.factors[index]?.successAnalysis || "",
failedAnalysis: savedByDimension[row.dimension]?.failedAnalysis || parsed.factors[index]?.failedAnalysis || "", failedAnalysis: savedByDimension[row.dimension]?.failedAnalysis || parsed.factors[index]?.failedAnalysis || "",
})); }));
} }
return createRows();
} }
function restoreCurrentStep(savedStep) { function restoreCurrentStep(savedStep) {
@ -281,7 +296,7 @@ function restoreCurrentStep(savedStep) {
function persistDraft() { function persistDraft() {
if (!draftReady.value) return; if (!draftReady.value) return;
try { try {
localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildOutcome())); localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildDraft()));
} catch (error) { } catch (error) {
// Local draft is only a convenience; backend save remains the source of truth. // Local draft is only a convenience; backend save remains the source of truth.
} }
@ -291,7 +306,10 @@ function loadDraft() {
try { try {
const raw = localStorage.getItem(getLocalDraftKey()); const raw = localStorage.getItem(getLocalDraftKey());
if (raw) { if (raw) {
applySavedAnswer(raw); const draft = parseSavedAnswer(raw);
if (draft?.stepOne) stepOneRows.value = restoreRows(draft.stepOne, createStepOneRows);
else stepOneRows.value = restoreRows(raw, createStepOneRows);
if (draft?.stepTwo) stepTwoRows.value = restoreRows(draft.stepTwo, createStepTwoRows);
} }
} catch (error) { } catch (error) {
clearDraft(); clearDraft();
@ -306,10 +324,10 @@ function clearDraft() {
} }
} }
function buildOutcome() { function buildStepOneOutcome() {
return { return {
title: taskTitle.value, title: taskTitle.value,
factors: factorRows.value.map((row) => ({ factors: stepOneRows.value.map((row) => ({
dimension: row.dimension, dimension: row.dimension,
criteria: row.criteria, criteria: row.criteria,
successAnalysis: row.successAnalysis, successAnalysis: row.successAnalysis,
@ -317,10 +335,12 @@ function buildOutcome() {
})), })),
}; };
} }
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 buildStepOneValidationPayload() { function buildStepOneValidationPayload() {
return { return {
factors: factorRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ factors: stepOneRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({
dimension, dimension,
successAnalysis, successAnalysis,
failedAnalysis, failedAnalysis,
@ -328,21 +348,23 @@ function buildStepOneValidationPayload() {
}; };
} }
function buildAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { function buildStepOneAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) {
return { return {
step1Answer: JSON.stringify(buildOutcome()), step1Answer: JSON.stringify(buildStepOneOutcome()),
currentStep: persistedStep, currentStep: persistedStep,
submitted: false, submitted: false,
saveAction, saveAction,
}; };
} }
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 }; }
async function saveAndGoToStepTwo() { async function saveAndGoToStepTwo() {
if (saving.value) return; if (saving.value) return;
saving.value = true; saving.value = true;
try { try {
await checkProductDevelopmentFactorsStepOne(buildStepOneValidationPayload()); await checkProductDevelopmentFactorsStepOne(buildStepOneValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload("SAVE", 2)); await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("SAVE", 2));
clearDraft(); clearDraft();
if (trainingSteps.value.length < 2) { if (trainingSteps.value.length < 2) {
proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 2 步"); proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 2 步");
@ -357,13 +379,27 @@ async function saveAndGoToStepTwo() {
} }
} }
async function saveAndGoToStepThree() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepTwo(buildStepTwoValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepTwoAnswerPayload("SAVE", 3));
clearDraft();
if (trainingSteps.value.length < 3) { proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 3 步"); return; }
currentStep.value = 2;
proxy?.$modal?.msgSuccess("已保存,已进入第 3 步");
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成技术应用和合规与认证分析"); } finally { saving.value = false; }
}
async function resetTraining() { async function resetTraining() {
factorRows.value = createFactorRows(); stepOneRows.value = createStepOneRows();
stepTwoRows.value = createStepTwoRows();
currentStep.value = 0; currentStep.value = 0;
clearDraft(); clearDraft();
try { try {
saving.value = true; saving.value = true;
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload("RESET", 1)); await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("RESET", 1));
} catch (error) { } catch (error) {
// Clearing the page should still work locally even when backend reset fails. // Clearing the page should still work locally even when backend reset fails.
} finally { } finally {
@ -373,7 +409,7 @@ async function resetTraining() {
} }
function exportOutcome() { function exportOutcome() {
const outcome = buildOutcome(); const outcome = buildDraft();
const blob = new Blob([JSON.stringify(outcome, null, 2)], { type: "application/json;charset=utf-8" }); const blob = new Blob([JSON.stringify(outcome, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");

@ -13,11 +13,21 @@ assert.match(page, /生态与兼容/, "step one must include ecosystem compatibi
assert.match(page, /v-if="currentStep === 0"/, "the factor table must only render on step one"); assert.match(page, /v-if="currentStep === 0"/, "the factor table must only render on step one");
assert.match(page, /saveAndGoToStepTwo/, "step one must expose a save-and-advance action"); assert.match(page, /saveAndGoToStepTwo/, "step one must expose a save-and-advance action");
assert.match(page, /await checkProductDevelopmentFactorsStepOne\(buildStepOneValidationPayload\(\)\)/, "advance must validate before saving"); assert.match(page, /await checkProductDevelopmentFactorsStepOne\(buildStepOneValidationPayload\(\)\)/, "advance must validate before saving");
assert.match(page, /buildAnswerPayload\("SAVE", 2\)/, "advance must save step two progress without submitting"); assert.match(page, /buildStepOneAnswerPayload\("SAVE", 2\)/, "advance must save step two progress without submitting");
assert.doesNotMatch(page, /v-model="form\.successProduct"/, "the prototype must not display a success-product name input"); assert.doesNotMatch(page, /v-model="form\.successProduct"/, "the prototype must not display a success-product name input");
assert.doesNotMatch(page, /v-model="form\.failedProduct"/, "the prototype must not display a failed-product name input"); assert.doesNotMatch(page, /v-model="form\.failedProduct"/, "the prototype must not display a failed-product name input");
assert.doesNotMatch(page, /buildAnswerPayload\("SUBMIT"\)/, "step one must not submit the entire task"); assert.doesNotMatch(page, /buildStepOneAnswerPayload\("SUBMIT"\)/, "step one must not submit the entire task");
assert.match(api, /export function checkProductDevelopmentFactorsStepOne\(/, "the student API must expose the step-one validator"); assert.match(api, /export function checkProductDevelopmentFactorsStepOne\(/, "the student API must expose the step-one validator");
assert.match(api, /product-development-factors\/step-1\/validate/, "the student API must call the step-one validation route"); assert.match(api, /product-development-factors\/step-1\/validate/, "the student API must call the step-one validation route");
assert.match(page, /const STEP_TWO_FACTOR_ROWS = \[/, "step two must define its fixed rows");
assert.match(page, /技术应用/, "step two must include technology application");
assert.match(page, /合规与认证/, "step two must include compliance and certification");
assert.match(page, /v-else-if="currentStep === 1"/, "the second table must render only on step two");
assert.match(page, /await checkProductDevelopmentFactorsStepTwo\(buildStepTwoValidationPayload\(\)\)/, "step two must validate before saving");
assert.match(page, /step2Answer: JSON\.stringify\(buildStepTwoOutcome\(\)\)/, "step two must save its own answer field");
assert.match(page, /buildStepTwoAnswerPayload\("SAVE", 3\)/, "step two must save progress for step three");
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, /product-development-factors\/step-2\/validate/, "the student API must call the step-two validation route");
console.log("product development factors step-one contract passed"); console.log("product development factors step-one contract passed");

Loading…
Cancel
Save