Merge branch 'fix/student-step-action-contract'

bugfix/ai-assessment-evidence-rescore
陈沅 2 months ago
commit 182c42e38c

@ -14,7 +14,7 @@
<p class="section-eyebrow">CATEGORY OPERATIONS</p>
<h2>{{ activeStepName }}</h2>
</div>
<TrainingTaskToolbar class="optimization-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="optimization-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTask" :on-export="exportOutcome" />
</div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="品类结构优化步骤">
@ -170,10 +170,6 @@
<button type="button" class="nav-button nav-button--save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon>保存当前进度</button>
<button type="button" class="nav-button nav-button--primary" :disabled="activeStep === configuredSteps.length" @click="goNextStep">下一步<el-icon><ArrowRight /></el-icon></button>
</div>
<div class="training-actions">
<el-button class="training-action" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action" :loading="saving" @click="resetTask">清空填写</el-button>
</div>
</section>
</main>
<TrainingAiSidebar study-tip="先完成品类营收数据排序和占比校验,再开展后续分析。" :progress-items="progressItems" />

@ -4,7 +4,7 @@
<section class="task-header"><div class="task-badge"><el-icon><TrendCharts /></el-icon> 核心实训 · 供应渠道管理</div><h1 class="task-title">{{ taskTitle }}</h1></section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" :show-toolbar="false" />
<section class="task-card">
<div class="workbench-head"><div><p class="section-eyebrow">PURCHASE DEMAND FORECAST</p><h2>{{ activeStepName }}</h2></div><TrainingTaskToolbar class="purchase-forecast-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" /></div>
<div class="workbench-head"><div><p class="section-eyebrow">PURCHASE DEMAND FORECAST</p><h2>{{ activeStepName }}</h2></div><TrainingTaskToolbar class="purchase-forecast-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTask" :on-export="exportOutcome" /></div>
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="采购需求预测步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(index+1)"><span class="step-marker">{{ String(index+1).padStart(2,'0') }}</span><span class="step-copy"><strong class="step-name" :title="step">{{ step }}</strong><span class="step-action">{{ activeStep===index+1?'当前步骤':'点击切换' }} <span aria-hidden="true">→</span></span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section v-show="isSectionVisible(1)" class="step-card">
@ -121,7 +121,6 @@
<div id="forecastChart" ref="forecastChart" style="width: 80%; height: 400px"></div>
</section>
<div v-if="hasConfiguredSteps" class="step-navigation"><button type="button" class="btn-nav" :disabled="activeStep===1" @click="goPreviousStep"><el-icon><ArrowLeft /></el-icon>上一步</button><button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon>保存当前进度</button><button type="button" class="btn-nav btn-primary" :disabled="activeStep===configuredSteps.length" @click="goNextStep">下一步<el-icon><ArrowRight /></el-icon></button></div>
<div class="taskfooter"><el-button @click="submitTask" :loading="saving">提交任务</el-button><el-button @click="resetTask" :loading="saving">清空填写</el-button></div>
<pop-model :showModel="runResultShow" title="设置代码" @closePop="closeCode">
<template v-slot:content>
<div>

@ -41,6 +41,10 @@
class="brief-material"
:material-name="materialName"
:material-url="materialUrl"
:task-key="taskKey"
:on-submit="onSubmit"
:on-reset="onReset"
:on-export="onExport"
/>
</div>
@ -96,6 +100,22 @@ const props = defineProps({
type: Boolean,
default: true,
},
taskKey: {
type: String,
default: "",
},
onSubmit: {
type: Function,
default: null,
},
onReset: {
type: Function,
default: null,
},
onExport: {
type: Function,
default: null,
},
});
const { materialName, materialUrl } = toRefs(props);

