You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.6 KiB
Vue
187 lines
5.6 KiB
Vue
|
4 months ago
|
<template>
|
||
|
|
<div class="training-legacy-actions training-actions">
|
||
|
|
<el-button class="training-action training-action--save" :loading="saving" @click="saveProgress">保存当前进度</el-button>
|
||
|
|
<el-button class="training-action training-action--submit" :loading="saving" @click="submitAnswer">提交任务</el-button>
|
||
|
|
<el-button class="training-action training-action--reset" :loading="saving" @click="resetAnswer">重新实训</el-button>
|
||
|
|
<el-button class="training-action training-action--export" @click="exportOutcome">导出实训成果</el-button>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
|
||
|
|
import { resolveTrainingTaskKey } from "@/views/training/taskKeyMap";
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
taskKey: {
|
||
|
|
type: String,
|
||
|
|
default: "",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const route = useRoute();
|
||
|
|
const { proxy } = getCurrentInstance();
|
||
|
|
const saving = ref(false);
|
||
|
|
const resolvedTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey));
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
loadSavedAnswer();
|
||
|
|
});
|
||
|
|
|
||
|
|
watch(
|
||
|
|
() => resolvedTaskKey.value,
|
||
|
|
() => loadSavedAnswer()
|
||
|
|
);
|
||
|
|
|
||
|
|
async function loadSavedAnswer() {
|
||
|
|
if (!resolvedTaskKey.value) return;
|
||
|
|
try {
|
||
|
|
const res = await getStudentTrainingAnswer(resolvedTaskKey.value);
|
||
|
|
const saved = parseSavedPayload(res?.data?.step1Answer);
|
||
|
|
if (saved?.fields?.length) {
|
||
|
|
await nextTick();
|
||
|
|
applyFields(saved.fields);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Old pages remain usable when no saved answer exists.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function saveProgress() {
|
||
|
|
await persist("SAVE", "保存成功");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function submitAnswer() {
|
||
|
|
await persist("SUBMIT", "提交成功");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function resetAnswer() {
|
||
|
|
if (!resolvedTaskKey.value || saving.value) return;
|
||
|
|
saving.value = true;
|
||
|
|
try {
|
||
|
|
await deleteStudentTrainingAnswer(resolvedTaskKey.value);
|
||
|
|
clearFields();
|
||
|
|
proxy?.$modal?.msgSuccess?.("已清空");
|
||
|
|
} catch (error) {
|
||
|
|
await saveStudentTrainingAnswer(resolvedTaskKey.value, {
|
||
|
|
saveAction: "RESET",
|
||
|
|
currentStep: 1,
|
||
|
|
});
|
||
|
|
clearFields();
|
||
|
|
proxy?.$modal?.msgSuccess?.("已清空");
|
||
|
|
} finally {
|
||
|
|
saving.value = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function persist(saveAction, message) {
|
||
|
|
if (!resolvedTaskKey.value || saving.value) return;
|
||
|
|
saving.value = true;
|
||
|
|
try {
|
||
|
|
const fields = collectFields();
|
||
|
|
await saveStudentTrainingAnswer(resolvedTaskKey.value, {
|
||
|
|
saveAction,
|
||
|
|
currentStep: 1,
|
||
|
|
step1Answer: JSON.stringify({
|
||
|
|
type: "legacy-dom-form",
|
||
|
|
route: route.path,
|
||
|
|
savedAt: new Date().toISOString(),
|
||
|
|
fields,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
proxy?.$modal?.msgSuccess?.(message);
|
||
|
|
} finally {
|
||
|
|
saving.value = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getScope() {
|
||
|
|
return document.querySelector(".legacy-training-page") || document.querySelector(".training-step-area") || document.body;
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectFields() {
|
||
|
|
const scope = getScope();
|
||
|
|
const controls = Array.from(scope.querySelectorAll("input, textarea, select"))
|
||
|
|
.filter((el) => !el.closest(".training-ai-sidebar") && !el.closest(".training-legacy-actions"));
|
||
|
|
return controls.map((el) => ({
|
||
|
|
selector: buildStableSelector(el),
|
||
|
|
type: el.type || el.tagName.toLowerCase(),
|
||
|
|
value: getControlValue(el),
|
||
|
|
checked: Boolean(el.checked),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyFields(fields) {
|
||
|
|
fields.forEach((field) => {
|
||
|
|
const el = findControl(field.selector);
|
||
|
|
if (!el) return;
|
||
|
|
if (el.type === "checkbox" || el.type === "radio") {
|
||
|
|
el.checked = Boolean(field.checked);
|
||
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
el.value = field.value ?? "";
|
||
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearFields() {
|
||
|
|
collectFields().forEach((field) => {
|
||
|
|
const el = findControl(field.selector);
|
||
|
|
if (!el) return;
|
||
|
|
if (el.type === "checkbox" || el.type === "radio") {
|
||
|
|
el.checked = false;
|
||
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
el.value = "";
|
||
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
|
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildStableSelector(el) {
|
||
|
|
const scope = getScope();
|
||
|
|
const controls = Array.from(scope.querySelectorAll("input, textarea, select"))
|
||
|
|
.filter((item) => !item.closest(".training-ai-sidebar") && !item.closest(".training-legacy-actions"));
|
||
|
|
return `${el.tagName.toLowerCase()}:${controls.indexOf(el)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function findControl(selector) {
|
||
|
|
const [tag, indexText] = String(selector || "").split(":");
|
||
|
|
const index = Number(indexText);
|
||
|
|
if (!tag || Number.isNaN(index)) return null;
|
||
|
|
const scope = getScope();
|
||
|
|
const controls = Array.from(scope.querySelectorAll("input, textarea, select"))
|
||
|
|
.filter((el) => !el.closest(".training-ai-sidebar") && !el.closest(".training-legacy-actions"));
|
||
|
|
return controls[index] && controls[index].tagName.toLowerCase() === tag ? controls[index] : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getControlValue(el) {
|
||
|
|
if (el.type === "checkbox" || el.type === "radio") {
|
||
|
|
return el.value;
|
||
|
|
}
|
||
|
|
return el.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseSavedPayload(value) {
|
||
|
|
if (!value) return null;
|
||
|
|
try {
|
||
|
|
return JSON.parse(value);
|
||
|
|
} catch (error) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function exportOutcome() {
|
||
|
|
const blob = new Blob([JSON.stringify(collectFields(), null, 2)], { type: "application/json;charset=utf-8" });
|
||
|
|
const link = document.createElement("a");
|
||
|
|
link.href = URL.createObjectURL(blob);
|
||
|
|
link.download = `${route.meta?.title || "实训"}-实训成果.json`;
|
||
|
|
document.body.appendChild(link);
|
||
|
|
link.click();
|
||
|
|
document.body.removeChild(link);
|
||
|
|
URL.revokeObjectURL(link.href);
|
||
|
|
}
|
||
|
|
</script>
|