fix(pl): 统一计划用证三页的检索工具栏与漏斗以保持筛选和分页一致。
This commit is contained in:
1 parent
c4f173bc3a
commit
c16681a16f
8 files changed
+982
-235
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import * as Vue from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('../src/components/business/column-header-filter/index.vue', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
const countTemplate = source.match(/<span\b[^>]*class="value-count"[^>]*>[\s\S]*?<\/span>/)![0]
|
||||
const render = Vue.compile(countTemplate)
|
||||
for (const [count, visible] of [
|
||||
[undefined, false],
|
||||
[0, true],
|
||||
[7, true]
|
||||
] as const) {
|
||||
test(`count ${count} renders only when supplied, including zero`, async () => {
|
||||
const html = await renderToString(
|
||||
Vue.createSSRApp({ data: () => ({ opt: { count } }), render })
|
||||
)
|
||||
assert.equal(html.includes('class="value-count"'), visible)
|
||||
if (visible) assert.ok(html.includes(`>${count}</span>`))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { parse } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { useClientColumnFilters } from '../src/composables/useClientColumnFilters'
|
||||
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
|
||||
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
|
||||
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 20; i++) await Promise.resolve()
|
||||
}
|
||||
function deferred() {
|
||||
let resolve!: (value?: any) => void
|
||||
let reject!: (error: any) => void
|
||||
const promise = new Promise<any>((a, b) => {
|
||||
resolve = a
|
||||
reject = b
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
// Run production setup, including its real synchronous session watchers. Only I/O is substituted.
|
||||
function pageHarness(kind: 'quota' | 'legal' | 'cert', props: any = { mobile: true }) {
|
||||
const lists: any[] = [],
|
||||
gets: any[] = [],
|
||||
writes: any[] = [],
|
||||
permissions: any[] = []
|
||||
const events: string[] = [],
|
||||
mounted: Function[] = [],
|
||||
unmount: Function[] = []
|
||||
const user = vue.reactive({ accessToken: 'a', getUserInfo: { tenantNo: 'a', roles: ['sales'] } })
|
||||
const queue = (items: any[], params?: any) => {
|
||||
const pending = deferred()
|
||||
items.push({ ...pending, params })
|
||||
return pending.promise
|
||||
}
|
||||
const env = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useClientColumnFilters,
|
||||
useServerColumnFilters,
|
||||
usePersistedListColumns,
|
||||
useRoute: () => ({ query: {} }),
|
||||
newCert: () => ({}),
|
||||
commonFields: [],
|
||||
purchaseFields: [],
|
||||
transportFields: [],
|
||||
quotaCertApi: { list: (kind: string) => queue(lists, { kind }) },
|
||||
useUserStore: () => user,
|
||||
defineProps: () => props,
|
||||
onMounted: (fn: Function) => mounted.push(fn),
|
||||
onBeforeUnmount: (fn: Function) => unmount.push(fn),
|
||||
request: { get: (url: string, options: any) => queue(lists, { url, ...options.params }) },
|
||||
pickBxData: (value: any) => value,
|
||||
legalDocumentApi: {
|
||||
list: (params: any) => queue(lists, params),
|
||||
get: (id: string) => queue(gets, id),
|
||||
save: (id: string, products: any) => queue(writes, { id, products })
|
||||
},
|
||||
fetchGetMenuCodes: () => queue(permissions),
|
||||
ElMessage: {
|
||||
warning: (msg: string) => events.push(msg),
|
||||
success: (msg: string) => events.push(msg)
|
||||
}
|
||||
}
|
||||
const path =
|
||||
kind === 'quota'
|
||||
? '../src/views/pl/plan-quota/index.vue'
|
||||
: kind === 'cert'
|
||||
? '../src/views/pl/quota-cert/index.vue'
|
||||
: '../src/views/pl/legal-use-document/index.vue'
|
||||
const source = ts.createSourceFile(
|
||||
'page.ts',
|
||||
parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor.scriptSetup!.content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
const statements = source.statements.filter((s) => !ts.isImportDeclaration(s))
|
||||
const names = statements.flatMap((s) =>
|
||||
ts.isVariableStatement(s)
|
||||
? s.declarationList.declarations.flatMap((d) =>
|
||||
ts.isIdentifier(d.name)
|
||||
? [d.name.getText(source)]
|
||||
: ts.isObjectBindingPattern(d.name)
|
||||
? d.name.elements.map((e) => e.name.getText(source))
|
||||
: []
|
||||
)
|
||||
: ts.isFunctionDeclaration(s) && s.name
|
||||
? [s.name.text]
|
||||
: []
|
||||
)
|
||||
const code = ts.transpile(statements.map((s) => s.getText(source)).join('\n'), {
|
||||
target: ts.ScriptTarget.ESNext
|
||||
})
|
||||
const scope = vue.effectScope()
|
||||
const state = scope.run(() =>
|
||||
new Function(...Object.keys(env), `${code}; return {${names.join(',')}}`)(...Object.values(env))
|
||||
)
|
||||
return {
|
||||
state,
|
||||
user,
|
||||
lists,
|
||||
gets,
|
||||
writes,
|
||||
permissions,
|
||||
events,
|
||||
mounted,
|
||||
stop() {
|
||||
unmount.forEach((fn) => fn())
|
||||
scope.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const kind of ['cert', 'legal'] as const) {
|
||||
test(`${kind}: late list cannot overwrite latest refresh`, async () => {
|
||||
const h = pageHarness(kind),
|
||||
s = h.state
|
||||
const first = s.load(),
|
||||
second = s.load()
|
||||
h.lists[1].resolve([{ id: 'new' }])
|
||||
await second
|
||||
h.lists[0].resolve([{ id: 'old' }])
|
||||
await first
|
||||
assert.equal(s.rows.value[0].id, 'new')
|
||||
h.stop()
|
||||
})
|
||||
}
|
||||
for (const kind of ['cert', 'legal'] as const) {
|
||||
const fixture = (index: number) =>
|
||||
kind === 'cert'
|
||||
? {
|
||||
id: String(index),
|
||||
certNo: `CERT-${index}`,
|
||||
productNameOnCert: index === 25 ? '目标品名' : '普通',
|
||||
status: index === 25 ? 'active' : 'draft',
|
||||
totalQty: String(index),
|
||||
reservedQty: '0',
|
||||
usedQty: '0',
|
||||
availableQty: String(index),
|
||||
validTo: index === 25 ? '2026-11-01' : '2025-01-01'
|
||||
}
|
||||
: {
|
||||
id: String(index),
|
||||
planNo: `PLAN-${index}`,
|
||||
products: [
|
||||
{
|
||||
productName: index === 25 ? '目标品名' : '普通',
|
||||
documentStatus: index === 25 ? 'pending' : 'uploaded'
|
||||
}
|
||||
]
|
||||
}
|
||||
test(`${kind}: filters operate on full rows before pagination, combine, reset and clear hidden columns`, async () => {
|
||||
const h = pageHarness(kind),
|
||||
s = h.state
|
||||
s.rows.value = Array.from({ length: 45 }, (_, i) => fixture(i))
|
||||
s.page.value = 3
|
||||
s.setFilterState(kind === 'cert' ? 'productNameOnCert' : 'productNames', { text: '目标' })
|
||||
assert.equal(s.page.value, 1)
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['25']
|
||||
)
|
||||
s.setFilterState(kind === 'cert' ? 'status' : 'documentStatus', {
|
||||
selected: [kind === 'cert' ? 'draft' : 'uploaded']
|
||||
})
|
||||
assert.equal(s.filteredRows.value.length, 0)
|
||||
s.visibleColumns.value = s.visibleColumns.value.filter(
|
||||
(p: string) => p !== (kind === 'cert' ? 'status' : 'documentStatus')
|
||||
)
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['25']
|
||||
)
|
||||
s.reset()
|
||||
assert.equal(s.filteredRows.value.length, 45)
|
||||
s.keyword.value = kind === 'cert' ? 'CERT-25' : 'PLAN-25'
|
||||
s.page.value = 2
|
||||
s.search()
|
||||
assert.equal(s.page.value, 1)
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['25']
|
||||
)
|
||||
const refresh = s.load()
|
||||
h.lists[0].resolve(Array.from({ length: 45 }, (_, i) => fixture(i)))
|
||||
await refresh
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['25']
|
||||
)
|
||||
h.stop()
|
||||
})
|
||||
test(`${kind}: numeric and enum filters compare business values`, () => {
|
||||
const h = pageHarness(kind),
|
||||
s = h.state
|
||||
s.rows.value = Array.from({ length: 45 }, (_, i) => fixture(i))
|
||||
s.setFilterState(kind === 'cert' ? 'availableQty' : 'pendingCount', {
|
||||
op: 'eq',
|
||||
value: kind === 'cert' ? '25' : '1',
|
||||
min: '',
|
||||
max: ''
|
||||
})
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['25']
|
||||
)
|
||||
if (kind === 'cert') {
|
||||
s.setFilterState('validTo', { from: '2026-11-01', to: '2026-11-01' })
|
||||
assert.equal(s.filteredRows.value.length, 1)
|
||||
s.setFilterState('validTo', { from: '2026-11-02', to: '' })
|
||||
assert.equal(s.filteredRows.value.length, 0)
|
||||
}
|
||||
h.stop()
|
||||
})
|
||||
for (const change of ['token', 'tenant', 'roles', 'unmount']) {
|
||||
test(`${kind}: ${change} rejects stale list and permission responses`, async () => {
|
||||
const h = pageHarness(kind),
|
||||
s = h.state
|
||||
const init = s.init()
|
||||
const pending = s.load()
|
||||
s.rows.value = [fixture(25)]
|
||||
if (change === 'token') h.user.accessToken = 'b'
|
||||
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b'
|
||||
if (change === 'roles') h.user.getUserInfo.roles = ['viewer']
|
||||
if (change === 'unmount') h.stop()
|
||||
h.permissions[0].resolve({ all: true })
|
||||
await init
|
||||
h.lists[0].resolve([fixture(1)])
|
||||
await pending
|
||||
assert.notEqual(s.rows.value[0]?.id, '1')
|
||||
assert.equal((kind === 'cert' ? s.canEdit : s.canUpload).value, false)
|
||||
h.stop()
|
||||
})
|
||||
}
|
||||
}
|
||||
test('cert: changing kind immediately invalidates an old purchase response', async () => {
|
||||
const h = pageHarness('cert'),
|
||||
s = h.state
|
||||
const pending = s.load()
|
||||
s.kind.value = 'transport'
|
||||
assert.equal(h.lists[1].params.kind, 'transport')
|
||||
h.lists[1].resolve([{ id: 'transport' }])
|
||||
await flush()
|
||||
h.lists[0].resolve([{ id: 'purchase' }])
|
||||
await pending
|
||||
assert.equal(s.rows.value[0].id, 'transport')
|
||||
h.stop()
|
||||
})
|
||||
test('plan: actual header filters send server conditions, reset page, preserve refresh and clear hidden conditions', async () => {
|
||||
const h = pageHarness('quota', { mobile: false }),
|
||||
s = h.state
|
||||
s.page.value = 3
|
||||
s.setFilterState('planNo', { text: 'PLAN' })
|
||||
assert.equal(s.page.value, 1)
|
||||
assert.equal(h.lists.at(-1).params.planNo, 'PLAN')
|
||||
s.setFilterState('quotaStatus', { selected: ['reserved', 'settled'] })
|
||||
s.setFilterState('planDate', { from: '2026-11-01', to: '2026-11-30' })
|
||||
s.setFilterState('licensePlate', { text: '鄂A' })
|
||||
assert.deepEqual(s.toQueryParams(), {
|
||||
planNo: 'PLAN',
|
||||
quotaStatusIn: 'reserved,settled',
|
||||
planDateFrom: '2026-11-01',
|
||||
planDateTo: '2026-11-30',
|
||||
licensePlate: '鄂A'
|
||||
})
|
||||
void s.load()
|
||||
assert.equal(h.lists.at(-1).params.licensePlate, '鄂A')
|
||||
s.visibleColumns.value = s.visibleColumns.value.filter((p: string) => p !== 'licensePlate')
|
||||
await vue.nextTick()
|
||||
assert.equal(h.lists.at(-1).params.licensePlate, undefined)
|
||||
s.reset()
|
||||
assert.deepEqual(s.toQueryParams(), {})
|
||||
h.lists.forEach((r) => r.resolve({ list: [], total: 0 }))
|
||||
await flush()
|
||||
h.stop()
|
||||
})
|
||||
|
||||
test('legal: mixed-product upload filters preserve any-match and OR semantics', async () => {
|
||||
const h = pageHarness('legal'),
|
||||
s = h.state
|
||||
const row = (id: string, statuses: string[]) => ({
|
||||
id,
|
||||
planNo: id,
|
||||
products: statuses.map((documentStatus) => ({ productName: '产品', documentStatus }))
|
||||
})
|
||||
s.rows.value = [
|
||||
row('mixed', ['pending', 'uploaded']),
|
||||
row('pending', ['pending']),
|
||||
row('uploaded', ['uploaded'])
|
||||
]
|
||||
s.setFilterState('documentStatus', { selected: ['uploaded'] })
|
||||
assert.deepEqual(
|
||||
s.filteredRows.value.map((r: any) => r.id),
|
||||
['mixed', 'uploaded']
|
||||
)
|
||||
s.setFilterState('documentStatus', { selected: ['pending'] })
|
||||
assert.deepEqual(
|
||||
s.filteredRows.value.map((r: any) => r.id),
|
||||
['mixed', 'pending']
|
||||
)
|
||||
s.setFilterState('documentStatus', { selected: ['pending', 'uploaded'] })
|
||||
assert.deepEqual(
|
||||
s.filteredRows.value.map((r: any) => r.id),
|
||||
['mixed', 'pending', 'uploaded']
|
||||
)
|
||||
assert.deepEqual(
|
||||
s.collectUniqueOptions('documentStatus').map((o: any) => o.count),
|
||||
[2, 2]
|
||||
)
|
||||
s.setFilterState('documentStatus', { selected: ['uploaded'] })
|
||||
s.setFilterState('planNo', { text: 'mixed' })
|
||||
assert.deepEqual(
|
||||
s.pageRows.value.map((r: any) => r.id),
|
||||
['mixed']
|
||||
)
|
||||
s.clearFilter('planNo')
|
||||
s.visibleColumns.value = s.visibleColumns.value.filter((p: string) => p !== 'documentStatus')
|
||||
await vue.nextTick()
|
||||
assert.equal(s.filteredRows.value.length, 3)
|
||||
assert.equal(s.hasActiveFilters.value, false)
|
||||
h.stop()
|
||||
})
|
||||
@@ -4,6 +4,8 @@ import test from 'node:test'
|
||||
import { parse } from '@vue/compiler-sfc'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
|
||||
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
|
||||
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 20; i++) await Promise.resolve()
|
||||
@@ -36,6 +38,9 @@ function pageHarness(kind: 'quota' | 'legal', props: any = { mobile: true }) {
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useServerColumnFilters,
|
||||
usePersistedListColumns,
|
||||
useUserStore: () => user,
|
||||
defineProps: () => props,
|
||||
onMounted: (fn: Function) => mounted.push(fn),
|
||||
|
||||
Reference in new issue
Block a user