diff --git a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs index 26d93c4..53decb7 100644 --- a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +++ b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs @@ -657,7 +657,20 @@ test('六个基础资料页面逐页满足 API、权限、分页和安全删除 assert.match(printHandler, /document\.createElement\(['"](?:h1|img|style)['"]\)/) assert.match(printHandler, /\.textContent\s*=/) assert.match(printHandler, /['"]load['"][\s\S]*?printWindow\.print\(\)/) - assert.match(printHandler, /['"]afterprint['"][\s\S]*?URL\.revokeObjectURL/) + assert.match(printHandler, /let closeMonitorId;/) + assert.match(printHandler, /let cleanupFinished = false;/) + assert.match(printHandler, /const cleanupPrint = \(\) => \{\s*if \(cleanupFinished\) return;\s*cleanupFinished = true;/) + assert.match(printHandler, /window\.clearInterval\(closeMonitorId\)/) + assert.equal((printHandler.match(/URL\.revokeObjectURL\(objectUrl\)/g) || []).length, 1) + assert.equal((printHandler.match(/setPrinting\(row\.palletId, false\)/g) || []).length, 1) + assert.match(printHandler, /closeMonitorId = window\.setInterval\(\(\) => \{\s*if \(printWindow\.closed\) cleanupPrint\(\);\s*}, \d+\)/) + assert.ok( + printHandler.indexOf('window.setInterval(') < printHandler.indexOf('getPalletQrcode('), + '打印窗口关闭监测必须在异步二维码请求前启动' + ) + assert.match(printHandler, /['"]error['"][\s\S]*?cleanupPrint\(\)[\s\S]*?printWindow\.close\(\)/) + assert.match(printHandler, /['"]afterprint['"][\s\S]*?cleanupPrint\(\)[\s\S]*?printWindow\.close\(\)/) + assert.match(printHandler, /\.catch\(\(\) => \{\s*cleanupPrint\(\)/) assert.doesNotMatch(printHandler, /document\.write|innerHTML|BarcodeDetector|getUserMedia|camera/i) assert.match(source, /v-if="isEdit"[\s\S]*?v-model="form\.palletCode"[\s\S]*?readonly/) assert.match(source, /v-if="isEdit"[\s\S]*?v-model="form\.qrToken"[\s\S]*?readonly/) @@ -682,6 +695,17 @@ test('业务用户分配组件使用固定映射并允许空数组完整替换', assert.match(source, /const resourceApiMap = Object\.freeze\(\{[\s\S]*?site:\s*Object\.freeze\(\{\s*getUsers:\s*getSiteUsers,\s*assignUsers:\s*assignSiteUsers\s*}\)[\s\S]*?warehouse:\s*Object\.freeze\(\{\s*getUsers:\s*getWarehouseUsers,\s*assignUsers:\s*assignWarehouseUsers\s*}\)[\s\S]*?}\)/) assert.doesNotMatch(source, /defineProps\(/) assert.match(source, /function show\(\{ resourceType, resourceId, resourceName }\)/) + assert.match(source, /let loadGeneration = 0;/) + assert.match(source, /function isCurrentUserLoad\(loadContext\)\s*{[\s\S]*?loadContext\.generation === loadGeneration[\s\S]*?loadContext\.resourceType === resourceType\.value[\s\S]*?loadContext\.resourceId === resourceId\.value[\s\S]*?}/) + const loadUsersSource = source.slice(source.indexOf('function loadUsers()'), source.indexOf('function resetDialogState()')) + const staleLoadGuard = 'if \\(!isCurrentUserLoad\\(loadContext\\)\\) \\{\\s*return;\\s*}' + assert.match(loadUsersSource, /const loadContext = \{\s*generation: \+\+loadGeneration,\s*resourceType: resourceType\.value,\s*resourceId: resourceId\.value\s*};/) + assert.match(loadUsersSource, /resourceApi\.getUsers\(loadContext\.resourceId\)/) + assert.match(loadUsersSource, new RegExp(`\\.then\\(\\(response\\) => \\{\\s*${staleLoadGuard}`)) + assert.match(loadUsersSource, new RegExp(`\\.catch\\(\\(\\) => \\{\\s*${staleLoadGuard}`)) + assert.match(loadUsersSource, new RegExp(`\\.finally\\(\\(\\) => \\{\\s*${staleLoadGuard}`)) + const resetDialogSource = source.slice(source.indexOf('function resetDialogState()'), source.indexOf('function show(')) + assert.match(resetDialogSource, /loadGeneration \+= 1;/) assert.match(source, /response\.data/) assert.match(source, /userId:\s*row\.userId,\s*userName:\s*row\.userName,\s*nickName:\s*row\.nickName,\s*assigned:\s*row\.assigned === true/) assert.doesNotMatch(source, /\.\.\.row|email:\s*row\.|phonenumber:\s*row\.|status:\s*row\./) diff --git a/shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue b/shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue index 8c4bc46..265182f 100644 --- a/shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue +++ b/shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue @@ -104,6 +104,7 @@ const users = ref([]); const selectedUserIds = ref([]); const userTableRef = ref(); const restoringSelection = ref(false); +let loadGeneration = 0; const dialogTitle = computed(() => { const label = resourceType.value === "site" ? "客户现场" : "仓库"; @@ -171,12 +172,26 @@ function handleRowClick(row, column) { restoreSelection(); } +function isCurrentUserLoad(loadContext) { + return loadContext.generation === loadGeneration + && loadContext.resourceType === resourceType.value + && loadContext.resourceId === resourceId.value; +} + function loadUsers() { - const resourceApi = resourceApiMap[resourceType.value]; + const loadContext = { + generation: ++loadGeneration, + resourceType: resourceType.value, + resourceId: resourceId.value + }; + const resourceApi = resourceApiMap[loadContext.resourceType]; loading.value = true; loadFailed.value = false; loadError.value = ""; - resourceApi.getUsers(resourceId.value).then((response) => { + resourceApi.getUsers(loadContext.resourceId).then((response) => { + if (!isCurrentUserLoad(loadContext)) { + return; + } const rows = Array.isArray(response.data) ? response.data : []; users.value = normalizeUserRows(rows); selectedUserIds.value = users.value @@ -184,16 +199,24 @@ function loadUsers() { .map((user) => user.userId); restoreSelection(); }).catch(() => { + if (!isCurrentUserLoad(loadContext)) { + return; + } users.value = []; selectedUserIds.value = []; loadError.value = "用户列表加载失败,请稍后重试"; loadFailed.value = true; }).finally(() => { + if (!isCurrentUserLoad(loadContext)) { + return; + } loading.value = false; }); } function resetDialogState() { + loadGeneration += 1; + loading.value = false; loadError.value = ""; keyword.value = ""; resourceType.value = ""; diff --git a/shierfeng-fastapi-frontend/src/views/master/pallet/index.vue b/shierfeng-fastapi-frontend/src/views/master/pallet/index.vue index 247507b..f8f4fa7 100644 --- a/shierfeng-fastapi-frontend/src/views/master/pallet/index.vue +++ b/shierfeng-fastapi-frontend/src/views/master/pallet/index.vue @@ -255,14 +255,25 @@ function handlePrint(row) { } let objectUrl; + let closeMonitorId; + let cleanupFinished = false; setPrinting(row.palletId, true); - const revokeObjectUrl = () => { + const cleanupPrint = () => { + if (cleanupFinished) return; + cleanupFinished = true; + if (closeMonitorId !== undefined) { + window.clearInterval(closeMonitorId); + closeMonitorId = undefined; + } if (objectUrl) { URL.revokeObjectURL(objectUrl); objectUrl = undefined; } setPrinting(row.palletId, false); }; + closeMonitorId = window.setInterval(() => { + if (printWindow.closed) cleanupPrint(); + }, 250); getPalletQrcode(row.palletId).then((qrCodeBlob) => { if (printWindow.closed) throw new Error("打印窗口已关闭"); @@ -282,16 +293,12 @@ function handlePrint(row) { printWindow.print(); }, { once: true }); imageElement.addEventListener("error", () => { - revokeObjectUrl(); + cleanupPrint(); printWindow.close(); proxy.$modal.msgError("二维码图片加载失败"); }, { once: true }); printWindow.addEventListener("afterprint", () => { - if (objectUrl) { - URL.revokeObjectURL(objectUrl); - objectUrl = undefined; - } - setPrinting(row.palletId, false); + cleanupPrint(); printWindow.close(); }, { once: true }); @@ -299,7 +306,7 @@ function handlePrint(row) { printWindow.document.head.append(styleElement); printWindow.document.body.replaceChildren(titleElement, imageElement); }).catch(() => { - revokeObjectUrl(); + cleanupPrint(); if (!printWindow.closed) printWindow.close(); proxy.$modal.msgError("二维码获取失败"); });