feat(pl): 合同用证改为客户用证并修复列表筛选。

证件额度按客户展示;Web 客户列表改用 toQueryParams,修复 buildQuery is not a function。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-15 14:46:46 +08:00
1 parent d5f3a79ea5
commit ea46761f40
29 files changed
+1777 -295

No files matched your search

+17 -4
View File
@@ -203,13 +203,14 @@ test('real template keeps plans quota proof order and separate lazy contract sco
canViewQuotaCert: true,
canViewProof: true,
detailActiveTab: 'proof',
detailContract: { id: 42 }
detailContract: { id: 42, customerId: 'cust-42' }
}
const rows = panes(ctx)
assert.deepEqual(
rows.map((v: any) => v.props.name),
['plans', 'quota', 'proof']
)
assert.equal(rows[1].props.label, '客户证件与额度')
assert.equal(rows[2].props.label, '运输计划合法使用证明')
assert.equal(rows[2].props.lazy, '')
assert.equal(panel(rows[1]), undefined)
@@ -219,14 +220,19 @@ test('real template keeps plans quota proof order and separate lazy contract sco
assert.equal(proof.props.active, true)
assert.equal(proof.props.embedded, '')
ctx.detailContract.id = 43
ctx.detailContract.customerId = 'cust-43'
ctx.currentDetailId = 43
assert.equal(panel(panes(ctx)[2]).key, 43)
ctx.detailActiveTab = 'quota'
const quota = panel(panes(ctx)[1])
assert.equal(quota.type?.name || quota.type, 'CustomerCertQuotaPanel')
assert.equal(quota.props['customer-id'] ?? quota.props.customerId, 'cust-43')
assert.equal(quota.props.active, true)
ctx.canViewQuotaCert = false
assert.deepEqual(
panes(ctx).map((v: any) => v.props.name),
['plans', 'proof']
)
ctx.detailActiveTab = 'quota'
assert.equal(panel(panes(ctx)[1]), undefined)
})
@@ -265,7 +271,14 @@ test('mounted template unmounts old contract and never mounts quota for proof-on
}
}),
StopDocuments: Child('proof'),
ContractQuotaPanel: Child('quota'),
CustomerCertQuotaPanel: vue.defineComponent({
props: ['customerId', 'active'],
setup(props) {
lifecycle.push(`mount:quota:${props.customerId}`)
vue.onBeforeUnmount(() => lifecycle.push(`unmount:quota:${props.customerId}`))
return () => vue.h('span')
}
}),
PlContractLinkedPlansPanel: vue.defineComponent({
setup() {
return () => vue.h('span')
@@ -282,7 +295,7 @@ test('mounted template unmounts old contract and never mounts quota for proof-on
canViewQuotaCert: false,
canViewProof: true,
detailActiveTab: 'proof',
detailContract: { id: 'A' }
detailContract: { id: 'A', customerId: 'cust-A' }
})
const renderer = vue.createRenderer<any, any>({
createElement: () => ({ children: [] }),
@@ -0,0 +1,75 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
test('web contract detail embeds customer cert panel and renamed tab', () => {
const source = readFileSync(
new URL('../src/views/crm/contract/components/ContractDetailDialog.vue', import.meta.url),
'utf8'
)
assert.match(source, /label="客户证件与额度"/)
assert.match(source, /CustomerCertQuotaPanel/)
assert.doesNotMatch(source, /ContractQuotaPanel/)
assert.match(source, /:customer-id="detailContract\.customerId"/)
})
test('h5 contract detail includes customer cert tab', () => {
const source = readFileSync(
new URL('../src/views/m/crm/contract/detail.vue', import.meta.url),
'utf8'
)
assert.match(source, /CustomerCertQuotaTab/)
assert.match(source, /客户证件与额度/)
assert.match(source, /canViewQuotaCert/)
assert.match(source, /:customer-id="contract\.customerId"/)
})
test('customer cert panel lists by customerId without contract bindings', () => {
const source = readFileSync(
new URL('../src/views/pl/contract-quota/CustomerCertQuotaPanel.vue', import.meta.url),
'utf8'
)
assert.match(source, /quotaCertApi\.list\('purchase', \{ customerId \}\)/)
assert.match(source, /quotaCertApi\.list\('transport', \{ customerId \}\)/)
assert.match(source, /有效次数/)
assert.match(source, /singleUseConsumed/)
assert.doesNotMatch(source, /saveBindings|openCreate|bindings\(/)
})
test('standalone contract-quota page is customer quota list', () => {
const panel = readFileSync(
new URL('../src/views/pl/contract-quota/CustomerQuotaPanel.vue', import.meta.url),
'utf8'
)
const wrapper = readFileSync(
new URL('../src/views/pl/contract-quota/ContractQuotaPanel.vue', import.meta.url),
'utf8'
)
const router = readFileSync(new URL('../src/router/modules/pl.ts', import.meta.url), 'utf8')
assert.match(panel, /客户用证/)
assert.match(panel, /quotaStatisticsApi\.customers/)
assert.match(wrapper, /CustomerQuotaPanel/)
assert.match(router, /title: '客户用证'/)
})
test('web customer quota panel uses toQueryParams from useServerColumnFilters', () => {
const panel = readFileSync(
new URL('../src/views/pl/contract-quota/CustomerQuotaPanel.vue', import.meta.url),
'utf8'
)
assert.match(panel, /toQueryParams/)
assert.match(panel, /\.\.\.toQueryParams\(\)/)
assert.doesNotMatch(panel, /\bbuildQuery\b/)
assert.doesNotMatch(panel, /\bresetFilters\b/)
assert.match(panel, /clearAllFilters/)
assert.match(panel, /watchVisibleColumns/)
})
test('h5 customer quota tab does not call buildQuery', () => {
const tab = readFileSync(
new URL('../src/views/m/pl/plan-cert/CustomerTab.vue', import.meta.url),
'utf8'
)
assert.doesNotMatch(tab, /\bbuildQuery\b/)
assert.match(tab, /quotaStatisticsApi\.customers/)
})
+10 -4
View File
@@ -7,7 +7,11 @@ const readyTab = readFileSync(
'utf8'
)
const contractTab = readFileSync(
new URL('../src/views/m/pl/plan-cert/ContractTab.vue', import.meta.url),
new URL('../src/views/m/pl/plan-cert/CustomerTab.vue', import.meta.url),
'utf8'
)
const planCertIndex = readFileSync(
new URL('../src/views/m/pl/plan-cert/index.vue', import.meta.url),
'utf8'
)
const archiveForm = readFileSync(
@@ -30,9 +34,11 @@ test('H5 证书齐备对齐 2.0:待完善列表与完善并关闭,且文案
assert.doesNotMatch(readyTab, /也不扣减额度/)
})
test('H5 合同用证页对新规则给出只读提示', () => {
assert.match(contractTab, /客户\+证用品类/)
assert.match(contractTab, /仅供历史查看/)
test('H5 客户用证按客户列表展示证件', () => {
assert.match(contractTab, /quotaStatisticsApi\.customers/)
assert.match(contractTab, /\/m\/pl\/plan-cert\/customer\//)
assert.match(contractTab, /暂无客户用证数据/)
assert.match(planCertIndex, /title: '客户用证'/)
})
test('H5 建档页使用关联购买证且无证面引用残留', () => {
+61 -66
View File
@@ -105,6 +105,9 @@ function harness(fixed?: string) {
useServerColumnFilters,
createStatisticsLoader,
quotaStatisticsApi: api,
certQuotaRuleApi: {
get: async () => ({ configured: false })
},
onBeforeUnmount: (fn: Function) => unmount.push(fn)
}
const src = ts.createSourceFile(
@@ -154,16 +157,21 @@ test('cert tabs keep their identities and legal proof is a sibling menu', () =>
assert.equal(legal.meta!.title, '合法使用证明')
assert.deepEqual(
group.children!.filter((r) => !r.meta?.isHide).map((r) => r.path),
['quota-cert', 'contract-quota', 'certificate-usage', 'plan-cert', 'cert-ready']
['quota-cert', 'cert-category', 'contract-quota', 'certificate-usage', 'plan-cert', 'cert-ready']
)
assert.equal(group.children![0].id, 'PL_QUOTA_CERT')
assert.equal(group.children![3].id, 'PL00000000000000000000000000015')
assert.equal(group.children!.find((r) => r.path === 'contract-quota')?.meta?.title, '客户用证')
assert.equal(
group.children!.find((r) => r.path === 'plan-cert')?.id,
'PL00000000000000000000000000015'
)
assert.deepEqual(
group.children!.filter((r) => !r.meta?.isHide).map((r) => r.meta!.menuCode),
[
'pl:quota-cert:list',
'pl:quota-cert:list',
'pl:quota-cert:list',
'pl:quota-cert:list',
'pl:plan-cert:list',
'pl:cert-ready:list'
]
@@ -648,71 +656,58 @@ function compiledSfc(path: string, dependencies: Record<string, any>, inlineTemp
)
return new Function(...Object.keys(env), `${code}; return component`)(...Object.values(env))
}
for (const wrapper of [
'contract-quota/ContractQuotaPanel.vue',
'certificate-usage/CertificateUsagePanel.vue'
]) {
for (const active of [undefined, false])
test(`compiled ${wrapper} ${active === false ? 'inactive skips requests' : 'defaults to active and loads'}`, async () => {
const calls: string[] = []
const dependencies = {
useUserStore: () => vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } }),
useRouter: () => ({ push() {}, replace() {} }),
fetchGetMenuCodes: async () => {
calls.push('permissions')
return { all: true }
},
quotaStatisticsApi: {
contracts: async () => {
calls.push('contracts')
return { list: [], total: 0 }
},
certificates: async () => {
calls.push('certificates')
return { list: [], total: 0 }
}
},
usePersistedListColumns,
useServerColumnFilters,
createStatisticsLoader,
useElementSize: () => ({ width: vue.ref(1120) }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
statusLabel: statisticsLabels.statusLabel,
contractTypeLabel: statisticsLabels.contractTypeLabel
}
// Compile the real inner setup too; only its visual child tree is omitted here.
const StatisticsPanel = compiledSfc(
'../src/views/pl/quota-statistics/StatisticsPanel.vue',
dependencies,
false
)
StatisticsPanel.render = () => null
const Wrapper = compiledSfc(`../src/views/pl/${wrapper}`, { StatisticsPanel })
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText: () => {},
setElementText: () => {},
parentNode: () => null,
nextSibling: () => null,
insert: () => {},
remove: () => {},
patchProp: () => {}
})
const app = renderer.createApp(Wrapper, active === undefined ? {} : { active })
app.mount({})
await flush()
await vue.nextTick()
assert.deepEqual(
calls,
active === false
? []
: ['permissions', wrapper.startsWith('contract') ? 'contracts' : 'certificates']
)
app.unmount()
for (const active of [undefined, false])
test(`compiled certificate-usage/CertificateUsagePanel.vue ${active === false ? 'inactive skips requests' : 'defaults to active and loads'}`, async () => {
const calls: string[] = []
const dependencies = {
useUserStore: () => vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } }),
useRouter: () => ({ push() {}, replace() {} }),
fetchGetMenuCodes: async () => {
calls.push('permissions')
return { all: true }
},
quotaStatisticsApi: {
certificates: async () => {
calls.push('certificates')
return { list: [], total: 0 }
}
},
usePersistedListColumns,
useServerColumnFilters,
createStatisticsLoader,
useElementSize: () => ({ width: vue.ref(1120) }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
statusLabel: statisticsLabels.statusLabel,
contractTypeLabel: statisticsLabels.contractTypeLabel
}
const StatisticsPanel = compiledSfc(
'../src/views/pl/quota-statistics/StatisticsPanel.vue',
dependencies,
false
)
StatisticsPanel.render = () => null
const Wrapper = compiledSfc('../src/views/pl/certificate-usage/CertificateUsagePanel.vue', {
StatisticsPanel
})
}
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText: () => {},
setElementText: () => {},
parentNode: () => null,
nextSibling: () => null,
insert: () => {},
remove: () => {},
patchProp: () => {}
})
const app = renderer.createApp(Wrapper, active === undefined ? {} : { active })
app.mount({})
await flush()
await vue.nextTick()
assert.deepEqual(calls, active === false ? [] : ['permissions', 'certificates'])
app.unmount()
})
test('expanded content follows the table viewport width and keeps alignment local', () => {
const h = harness()