学生端实训页面按钮统一:TrainingTaskToolbar新增保存当前进度按钮(优先触发页面内保存,fallback快照SAVE与提交区分),25个缺失页面批量接入统一工具栏,所有实训页面具备保存+提交两个按钮

fix/exam-question-access-material-excel-delete-draft
陈沅 2 weeks ago
parent 1e251d7169
commit 4319e2c980

@ -1,5 +1,6 @@
<template>
<div class="five">
<TrainingTaskToolbar />
<el-scrollbar :height="`${height - 100}px`">
<TrainingIntro title="五维四率模型">
<template #background>
@ -177,6 +178,7 @@ import TrainingIntro from '@/views/components/TrainingIntro.vue';
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import * as analysisAPI from "@/api/market-demand-mining.js";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const height = ref(800);
const liveRoomDataWithConversion = [

@ -1,5 +1,6 @@
<template>
<div class="kano">
<TrainingTaskToolbar />
<el-scrollbar :height="`${height - 100}px`">
<TrainingIntro title="KANO模型">
<template #background>
@ -320,6 +321,7 @@ import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import * as analysisAPI from "@/api/market-demand-mining.js";
import { ref } from "vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const height = ref(800);
const form = ref({

@ -8,6 +8,7 @@
-->
<template>
<div class="market">
<TrainingTaskToolbar />
<el-scrollbar :height="`${height - 100}px`">
<TrainingIntro title="市场机会矩阵">
<template #background>
@ -215,6 +216,7 @@ import TrainingIntro from '@/views/components/TrainingIntro.vue';
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import * as analysisAPI from "@/api/market-demand-mining.js";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const height = ref(800);
const salesData = [

@ -8,6 +8,7 @@
-->
<template>
<div class="traffic">
<TrainingTaskToolbar />
<el-scrollbar :height="`${height - 100}px`">
<TrainingIntro title="流量问题诊断">
<template #background>
@ -107,6 +108,7 @@ import TrainingIntro from '@/views/components/TrainingIntro.vue';
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import * as analysisAPI from "@/api/market-demand-mining.js";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const height = ref(800);
const from = ref({

@ -1,5 +1,6 @@
<template>
<div class="channel student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<div class="task-head-main">
@ -199,6 +200,7 @@ import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
from sklearn.linear_model import LinearRegression
mpl.rcParams['font.sans-serif'] = ['SimHei']#SimHeri是中文字体名称
mpl.rcParams['axes.unicode_minus'] = False

@ -8,6 +8,7 @@
-->
<template>
<div class="purchase-page student-training-shell">
<TrainingTaskToolbar />
<main class="purchase-core">
<section class="task-header">
<div class="task-head-main">
@ -142,6 +143,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import * as analysisAPI from "@/api/market-demand-mining.js";
import * as echarts from "echarts";
import useUserStore from "@/store/modules/user";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const briefOpen = ref(false);

@ -1,5 +1,6 @@
<template>
<div class="supplier-page student-training-shell" v-loading="loading">
<TrainingTaskToolbar />
<main class="supplier-core">
<section class="task-header">
<div class="task-head-main">
@ -220,6 +221,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import {
getStudentTrainingAnswer,
saveStudentTrainingAnswer,

@ -10,6 +10,9 @@
:ai-manual-name="resolvedAiManual.name"
:ai-manual-url="resolvedAiManual.url"
/>
<el-button class="task-toolbar-button task-toolbar-button--save" :loading="savingProgress" @click="triggerExistingAction('save')">
<el-icon><CircleCheck /></el-icon>保存当前进度
</el-button>
<el-button class="task-toolbar-button task-toolbar-button--submit" @click="triggerExistingAction('submit')">
<el-icon><CircleCheck /></el-icon>提交任务
</el-button>
@ -60,6 +63,7 @@ const loadedMaterial = ref({
});
const loadedTask = ref(null);
const exporting = ref(false);
const savingProgress = ref(false);
const resolvedTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey));
const resolvedMaterial = computed(() => ({
name: String(props.materialName || loadedMaterial.value.name || ""),
@ -139,6 +143,11 @@ function triggerExistingAction(action) {
reset: props.onReset,
};
if (action === "save") {
saveProgress();
return;
}
if (directActions[action]) {
directActions[action]();
return;
@ -201,6 +210,50 @@ function collectLegacyFields() {
}));
}
async function saveProgress() {
if (savingProgress.value) return;
// 优先触发页面自身的保存逻辑(保留各页面的校验与状态文案)
const saveLabelPriority = [[/保存当前进度/], [/保存进度/], [/保存(?!成功|失败|中)/]];
const buttons = getCurrentTaskActionButtons();
const target = saveLabelPriority
.map((matchers) => buttons.find((button) => matchers.some((matcher) => matcher.test(button.innerText || ""))))
.find(Boolean);
if (target) {
savingProgress.value = true;
try {
target.click();
await new Promise((resolve) => setTimeout(resolve, 600));
ElMessage.success("进度已保存");
} finally {
savingProgress.value = false;
}
return;
}
// 页面没有保存按钮时保存表单快照(SAVE 不等于提交)
if (!resolvedTaskKey.value) {
ElMessage.warning("当前任务暂未配置,无法保存。");
return;
}
savingProgress.value = true;
try {
await saveStudentTrainingAnswer(resolvedTaskKey.value, {
saveAction: "SAVE",
currentStep: 1,
step1Answer: JSON.stringify({
type: "legacy-training-snapshot-v3",
route: route.path,
savedAt: new Date().toISOString(),
fields: collectLegacyFields(),
}),
});
ElMessage.success("进度已保存");
} catch {
ElMessage.error("保存失败,请稍后重试。");
} finally {
savingProgress.value = false;
}
}
async function saveLegacyTraining() {
if (!resolvedTaskKey.value) {
ElMessage.warning("当前任务暂未配置,无法提交。");
@ -263,6 +316,8 @@ function exportLegacyTraining() {
.training-task-toolbar { display:flex; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:8px; }
.task-toolbar-button { min-height:34px; height:34px; padding:0 14px; border-color:rgba(37,99,235,.35); border-radius:8px; color:#2563eb; background:#fff; font-size:13px; font-weight:600; }
.task-toolbar-button:hover { border-color:#2563eb; color:#1d4ed8; background:#eff6ff; }
.task-toolbar-button--save { border-color:rgba(43,164,113,.45); color:#2ba471; }
.task-toolbar-button--save:hover { border-color:#2ba471; color:#1f8f62; background:#f0fbf6; }
.task-toolbar-button--submit { border-color:#2563eb; color:#fff; background:#2563eb; }
.task-toolbar-button--submit:hover { border-color:#1d4ed8; color:#fff; background:#1d4ed8; }
@media (max-width:768px) { .training-task-toolbar { justify-content:stretch; } .training-task-toolbar > * { flex:1 1 auto; } }

@ -1,5 +1,6 @@
<template>
<div class="feasibility-page student-training-shell">
<TrainingTaskToolbar />
<main class="feasibility-core">
<section class="task-header">
<div class="task-head-main">
@ -379,6 +380,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "business-feasibility"
const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告", "决策结论"]

@ -1,5 +1,6 @@
<template>
<div class="consumer-behavior-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<div class="task-head-main">
@ -303,6 +304,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "consumer-behavior";
const QUESTIONNAIRE_TEMPLATE_URL = "/file/training-materials/consumer-behavior/9.问卷调查报告模板.docx";

@ -1,5 +1,6 @@
<template>
<div class="consumer-scenario-page student-training-shell">
<TrainingTaskToolbar />
<main class="consumer-scenario-core">
<section class="task-header">
<div class="task-head-main">
@ -279,6 +280,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, checkConsumerScenarioStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "consumer-scenario";
const JOURNEY_TEMPLATE_URL = "/file/training-materials/consumer-scenario/10.用户购买旅程模版.pptx";

@ -1,5 +1,6 @@
<template>
<div class="competitive-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<h1 class="task-title">{{ taskTitle }}</h1>
@ -149,6 +150,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "competitive-environment-analysis";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="industry-demand-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<h1 class="task-title">{{ taskTitle }}</h1>
@ -137,6 +138,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "industry-demand-analysis";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="target-profile-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<div class="task-head-main">
@ -378,6 +379,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import { checkTargetUserProfileStepFive, checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "target-user-profile"
const RFM_INSTRUCTIONS_URL = "/file/training-materials/target-user-profile/8.实训操作说明.docx"

@ -8,6 +8,7 @@
-->
<template>
<div class="rgzn_top app-container2 student-training-shell legacy-training-page" v-loading="loading" element-loading-text="Loading..." :element-loading-spinner="svg" element-loading-svg-view-box="-10, -10, 50, 50" element-loading-background="rgba(122, 122, 122, 0.8)">
<TrainingTaskToolbar />
<div class="top_item">
<div>
<template v-for="(item, index) in tabsList" :key="item.id">
@ -59,6 +60,7 @@ import { getAssetsFile, isModuleUnique } from "@/utils/index.js";
import useAlgorithmStore from "@/store/modules/algorithm.js";
import { getUserInfo } from "@/utils/auth";
import { onMounted } from "vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const algorithmStore = useAlgorithmStore();
const userInfo = JSON.parse(getUserInfo());

@ -1,5 +1,6 @@
<template>
<div class="product-factors student-training-shell">
<TrainingTaskToolbar />
<main class="factors-core">
<section class="task-header">
<div class="task-head-main">
@ -162,6 +163,7 @@ import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsSt
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const TASK_KEY = "product-development-factors";

@ -1,5 +1,6 @@
<template>
<div class="product-process student-training-shell">
<TrainingTaskToolbar />
<main class="process-core">
<section class="task-header">
<div class="task-head-main">
@ -196,6 +197,7 @@ import { isExternal } from "@/utils/validate";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();

@ -1,5 +1,6 @@
<template>
<div class="after-sales-page student-training-shell">
<TrainingTaskToolbar />
<main class="after-sales-core">
<section class="task-header">
<div class="task-head-main">
@ -256,6 +257,7 @@ import { checkAfterSalesServiceStepOne, checkAfterSalesServiceStepTwo, checkAfte
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingPage } from "@/views/training/studentTrainingPages";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "after-sales-service";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="product-positioning-page student-training-shell">
<TrainingTaskToolbar />
<main class="product-positioning-core">
<section class="task-header">
<div class="task-head-main">
@ -277,6 +278,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, checkProductValuePositioningStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "product-value-positioning";
const VALUE_PROPOSITION_CANVAS_TEMPLATE_URL = "/file/training-materials/product-value-positioning/12.产品价值主张画布模版.pptx";

@ -1,5 +1,6 @@
<template>
<div class="hardware-cost-page student-training-shell">
<TrainingTaskToolbar />
<main class="hardware-core">
<section class="task-header">
<div class="task-head-main">
@ -332,6 +333,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile, validateHardwareCostStepOne, validateHardwareCostStepTwo, validateHardwareCostStepThree, validateHardwareCostStepFour } from "@/api/studentTrainingAnswer";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "hardware-cost";
const HARDWARE_COST_GUIDE_URL = "/file/training-materials/hardware-cost/16.饼图制作操作说明.docx";

@ -1,5 +1,6 @@
<template>
<div class="market-selection-page student-training-shell">
<TrainingTaskToolbar />
<main class="training-core">
<section class="task-header">
<div class="task-head-main">
@ -228,6 +229,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "market-opportunity-selection";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="price-page student-training-shell">
<TrainingTaskToolbar />
<main class="price-core">
<section class="task-header">
<div class="task-head-main">
@ -485,6 +486,7 @@ import {
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "product-pricing";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="requirements-doc-page student-training-shell">
<TrainingTaskToolbar />
<main class="requirements-core">
<section class="task-header">
<div class="task-head-main">
@ -232,6 +233,7 @@ import { checkRequirementsDocumentStepOne, getStudentTrainingAnswer, previewRequ
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "requirements-doc";
const { proxy } = getCurrentInstance();

@ -1,5 +1,6 @@
<template>
<div class="architecture-page student-training-shell">
<TrainingTaskToolbar />
<main class="architecture-core">
<section class="task-header">
<div class="task-head-main">
@ -173,6 +174,7 @@ import { parseTrainingArray } from "@/views/training/taskKeyMap";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import { SCENARIO_FIELDS, getCompletedScenarioCount, getRowsForScenarioValidation, getScenarioValidationMessage } from "./productArchitectureScenarioValidation.mjs";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const { proxy } = getCurrentInstance();
const TASK_KEY = "product-structure";

@ -1,5 +1,6 @@
<template>
<div class="release-channel-page student-training-shell">
<TrainingTaskToolbar />
<main class="release-channel-core">
<section class="task-header">
<div class="task-head-main">
@ -198,6 +199,7 @@ import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vu
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, validateReleaseChannelEvaluationStepOne, validateReleaseChannelEvaluationStepTwo, validateReleaseChannelEvaluationStepThree, validateReleaseChannelEvaluationStepFour } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "release-channel-evaluation";
const RELEASE_CHANNEL_GUIDE_URL = "/file/training-materials/release-channel-evaluation/21.实训操作说明.docx";

@ -8,6 +8,7 @@
-->
<template>
<div class="marketing app-container student-training-shell legacy-training-page">
<TrainingTaskToolbar />
<TrainingIntro title="四象限法则">
<template #background>
<p>
@ -103,6 +104,7 @@ import popModel from "@/views/components/popModal.vue";
import * as analysisAPI from "@/api/market-demand-mining.js";
import * as echarts from "echarts";
import { onMounted } from "vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const textarea = ref("");
const dialogVisible = ref(false);
const { proxy } = getCurrentInstance();

Loading…
Cancel
Save