feat(pl): 支持证件建档时原子保存多合同产品绑定并保护受限关系。
This commit is contained in:
1 parent
d8a9aef90a
commit
136a85dda7
4 files changed
+484
-11
No files matched your search
@@ -20,6 +20,13 @@ function harness() {
|
||||
writes: any[] = [],
|
||||
parses: any[] = [],
|
||||
messages: string[] = []
|
||||
let certData: any = {}
|
||||
let saveError = false
|
||||
let pendingSave: ReturnType<typeof deferred> | undefined
|
||||
let pendingGet: ReturnType<typeof deferred> | undefined
|
||||
const user = vue.reactive({ accessToken: 'synthetic', getUserInfo: {} })
|
||||
const contractRequests: any[] = []
|
||||
const productRequests: any[] = []
|
||||
let confirmSwitch = true
|
||||
let pendingOptions: ReturnType<typeof deferred> | undefined
|
||||
const env = {
|
||||
@@ -31,7 +38,7 @@ function harness() {
|
||||
usePersistedListColumns,
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useRoute: () => ({ query: {} }),
|
||||
useUserStore: () => ({ accessToken: 'synthetic', getUserInfo: {} }),
|
||||
useUserStore: () => user,
|
||||
onMounted: () => {},
|
||||
onBeforeUnmount: () => {},
|
||||
fetchGetMenuCodes: async () => ({ all: true }),
|
||||
@@ -45,18 +52,37 @@ function harness() {
|
||||
}
|
||||
},
|
||||
quotaCertApi: {
|
||||
contracts: (keyword: string) => {
|
||||
const pending = deferred()
|
||||
contractRequests.push({ keyword, ...pending })
|
||||
return pending.promise
|
||||
},
|
||||
products: (id: string) => {
|
||||
const pending = deferred()
|
||||
productRequests.push({ id, ...pending })
|
||||
return pending.promise
|
||||
},
|
||||
certificateContracts: async () => [
|
||||
{ id: 'old', contractNo: '历史合同', contractName: '真实标签' }
|
||||
],
|
||||
list: async (kind: string) => {
|
||||
reads.push(kind)
|
||||
return pendingOptions ? pendingOptions.promise : []
|
||||
},
|
||||
get: async (kind: string, id: string) => ({
|
||||
...model.newCert(kind as any),
|
||||
id,
|
||||
certNo: 'EXISTING',
|
||||
attachmentUrls: 'a.png,b.png'
|
||||
}),
|
||||
get: async (kind: string, id: string) =>
|
||||
pendingGet
|
||||
? pendingGet.promise
|
||||
: {
|
||||
...model.newCert(kind as any),
|
||||
id,
|
||||
certNo: 'EXISTING',
|
||||
attachmentUrls: 'a.png,b.png',
|
||||
...certData
|
||||
},
|
||||
save: async (kind: string, body: any, id?: string) => {
|
||||
writes.push({ kind, body, id })
|
||||
if (saveError) throw new Error('保存失败')
|
||||
if (pendingSave) await pendingSave.promise
|
||||
},
|
||||
recognize: (kind: string, url: string) => {
|
||||
const pending = deferred()
|
||||
@@ -89,6 +115,17 @@ function harness() {
|
||||
state.formRef.value = { validate: async () => true, clearValidate: () => {} }
|
||||
return {
|
||||
state,
|
||||
user,
|
||||
deferSave: () => (pendingSave = deferred()),
|
||||
deferGet: () => (pendingGet = deferred()),
|
||||
contractRequests,
|
||||
productRequests,
|
||||
setCert: (value: any) => {
|
||||
certData = value
|
||||
},
|
||||
failSave: () => {
|
||||
saveError = true
|
||||
},
|
||||
reads,
|
||||
writes,
|
||||
parses,
|
||||
@@ -264,3 +301,158 @@ test('closed or switched forms reject delayed recognition results', async () =>
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('多合同一次保存且失败保留输入;受限可见关系只展示并省略提交', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
const bindings = [
|
||||
{
|
||||
id: 'b',
|
||||
contractId: 'old',
|
||||
linkStatus: 'inactive',
|
||||
remark: '保留',
|
||||
orderNum: 4,
|
||||
products: [
|
||||
{
|
||||
id: 'scope',
|
||||
contractProductId: 'p',
|
||||
scopeStatus: 'inactive',
|
||||
remark: '范围',
|
||||
orderNum: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
try {
|
||||
h.setCert({ contractBindings: bindings, bindingsEditable: true })
|
||||
await s.edit({ id: 'existing' })
|
||||
assert.deepEqual(s.form.value.contractBindings, bindings)
|
||||
assert.equal(s.bindingContracts.value[0].contractNo, '历史合同')
|
||||
s.addContractBinding('new')
|
||||
assert.equal(s.form.value.contractBindings.length, 2)
|
||||
h.failSave()
|
||||
await assert.rejects(s.save(), /保存失败/)
|
||||
assert.equal(h.writes.length, 1)
|
||||
assert.equal(h.writes[0].body.contractBindings[0].products[0].id, 'scope')
|
||||
assert.equal(s.formVisible.value, true)
|
||||
assert.equal(s.form.value.contractBindings.length, 2)
|
||||
s.formVisible.value = false
|
||||
h.setCert({ contractBindings: bindings, bindingsEditable: false })
|
||||
await s.edit({ id: 'limited' })
|
||||
s.addContractBinding('forbidden')
|
||||
assert.equal(s.form.value.contractBindings.length, 1)
|
||||
await assert.rejects(s.save())
|
||||
assert.equal('contractBindings' in h.writes[1].body, false)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('产品勾选往返保留原范围ID;关闭重开后拒绝旧合同和产品请求', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.addContractBinding('c')
|
||||
s.form.value.contractBindings[0].products = [
|
||||
{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }
|
||||
]
|
||||
s.setBindingProducts(0, ['p', 'p2'])
|
||||
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
|
||||
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'inactive')
|
||||
const search = s.searchBindingContracts('old')
|
||||
const products = s.loadBindingProducts('c')
|
||||
s.formVisible.value = false
|
||||
await s.edit()
|
||||
h.contractRequests.at(-1).resolve([{ id: 'stale', contractNo: '旧合同' }])
|
||||
h.productRequests.forEach((r) => r.resolve([{ id: 'stale', productName: '旧产品' }]))
|
||||
await Promise.all([search, products])
|
||||
assert.deepEqual(s.bindingContracts.value, [])
|
||||
assert.deepEqual(s.bindingProducts.value, {})
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('移除后重选同一合同保留主关联和产品范围身份', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
h.setCert({
|
||||
contractBindings: [
|
||||
{
|
||||
id: 'binding',
|
||||
contractId: 'old',
|
||||
linkStatus: 'inactive',
|
||||
products: [{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
await s.edit({ id: 'cert' })
|
||||
s.removeContractBinding(0)
|
||||
s.addContractBinding('old')
|
||||
assert.equal(s.form.value.contractBindings[0].id, 'binding')
|
||||
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('后发合同搜索覆盖先发,租户切换使保存和读取旧结果失效', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
const first = s.searchBindingContracts('first'),
|
||||
second = s.searchBindingContracts('second')
|
||||
h.contractRequests[1].resolve([{ id: 'second', contractNo: 'SECOND', contractName: '' }])
|
||||
await second
|
||||
h.contractRequests[0].resolve([{ id: 'first', contractNo: 'FIRST', contractName: '' }])
|
||||
await first
|
||||
assert.equal(s.bindingContracts.value[0].id, 'second')
|
||||
const pending = h.deferSave()
|
||||
const saving = s.save()
|
||||
await vue.nextTick()
|
||||
h.user.accessToken = ''
|
||||
pending.resolve({})
|
||||
await saving
|
||||
assert.equal(h.messages.includes('证件已保存'), false)
|
||||
assert.equal(s.formVisible.value, false)
|
||||
const get = h.deferGet(),
|
||||
editing = s.edit({ id: 'old' })
|
||||
h.user.getUserInfo = { tenantNo: 'other' }
|
||||
get.resolve({ ...model.newCert('purchase'), contractBindings: [] })
|
||||
await editing
|
||||
assert.equal(s.formVisible.value, false)
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test('新增多合同连同证面成功提交一次,移除全部提交明确空集合', async () => {
|
||||
const h = harness(),
|
||||
s = h.state
|
||||
try {
|
||||
await s.edit()
|
||||
s.addContractBinding('one')
|
||||
s.addContractBinding('two')
|
||||
s.setBindingProducts(0, ['p1'])
|
||||
s.setBindingProducts(1, ['p2'])
|
||||
s.form.value.certNo = 'MULTI'
|
||||
await s.save()
|
||||
assert.equal(h.writes.length, 1)
|
||||
assert.equal(h.writes[0].body.certNo, 'MULTI')
|
||||
assert.deepEqual(
|
||||
h.writes[0].body.contractBindings.map((b: any) => b.contractId),
|
||||
['one', 'two']
|
||||
)
|
||||
assert.equal(s.formVisible.value, false)
|
||||
h.setCert({ contractBindings: [{ id: 'b', contractId: 'one', products: [] }] })
|
||||
await s.edit({ id: 'existing' })
|
||||
s.removeContractBinding(0)
|
||||
await s.save()
|
||||
assert.deepEqual(h.writes[1].body.contractBindings, [])
|
||||
} finally {
|
||||
h.stop()
|
||||
}
|
||||
})
|
||||
@@ -261,6 +261,87 @@
|
||||
><el-icon><Plus /></el-icon>添加引用</el-button
|
||||
>
|
||||
</section>
|
||||
<section class="view-section">
|
||||
<h3 class="dialog-section-title">绑定合同产品</h3>
|
||||
<p v-if="form.bindingsEditable === false" class="form-tip">
|
||||
当前仅展示可见合同,绑定范围不可编辑;证面信息仍可保存。
|
||||
</p>
|
||||
<el-select
|
||||
v-if="form.bindingsEditable !== false"
|
||||
:model-value="undefined"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="搜索合同编号或名称,添加合同"
|
||||
:remote-method="searchBindingContracts"
|
||||
:loading="bindingSearching"
|
||||
:disabled="saving"
|
||||
@visible-change="(open: boolean) => open && searchBindingContracts('')"
|
||||
@change="addContractBinding"
|
||||
>
|
||||
<el-option
|
||||
v-for="contract in bindingContracts"
|
||||
:key="contract.id"
|
||||
:value="contract.id"
|
||||
:label="contract.contractNo + ' · ' + contract.contractName"
|
||||
:disabled="
|
||||
form.contractBindings?.some(
|
||||
(b: CertificateContractBinding) => b.contractId === contract.id
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-select>
|
||||
<el-table :data="form.contractBindings || []" stripe>
|
||||
<el-table-column label="合同" min-width="220">
|
||||
<template #default="{ row }">{{ bindingContractLabel(row.contractId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用合同产品" min-width="360">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
multiple
|
||||
:model-value="row.products.map((p: any) => p.contractProductId)"
|
||||
:disabled="
|
||||
saving || form.bindingsEditable === false || !bindingProducts[row.contractId]
|
||||
"
|
||||
placeholder="勾选适用产品"
|
||||
style="width: 100%"
|
||||
@change="(ids: string[]) => setBindingProducts($index, ids)"
|
||||
@visible-change="(open: boolean) => open && loadBindingProducts(row.contractId)"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in bindingProductOptions(row)"
|
||||
:key="product.id"
|
||||
:value="product.id"
|
||||
:label="
|
||||
product.productName +
|
||||
(product.model ? ' / ' + product.model : '') +
|
||||
(product.unit ? '(' + product.unit + ')' : '')
|
||||
"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }"
|
||||
><el-tag :type="row.linkStatus === 'inactive' ? 'info' : 'success'" size="small">{{
|
||||
row.linkStatus === 'inactive' ? '已停用' : '启用'
|
||||
}}</el-tag></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="form.bindingsEditable !== false"
|
||||
label="操作"
|
||||
min-width="260"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ $index }"
|
||||
><el-button :disabled="saving" @click="removeContractBinding($index)"
|
||||
><el-icon><Delete /></el-icon>移除合同</el-button
|
||||
></template
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<el-form-item v-if="editingId" label="停止新增使用">
|
||||
<el-select v-model="form.status" clearable placeholder="保持当前状态">
|
||||
<el-option label="停用(可复核恢复)" value="suspended" />
|
||||
@@ -413,6 +494,8 @@
|
||||
quotaCertApi,
|
||||
type CertType,
|
||||
type QuotaCert,
|
||||
type CertificateContractBinding,
|
||||
type ContractProduct,
|
||||
type ContractOption
|
||||
} from '@/api/pl/quota-cert'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
@@ -524,12 +607,104 @@
|
||||
}
|
||||
}
|
||||
let editSession = 0
|
||||
let bindingSearchEpoch = 0
|
||||
const bindingContracts = ref<ContractOption[]>([])
|
||||
const bindingProducts = ref<Record<string, ContractProduct[]>>({})
|
||||
const bindingSearching = ref(false)
|
||||
const removedBindings = new Map<string, CertificateContractBinding>()
|
||||
const originalScopes = new Map<string, CertificateContractBinding['products']>()
|
||||
function bindingContractLabel(id: string) {
|
||||
const contract = bindingContracts.value.find((item) => item.id === id)
|
||||
return contract
|
||||
? [contract.contractNo, contract.contractName].filter(Boolean).join(' · ')
|
||||
: '合同信息不可用'
|
||||
}
|
||||
function bindingProductOptions(binding: CertificateContractBinding): ContractProduct[] {
|
||||
const options = bindingProducts.value[binding.contractId] || []
|
||||
return [
|
||||
...options,
|
||||
...binding.products
|
||||
.filter((scope) => !options.some((product) => product.id === scope.contractProductId))
|
||||
.map((scope) => ({
|
||||
id: scope.contractProductId,
|
||||
productId: '',
|
||||
productName: '产品信息不可用'
|
||||
}))
|
||||
]
|
||||
}
|
||||
async function searchBindingContracts(keyword: string) {
|
||||
const session = editSession,
|
||||
current = ++bindingSearchEpoch
|
||||
bindingSearching.value = true
|
||||
try {
|
||||
const result = await quotaCertApi.contracts(keyword)
|
||||
if (session !== editSession || current !== bindingSearchEpoch || !formVisible.value) return
|
||||
const selected = new Set(
|
||||
(form.value.contractBindings || []).map((b: CertificateContractBinding) => b.contractId)
|
||||
)
|
||||
bindingContracts.value = [
|
||||
...bindingContracts.value.filter((c) => selected.has(c.id)),
|
||||
...result.filter(
|
||||
(c) => !selected.has(c.id) || !bindingContracts.value.some((old) => old.id === c.id)
|
||||
)
|
||||
]
|
||||
} finally {
|
||||
if (session === editSession && current === bindingSearchEpoch) bindingSearching.value = false
|
||||
}
|
||||
}
|
||||
async function loadBindingProducts(contractId: string) {
|
||||
const session = editSession
|
||||
const result = await quotaCertApi.products(contractId)
|
||||
if (
|
||||
session !== editSession ||
|
||||
!formVisible.value ||
|
||||
!form.value.contractBindings?.some(
|
||||
(b: CertificateContractBinding) => b.contractId === contractId
|
||||
)
|
||||
)
|
||||
return
|
||||
bindingProducts.value = { ...bindingProducts.value, [contractId]: result }
|
||||
}
|
||||
function addContractBinding(contractId: string) {
|
||||
if (
|
||||
!contractId ||
|
||||
saving.value ||
|
||||
form.value.bindingsEditable === false ||
|
||||
form.value.contractBindings?.some(
|
||||
(b: CertificateContractBinding) => b.contractId === contractId
|
||||
)
|
||||
)
|
||||
return
|
||||
form.value.contractBindings ||= []
|
||||
form.value.contractBindings.push(
|
||||
removedBindings.get(contractId) || { contractId, linkStatus: 'active', products: [] }
|
||||
)
|
||||
void loadBindingProducts(contractId).catch(() => {})
|
||||
}
|
||||
function removeContractBinding(index: number) {
|
||||
if (saving.value || form.value.bindingsEditable === false) return
|
||||
const [removed] = form.value.contractBindings.splice(index, 1)
|
||||
removedBindings.set(removed.contractId, removed)
|
||||
}
|
||||
function setBindingProducts(index: number, ids: string[]) {
|
||||
if (saving.value || form.value.bindingsEditable === false) return
|
||||
const binding = form.value.contractBindings[index] as CertificateContractBinding
|
||||
const previous = [...binding.products, ...(originalScopes.get(binding.contractId) || [])]
|
||||
binding.products = ids.map(
|
||||
(id) =>
|
||||
previous.find((p) => p.contractProductId === id) || {
|
||||
contractProductId: id,
|
||||
scopeStatus: 'active'
|
||||
}
|
||||
)
|
||||
}
|
||||
watch(
|
||||
formVisible,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
editSession++
|
||||
recognizing.value = false
|
||||
bindingSearching.value = false
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
@@ -551,9 +726,18 @@
|
||||
if (saving.value) return
|
||||
const session = ++editSession
|
||||
const targetKind = kind.value
|
||||
const value = row
|
||||
? certPayload(targetKind, await quotaCertApi.get(targetKind, row.id))
|
||||
: newCert(targetKind)
|
||||
const cert = row ? await quotaCertApi.get(targetKind, row.id) : null
|
||||
const value = cert
|
||||
? {
|
||||
...certPayload(targetKind, cert),
|
||||
contractBindings: (cert.contractBindings || []).map((b) => ({
|
||||
...b,
|
||||
products: b.products.map((p) => ({ ...p }))
|
||||
})),
|
||||
bindingsEditable: cert.bindingsEditable !== false
|
||||
}
|
||||
: { ...newCert(targetKind), contractBindings: [], bindingsEditable: true }
|
||||
const contracts = row ? await quotaCertApi.certificateContracts(targetKind, row.id) : []
|
||||
const options = targetKind === 'transport' ? await quotaCertApi.list('purchase') : []
|
||||
if (session !== editSession || kind.value !== targetKind) return
|
||||
editingId.value = row?.id || ''
|
||||
@@ -561,10 +745,22 @@
|
||||
selectedFormKind.value = targetKind
|
||||
edited.clear()
|
||||
recognitionImage.value = ''
|
||||
bindingContracts.value = contracts
|
||||
bindingProducts.value = {}
|
||||
bindingSearching.value = false
|
||||
originalScopes.clear()
|
||||
removedBindings.clear()
|
||||
for (const binding of value.contractBindings)
|
||||
originalScopes.set(
|
||||
binding.contractId,
|
||||
binding.products.map((p) => ({ ...p }))
|
||||
)
|
||||
form.value = value
|
||||
purchases.value = options
|
||||
recognizing.value = false
|
||||
formVisible.value = true
|
||||
for (const binding of value.contractBindings)
|
||||
void loadBindingProducts(binding.contractId).catch(() => {})
|
||||
}
|
||||
async function changeFormKind(value: string | number | boolean | undefined) {
|
||||
try {
|
||||
@@ -582,6 +778,7 @@
|
||||
const session = editSession
|
||||
const hasContent =
|
||||
formFields.value.some(([key]) => form.value[key] != null && form.value[key] !== '') ||
|
||||
form.value.contractBindings?.length ||
|
||||
form.value.purchaseRefs?.length ||
|
||||
form.value.vehicleNumbersJson?.length
|
||||
if (hasContent) {
|
||||
@@ -601,7 +798,20 @@
|
||||
const { entryMode, attachmentUrls, remark, orderNum } = form.value
|
||||
formKind.value = value
|
||||
selectedFormKind.value = value
|
||||
form.value = { ...newCert(value), entryMode, attachmentUrls, remark, orderNum }
|
||||
form.value = {
|
||||
...newCert(value),
|
||||
entryMode,
|
||||
attachmentUrls,
|
||||
remark,
|
||||
orderNum,
|
||||
contractBindings: [],
|
||||
bindingsEditable: true
|
||||
}
|
||||
bindingContracts.value = []
|
||||
bindingProducts.value = {}
|
||||
bindingSearching.value = false
|
||||
originalScopes.clear()
|
||||
removedBindings.clear()
|
||||
purchases.value = []
|
||||
edited.clear()
|
||||
formRef.value?.clearValidate()
|
||||
@@ -633,6 +843,7 @@
|
||||
certPayload(targetKind, form.value),
|
||||
editingId.value || undefined
|
||||
)
|
||||
if (session !== editSession || disposed) return
|
||||
formVisible.value = false
|
||||
ElMessage.success('证件已保存')
|
||||
if (kind.value !== targetKind) kind.value = targetKind
|
||||
@@ -771,6 +982,7 @@
|
||||
onMounted(init)
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true
|
||||
editSession++
|
||||
listEpoch++
|
||||
authEpoch++
|
||||
})
|
||||
|
||||
@@ -79,3 +79,54 @@ test('停用产品范围和停用主关联均不展示为可用', () => {
|
||||
)
|
||||
assert.equal(bindingProductEnabled({ ...binding, linkStatus: 'inactive' }, 'p'), false)
|
||||
})
|
||||
|
||||
test('证件绑定保留ID和业务字段,受限或遗漏集合不提交', () => {
|
||||
const contractBindings = [
|
||||
{
|
||||
id: 'b',
|
||||
contractId: 'c',
|
||||
linkStatus: 'inactive',
|
||||
remark: '备注',
|
||||
orderNum: 2,
|
||||
scopeRevision: 7,
|
||||
products: [
|
||||
{
|
||||
id: 's',
|
||||
contractProductId: 'p',
|
||||
scopeStatus: 'inactive',
|
||||
remark: '范围',
|
||||
orderNum: 3,
|
||||
productName: '只读'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
assert.deepEqual(certPayload('purchase', { contractBindings }).contractBindings, [
|
||||
{
|
||||
id: 'b',
|
||||
contractId: 'c',
|
||||
linkStatus: 'inactive',
|
||||
remark: '备注',
|
||||
orderNum: 2,
|
||||
products: [
|
||||
{ id: 's', contractProductId: 'p', scopeStatus: 'inactive', remark: '范围', orderNum: 3 }
|
||||
]
|
||||
}
|
||||
])
|
||||
assert.equal(
|
||||
'contractBindings' in certPayload('purchase', { contractBindings, bindingsEditable: false }),
|
||||
false
|
||||
)
|
||||
assert.equal('contractBindings' in certPayload('purchase', {}), false)
|
||||
assert.deepEqual(certPayload('purchase', { contractBindings: [] }).contractBindings, [])
|
||||
})
|
||||
test('识别不能修改合同绑定或只读字段', () => {
|
||||
const form = { ...newCert('purchase'), contractBindings: [] }
|
||||
mergeCandidates(
|
||||
form,
|
||||
{ contractBindings: [{ contractId: 'c', products: [] }], bindingsEditable: false } as any,
|
||||
new Set()
|
||||
)
|
||||
assert.deepEqual(form.contractBindings, [])
|
||||
assert.equal('bindingsEditable' in form, false)
|
||||
})
|
||||
@@ -67,6 +67,16 @@ export function certPayload(kind: CertType, form: Record<string, any>): CertBody
|
||||
.map((k) => [k, r[k]])
|
||||
)
|
||||
)
|
||||
if (form.bindingsEditable !== false && Array.isArray(form.contractBindings)) {
|
||||
const pick = (row: Record<string, unknown>, keys: string[]) =>
|
||||
Object.fromEntries(keys.filter((key) => key in row).map((key) => [key, row[key]]))
|
||||
result.contractBindings = form.contractBindings.map((binding: Record<string, any>) => ({
|
||||
...pick(binding, ['id', 'contractId', 'linkStatus', 'remark', 'orderNum']),
|
||||
products: binding.products.map((product: Record<string, unknown>) =>
|
||||
pick(product, ['id', 'contractProductId', 'scopeStatus', 'remark', 'orderNum'])
|
||||
)
|
||||
}))
|
||||
}
|
||||
return result as CertBody
|
||||
}
|
||||
|
||||
@@ -76,7 +86,15 @@ export function mergeCandidates(
|
||||
edited: Set<string>
|
||||
) {
|
||||
const target = form as unknown as Record<string, unknown>
|
||||
const allowed = new Set([
|
||||
...commonFields.map(([key]) => key),
|
||||
...purchaseFields.map(([key]) => key),
|
||||
...transportFields.map(([key]) => key),
|
||||
'vehicleNumbersJson',
|
||||
'purchaseRefs'
|
||||
])
|
||||
for (const [key, value] of Object.entries(candidates)) {
|
||||
if (!allowed.has(key)) continue
|
||||
const current = target[key]
|
||||
if (
|
||||
!edited.has(key) &&
|
||||
|
||||
Reference in new issue
Block a user