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.
dianshang-qianduan/src/views/components/TrainingLegacyActions.vue

254 lines
7.6 KiB
Vue

<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));
const savedPayload = ref(null);
onMounted(() => {
loadSavedAnswer();
document.addEventListener("click", restoreActiveSnapshot, true);
});
onBeforeUnmount(() => {
document.removeEventListener("click", restoreActiveSnapshot, true);
});
watch(
() => resolvedTaskKey.value,
() => loadSavedAnswer()
);
async function loadSavedAnswer() {
if (!resolvedTaskKey.value) return;
try {
const res = await getStudentTrainingAnswer(resolvedTaskKey.value);
const saved = parseSavedPayload(res?.data?.step1Answer);
savedPayload.value = saved;
await restoreSnapshot();
} 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, showMessage = true) {
if (!resolvedTaskKey.value || saving.value) return;
saving.value = true;
try {
const snapshotKey = getSnapshotKey();
const existingSnapshots = normalizeSnapshots(savedPayload.value);
const snapshots = {
...existingSnapshots,
[snapshotKey]: {
fields: collectFields(),
tables: collectTables(),
savedAt: new Date().toISOString(),
},
};
const payload = {
type: "legacy-training-snapshot-v2",
route: route.path,
savedAt: new Date().toISOString(),
snapshots,
};
await saveStudentTrainingAnswer(resolvedTaskKey.value, {
saveAction,
currentStep: 1,
step1Answer: JSON.stringify(payload),
});
savedPayload.value = payload;
if (showMessage) {
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 collectTables() {
const scope = getScope();
return Array.from(scope.querySelectorAll("table"))
.filter((table) => !table.closest(".training-ai-sidebar") && !table.closest(".training-legacy-actions"))
.map((table, tableIndex) => ({
tableIndex,
rows: Array.from(table.querySelectorAll("tr")).map((row) =>
Array.from(row.querySelectorAll("th, td")).map((cell) => cell.innerText.trim())
),
}));
}
function getSnapshotKey() {
const activeTab = getScope().querySelector(".top_item .active");
return activeTab?.innerText?.trim() || route.path;
}
function normalizeSnapshots(payload) {
if (!payload) return {};
if (payload.snapshots && typeof payload.snapshots === "object") return payload.snapshots;
if (Array.isArray(payload.fields)) {
return {
[payload.route || route.path]: {
fields: payload.fields,
tables: [],
savedAt: payload.savedAt || "",
},
};
}
return {};
}
async function restoreSnapshot() {
const snapshot = normalizeSnapshots(savedPayload.value)[getSnapshotKey()];
if (!snapshot?.fields?.length) return;
await nextTick();
applyFields(snapshot.fields);
}
function restoreActiveSnapshot(event) {
if (!event.target?.closest?.(".top_item button")) return;
persist("SAVE", "", false)
.catch(() => proxy?.$modal?.msgError?.("切换前的实训数据保存失败"))
.finally(() => {
setTimeout(() => {
restoreSnapshot();
}, 0);
});
}
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>