diff --git a/src/views/teacherEnd/task/index.vue b/src/views/teacherEnd/task/index.vue index fc52c3a..d33253f 100644 --- a/src/views/teacherEnd/task/index.vue +++ b/src/views/teacherEnd/task/index.vue @@ -46,6 +46,8 @@ const configured = ref(false); const loading = ref(false); const saving = ref(false); const tableRef = ref(null); +let activeLoadGeneration = 0; +let selectionSyncGeneration = -1; const currentUserId = computed(() => userStore.userInfo.userId); const teachingClassList = computed(() => classList.value.filter((item) => item.classType === "TEACHING" || !item.classType && item.createdBy)); @@ -58,48 +60,68 @@ async function loadClasses() { classId.value = teachingClassList.value[0]?.schoolClassId || ""; } -async function loadTasks() { +function isActiveLoad(requestedClassId, requestGeneration) { + return classId.value === requestedClassId && requestGeneration === activeLoadGeneration; +} + +function syncTableSelection(taskKeys, requestGeneration) { + selectionSyncGeneration = requestGeneration; + tableRef.value?.clearSelection(); + proxy.$nextTick(() => { + if (requestGeneration !== activeLoadGeneration) return; + const selected = new Set(taskKeys); + tasks.value.forEach((task) => tableRef.value?.toggleRowSelection(task, selected.has(task.taskKey))); + proxy.$nextTick(() => { + if (requestGeneration === activeLoadGeneration) selectionSyncGeneration = -1; + }); + }); +} + +async function loadTasks(requestGeneration) { if (!classId.value) { tasks.value = []; - return; + return false; } const requestedClassId = classId.value; tasks.value = []; const res = await listClassTrainingTasks(classId.value, { enabledOnly: false }); - if (classId.value !== requestedClassId) return; + if (!isActiveLoad(requestedClassId, requestGeneration)) return false; tasks.value = res.data || []; + return true; } -function applySelection() { - tableRef.value?.clearSelection(); - proxy.$nextTick(() => { - const selected = new Set(selectedTaskKeys.value); - tasks.value.forEach((task) => tableRef.value?.toggleRowSelection(task, selected.has(task.taskKey))); - }); +function applySelection(requestGeneration) { + syncTableSelection(selectedTaskKeys.value, requestGeneration); } async function loadPublication() { + const requestGeneration = ++activeLoadGeneration; selectedTaskKeys.value = []; configured.value = false; - if (!classId.value) return; + syncTableSelection([], requestGeneration); + if (!classId.value) { + tasks.value = []; + return; + } const requestedClassId = classId.value; loading.value = true; try { - await loadTasks(); - if (classId.value !== requestedClassId) return; + await loadTasks(requestGeneration); + if (!isActiveLoad(requestedClassId, requestGeneration)) return; const res = await indexApi.getTaskPublication({ classId: requestedClassId }); - if (classId.value !== requestedClassId) return; + if (!isActiveLoad(requestedClassId, requestGeneration)) return; configured.value = res.data?.configured === true; selectedTaskKeys.value = configured.value ? (res.data?.taskKeys || []) : tasks.value.map((task) => task.taskKey); - applySelection(); + applySelection(requestGeneration); } finally { - loading.value = false; + if (requestGeneration === activeLoadGeneration) loading.value = false; } } function handleSelectionChange(selection) { + if (selectionSyncGeneration === activeLoadGeneration) return; selectedTaskKeys.value = selection.map((task) => task.taskKey); } diff --git a/tests/task-publication.static.test.cjs b/tests/task-publication.static.test.cjs index 9afa785..82e2f1b 100644 --- a/tests/task-publication.static.test.cjs +++ b/tests/task-publication.static.test.cjs @@ -8,12 +8,17 @@ const taskPage = read("src/views/teacherEnd/task/index.vue"); const sidebar = read("src/layout/components/Sidebar/index.vue"); const api = read("src/api/teacher.js"); -assert(taskPage.includes("listClassTrainingTasks"), "task allocation must use training tasks as its data source"); +assert(taskPage.includes('import { listClassTrainingTasks } from "@/api/trainingTask";'), "task allocation must import the class-scoped training task source"); assert(taskPage.includes("listClassTrainingTasks"), "task allocation must load training tasks for the selected teaching class"); assert(taskPage.includes("await listClassTrainingTasks(classId.value, { enabledOnly: false })"), "class task loading must request disabled and enabled tasks"); assert(taskPage.includes("tasks.value = [];\n const res = await listClassTrainingTasks"), "class task loading must clear prior class rows before requesting the selected class"); assert((taskPage.match(/const requestedClassId = classId\.value;/g) || []).length >= 2, "task and publication loading must capture the requested class"); -assert((taskPage.match(/if \(classId\.value !== requestedClassId\) return;/g) || []).length >= 2, "stale class requests must not update task rows or publication selection"); +assert((taskPage.match(/if \(!isActiveLoad\(requestedClassId, requestGeneration\)\) return/g) || []).length >= 2, "stale class requests must not update task rows or publication selection"); +assert(taskPage.includes("let activeLoadGeneration = 0;"), "overlapping class loads must use a shared generation"); +assert(taskPage.includes("const requestGeneration = ++activeLoadGeneration;"), "each class load must advance the active generation"); +assert(taskPage.includes("if (requestGeneration === activeLoadGeneration) loading.value = false;"), "only the active class load may clear the loading indicator"); +assert(taskPage.includes("let selectionSyncGeneration = -1;"), "programmatic selection synchronization must be tracked"); +assert(taskPage.includes("if (selectionSyncGeneration === activeLoadGeneration) return;"), "programmatic table selection events must not overwrite task keys"); assert(taskPage.includes("getTaskPublication"), "task allocation must load the selected class publication state"); assert(taskPage.includes("tasks.value.map((task) => task.taskKey)"), "an unconfigured class must default to all tasks selected"); assert(taskPage.includes("保存发布配置"), "task allocation must require an explicit save");