优化
This commit is contained in:
@@ -4,7 +4,19 @@ import { describe, expect, it } from 'vitest'
|
||||
import { compileStyle, parse } from 'vue/compiler-sfc'
|
||||
|
||||
// Approved entry/consumer inventory. Do not discover unrelated menus recursively.
|
||||
const cargoConsumers = [
|
||||
'ocean-bookings/pages/OceanBookingsView.vue', 'ocean-bookings/pages/OceanBookingDetailView.vue',
|
||||
'ocean-bookings/components/OceanBookingDocumentsPanel.vue', 'ocean-bookings/components/OceanBookingPrintDialog.vue',
|
||||
'trucking/pages/TruckAppointmentsView.vue', 'trucking/pages/TruckAppointmentCreateView.vue',
|
||||
'trucking/pages/TruckAppointmentDetailView.vue', 'trucking/components/TruckAppointmentForm.vue',
|
||||
'trucking/components/TruckAttachmentDialog.vue', 'container-loading/pages/ContainersView.vue',
|
||||
'container-loading/pages/ContainerDetailView.vue', 'container-loading/pages/SmartLoadingWorkbenchView.vue',
|
||||
'container-loading/components/OceanBookingLinkDialog.vue', 'container-loading/components/ContainerOverviewPanel.vue',
|
||||
'container-loading/components/ContainerCustomsPanel.vue', 'container-loading/components/ContainerLastLegFeesPanel.vue',
|
||||
'loading-lists/components/ContainerFeesPanel.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue',
|
||||
]
|
||||
const consumers = [
|
||||
...cargoConsumers,
|
||||
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue',
|
||||
'customers/pages/OverseasStorageFeesView.vue', 'customers/components/CustomerBillingRulesPanel.vue',
|
||||
'customers/CustomerOverseasStorageFeePanel.vue', 'unified-orders/pages/UnifiedOrdersView.vue',
|
||||
@@ -28,19 +40,25 @@ const domesticPages = [
|
||||
{ title: '托盘管理', path: '/master-data/pallets', file: 'warehouse-resources/pallets/pages/PalletsView.vue' },
|
||||
{ title: '整柜业务', path: '/domestic-containers', file: 'domestic-containers/pages/DomesticContainersView.vue' },
|
||||
]
|
||||
const customerPages = [
|
||||
{ title: '客户列表', path: '/customers', file: 'customers/pages/CustomersView.vue' },
|
||||
{ title: '海外仓阶梯费用', path: '/customers/overseas-storage-fees', file: 'customers/pages/OverseasStorageFeesView.vue' },
|
||||
]
|
||||
const densityPages = [...new Set([
|
||||
...cargoConsumers.filter(file => file.includes('/pages/') || ['ContainerOverviewPanel.vue', 'ContainerCustomsPanel.vue', 'ContainerLastLegFeesPanel.vue', 'ContainerFeesPanel.vue', 'OceanBookingDocumentsPanel.vue'].some(name => file.endsWith(name))),
|
||||
...domesticPages.map(page => page.file), 'loading-lists/pages/PreloadPlanDetailView.vue',
|
||||
'loading-lists/components/LoadingListDetailDrawer.vue', 'domestic-containers/pages/DomesticContainerInventoryDetailView.vue',
|
||||
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue', 'customers/pages/OverseasStorageFeesView.vue',
|
||||
...customerPages.map(page => page.file), 'customers/pages/CustomerDetailView.vue', 'audit/components/CustomerAuditLogsPanel.vue',
|
||||
])]
|
||||
type TemplateNode = NonNullable<NonNullable<ReturnType<typeof parse>['descriptor']['template']>['ast']>['children'][number]
|
||||
type ElementNode = Extract<TemplateNode, { type: 1 }>
|
||||
const classesOf = (node: ElementNode, preload = true): string[] => node.props.flatMap(prop => {
|
||||
const classesOf = (node: ElementNode, preload = true, dense = preload, workbench = 'DOMESTIC'): string[] => node.props.flatMap(prop => {
|
||||
if (prop.type === 6 && prop.name === 'class') return prop.value?.content.split(/\s+/) ?? []
|
||||
if (prop.type !== 7 || prop.name !== 'bind' || prop.arg?.type !== 4 || prop.arg.content !== 'class' || prop.exp?.type !== 4 || !prop.exp.content.includes('density')) return []
|
||||
// Evaluate only the authored density class binding with the shared component's two modes.
|
||||
const value = new Function('mode', 'isPreload', 'dense', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, preload) as Record<string, boolean>
|
||||
return Object.entries(value).filter(([, enabled]) => enabled).flatMap(([names]) => names.split(/\s+/))
|
||||
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, workbench === 'DOMESTIC', workbench === 'OVERSEAS') as Record<string, boolean>
|
||||
const flatten = (value: unknown): string[] => typeof value === 'string' ? value.split(/\s+/) : Array.isArray(value) ? value.flatMap(flatten) : value && typeof value === 'object' ? Object.entries(value).filter(([, enabled]) => enabled).flatMap(([names]) => names.split(/\s+/)) : []
|
||||
return flatten(value)
|
||||
})
|
||||
const styles = [
|
||||
...consumers.flatMap(file => parse(read(`modules/${file}`)).descriptor.styles.map(style => ({ file, css: style.content }))),
|
||||
@@ -55,6 +73,60 @@ const cssRoot = (css: string) => {
|
||||
}
|
||||
|
||||
describe('approved Admin density ownership', () => {
|
||||
it('covers four cargo menus, independent detail routes and both workbench modes', () => {
|
||||
const navigation = read('core/navigation/adminNavigation.ts')
|
||||
for (const path of ['/ocean-bookings', '/trucking', '/container-loading/domestic', '/container-loading/overseas']) expect(navigation).toContain(`path: '${path}'`)
|
||||
for (const [prefix, workbench] of [['Domestic', 'DOMESTIC'], ['Overseas', 'OVERSEAS']]) {
|
||||
for (const [suffix, component] of [['ContainersView', 'ContainersView'], ['ContainerDetailView', 'ContainerDetailView']]) {
|
||||
expect(read(`modules/container-loading/pages/${prefix}${suffix}.vue`)).toContain(`<${component} workbench="${workbench}"`)
|
||||
const ast = parse(read(`modules/container-loading/pages/${component}.vue`)).descriptor.template!.ast!
|
||||
const root = ast.children.find(node => node.type === 1) as ElementNode
|
||||
expect(classesOf(root, false, true, workbench)).toContain('admin-density')
|
||||
}
|
||||
}
|
||||
const detail = read('modules/container-loading/pages/ContainerDetailView.vue')
|
||||
for (const component of ['CarrierScheduleQueryPanel', 'LoadingListDetailDrawer', 'PalletLabelPrintDialog']) expect(detail).toContain(`<${component} dense`)
|
||||
expect(read('modules/container-loading/pages/ContainersView.vue')).toContain('<ContainerSimulationCreate dense')
|
||||
for (const file of cargoConsumers) expect(read(`modules/${file}`), `${file}: direct entry`).toContain('@/styles/adminDensity.css')
|
||||
})
|
||||
|
||||
it('enables confirmed loading content and schedule overlays only for opted-in callers', () => {
|
||||
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue']) {
|
||||
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const root = ast.children.find(node => node.type === 1) as ElementNode
|
||||
expect(classesOf(root, false, true)).toContain('admin-density')
|
||||
expect(classesOf(root, false, false)).not.toContain('admin-density')
|
||||
if (file.includes('CarrierSchedule')) {
|
||||
const dialog = root.children.find(node => node.type === 1 && node.tag === 'AppDialog') as ElementNode
|
||||
expect(classesOf(dialog, false, true)).toContain('admin-density-dialog')
|
||||
expect(classesOf(dialog, false, false)).not.toContain('admin-density-dialog')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps cargo native tables, row actions and detail tabs connected to shared styles', () => {
|
||||
for (const file of cargoConsumers) {
|
||||
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const visit = (nodes: typeof ast.children, inCell = false, insideDensity = false) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
const classes = classesOf(node)
|
||||
const enabled = insideDensity || classes.some(name => ['admin-density', 'admin-density-dialog'].includes(name))
|
||||
if (node.tag === 'table') {
|
||||
expect(classes, `${file}: native table`).toContain('density-table')
|
||||
expect(insideDensity, `${file}: native table ancestor`).toBe(true)
|
||||
}
|
||||
if (node.tag === 'el-tabs') expect(classes).toContain('density-element-tabs')
|
||||
if (inCell && node.tag === 'el-button') {
|
||||
expect(classes, `${file}: native row action`).toContain('density-action')
|
||||
expect(node.children.some(child => child.type === 2 && child.content.trim() || child.type === 5), `${file}: empty action`).toBe(true)
|
||||
}
|
||||
visit(node.children, inCell || node.tag === 'td', enabled)
|
||||
}
|
||||
}
|
||||
visit(ast.children)
|
||||
}
|
||||
})
|
||||
it('keeps shared list actions and detail content inside a density-enabled page ancestor', () => {
|
||||
for (const file of densityPages) {
|
||||
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
@@ -63,7 +135,7 @@ describe('approved Admin density ownership', () => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
const classes = classesOf(node)
|
||||
if (node.tag === 'DataTable' || node.tag === 'LoadingListDetailDrawer' || classes.some(name => ['density-action', 'density-actions', 'density-tabs', 'density-fields', 'density-tab-icon'].includes(name))) {
|
||||
if (node.tag === 'DataTable' || node.tag === 'LoadingListDetailDrawer' || classes.some(name => ['density-action', 'density-actions', 'density-tabs', 'density-fields', 'density-tab-icon', 'density-back'].includes(name))) {
|
||||
expect(insideDensity, `${file}: ${classes.join(' ')} lacks its density ancestor`).toBe(true)
|
||||
checked += 1
|
||||
}
|
||||
@@ -87,15 +159,27 @@ describe('approved Admin density ownership', () => {
|
||||
for (const file of densityPages) expect(read(`modules/${file}`), `${file}: direct entry CSS`).toContain('@/styles/adminDensity.css')
|
||||
})
|
||||
|
||||
it('covers both customer menu entries and their shared detail primitives', () => {
|
||||
const navigation = read('core/navigation/adminNavigation.ts')
|
||||
const customers = navigation.slice(navigation.indexOf("key: 'customers'"), navigation.indexOf("key: 'domestic-warehouse'"))
|
||||
expect([...customers.matchAll(/path: '/g)]).toHaveLength(customerPages.length)
|
||||
for (const page of customerPages) {
|
||||
expect(customers).toContain(`title: '${page.title}'`)
|
||||
expect(customers).toContain(`path: '${page.path}'`)
|
||||
}
|
||||
expect(read('modules/customers/pages/CustomerDetailView.vue')).toContain('class="back-button density-back"')
|
||||
})
|
||||
|
||||
it('uses shared outlined actions across domestic tables and explicit classes on teleported dialogs', () => {
|
||||
for (const file of densityPages) {
|
||||
for (const file of [...new Set([...densityPages, ...consumers])]) {
|
||||
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const visit = (nodes: typeof template.children, actionSlot = false, parentTag = '') => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
const isActionSlot = actionSlot || (parentTag === 'DataTable' && node.props.some(prop => prop.type === 7 && prop.name === 'slot' && prop.arg?.type === 4 && prop.arg.content === 'actions'))
|
||||
if (isActionSlot && node.tag === 'el-button') expect(classesOf(node), `${file}: row action`).toContain('density-action')
|
||||
if (densityPages.includes(file) && isActionSlot && node.tag === 'el-button') expect(classesOf(node), `${file}: row action`).toContain('density-action')
|
||||
if (['AppDialog', 'el-dialog'].includes(node.tag)) expect(classesOf(node), `${file}: teleported dialog`).toContain('admin-density-dialog')
|
||||
if (['AppDrawer', 'el-drawer'].includes(node.tag)) expect(classesOf(node), `${file}: teleported drawer`).toContain('admin-density-drawer')
|
||||
visit(node.children, isActionSlot, node.tag)
|
||||
}
|
||||
}
|
||||
@@ -121,6 +205,22 @@ describe('approved Admin density ownership', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('connects configurable surface colors and semantic hover states to their consumers', () => {
|
||||
const root = cssRoot(shared)
|
||||
const rules = new Map<string, Record<string, string>>()
|
||||
root.walkRules(rule => {
|
||||
const declarations: Record<string, string> = {}
|
||||
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
|
||||
rules.set(rule.selector, declarations)
|
||||
})
|
||||
expect(rules.get('.admin-density .el-table__body tr.el-table__row--striped td.el-table__cell')?.background).toBe('var(--admin-row-stripe)')
|
||||
expect(rules.get('.admin-density-dialog .el-descriptions__label.el-descriptions__cell')?.background).toBe('var(--admin-label-bg)')
|
||||
for (const tone of ['warning', 'purple', 'primary', 'success', 'danger']) {
|
||||
expect(rules.get(`:is(.admin-density, .admin-density-dialog) .density-action[data-tone="${tone}"]`)?.['--action-hover-bg']).toBe(`var(--admin-action-${tone}-hover)`)
|
||||
}
|
||||
expect(rules.get(':is(.admin-density, .admin-density-dialog) .el-button.density-action:hover:not(:disabled)')?.background).toBe('var(--action-hover-bg)')
|
||||
})
|
||||
|
||||
it('prevents page copies of action paint, tab primitives and desktop density dimensions', () => {
|
||||
for (const { file, css } of styles) cssRoot(css).walkRules(rule => {
|
||||
expect(rule.selector, file).not.toMatch(/customer-action-button|order-action-button|\.density-action|\.density-tab-icon/)
|
||||
@@ -137,9 +237,22 @@ describe('approved Admin density ownership', () => {
|
||||
})
|
||||
|
||||
it('keeps conditional shared consumers opt-in instead of enabling other menus', () => {
|
||||
expect(read('shared/components/DataTable.vue')).toContain('margin-left: var(--cc-list-action-sibling-margin, 8px)')
|
||||
expect(shared).toContain('.density-actions { --cc-list-action-sibling-margin: 0px;')
|
||||
expect(read('modules/loading-lists/components/LoadingListsView.vue')).toContain("'admin-density': isPreload")
|
||||
expect(read('modules/loading-lists/components/PalletLabelPrintDialog.vue')).toContain("'admin-density-dialog': dense")
|
||||
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("'admin-density-dialog': dense")
|
||||
for (const file of ['loading-lists/components/LoadingListsView.vue', 'loading-lists/components/LoadingListDetailDrawer.vue', 'loading-lists/components/PalletLabelPrintDialog.vue', 'container-loading/components/ContainerSimulationCreate.vue']) {
|
||||
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const visit = (nodes: typeof template.children) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
expect(classesOf(node, false).filter(name => /^(admin-density|density-)/.test(name)), `${file}: unopted consumer`).toEqual([])
|
||||
visit(node.children)
|
||||
}
|
||||
}
|
||||
visit(template.children)
|
||||
}
|
||||
for (const file of ['customers/pages/CustomersView.vue', 'customers/pages/OverseasStorageFeesView.vue', 'unified-orders/pages/UnifiedOrdersView.vue']) {
|
||||
expect(read(`modules/${file}`), file).toContain('density-action')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user