优化
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user