This commit is contained in:
2026-09-15 09:58:15 +08:00
parent 3f998bdb88
commit 4bf66d9571
90 changed files with 1599 additions and 1388 deletions
+21 -8
View File
@@ -100,14 +100,17 @@
:is(.admin-density, .admin-density-dialog) .el-button.density-action:focus-visible { outline: 2px solid var(--action-color); outline-offset: 2px; }
/* Detail headings and summaries shared by domestic and overseas pages. */
.admin-density .detail-page-layout.density-detail-layout { gap: 0; background: var(--admin-surface); }
.admin-density .density-detail-layout > .detail-page-layout__header { border-bottom: 1px solid var(--cc-border); }
.admin-density .density-detail-layout > .detail-page-layout__content { padding: var(--admin-space-5); }
.admin-density .density-detail-header { display: flex; min-width: 0; min-height: 40px; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); margin-bottom: 0; padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); background: var(--admin-surface, #fff); }
.admin-density .density-detail-heading { display: flex; min-width: 0; align-items: center; gap: var(--admin-space-4, 10px); }
.admin-density .density-detail-title { min-width: 0; margin: 0; color: #172033; font-size: var(--admin-font-title, 16px); line-height: 1.5; overflow-wrap: anywhere; }
.admin-density .density-detail-overview { border-bottom: 1px solid var(--cc-border); display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px) var(--admin-space-7, 24px); margin: 0; padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); background: var(--admin-surface, #fff); }
.admin-density .density-detail-overview > div { display: flex; min-width: 0; align-items: baseline; gap: var(--admin-space-2, 6px); }
.admin-density .density-detail-overview dt { flex: 0 0 auto; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.admin-density .density-detail-overview dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.admin-density .density-detail-overview small { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
:is(.admin-density, .admin-density-dialog) .density-detail-overview { border-bottom: 1px solid var(--cc-border); display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px) var(--admin-space-7, 24px); margin: 0; padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); background: var(--admin-surface, #fff); }
:is(.admin-density, .admin-density-dialog) .density-detail-overview > div { display: flex; min-width: 0; align-items: baseline; gap: var(--admin-space-2, 6px); }
:is(.admin-density, .admin-density-dialog) .density-detail-overview dt { flex: 0 0 auto; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
:is(.admin-density, .admin-density-dialog) .density-detail-overview dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
:is(.admin-density, .admin-density-dialog) .density-detail-overview small { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
/* Back navigation stays in the detail heading, beside its business identifier. */
.admin-density .el-button.density-back { height: var(--admin-control-height); padding: 0; border: 0; background: transparent; color: var(--cc-muted); font-size: var(--admin-font-body); font-weight: 400; box-shadow: none; }
@@ -133,6 +136,7 @@
.admin-density-drawer .el-drawer__header { margin-bottom: 0; padding: var(--admin-space-5) var(--admin-space-6); border-bottom: 1px solid var(--cc-border); }
.admin-density-drawer .el-drawer__title { font-size: var(--admin-font-title); }
.admin-density-drawer .el-drawer__body { padding: var(--admin-space-5) var(--admin-space-6); }
.admin-density-drawer .el-drawer__footer { padding: var(--admin-space-5) var(--admin-space-6); border-top: 1px solid var(--cc-border); }
/* Reusable row action group; no business action names in the shared layer. */
.admin-density .data-table .density-actions { --cc-list-action-sibling-margin: 0px; display: flex; align-items: center; gap: var(--admin-space-2); }
@@ -153,9 +157,9 @@
.admin-density .density-contained-tabs > .el-tabs__content { padding: var(--admin-space-5); }
/* Explicit page descriptions share the teleported detail table scale. */
.admin-density .density-descriptions .el-descriptions__label.el-descriptions__cell { width: var(--admin-label-width); font-size: var(--admin-font-caption); background: var(--admin-label-bg); }
.admin-density .density-descriptions .el-descriptions__content.el-descriptions__cell { font-size: var(--admin-font-body); overflow-wrap: anywhere; }
.admin-density .density-descriptions .el-descriptions__cell { height: var(--admin-row-height); padding: var(--admin-space-2) var(--admin-space-3); }
:is(.admin-density, .admin-density-dialog) .density-descriptions .el-descriptions__label.el-descriptions__cell { width: var(--admin-label-width); font-size: var(--admin-font-caption); background: var(--admin-label-bg); }
:is(.admin-density, .admin-density-dialog) .density-descriptions .el-descriptions__content.el-descriptions__cell { font-size: var(--admin-font-body); overflow-wrap: anywhere; }
:is(.admin-density, .admin-density-dialog) .density-descriptions .el-descriptions__cell { height: var(--admin-row-height); padding: var(--admin-space-2) var(--admin-space-3); }
/* Opted-in grouped forms use the same labels and spacing on pages and in dialogs. */
:is(.admin-density, .admin-density-dialog) .density-form,
@@ -163,6 +167,15 @@
:is(.admin-density, .admin-density-dialog) .density-form .el-form-item,
.admin-density.density-form .el-form-item { margin-bottom: var(--admin-space-5); }
/* Opted-in FormSection consumers share form headings without page-level paint. */
:is(.admin-density, .admin-density-dialog) .form-section.density-form-section { padding: var(--admin-space-5) 0; }
:is(.admin-density, .admin-density-dialog) .form-section.density-form-section:first-child { padding-top: 0; }
:is(.admin-density, .admin-density-dialog) .form-section.density-form-section:last-child { padding-bottom: 0; }
:is(.admin-density, .admin-density-dialog) .density-form-section .form-section__heading { margin-bottom: var(--admin-space-3); }
:is(.admin-density, .admin-density-dialog) .density-form-section .form-section__heading h2 { font-size: var(--admin-font-section); }
:is(.admin-density, .admin-density-dialog) .density-form-section .form-section__heading p { margin-top: var(--admin-space-1); font-size: var(--admin-font-caption); }
:is(.admin-density, .admin-density-dialog) .density-form :is(.el-select, .el-input-number, .el-date-editor) { width: 100%; }
/* Existing native data tables keep their business column widths and sticky cells. */
:is(.admin-density, .admin-density-dialog) .density-table { width: 100%; border-collapse: collapse; font-size: var(--admin-font-body); }
:is(.admin-density, .admin-density-dialog) .density-table :is(th, td) { height: var(--admin-row-height); padding: var(--admin-space-1) var(--admin-space-3); border: 1px solid var(--admin-table-border); text-align: left; vertical-align: middle; overflow-wrap: anywhere; }
+126 -2
View File
@@ -1,5 +1,6 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { dirname, resolve } from 'node:path'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
import { compileStyle, parse } from 'vue/compiler-sfc'
@@ -38,11 +39,54 @@ const overseasConsumers = [
'overseas-warehouse/components/SmartDispatchDialog.vue',
'overseas-warehouse/components/DeliveryDocumentsDialog.vue',
]
const overseasBusinessConsumers = [
'overseas-business/pages/PurchaseOrdersView.vue', 'overseas-business/pages/PurchaseOrderDetailView.vue',
'overseas-business/pages/PurchaseOrderEditView.vue', 'overseas-business/pages/TransportOrdersView.vue',
'overseas-business/pages/TransportOrderFormView.vue',
]
const customerServiceConsumers = [
'overseas-warehouse/pages/ExceptionOrdersView.vue', 'overseas-warehouse/pages/CustomerVisitsView.vue',
]
const financeConsumers = [
'finance/pages/FinanceCustomerAccountsView.vue', 'finance/pages/FinanceCustomerArapView.vue',
'finance/pages/FinanceCustomerStatementsView.vue', 'finance/pages/FinanceFormalSettlementPage.vue',
'finance/pages/FinanceReceivableStatisticsView.vue',
]
const generalCatalogConsumers = [
'suppliers/pages/SuppliersView.vue',
'suppliers/pages/SupplierDetailView.vue',
'suppliers/pages/SupplierFormView.vue',
'suppliers/components/SupplierLicenseDialog.vue',
'overseas-master-data/pages/ExchangeRatesView.vue',
'overseas-master-data/pages/OverseasQuotesView.vue',
'overseas-master-data/pages/VehiclesView.vue',
'overseas-master-data/pages/VehicleDetailView.vue',
'master-data/express-companies/pages/ExpressCompaniesView.vue',
'master-data/counterparties/pages/PartnersView.vue',
'fee-items/pages/FeeItemsView.vue',
'warehouses/pages/WarehousesView.vue',
'fba-pricing/carrier-rates/pages/CarrierRatesView.vue',
'fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue',
'downloads/pages/DownloadCenterView.vue',
]
const notificationConsumers = [
'notifications/pages/PlatformNotificationsView.vue', 'notifications/pages/MessageManagementView.vue',
'notifications/pages/MessageTemplatesView.vue', 'notifications/components/MessageComposerDialog.vue',
'notifications/components/TemplateEditorDialog.vue',
]
const systemConsumers = ['iam/pages/InternalUsersView.vue', 'iam/pages/RolesView.vue', 'iam/pages/PermissionsView.vue', 'dashboard/pages/DashboardView.vue']
const consumers = [
...notificationConsumers,
...systemConsumers,
...generalCatalogConsumers,
...financeConsumers,
...customerServiceConsumers,
...overseasBusinessConsumers,
...overseasConsumers,
...cargoConsumers,
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue',
'customers/pages/OverseasStorageFeesView.vue', 'customers/components/CustomerBillingRulesPanel.vue',
'unified-orders/components/InboundFeeDetailManager.vue', 'unified-orders/components/OrderDeclarationEditorDialog.vue',
'customers/CustomerOverseasStorageFeePanel.vue', 'unified-orders/pages/UnifiedOrdersView.vue',
'inbounds/components/InboundTaskList.vue', 'inbounds/measurement/InboundMeasurementDialog.vue',
'inbounds/measurement/InboundMeasurementPanel.vue', 'inbounds/review/MeasurementReviewPanel.vue',
@@ -53,7 +97,7 @@ const consumers = [
'domestic-containers/pages/DomesticContainerInventoryDetailView.vue',
'container-loading/components/ContainerSimulationCreate.vue', 'audit/components/CustomerAuditLogsPanel.vue',
]
const moduleCss = ['customers/customerDensity.css', 'unified-orders/styles/orderDensity.css', 'inbounds/measurement/measurementDensity.css', 'domestic-containers/pages/domesticContainersSummary.css']
const moduleCss = ['iam/iamDensity.css', 'finance/financeDensity.css', 'overseas-business/overseasBusinessDensity.css', 'customers/customerDensity.css', 'unified-orders/styles/orderDensity.css', 'inbounds/measurement/measurementDensity.css', 'domestic-containers/pages/domesticContainersSummary.css']
const read = (file: string) => readFileSync(resolve(process.cwd(), 'src', file), 'utf8')
const domesticPages = [
{ title: '订单列表', path: '/orders', file: 'unified-orders/pages/UnifiedOrdersView.vue' },
@@ -69,6 +113,12 @@ const customerPages = [
{ title: '海外仓阶梯费用', path: '/customers/overseas-storage-fees', file: 'customers/pages/OverseasStorageFeesView.vue' },
]
const densityPages = [...new Set([
...notificationConsumers.filter(file => file.includes('/pages/')),
...systemConsumers,
...generalCatalogConsumers.filter(file => file.includes('/pages/')),
...financeConsumers,
...customerServiceConsumers,
...overseasBusinessConsumers,
...overseasConsumers.filter(file => !file.endsWith('PalletDetailView.vue')),
...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',
@@ -86,6 +136,7 @@ const classesOf = (node: ElementNode, preload = true, dense = preload, workbench
return flatten(value)
})
const styles = [
...parse(read('shared/components/BatchImportDialog.vue')).descriptor.styles.map(style => ({ file: 'BatchImportDialog.vue', css: style.content })),
...consumers.flatMap(file => parse(read(`modules/${file}`)).descriptor.styles.map(style => ({ file, css: style.content }))),
...moduleCss.map(file => ({ file, css: read(`modules/${file}`) })),
]
@@ -98,6 +149,78 @@ const cssRoot = (css: string) => {
}
describe('approved Admin density ownership', () => {
it('opts application-shell confirmations in even before a business page loads', () => {
const entries = ['app/layouts/AppLayout.vue', 'app/components/WorkspaceTabs.vue', 'app/router/index.ts', 'main.ts']
let count = 0
for (const file of entries) {
const source = read(file)
expect(source, file).toContain("@/styles/adminDensity.css")
const script = file.endsWith('.vue') ? parse(source).descriptor.scriptSetup!.content : source
const tree = ts.createSourceFile(file, script, ts.ScriptTarget.Latest, true)
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(tree) === 'ElMessageBox') {
count += 1
const options = node.arguments[2]
expect(options && ts.isObjectLiteralExpression(options), file).toBe(true)
const customClass = options && ts.isObjectLiteralExpression(options) ? options.properties.find(prop => ts.isPropertyAssignment(prop) && prop.name.getText(tree) === 'customClass') : undefined
expect(customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer) && customClass.initializer.text, file).toBe('admin-density-confirm')
}
ts.forEachChild(node, visit)
}
visit(tree)
}
expect(count).toBe(5)
})
it('checks dialog roots along actual static and dynamic Vue imports from approved consumers', () => {
const pending = consumers.map(file => resolve(process.cwd(), 'src/modules', file))
const seen = new Set<string>()
while (pending.length) {
const file = pending.shift()!
if (seen.has(file)) continue
seen.add(file)
const descriptor = parse(readFileSync(file, 'utf8')).descriptor
const enqueue = (reference: string) => {
if (!reference.endsWith('.vue')) return
if (reference.startsWith('@/')) pending.push(resolve(process.cwd(), 'src', reference.slice(2)))
else if (reference.startsWith('.')) pending.push(resolve(dirname(file), reference))
}
for (const script of [descriptor.script, descriptor.scriptSetup]) {
if (!script) continue
const ast = ts.createSourceFile(file, script.content, ts.ScriptTarget.Latest, true)
const visitImports = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(ast) === 'ElMessageBox') {
expect(node.arguments[2]?.getText(ast), file + ': reachable confirmation').toContain('admin-density-confirm')
}
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) enqueue(node.moduleSpecifier.text)
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const reference = node.arguments[0]
if (reference && ts.isStringLiteral(reference)) enqueue(reference.text)
}
ts.forEachChild(node, visitImports)
}
visitImports(ast)
}
if (['AppDialog.vue', 'AppDrawer.vue', 'BatchImportDialog.vue'].some(name => file.endsWith(name))) continue
const visitTemplate = (nodes: TemplateNode[]) => {
for (const node of nodes) {
if (node.type !== 1) continue
if (['AppDialog', 'el-dialog', 'BatchImportDialog'].includes(node.tag)) expect(classesOf(node), `${file}: reachable modal`).toContain('admin-density-dialog')
if (['AppDrawer', 'el-drawer'].includes(node.tag)) expect(classesOf(node), `${file}: reachable drawer`).toContain('admin-density-drawer')
visitTemplate(node.children)
}
}
if (descriptor.template?.ast) visitTemplate(descriptor.template.ast.children)
}
})
it('keeps the supplier license dialog styled at its own teleported root', () => {
const source = read('modules/suppliers/components/SupplierLicenseDialog.vue')
expect(source).toContain('<AppDialog class="admin-density-dialog"')
expect(source).toContain('width="min(560px, calc(100vw - 24px))"')
expect(source).toContain('aria-label="营业执照信息"')
})
it('uses identical authored tab paint for native domestic and Element overseas tabs', () => {
const element = '.admin-density .density-element-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item'
@@ -321,6 +444,7 @@ describe('approved Admin density ownership', () => {
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.trim(), file + ': empty selector').not.toBe('')
expect(rule.selector, file).not.toMatch(/customer-action-button|order-action-button|\.density-action|\.density-tab-icon/)
let desktop = true
for (let parent: typeof rule.parent | ReturnType<typeof cssRoot>['parent'] = rule.parent; parent; parent = parent.parent) {