@ -25,6 +25,8 @@ const props = defineProps({
materialName: { type: String, default: "" },
materialUrl: { type: String, default: "" },
taskKey: { type: String, default: "" },
onSubmit: { type: Function, default: null },
onReset: { type: Function, default: null },
onExport: { type: Function, default: null },
});
@ -54,8 +56,14 @@ async function loadMaterial() {
}
function triggerExistingAction(action) {
if (action === "export" && props.onExport) {
props.onExport();
const directActions = {
submit: props.onSubmit,
reset: props.onReset,
export: props.onExport,
};
if (directActions[action]) {
directActions[action]();
return;
}

@ -9,7 +9,7 @@
<p class="workbench-eyebrow">MARKET INSIGHT · COMPETITIVE ANALYSIS</p>
<h2>{{ trainingSteps[currentStep - 1] || taskTitle }}</h2>
</div>
<TrainingTaskToolbar class="competitive-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="competitive-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav class="step-indicator" aria-label="竞争环境分析步骤"><template v-for="(step, index) in trainingSteps" :key="`${index}-${step}`"><button type="button" :class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @click="switchStep(index + 1)"><span class="step-marker">{{ String(index + 1).padStart(2, '0') }}</span><span class="step-copy"><strong>{{ step }}</strong><small>{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</small></span></button><i v-if="index < trainingSteps.length - 1" class="step-connector" /></template></nav>
<section class="step-card">
@ -17,21 +17,21 @@
<div class="step-title-row"><div><p>Step 01</p><h2>{{ trainingSteps[0] }}</h2></div><span>SWOT 分类练习</span></div>
<p class="step-desc">请根据案例文档中信息进行 SWOT 分析,进行各项内容归类,把文档中相应内容填写到以下四类中。</p>
<div class="swot-table-wrap"><table class="swot-table"><thead><tr><th>序号</th><th>SWOT维度</th><th>关键信息要点</th></tr></thead><tbody><tr v-for="(row, index) in swotRows" :key="row.key"><td>{{ index + 1 }}</td><td class="dimension-cell">{{ row.label }}</td><td><textarea v-model.trim="form[row.key]" :placeholder="row.placeholder" /></td></tr></tbody></table></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving" @click="saveAndGoToStepTwo">保存并进入第 2 步</button><button type="button" class="btn" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving" @click="saveAndGoToStepTwo">保存并进入第 2 步</button><button type="button" class="btn" :disabled="saving" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 2">
<div class="step-title-row"><div><p>Step 02</p><h2>{{ trainingSteps[1] }}</h2></div><span>资料补充</span></div>
<div class="research-guide"><p>请自行收集资料,补充完善 SWOT 信息,每个维度至少补充 1 条。信息收集可参考以下方法:</p><ol><li>自行通过互联网平台收集;可在电商平台(京东/天猫)搜索“智能健身镜”,浏览竞品用户评价,从中发现竞品的优劣势和用户未满足的需求。</li><li>使用 AI 大模型智能搜索功能进行信息收集,参考提示词见实训任务案例文档。</li></ol><p>请把补充的 SWOT 信息资讯填入下表:</p></div>
<p v-if="!stepOnePassed" class="step-warning">请先完成并保存第 1 步的 SWOT 信息归类,再填写本步骤。</p>
<div class="swot-table-wrap"><table class="swot-table"><thead><tr><th>序号</th><th>SWOT维度</th><th>补充信息要点</th></tr></thead><tbody><tr v-for="(row, index) in swotRows" :key="row.key"><td>{{ index + 1 }}</td><td class="dimension-cell">{{ row.label }}</td><td><textarea v-model.trim="supplementForm[row.key]" :disabled="!stepOnePassed" :placeholder="row.placeholder" /></td></tr></tbody></table></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button type="button" class="btn" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button type="button" class="btn" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 3">
<div class="step-title-row"><div><p>Step 03</p><h2>{{ trainingSteps[2] }}</h2></div><span>SWOT 策略矩阵</span></div>
<p class="step-desc">结合 SWOT 因素矩阵,将内部因素与外部因素交叉组合,分别提出四种策略。每类策略至少提出 1 条。</p>
<p v-if="!stepTwoPassed" class="step-warning">请先完成并保存第 2 步的 SWOT 补充信息,再填写本步骤。</p>
<div class="strategy-table-wrap"><table class="strategy-table"><thead><tr><th>策略类型</th><th>组合逻辑(自动生成)</th><th>具体策略建议</th><th>预期效果</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td class="strategy-type">{{ row.label }}</td><td class="logic-cell"><p v-for="item in getCombinedLogic(row)" :key="item.label"><strong>{{ item.label }}</strong>{{ item.value || '暂无已保存信息' }}</p></td><td><textarea v-model.trim="strategyForm[row.key].recommendation" :disabled="!stepTwoPassed" placeholder="填写具体策略建议" /></td><td><textarea v-model.trim="strategyForm[row.key].expectedEffect" :disabled="!stepTwoPassed" placeholder="填写预期效果" /></td></tr></tbody></table></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button type="button" class="btn" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button type="button" class="btn" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 4">
<div class="step-title-row"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><span>最终决策</span></div>
@ -40,7 +40,7 @@
<section class="decision-section"><h3>(1)优先策略排序</h3><p class="section-tip">策略内容由第 3 步填写的“具体策略建议”自动带入;请完成优先级排序并说明理由。</p><div class="priority-table-wrap"><table class="priority-table"><thead><tr><th>优先级排序</th><th>策略</th><th>理由</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td><select v-model.number="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`"><option :value="null">请选择</option><option v-for="rank in 4" :key="rank" :value="rank">{{ rank }}</option></select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><textarea v-model.trim="finalForm.priorities[row.key].reason" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section>
<section class="decision-section"><h3>(2)需关注的风险点</h3><p>请至少指出 2 个风险点。</p><div class="risk-grid"><label><span>核心风险点 1</span><textarea v-model.trim="finalForm.coreRiskOne" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><textarea v-model.trim="finalForm.coreRiskTwo" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><textarea v-model.trim="finalForm.otherRisk" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
<section class="decision-section"><h3>(3)总结 SWOT 竞争环境评估结论</h3><textarea v-model.trim="finalForm.conclusion" class="conclusion-textarea" :disabled="!stepThreePassed" placeholder="综合说明优先策略、风险应对方向及最终结论" /></section>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveAndCompleteTraining">保存并完成实训</button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveCurrentStepFour">保存当前步骤</button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<div v-else class="step-placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步 SWOT 分类已保存,可从上方步骤栏返回查看。</span></div>
</section>
@ -52,7 +52,7 @@
<script setup>
import { RefreshLeft, TrendCharts } from "@element-plus/icons-vue";
import { checkCompetitiveEnvironmentAnalysisStepOne, checkCompetitiveEnvironmentAnalysisStepTwo, checkCompetitiveEnvironmentAnalysisStepThree, checkCompetitiveEnvironmentAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { checkCompetitiveEnvironmentAnalysisStepOne, checkCompetitiveEnvironmentAnalysisStepTwo, checkCompetitiveEnvironmentAnalysisStepThree, checkCompetitiveEnvironmentAnalysisStepFour, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import useUserStore from "@/store/modules/user";
@ -96,8 +96,10 @@ function switchStep(step) { if (step === 2 && !stepOnePassed.value) { proxy?.$mo
async function saveAndGoToStepTwo() { if (saving.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepOne(form.value); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; clearDraft(); currentStep.value = 2; proxy?.$modal?.msgSuccess("已保存,已进入第 2 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写 SWOT 四类信息"); } finally { saving.value = false; } }
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepTwo(supplementForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; clearDraft(); currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请为每个 SWOT 维度至少补充 1 条信息"); } finally { saving.value = false; } }
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepThree({ strategies: strategyDefinitions.map((row) => ({ strategyType: row.key, ...strategyForm.value[row.key] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; clearDraft(); currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成四类 SWOT 策略建议和预期效果"); } finally { saving.value = false; } }
async function saveAndCompleteTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("已保存并完成实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"); } finally { saving.value = false; } }
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); strategyForm.value = createStrategyForm(); finalForm.value = createFinalForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" }); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已清空填写内容"); }
async function saveCurrentStepFour() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); clearDraft(); proxy?.$modal?.msgSuccess("第 4 步已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"); } finally { saving.value = false; } }
async function submitTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkCompetitiveEnvironmentAnalysisStepFour({ priorities: strategyDefinitions.map((row) => ({ strategyType: row.key, ...finalForm.value.priorities[row.key] })), coreRiskOne: finalForm.value.coreRiskOne, coreRiskTwo: finalForm.value.coreRiskTwo, otherRisk: finalForm.value.otherRisk, conclusion: finalForm.value.conclusion }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("任务提交成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写策略优先级、至少 2 个核心风险点和总结 SWOT 竞争环境评估结论"); } finally { saving.value = false; } }
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); strategyForm.value = createStrategyForm(); finalForm.value = createFinalForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await deleteStudentTrainingAnswer(TASK_KEY); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已重新开始本次实训"); }
function resetStepOne() { form.value = createForm(); stepOnePassed.value = false; proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); }
function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function resetStepThree() { strategyForm.value = createStrategyForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }
function resetStepFour() { finalForm.value = createFinalForm(); proxy?.$modal?.msgSuccess("已清空第 4 步填写内容"); }

@ -20,7 +20,7 @@
<p class="section-eyebrow">Consumer demand behavior</p>
<h3>{{ activeStepName || taskTitle }}</h3>
</div>
<TrainingTaskToolbar class="consumer-behavior-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="consumer-behavior-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="flowSteps.length" class="step-indicator" aria-label="消费者需求与行为步骤">
@ -258,14 +258,6 @@
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -20,7 +20,7 @@
<p class="section-eyebrow">Consumer scenario analysis</p>
<h3>{{ activeStepName || taskTitle }}</h3>
</div>
<TrainingTaskToolbar class="consumer-scenario-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="consumer-scenario-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="消费场景解构步骤">
@ -231,14 +231,6 @@
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -20,7 +20,7 @@
<p class="section-eyebrow">Feature conversion</p>
<h3>{{ activeStepName || taskTitle }}</h3>
</div>
<TrainingTaskToolbar class="feature-conversion-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="feature-conversion-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="flowSteps.length" class="step-indicator" aria-label="需求转化为功能步骤">
@ -201,14 +201,6 @@
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
清空填写
</el-button>
</div>
</section>
</main>

@ -13,7 +13,7 @@
<p class="workbench-eyebrow">MARKET INSIGHT · INDUSTRY DEMAND</p>
<h2>{{ trainingSteps[currentStep - 1] || taskTitle }}</h2>
</div>
<TrainingTaskToolbar class="industry-demand-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="industry-demand-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav class="step-indicator" aria-label="行业需求分析步骤">
<template v-for="(step, index) in trainingSteps" :key="`${step}-${index}`">
@ -34,7 +34,7 @@
<tbody><tr v-for="(row, index) in pestRows" :key="row.key"><td>{{ index + 1 }}</td><td class="type-cell">{{ row.label }}</td><td><textarea v-model.trim="form[row.key]" :placeholder="row.placeholder" /></td></tr></tbody>
</table>
</div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepTwo">保存并进入第 2 步</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="saveAndGoToStepTwo">保存并进入第 2 步</button><button type="button" class="btn-nav" :disabled="saving" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 2">
@ -53,7 +53,7 @@
<tbody><tr v-for="(row, index) in pestRows" :key="row.key"><td>{{ index + 1 }}</td><td class="type-cell">{{ row.label }}</td><td><textarea v-model.trim="supplementForm[row.key]" :disabled="!stepOnePassed" :placeholder="row.placeholder" /></td></tr></tbody>
</table>
</div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button type="button" class="btn-nav" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button type="button" class="btn-nav" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 3">
@ -65,7 +65,7 @@
<tbody><tr v-for="row in pestAnalysisRows" :key="row.key"><td class="type-cell">{{ row.label }}</td><td><textarea v-model.trim="analysisForm[row.key].opportunities" :disabled="!stepTwoPassed" placeholder="例如:政策、需求或技术带来的增长机会" /></td><td><textarea v-model.trim="analysisForm[row.key].challenges" :disabled="!stepTwoPassed" placeholder="例如:合规、成本、竞争或使用门槛" /></td><td><textarea v-model.trim="analysisForm[row.key].strategies" :disabled="!stepTwoPassed" placeholder="例如:针对挑战给出的产品、运营或合规方案" /></td></tr></tbody>
</table>
</div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button type="button" class="btn-nav" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button type="button" class="btn-nav" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 4">
@ -78,7 +78,7 @@
<section class="conclusion-section"><h4>(3)市场进入需要关注的风险点</h4><p>请指出 2 个以上的风险点。</p><div class="risk-grid"><label>核心风险点 1:<textarea v-model.trim="conclusionForm.riskOne" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label>核心风险点 2:<textarea v-model.trim="conclusionForm.riskTwo" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label>其他风险点:<textarea v-model.trim="conclusionForm.otherRisks" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
<section class="conclusion-section"><h4>(4)总结 PEST 环境评估结论</h4><textarea v-model.trim="conclusionForm.summary" :disabled="!stepThreePassed" placeholder="总结该产品的 PEST 环境评估结论" /></section>
</div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepThreePassed" @click="submitTraining">提交任务</button><button type="button" class="btn-nav" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving || !stepThreePassed" @click="saveCurrentStepFour">保存当前步骤</button><button type="button" class="btn-nav" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<div v-else class="step-placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>前序步骤已保存,可从上方步骤栏返回查看。</span></div>
@ -91,7 +91,7 @@
<script setup>
import { DataAnalysis, RefreshLeft } from "@element-plus/icons-vue";
import { checkIndustryDemandAnalysisStepOne, checkIndustryDemandAnalysisStepTwo, checkIndustryDemandAnalysisStepThree, checkIndustryDemandAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { checkIndustryDemandAnalysisStepOne, checkIndustryDemandAnalysisStepTwo, checkIndustryDemandAnalysisStepThree, checkIndustryDemandAnalysisStepFour, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import useUserStore from "@/store/modules/user";
@ -177,7 +177,9 @@ async function saveAndGoToStepTwo() { if (saving.value) return; saving.value = t
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepTwo(supplementForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; clearDraft(); currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存,已进入第 3 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请为每个 PEST 维度至少补充 1 条信息"); } finally { saving.value = false; } }
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepThree({ rows: pestAnalysisRows.map((row) => ({ dimension: row.key, ...analysisForm.value[row.key] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; clearDraft(); currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成每个 PEST 维度的机遇、挑战和应对思路"); } finally { saving.value = false; } }
async function submitTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepFour(conclusionForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); clearDraft(); proxy?.$modal?.msgSuccess("任务提交成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成 PEST 环境评估结论"); } finally { saving.value = false; } }
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); analysisForm.value = createAnalysisForm(); conclusionForm.value = createConclusionForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" }); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已清空填写内容"); }
async function saveCurrentStepFour() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkIndustryDemandAnalysisStepFour(conclusionForm.value); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); clearDraft(); proxy?.$modal?.msgSuccess("第 4 步已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成 PEST 环境评估结论"); } finally { saving.value = false; } }
async function resetTraining() { form.value = createForm(); supplementForm.value = createForm(); analysisForm.value = createAnalysisForm(); conclusionForm.value = createConclusionForm(); stepOnePassed.value = false; stepTwoPassed.value = false; stepThreePassed.value = false; currentStep.value = 1; clearDraft(); try { saving.value = true; await deleteStudentTrainingAnswer(TASK_KEY); } catch (error) { /* Local clearing still succeeds. */ } finally { saving.value = false; } proxy?.$modal?.msgSuccess("已重新开始本次实训"); }
function resetStepOne() { form.value = createForm(); stepOnePassed.value = false; proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); }
function resetStepTwo() { supplementForm.value = createForm(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function resetStepThree() { analysisForm.value = createAnalysisForm(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }
function resetStepFour() { conclusionForm.value = createConclusionForm(); proxy?.$modal?.msgSuccess("已清空第 4 步填写内容"); }

@ -20,7 +20,7 @@
<p class="section-eyebrow">Target user profile</p>
<h3>{{ activeStepName || taskTitle }}</h3>
</div>
<TrainingTaskToolbar class="target-profile-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="target-profile-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="steps.length" class="step-indicator" aria-label="目标用户画像步骤">
@ -309,14 +309,6 @@
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -17,7 +17,7 @@
<p class="workbench-eyebrow">PRODUCT DEVELOPMENT · KEY FACTORS</p>
<h2>{{ trainingSteps[currentStep] || taskTitle }}</h2>
</div>
<TrainingTaskToolbar class="product-factors-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="product-factors-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="实训步骤">
@ -87,9 +87,9 @@
<button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepTwo">
保存并进入第 2 步
</button>
<button type="button" class="btn-nav" :disabled="saving" @click="resetTraining">
<button type="button" class="btn-nav" :disabled="saving" @click="resetStepOne">
<el-icon><RefreshLeft /></el-icon>
清空填写
清空当前步骤
</button>
</div>
</template>
@ -99,7 +99,7 @@
<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>
<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="resetStepTwo"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 2">
@ -107,14 +107,14 @@
<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>
<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="resetStepThree"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 3">
<p class="factor-intro">结合案例信息及前述步骤,分析案例资料中产品一成功的关键因素是什么,产品二失败的关键因素是什么。</p>
<label class="factor-intro">产品成功的关键因素:</label><textarea v-model="stepFourForm.successFactors" class="table-textarea summary-textarea" />
<label class="factor-intro">产品失败的关键因素:</label><textarea v-model="stepFourForm.failedFactors" class="table-textarea summary-textarea" />
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="submitTraining">提交任务</button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining">清空填写</button></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveCurrentStepFour">保存当前步骤</button><button type="button" class="btn-nav" :disabled="saving" @click="resetStepFour">清空当前步骤</button></div>
</template>
<div v-else class="step-placeholder">
<p>第 {{ currentStep + 1 }} 步内容待配置</p>
@ -138,7 +138,7 @@
import { Operation, RefreshLeft } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, checkProductDevelopmentFactorsStepThree, checkProductDevelopmentFactorsStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, checkProductDevelopmentFactorsStepThree, checkProductDevelopmentFactorsStepFour, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -446,13 +446,48 @@ async function resetTraining() {
clearDraft();
try {
saving.value = true;
await saveStudentTrainingAnswer(TASK_KEY, buildStepOneAnswerPayload("RESET", 1));
await deleteStudentTrainingAnswer(TASK_KEY);
} catch (error) {
// Clearing the page should still work locally even when backend reset fails.
} finally {
saving.value = false;
}
proxy?.$modal?.msgSuccess("已清空填写内容");
proxy?.$modal?.msgSuccess("已重新开始本次实训");
}
function resetStepOne() {
stepOneRows.value = createStepOneRows();
proxy?.$modal?.msgSuccess("已清空第 1 步填写内容");
}
function resetStepTwo() {
stepTwoRows.value = createStepTwoRows();
proxy?.$modal?.msgSuccess("已清空第 2 步填写内容");
}
function resetStepThree() {
stepThreeRows.value = createStepThreeRows();
proxy?.$modal?.msgSuccess("已清空第 3 步填写内容");
}
function resetStepFour() {
stepFourForm.value = { successFactors: "", failedFactors: "" };
proxy?.$modal?.msgSuccess("已清空第 4 步填写内容");
}
async function saveCurrentStepFour() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepFour(buildStepFourValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepFourAnswerPayload("SAVE", 4));
clearDraft();
proxy?.$modal?.msgSuccess("第 4 步已保存");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "请完成关键因素分析");
} finally {
saving.value = false;
}
}
function exportOutcome() {

@ -14,7 +14,7 @@
<p class="workbench-eyebrow">PRODUCT DEVELOPMENT · PROCESS</p>
<h2>{{ trainingSteps[currentStep - 1] || taskTitle }}</h2>
</div>
<TrainingTaskToolbar class="product-process-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="product-process-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="产品开发主要流程步骤">
@ -63,7 +63,7 @@
<p v-if="checkResult" class="classification-summary" :class="{ 'is-success': checkResult.allCorrect }">本次判题:{{ checkResult.correctCount }} / {{ checkResult.totalCount }} 项分类正确</p>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="checking" @click="saveAndGoToStepTwo">{{ checking ? '判题中…' : '保存并进入第 2 步' }}</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetTraining"><el-icon><RefreshLeft /></el-icon>清空填写</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button>
</div>
</template>
<template v-else-if="currentStep === 2">
@ -94,7 +94,7 @@
</div>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="checking || !hasCompletedStepOne" @click="saveAndGoToStepThree">{{ checking ? '校验中…' : '保存并进入第 3 步' }}</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon>清空本步</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button>
</div>
</template>
<template v-else-if="currentStep === 3">
@ -126,7 +126,7 @@
</section>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="uploadingGantt || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button>
<button type="button" class="btn-nav" :disabled="uploadingGantt" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon>清空本步</button>
<button type="button" class="btn-nav" :disabled="uploadingGantt" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button>
</div>
</template>
<template v-else-if="currentStep === 4">
@ -147,8 +147,8 @@
</div>
</section>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="checking || !stepThreePassed" @click="submitTraining">提交任务</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon>清空本步</button>
<button type="button" class="btn-nav btn-submit" :disabled="checking || !stepThreePassed" @click="saveCurrentStepFour">保存当前步骤</button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button>
</div>
</template>
<div v-else class="step-placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步答案已保存,可从上方步骤栏返回查看。</span></div>
@ -168,7 +168,7 @@
<script setup>
import { Download, Operation, RefreshLeft, Upload } from "@element-plus/icons-vue";
import * as XLSX from "xlsx";
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, checkProductDevelopmentProcessStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, checkProductDevelopmentProcessStepFour, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import useUserStore from "@/store/modules/user";
@ -426,13 +426,20 @@ async function resetTraining() {
clearDraft();
try {
checking.value = true;
await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildOutcome()), currentStep: 1, submitted: false, saveAction: "RESET" });
await deleteStudentTrainingAnswer(TASK_KEY);
} catch (error) {
// The local reset remains effective if the backend is temporarily unavailable.
} finally {
checking.value = false;
}
proxy?.$modal?.msgSuccess("已清空填写内容");
proxy?.$modal?.msgSuccess("已重新开始本次实训");
}
function resetStepOne() {
processRows.value = processRows.value.map((row) => ({ ...row, classification: "" }));
checkResult.value = null;
stepOnePassed.value = false;
proxy?.$modal?.msgSuccess("已清空第 1 步填写内容");
}
async function saveAndGoToStepThree() {
@ -527,6 +534,21 @@ async function submitTraining() {
}
}
async function saveCurrentStepFour() {
if (checking.value || !stepThreePassed.value) return;
checking.value = true;
try {
await checkProductDevelopmentProcessStepFour(buildStepFourOutcome());
await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" });
clearDraft();
proxy?.$modal?.msgSuccess("第 4 步已保存");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "请完成并行协同复盘与两个AI应用场景");
} finally {
checking.value = false;
}
}
function resetStepFour() {
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" };
proxy?.$modal?.msgSuccess("已清空第 4 步填写内容");

@ -6,7 +6,7 @@
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" :task-key="taskKey" :on-submit="submitTask" :on-reset="resetTask" :on-export="exportOutcome" />
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
@ -60,13 +60,10 @@
<div v-if="hasConfiguredSteps" class="step-navigation">
<button type="button" class="btn-nav" :disabled="activeStep === 1" @click="goPreviousStep"><el-icon><ArrowLeft /></el-icon>上一步</button>
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon>保存当前进度</button>
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon>保存当前步骤</button>
<button type="button" class="btn-nav" :disabled="saving" @click="clearCurrentStep"><el-icon><RefreshLeft /></el-icon>清空当前步骤</button>
<button type="button" class="btn-nav btn-primary" :disabled="activeStep === configuredSteps.length" @click="goNextStep">下一步<el-icon><ArrowRight /></el-icon></button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" :loading="saving" @click="resetTask">清空填写</el-button>
</div>
</section>
</main>
<TrainingAiSidebar :study-tip="`当前任务建议:围绕“${taskTitle}”先明确产品对象,再按配置步骤补全实训内容。`" :progress-items="progressItems" />
@ -75,7 +72,7 @@
<script setup>
import { computed, getCurrentInstance, onMounted, ref, useSlots } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, RefreshLeft } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
@ -85,7 +82,7 @@ import { getStudentTrainingPage } from "@/views/training/studentTrainingPages";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const props = defineProps({ taskKey: { type: String, required: true }, title: { type: String, default: "" }, background: { type: String, default: "" }, requirement: { type: String, default: "" }, goals: { type: Array, default: () => [] }, badgeText: { type: String, default: "核心实训 · 产品规划与设计" } });
const emit = defineEmits(["save", "submit", "reset"]);
const emit = defineEmits(["save", "submit", "reset", "clear-current-step"]);
const slots = useSlots();
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
@ -127,8 +124,15 @@ function setCurrentStep(step) { activeStep.value = normalizeActiveStep(step); pe
function goPreviousStep() { setCurrentStep(activeStep.value - 1); }
function goNextStep() { setCurrentStep(activeStep.value + 1); }
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) { emit("save"); await persistProgress("SAVE", true); } else { await persistProgress(); } } finally { saving.value = false; } }
function clearCurrentStep() {
const fields = visibleFields.value;
fields.forEach((field) => { form.value[field.key] = ""; });
if (showWorkTable.value) workRows.value = createRows();
if (hasCustomContent.value) emit("clear-current-step", activeStep.value);
proxy?.$modal?.msgSuccess(`已清空第 ${activeStep.value} 步填写内容`);
}
async function submitTask() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) { emit("submit"); await persistProgress("SAVE", true); } else { await persistProgress("SUBMIT"); } } finally { saving.value = false; } }
async function resetTask() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) emit("reset"); form.value = createForm(); workRows.value = createRows(); activeStep.value = 1; await persistProgress("RESET", hasCustomContent.value); if (!hasCustomContent.value) proxy?.$modal?.msgSuccess("清空填写成功"); } finally { saving.value = false; } }
async function resetTask() { if (saving.value) return; saving.value = true; try { if (hasCustomContent.value) emit("reset"); form.value = createForm(); workRows.value = createRows(); activeStep.value = 1; await persistProgress("RESET", hasCustomContent.value); if (!hasCustomContent.value) proxy?.$modal?.msgSuccess("已重新开始本次实训"); } finally { saving.value = false; } }
function exportOutcome() { const blob = new Blob([JSON.stringify(buildProgress(), null, 2)], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${taskTitle.value}-实训成果.json`; link.click(); URL.revokeObjectURL(url); }
</script>

@ -17,7 +17,7 @@
<p class="section-eyebrow">Value proposition positioning</p>
<h3>{{ activeStepName || taskTitle }}</h3>
</div>
<TrainingTaskToolbar class="product-positioning-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="product-positioning-toolbar" :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="产品价值主张及定位步骤">
@ -233,10 +233,6 @@
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">清空填写</el-button>
</div>
</section>
</main>

@ -9,7 +9,7 @@
<p class="workbench-eyebrow">PRODUCT PLANNING · MARKET SIGNALS</p>
<h2>{{ trainingSteps[currentStep - 1] || taskTitle }}</h2>
</div>
<TrainingTaskToolbar class="market-opportunity-toolbar" :task-key="TASK_KEY" :on-export="exportOutcome" />
<TrainingTaskToolbar class="market-opportunity-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</div>
<nav class="step-indicator" aria-label="市场机会识别与选品步骤"><template v-for="(step, index) in trainingSteps" :key="step"><button type="button" :class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @click="switchStep(index + 1)"><b>{{ String(index + 1).padStart(2, '0') }}</b><span><strong>{{ step }}</strong><small>{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</small></span></button><i v-if="index < trainingSteps.length - 1" /></template></nav>
<section class="step-card">
@ -17,7 +17,7 @@
<div class="step-heading"><div><p>Step 01</p><h2>{{ trainingSteps[0] }}</h2></div><em>指标分类练习</em></div>
<p class="intro">请对实训数据表中的各项指标,按照市场规模、市场潜力、竞争强度、运营难度 4 个类别进行分类。</p>
<div class="table-wrap"><table><thead><tr><th>序号</th><th>指标</th><th>分类</th></tr></thead><tbody><tr v-for="(item, index) in indicators" :key="item.id"><td>{{ index + 1 }}</td><td class="metric">{{ item.name }}</td><td><select v-model="answers[item.id]" :class="{ 'input-error': stepOneCheckResults[item.id] && !stepOneCheckResults[item.id].correct }" :aria-label="`${item.name} 的分类`" @change="clearStepOneCheck(item.id)"><option value="">请选择</option><option v-for="category in categories" :key="category" :value="category">{{ category }}</option></select><small v-if="stepOneCheckResults[item.id] && !stepOneCheckResults[item.id].correct" class="classification-error">{{ stepOneCheckResults[item.id].message }}</small></td></tr></tbody></table></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving" @click="checkAndContinue">判题并进入第 2 步</button><button class="btn" type="button" :disabled="saving" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving" @click="checkAndContinue">判题并进入第 2 步</button><button class="btn" type="button" :disabled="saving" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 2">
<div class="step-heading"><div><p>Step 02</p><h2>{{ trainingSteps[1] }}</h2></div><em>指标权重设定</em></div>
@ -25,7 +25,7 @@
<p v-if="!stepOnePassed" class="warning">请先完成并保存第 1 步的指标分类,再填写本步骤。</p>
<div class="weight-summary"><span>一级指标权重合计</span><strong :class="{ valid: primaryWeightTotal === 100 }">{{ primaryWeightTotal }}%</strong><span>对应总权重合计</span><strong :class="{ valid: correspondingWeightTotal === 100 }">{{ correspondingWeightTotal }}%</strong><small>一级权重合计、每类维度内权重合计、对应总权重均须为 100%</small></div>
<div class="table-wrap"><table class="weight-table"><thead><tr><th>一级指标</th><th>权重占比(%)</th><th>二级指标</th><th>维度内权重(%)</th><th>对应总权重</th></tr></thead><tbody><template v-for="group in indicatorGroups" :key="group.name"><tr v-for="(item, index) in group.items" :key="item.id"><td v-if="index === 0" :rowspan="group.items.length" class="first-level">{{ group.name }}</td><td v-if="index === 0" :rowspan="group.items.length" class="primary-weight"><input v-model.number="primaryWeights[group.name]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${group.name} 的一级指标权重`" /><span class="percent">%</span><small>建议 {{ group.suggestion }}</small><small>维度内权重合计 <strong :class="{ valid: dimensionWeightTotal(group.name) === 100 }">{{ dimensionWeightTotal(group.name) }}%</strong></small></td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的维度内权重`" /><span class="percent">%</span><small>建议 {{ item.suggestion }}</small></td><td class="corresponding-weight">{{ formatWeight(correspondingWeight(item)) }}%</td></tr></template></tbody></table></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button class="btn" type="button" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button class="btn" type="button" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 3">
<div class="step-heading"><div><p>Step 03</p><h2>{{ trainingSteps[2] }}</h2></div><em>加权评分计算</em></div>
@ -33,7 +33,7 @@
<p v-if="!stepTwoPassed" class="warning">请先完成并保存第 2 步的指标权重设定,再填写本步骤。</p>
<div class="score-table-wrap"><table class="score-table"><thead><tr><th>序号</th><th>产品品类</th><th>综合得分</th></tr></thead><tbody><tr v-for="(product, index) in products" :key="product.id"><td>{{ index + 1 }}</td><td class="metric">{{ product.name }}</td><td><input v-model.number="scores[product.id]" :disabled="!stepTwoPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的综合得分`" /><span class="percent">分</span></td></tr></tbody></table></div>
<div class="calculation-guide"><div><h3>推荐工具及方法如下,自行选择:</h3><p>1. Excel/WPS 表格,自行操作计算,可参考《实训操作说明》完成。</p><p>2. 下载自动化模板,填写案例数据及第 2 步权重,完成指标计算。</p><p>3. 导出实训数据表,上传到通用 AI 大模型,参考提示词完成计算。</p><p>先按 Min-Max 归一法和加权评分法计算品类综合得分;如案例资料提供三段式时间权重,则分别按第 1–20 天、第 21–40 天、第 41–60 天的权重计算最终得分。</p></div><div class="download-stack"><button class="btn" type="button" @click="downloadInstructions">下载实训操作说明</button><button class="btn primary" type="button" @click="downloadTemplate">下载自动化模板</button></div></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button class="btn" type="button" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button class="btn" type="button" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 4">
<div class="step-heading"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><em>可视化结果提交</em></div>
@ -46,7 +46,7 @@
<div v-if="visualization.url" class="visualization-preview"><el-image :src="visualization.url" :preview-src-list="[visualization.url]" fit="cover" preview-teleported /><div><strong>{{ visualization.name || "数据可视化结果图" }}</strong><span>点击缩略图可放大查看</span><button class="text-button" type="button" @click="clearVisualization">移除图片</button></div></div>
</div>
<div class="visual-guide"><div><h3>推荐工具及方法如下,自行选择:</h3><p>1. 使用 Excel/WPS 表格计算三类产品的平均得分、最高分和最低分,并生成图表。</p><p>2. 下载自动化模板,填写关键数据,完成指标计算和可视化。</p><p>3. 导出实训数据表,上传到通用 AI 大模型,参考提示词完成计算与图表生成。</p><p>请根据数据表及各指标权重设定表,使用 Min-Max 归一法与加权评分法计算产品综合得分,并汇总平均分、最高分和最低分。</p></div><div class="download-stack"><button class="btn" type="button" @click="downloadVisualizationInstructions">下载实训操作说明</button><button class="btn primary" type="button" @click="downloadVisualizationTemplate">下载自动化模板</button></div></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || uploading || !stepThreePassed" @click="saveAndGoToStepFive">保存并进入第 5 步</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || uploading || !stepThreePassed" @click="saveAndGoToStepFive">保存并进入第 5 步</button><button class="btn" type="button" :disabled="saving || uploading || !stepThreePassed" @click="clearVisualization"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 5">
<div class="step-heading"><div><p>Step 05</p><h2>{{ trainingSteps[4] }}</h2></div><em>评论数据洞察</em></div>
@ -54,14 +54,14 @@
<p v-if="!stepFourPassed" class="warning">请先完成并保存第 4 步的数据可视化结果,再填写本步骤。</p>
<div class="table-wrap"><table class="insight-table"><thead><tr><th>品类</th><th>好评率(%)</th><th>用户痛点</th><th>未被满足的需求</th></tr></thead><tbody><tr v-for="product in products" :key="product.id"><td class="metric">{{ product.name }}</td><td><input v-model.number="reviewInsights[product.id].favorableRate" :disabled="!stepFourPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的好评率`" /><span class="percent">%</span></td><td><textarea v-model.trim="reviewInsights[product.id].painPoint" :disabled="!stepFourPassed" :aria-label="`${product.name} 的用户痛点`" placeholder="请根据评论数据提炼用户痛点" /></td><td><textarea v-model.trim="reviewInsights[product.id].unmetNeed" :disabled="!stepFourPassed" :aria-label="`${product.name} 的未被满足的需求`" placeholder="请根据评论数据提炼未被满足的需求" /></td></tr></tbody></table></div>
<div class="insight-guide"><div><h3>可参考以下方法:</h3><p>1. 自行根据评价数据计算好评率,阅读评论后分析用户痛点及未被满足的需求。</p><p>2. 导出评论数据表,上传到通用 AI 大模型,通过文本分析完成计算与提炼。</p><p>提示词:请计算 Excel 表中产品评论数据的好评率,提炼用户痛点和未被满足的需求。</p></div></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepFourPassed" @click="saveAndGoToStepSix">保存并进入第 6 步</button><button class="btn" type="button" :disabled="saving || !stepFourPassed" @click="resetStepFive"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepFourPassed" @click="saveAndGoToStepSix">保存并进入第 6 步</button><button class="btn" type="button" :disabled="saving || !stepFourPassed" @click="resetStepFive"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<template v-else-if="currentStep === 6">
<div class="step-heading"><div><p>Step 06</p><h2>{{ trainingSteps[5] }}</h2></div><em>选品结论</em></div>
<p class="intro">结合前面的数据分析,请说明应该进行开发什么产品,具体原因是什么。</p>
<p v-if="!stepFivePassed" class="warning">请先完成并保存第 5 步的用户需求洞察,再填写本步骤。</p>
<div class="conclusion-field"><label for="market-opportunity-conclusion">开发建议与原因</label><textarea id="market-opportunity-conclusion" v-model.trim="developmentConclusion" :disabled="!stepFivePassed" placeholder="请结合综合得分、评论痛点与未被满足的需求,说明建议开发的产品及具体原因。" /></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepFivePassed" @click="saveAndCompleteTraining">保存并完成实训</button><button class="btn" type="button" :disabled="saving || !stepFivePassed" @click="resetStepSix"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepFivePassed" @click="saveCurrentStepSix">保存当前步骤</button><button class="btn" type="button" :disabled="saving || !stepFivePassed" @click="resetStepSix"><el-icon><RefreshLeft /></el-icon> 清空当前步骤</button></div>
</template>
<div v-else class="placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>已保存的步骤可从上方步骤栏返回查看。</span></div>
</section>
@ -134,6 +134,12 @@ async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepThree({ scores: products.map((product) => ({ id: product.id, score: scores.value[product.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; currentStep.value = 4; proxy?.$modal?.msgSuccess("综合得分校验通过,已进入第 4 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写 0 至 100 分、最多保留两位小数的综合得分"); } finally { saving.value = false; } }
function clearStepOneCheck(id) { if (stepOneCheckResults.value[id]) { const next = { ...stepOneCheckResults.value }; delete next[id]; stepOneCheckResults.value = next; } }
async function resetStepOne() {
answers.value = createAnswers();
stepOneCheckResults.value = {};
stepOnePassed.value = false;
proxy?.$modal?.msgSuccess("已清空第 1 步填写内容");
}
async function resetTraining() {
if (saving.value) return;
saving.value = true;
try {
@ -154,7 +160,7 @@ async function resetStepOne() {
stepFourPassed.value = false;
stepFivePassed.value = false;
currentStep.value = 1;
proxy?.$modal?.msgSuccess("已清空本实训填写内容");
proxy?.$modal?.msgSuccess("已重新开始本次实训");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "清空失败,请重试");
} finally {
@ -170,7 +176,8 @@ function clearVisualization() { if (visualization.value.url.startsWith("blob:"))
async function handleVisualizationUpload(event) { const file = event.target?.files?.[0]; if (!file) return; const acceptedTypes = ["image/jpeg", "image/png", "image/webp"]; if (!acceptedTypes.includes(file.type)) { proxy?.$modal?.msgError?.("请上传 PNG、JPG 或 WEBP 格式的图片"); event.target.value = ""; return; } if (file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("图片大小不能超过 5MB"); event.target.value = ""; return; } uploading.value = true; try { if (isStudentDemo()) { clearVisualization(); visualization.value = { url: URL.createObjectURL(file), name: file.name }; } else { const formData = new FormData(); formData.append("file", file); const result = await uploadStudentTrainingFile(formData); const url = result?.url || result?.data?.url; if (!url) throw new Error("图片上传失败,请重试"); visualization.value = { url, name: file.name }; } proxy?.$modal?.msgSuccess("可视化结果图上传成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败,请重试"); } finally { uploading.value = false; event.target.value = ""; } }
async function saveAndGoToStepFive() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepFour({ visualizationUrl: visualization.value.url }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 5, submitted: false, saveAction: "SAVE" }); stepFourPassed.value = true; currentStep.value = 5; proxy?.$modal?.msgSuccess("可视化结果已保存,已进入第 5 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请上传数据可视化结果图"); } finally { saving.value = false; } }
async function saveAndGoToStepSix() { if (saving.value || !stepFourPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepFive({ insights: products.map((product) => ({ productId: product.id, ...reviewInsights.value[product.id] })) }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: false, saveAction: "SAVE" }); stepFivePassed.value = true; currentStep.value = 6; proxy?.$modal?.msgSuccess("用户需求洞察已保存,已进入第 6 步"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完整填写三类产品的好评率、用户痛点和未被满足的需求"); } finally { saving.value = false; } }
async function saveAndCompleteTraining() { if (saving.value || !stepFivePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepSix({ developmentConclusion: developmentConclusion.value }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: true, saveAction: "SUBMIT" }); proxy?.$modal?.msgSuccess("实训成果已保存,恭喜完成本次实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写建议开发的产品及具体原因"); } finally { saving.value = false; } }
async function saveCurrentStepSix() { if (saving.value || !stepFivePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepSix({ developmentConclusion: developmentConclusion.value }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: false, saveAction: "SAVE" }); proxy?.$modal?.msgSuccess("第 6 步已保存"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写建议开发的产品及具体原因"); } finally { saving.value = false; } }
async function submitTraining() { if (saving.value || !stepFivePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepSix({ developmentConclusion: developmentConclusion.value }); dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); await saveStudentTrainingAnswer(TASK_KEY, { dynamicStepAnswers: JSON.stringify(dynamicStepAnswers.value), currentStep: 6, submitted: true, saveAction: "SUBMIT" }); proxy?.$modal?.msgSuccess("实训成果已提交"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写建议开发的产品及具体原因"); } finally { saving.value = false; } }
function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }
function downloadInstructions() { downloadBuiltInFile(SCORE_CALCULATION_GUIDE_URL, "6.综合得分计算操作说明.docx"); }
// 远端版本:下载入口已改为后端内置 Excel 模板;以下旧生成逻辑仅保留作迁移参考。

@ -18,6 +18,8 @@
class="comprehensive-training-toolbar"
:material-name="page.material?.name"
:material-url="page.material?.url"
:on-submit="submitTask"
:on-reset="resetTraining"
:on-export="exportOutcome"
/>
</div>
@ -384,17 +386,6 @@
</section>
<div v-if="!processRows.length" class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">重新实训</el-button>
</div>
<div v-if="processRows.length" class="training-actions">
<el-button class="training-action training-action--save" :loading="saving" @click="saveCurrentProgress">保存当前进度</el-button>
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">提交任务</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">重新实训</el-button>
</div>
</section>
</main>

Loading…
Cancel
Save