From bbe0144cdde320d972af211452d0ffdf693596c7 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 1 Nov 2026 22:13:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E9=9A=94=E7=A6=BB=E5=90=88=E5=90=8C?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E5=BC=B9=E7=AA=97=E9=87=8D=E5=BC=80=E5=90=8E?= =?UTF-8?q?=E7=9A=84=E5=8A=A0=E8=BD=BD=E4=B8=8E=E6=90=9C=E7=B4=A2=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=BB=A5=E9=98=BB=E6=AD=A2=E6=97=A7=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-admin/scripts/quota-statistics.test.ts | 115 ++++++++++++++++++ .../pl/quota-cert/ContractScopeDialog.vue | 55 +++++++-- 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/web-admin/scripts/quota-statistics.test.ts b/web-admin/scripts/quota-statistics.test.ts index 54e5e4174..d711c1d6e 100644 --- a/web-admin/scripts/quota-statistics.test.ts +++ b/web-admin/scripts/quota-statistics.test.ts @@ -375,3 +375,118 @@ test('quick search and number header filter are sent as independent conditions', await flush() h.stop() }) + +import { bindingPayload } from '../src/views/pl/quota-cert/model' +function scopeDialogHarness() { + const calls: any[] = [], + unmount: Function[] = [] + const queue = (method: string, args: any[]) => { + const p = deferred() + calls.push({ method, args, ...p }) + return p.promise + } + const api = Object.fromEntries( + ['list', 'contracts', 'products', 'bindings', 'saveBindings'].map((method) => [ + method, + (...args: any[]) => queue(method, args) + ]) + ) + const source = ts.createSourceFile( + 'scope.ts', + parse( + readFileSync( + new URL('../src/views/pl/quota-cert/ContractScopeDialog.vue', import.meta.url), + 'utf8' + ) + ).descriptor.scriptSetup!.content, + ts.ScriptTarget.Latest, + true + ) + const code = ts.transpile( + source.statements + .filter((s) => !ts.isImportDeclaration(s)) + .map((s) => s.getText(source)) + .join('\n'), + { target: ts.ScriptTarget.ESNext } + ) + const env = { + ...Object.fromEntries( + Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default') + ), + defineProps: () => ({ canEdit: true, fixedContractId: 'c1' }), + defineEmits: () => () => {}, + defineExpose: () => {}, + onBeforeUnmount: (fn: Function) => unmount.push(fn), + quotaCertApi: api, + quotaStatisticsApi: { contract: (...args: any[]) => queue('contract', args) }, + bindingPayload, + ElMessage: { success: () => {} } + } + const scope = vue.effectScope() + const state = scope.run(() => + new Function( + ...Object.keys(env), + `${code};return {open, visible, loading, searching, loadedContractId, bindings, save, searchContracts, contracts}` + )(...Object.values(env)) + ) + return { + state, + calls, + stop() { + unmount.forEach((fn) => fn()) + scope.stop() + } + } +} +test('closing and reopening same contract rejects old scope response and keeps save disabled', async () => { + const h = scopeDialogHarness(), + s = h.state + const first = s.open('c1') + h.calls[0].resolve([]) + h.calls[1].resolve([]) + h.calls[2].resolve({ id: 'c1', contractNo: 'HT-1', contractName: '合同' }) + await flush() + assert.deepEqual( + h.calls.slice(3).map((c) => c.method), + ['products', 'bindings'] + ) + s.visible.value = false + const second = s.open('c1') + h.calls[3].resolve([{ id: 'old-product' }]) + h.calls[4].resolve([{ id: 'old-binding', certType: 'purchase', certId: 'p', products: [] }]) + await first + assert.equal(s.loadedContractId.value, '') + assert.equal(s.loading.value, true) + assert.deepEqual(s.bindings.value, []) + const count = h.calls.length + await s.save() + assert.equal(h.calls.length, count) + h.calls[5].resolve([]) + h.calls[6].resolve([]) + h.calls[7].resolve({ id: 'c1', contractNo: 'HT-1', contractName: '合同' }) + await flush() + h.calls[8].resolve([{ id: 'new-product' }]) + h.calls[9].resolve([]) + await second + assert.equal(s.loadedContractId.value, 'c1') + assert.equal(s.loading.value, false) + h.stop() +}) +test('closing dialog invalidates pending searches including their finally state', async () => { + const h = scopeDialogHarness(), + s = h.state + s.visible.value = true + const old = s.searchContracts('old') + s.visible.value = false + s.visible.value = true + const fresh = s.searchContracts('new') + h.calls[0].resolve([{ id: 'old' }]) + await old + assert.deepEqual(s.contracts.value, []) + assert.equal(s.searching.value, true) + h.calls[1].resolve([{ id: 'new' }]) + await fresh + assert.equal(s.contracts.value[0].id, 'new') + assert.equal(s.searching.value, false) + h.stop() +}) diff --git a/web-admin/src/views/pl/quota-cert/ContractScopeDialog.vue b/web-admin/src/views/pl/quota-cert/ContractScopeDialog.vue index af2ce2ced..f1727c371 100644 --- a/web-admin/src/views/pl/quota-cert/ContractScopeDialog.vue +++ b/web-admin/src/views/pl/quota-cert/ContractScopeDialog.vue @@ -215,18 +215,33 @@ return `车号:${c?.vehicleNumbersJson?.join('、') || '未填写'};引用:${c?.purchaseRefs?.map((r) => r.referenceNo).join('、') || '未填写'}` } let sessionVersion = 0 - onBeforeUnmount(() => { + let loadVersion = 0 + let searchVersion = 0 + function invalidateSession() { sessionVersion++ loadVersion++ - }) + searchVersion++ + loadedContractId.value = '' + loading.value = false + searching.value = false + } + watch( + visible, + (value) => { + if (!value) invalidateSession() + }, + { flush: 'sync' } + ) + onBeforeUnmount(invalidateSession) async function searchContracts(keyword = '') { - const version = sessionVersion + const session = sessionVersion + const version = ++searchVersion searching.value = true try { const result = await quotaCertApi.contracts(keyword) - if (version === sessionVersion) contracts.value = result + if (session === sessionVersion && version === searchVersion) contracts.value = result } finally { - searching.value = false + if (session === sessionVersion && version === searchVersion) searching.value = false } } async function loadFixedContract(id: string) { @@ -239,7 +254,8 @@ } async function open(id = '') { if (saving.value) return - const session = ++sessionVersion + invalidateSession() + const session = sessionVersion visible.value = true id = props.fixedContractId || id contractId.value = id @@ -247,6 +263,8 @@ bindings.value = [] original.value = [] products.value = [] + selectedProducts.value = [] + contracts.value = [] loading.value = true try { const [purchase, transport] = await Promise.all([ @@ -259,11 +277,11 @@ certificates.transport = transport if (id) await loadContract() } finally { - loading.value = false + if (session === sessionVersion) loading.value = false } } - let loadVersion = 0 async function loadContract() { + const session = sessionVersion const version = ++loadVersion const id = contractId.value loadedContractId.value = '' @@ -273,7 +291,7 @@ quotaCertApi.products(id), quotaCertApi.bindings(id) ]) - if (version !== loadVersion) return + if (session !== sessionVersion || version !== loadVersion) return products.value = rows bindings.value = bindingPayload(scope) original.value = JSON.parse(JSON.stringify(bindings.value)) @@ -282,7 +300,7 @@ ] loadedContractId.value = id } finally { - if (version === loadVersion) loading.value = false + if (session === sessionVersion && version === loadVersion) loading.value = false } } function setCertificates(kind: CertType, ids: string[]) { @@ -321,13 +339,26 @@ ) } async function save() { - if (!props.canEdit) return + if ( + !props.canEdit || + !visible.value || + loading.value || + !contractId.value || + loadedContractId.value !== contractId.value + ) + return + const session = sessionVersion const targetContract = contractId.value const version = loadVersion saving.value = true try { const result = await quotaCertApi.saveBindings(targetContract, bindingPayload(bindings.value)) - if (targetContract !== contractId.value || version !== loadVersion) return + if ( + session !== sessionVersion || + targetContract !== contractId.value || + version !== loadVersion + ) + return bindings.value = bindingPayload(result) original.value = JSON.parse(JSON.stringify(bindings.value)) ElMessage.success('合同及产品关联已保存')