fix(pl): 隔离合同范围弹窗重开后的加载与搜索请求以阻止旧数据保存。

This commit is contained in:
wuzilong committed 2026-11-01 22:13:28 +08:00
1 parent 726575aaf2
commit bbe0144cdd
2 files changed
+158 -12

No files matched your search

+115
View File
@@ -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()
})
@@ -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('合同及产品关联已保存')