feat(pl): 增加合同与证件用量视图以追踪共享额度及段产品使用。
This commit is contained in:
1 parent
ce6f102dae
commit
0d1d264bc7
18 files changed
+1017
-16
No files matched your search
@@ -0,0 +1,263 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createStatisticsLoader } from '../src/views/pl/quota-statistics/loader'
|
||||
test('fixed contract skips global list and retains each decimal balance', async () => {
|
||||
const calls: string[] = []
|
||||
const loader = createStatisticsLoader({
|
||||
contracts: async () => {
|
||||
calls.push('global')
|
||||
},
|
||||
contract: async (id: string) => {
|
||||
calls.push(id)
|
||||
return { certificates: [{ availableQty: '0.10000001' }, { availableQty: '7.2' }] }
|
||||
}
|
||||
} as any)
|
||||
const result = await loader.load({ mode: 'contract', contractId: 'c1', allowed: true })
|
||||
assert.deepEqual(calls, ['c1'])
|
||||
assert.deepEqual(
|
||||
result.certificates.map((c: any) => c.availableQty),
|
||||
['0.10000001', '7.2']
|
||||
)
|
||||
})
|
||||
test('permission denied never requests statistics', async () => {
|
||||
const loader = createStatisticsLoader(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: () => () => {
|
||||
throw Error('unexpected request')
|
||||
}
|
||||
}
|
||||
) as any
|
||||
)
|
||||
assert.equal(await loader.load({ mode: 'contract', allowed: false }), undefined)
|
||||
})
|
||||
test('context invalidation discards pending response', async () => {
|
||||
let resolve!: (v: any) => void
|
||||
const loader = createStatisticsLoader({
|
||||
contract: () =>
|
||||
new Promise((r) => {
|
||||
resolve = r
|
||||
})
|
||||
} as any)
|
||||
const pending = loader.load({ mode: 'contract', contractId: 'old', allowed: true })
|
||||
loader.invalidate()
|
||||
resolve({ id: 'old' })
|
||||
assert.equal(await pending, undefined)
|
||||
})
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { parse } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { plRoutes } from '../src/router/modules/pl'
|
||||
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
|
||||
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 30; i++) await Promise.resolve()
|
||||
}
|
||||
function deferred() {
|
||||
let resolve!: (value: any) => void, reject!: (error: any) => void
|
||||
const promise = new Promise((a, b) => {
|
||||
resolve = a
|
||||
reject = b
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
function harness(fixed?: string) {
|
||||
const calls: any[] = [],
|
||||
permissions: any[] = [],
|
||||
unmount: Function[] = []
|
||||
const props = vue.reactive({
|
||||
mode: 'contract',
|
||||
contractId: fixed,
|
||||
active: true,
|
||||
embedded: !!fixed
|
||||
})
|
||||
const user = vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } })
|
||||
const queue = (method: string, args: any[]) => {
|
||||
const pending = deferred()
|
||||
calls.push({ method, args, ...pending })
|
||||
return pending.promise
|
||||
}
|
||||
const api = Object.fromEntries(
|
||||
['contracts', 'contract', 'certificates', 'certificate'].map((method) => [
|
||||
method,
|
||||
(...args: any[]) => queue(method, args)
|
||||
])
|
||||
)
|
||||
const env = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
defineProps: () => props,
|
||||
withDefaults: (v: any) => v,
|
||||
useUserStore: () => user,
|
||||
fetchGetMenuCodes: () => {
|
||||
const p = deferred()
|
||||
permissions.push(p)
|
||||
return p.promise
|
||||
},
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
usePersistedListColumns,
|
||||
useServerColumnFilters,
|
||||
createStatisticsLoader,
|
||||
quotaStatisticsApi: api,
|
||||
onBeforeUnmount: (fn: Function) => unmount.push(fn)
|
||||
}
|
||||
const src = ts.createSourceFile(
|
||||
'panel.ts',
|
||||
parse(
|
||||
readFileSync(
|
||||
new URL('../src/views/pl/quota-statistics/StatisticsPanel.vue', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
).descriptor.scriptSetup!.content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
const code = ts.transpile(
|
||||
src.statements
|
||||
.filter((s) => !ts.isImportDeclaration(s))
|
||||
.map((s) => s.getText(src))
|
||||
.join('\n'),
|
||||
{ target: ts.ScriptTarget.ESNext }
|
||||
)
|
||||
const scope = vue.effectScope()
|
||||
const state = scope.run(() =>
|
||||
new Function(
|
||||
...Object.keys(env),
|
||||
`${code}; return { rows, fixedDetail, details, load, loadDetail, allowed, canEdit }`
|
||||
)(...Object.values(env))
|
||||
)
|
||||
return {
|
||||
state,
|
||||
calls,
|
||||
permissions,
|
||||
props,
|
||||
user,
|
||||
stop: () => {
|
||||
unmount.forEach((fn) => fn())
|
||||
scope.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
test('five leaves retain IDs and individual permissions', () => {
|
||||
const group = plRoutes[0].children!.find((r) => r.id === 'PL_GROUP_CERT')!
|
||||
assert.deepEqual(
|
||||
group.children!.map((r) => r.path),
|
||||
['quota-cert', 'contract-quota', 'certificate-usage', 'plan-cert', 'legal-use-document']
|
||||
)
|
||||
assert.equal(group.children![0].id, 'PL_QUOTA_CERT')
|
||||
assert.equal(group.children![3].id, 'PL00000000000000000000000000015')
|
||||
assert.deepEqual(
|
||||
group.children!.map((r) => r.meta!.menuCode),
|
||||
[
|
||||
'pl:quota-cert:list',
|
||||
'pl:quota-cert:list',
|
||||
'pl:quota-cert:list',
|
||||
'pl:plan-cert:list',
|
||||
'pl:legal-use-document:list'
|
||||
]
|
||||
)
|
||||
})
|
||||
test('actual fixed panel loads only current contract and drops old response', async () => {
|
||||
const h = harness('one')
|
||||
h.permissions[0].resolve({ menuCodes: ['pl:quota-cert:list'] })
|
||||
await flush()
|
||||
assert.equal(h.calls[0].method, 'contract')
|
||||
assert.deepEqual(h.calls[0].args, ['one'])
|
||||
h.props.contractId = 'two'
|
||||
h.permissions[1].resolve({ all: true })
|
||||
await flush()
|
||||
h.calls[1].resolve({ id: 'two', certificates: [] })
|
||||
await flush()
|
||||
h.calls[0].resolve({ id: 'one', certificates: [] })
|
||||
await flush()
|
||||
assert.equal(h.state.fixedDetail.value.id, 'two')
|
||||
h.stop()
|
||||
})
|
||||
for (const change of ['tenant', 'logout', 'unmount'])
|
||||
test(`actual panel invalidates data on ${change}`, async () => {
|
||||
const h = harness()
|
||||
h.permissions[0].resolve({ all: true })
|
||||
await flush()
|
||||
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b'
|
||||
if (change === 'logout') h.user.accessToken = ''
|
||||
if (change === 'unmount') h.stop()
|
||||
h.calls[0].resolve({ list: [{ id: 'stale' }], total: 1 })
|
||||
await flush()
|
||||
assert.deepEqual(h.state.rows.value, [])
|
||||
assert.equal(h.state.allowed.value, false)
|
||||
h.stop()
|
||||
})
|
||||
test('proof-only permission cannot request quota', async () => {
|
||||
const h = harness()
|
||||
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list'] })
|
||||
await flush()
|
||||
assert.equal(h.calls.length, 0)
|
||||
h.stop()
|
||||
})
|
||||
test('expand failure clears stale data and retry works', async () => {
|
||||
const h = harness()
|
||||
h.permissions[0].resolve({ all: true })
|
||||
await flush()
|
||||
h.calls[0].resolve({ list: [{ id: 'c' }], total: 1 })
|
||||
await flush()
|
||||
const first = h.state.loadDetail({ id: 'c' })
|
||||
h.calls[1].resolve({ id: 'c', certificates: [] })
|
||||
await first
|
||||
assert.equal(h.state.details.c.data.id, 'c')
|
||||
const failure = h.state.loadDetail({ id: 'c' })
|
||||
h.calls[2].reject(Error('failed'))
|
||||
await failure
|
||||
assert.equal(h.state.details.c.data, undefined)
|
||||
assert.equal(h.state.details.c.error, true)
|
||||
const retry = h.state.loadDetail({ id: 'c' })
|
||||
h.calls[3].resolve({ id: 'c', certificates: [] })
|
||||
await retry
|
||||
assert.equal(h.state.details.c.data.id, 'c')
|
||||
h.stop()
|
||||
})
|
||||
test('statistics API maps each view to the server endpoint and preserves response decimals', async () => {
|
||||
const calls: any[] = []
|
||||
const raw = { list: [{ availableQty: '1.00000001' }], total: 1 }
|
||||
const source = ts.createSourceFile(
|
||||
'api.ts',
|
||||
readFileSync(new URL('../src/api/pl/quota-cert.ts', import.meta.url), 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
const code = ts.transpile(
|
||||
source.statements
|
||||
.filter((s) => !ts.isImportDeclaration(s))
|
||||
.map((s) => s.getText(source).replace(/^export /, ''))
|
||||
.join('\n'),
|
||||
{ target: ts.ScriptTarget.ESNext }
|
||||
)
|
||||
const api = new Function('request', 'pickBxData', `${code};return quotaStatisticsApi`)(
|
||||
{
|
||||
get: (url: string, options: any) => {
|
||||
calls.push({ url, options })
|
||||
return Promise.resolve(raw)
|
||||
}
|
||||
},
|
||||
(v: any) => v
|
||||
)
|
||||
assert.equal(
|
||||
(await api.contracts({ page: 2, keyword: '合同' })).list[0].availableQty,
|
||||
'1.00000001'
|
||||
)
|
||||
await api.certificates({ certType: 'transport', pageSize: 20 })
|
||||
await api.contract('c')
|
||||
await api.certificate('purchase', 'p')
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c.url),
|
||||
[
|
||||
'/transport/quota-certs/statistics/contracts',
|
||||
'/transport/quota-certs/statistics/certificates',
|
||||
'/transport/quota-certs/contracts/c/statistics',
|
||||
'/transport/quota-certs/purchase/p/statistics'
|
||||
]
|
||||
)
|
||||
assert.deepEqual(calls[1].options.params, { certType: 'transport', pageSize: 20 })
|
||||
})
|
||||
Reference in new issue
Block a user