|
|
|
|
@ -63,7 +63,7 @@
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<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="criteria-cell">{{ row.criteria }}</td>
|
|
|
|
|
<td>
|
|
|
|
|
@ -96,9 +96,17 @@
|
|
|
|
|
</div>
|
|
|
|
|
</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">
|
|
|
|
|
<p>第 {{ currentStep + 1 }} 步内容待配置</p>
|
|
|
|
|
<span>第 1 步分析已保存,可从上方步骤栏返回查看。</span>
|
|
|
|
|
<span>已保存的分析可从上方步骤栏返回查看。</span>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
@ -117,7 +125,7 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import { Download, Operation, RefreshLeft } from "@element-plus/icons-vue";
|
|
|
|
|
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 TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
|
@ -170,14 +178,19 @@ const STEP_ONE_FACTOR_ROWS = [
|
|
|
|
|
"是否能接入主流平台?是否支持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,
|
|
|
|
|
successAnalysis: "",
|
|
|
|
|
failedAnalysis: "",
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const factorRows = ref(createFactorRows());
|
|
|
|
|
const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
|
|
|
|
|
const stepOneRows = ref(createStepOneRows());
|
|
|
|
|
const stepTwoRows = ref(createStepTwoRows());
|
|
|
|
|
|
|
|
|
|
const progressItems = [
|
|
|
|
|
{ text: "进行中:成功/失败产品对比", status: "doing" },
|
|
|
|
|
@ -193,7 +206,7 @@ onMounted(async () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => JSON.stringify(buildOutcome()),
|
|
|
|
|
() => JSON.stringify(buildDraft()),
|
|
|
|
|
() => persistDraft()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@ -213,9 +226,10 @@ async function loadSavedAnswer() {
|
|
|
|
|
const res = await getStudentTrainingAnswer(TASK_KEY);
|
|
|
|
|
const answer = res?.data;
|
|
|
|
|
if (answer?.step1Answer) {
|
|
|
|
|
applySavedAnswer(answer.step1Answer);
|
|
|
|
|
stepOneRows.value = restoreRows(answer.step1Answer, createStepOneRows);
|
|
|
|
|
clearDraft();
|
|
|
|
|
}
|
|
|
|
|
if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows);
|
|
|
|
|
restoreCurrentStep(answer?.currentStep);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 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);
|
|
|
|
|
if (!parsed || typeof parsed !== "object") {
|
|
|
|
|
return;
|
|
|
|
|
return createRows();
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(parsed.factors)) {
|
|
|
|
|
const savedByDimension = parsed.factors.reduce((map, row) => {
|
|
|
|
|
@ -262,12 +276,13 @@ function applySavedAnswer(value) {
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, {});
|
|
|
|
|
factorRows.value = createFactorRows().map((row, index) => ({
|
|
|
|
|
return createRows().map((row, index) => ({
|
|
|
|
|
...row,
|
|
|
|
|
successAnalysis: savedByDimension[row.dimension]?.successAnalysis || parsed.factors[index]?.successAnalysis || "",
|
|
|
|
|
failedAnalysis: savedByDimension[row.dimension]?.failedAnalysis || parsed.factors[index]?.failedAnalysis || "",
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
return createRows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function restoreCurrentStep(savedStep) {
|
|
|
|
|
@ -281,7 +296,7 @@ function restoreCurrentStep(savedStep) {
|
|
|
|
|
function persistDraft() {
|
|
|
|
|
if (!draftReady.value) return;
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildOutcome()));
|
|
|
|
|
localStorage.setItem(getLocalDraftKey(), JSON.stringify(buildDraft()));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Local draft is only a convenience; backend save remains the source of truth.
|
|
|
|
|
}
|
|
|
|
|
@ -291,7 +306,10 @@ function loadDraft() {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(getLocalDraftKey());
|
|
|
|
|
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) {
|
|
|
|
|
clearDraft();
|
|
|
|
|
@ -306,10 +324,10 @@ function clearDraft() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
|
function buildStepOneOutcome() {
|
|
|
|
|
return {
|
|
|
|
|
title: taskTitle.value,
|
|
|
|
|
factors: factorRows.value.map((row) => ({
|
|
|
|
|
factors: stepOneRows.value.map((row) => ({
|
|
|
|
|
dimension: row.dimension,
|
|
|
|
|
criteria: row.criteria,
|
|
|
|
|
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() {
|
|
|
|
|
return {
|
|
|
|
|
factors: factorRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({
|
|
|
|
|
factors: stepOneRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({
|
|
|
|
|
dimension,
|
|
|
|
|
successAnalysis,
|
|
|
|
|
failedAnalysis,
|
|
|
|
|
@ -328,21 +348,23 @@ function buildStepOneValidationPayload() {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) {
|
|
|
|
|
function buildStepOneAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) {
|
|
|
|
|
return {
|
|
|
|
|
step1Answer: JSON.stringify(buildOutcome()),
|
|
|
|
|
step1Answer: JSON.stringify(buildStepOneOutcome()),
|
|
|
|
|
currentStep: persistedStep,
|
|
|
|
|
submitted: false,
|
|
|
|
|
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() {
|
|
|
|
|
if (saving.value) return;
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await checkProductDevelopmentFactorsStepOne(buildStepOneValidationPayload());
|
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload("SAVE", 2));
|
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("SAVE", 2));
|
|
|
|
|
clearDraft();
|
|
|
|
|
if (trainingSteps.value.length < 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() {
|
|
|
|
|
factorRows.value = createFactorRows();
|
|
|
|
|
stepOneRows.value = createStepOneRows();
|
|
|
|
|
stepTwoRows.value = createStepTwoRows();
|
|
|
|
|
currentStep.value = 0;
|
|
|
|
|
clearDraft();
|
|
|
|
|
try {
|
|
|
|
|
saving.value = true;
|
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload("RESET", 1));
|
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("RESET", 1));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Clearing the page should still work locally even when backend reset fails.
|
|
|
|
|
} finally {
|
|
|
|
|
@ -373,7 +409,7 @@ async function resetTraining() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exportOutcome() {
|
|
|
|
|
const outcome = buildOutcome();
|
|
|
|
|
const outcome = buildDraft();
|
|
|
|
|
const blob = new Blob([JSON.stringify(outcome, null, 2)], { type: "application/json;charset=utf-8" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
|