style(finance): unify list and detail layouts and receipt controls

This commit is contained in:
2026-10-27 16:55:34 +08:00
parent b9b930936d
commit b3ab87f22c
17 changed files with 385 additions and 218 deletions
+104
View File
@@ -221,6 +221,110 @@ const cssRoot = (css: string) => {
}
describe('approved Admin density ownership', () => {
it('draws only one separator between an Element table and pagination', () => {
const style = parse(read('shared/components/DataTable.vue')).descriptor.styles[0]!
const compiled = compileStyle({ source: style.content, filename: 'DataTable.vue', id: 'data-v-border-test', scoped: true })
expect(compiled.errors).toHaveLength(0)
const selectors: string[] = []
compiled.rawResult!.root.walkRules(rule => {
if (rule.selector.includes('.el-table + .data-table__pagination')) {
selectors.push(rule.selector)
expect(rule.nodes.some(node => node.type === 'decl' && node.prop === 'border-top' && node.value === '0')).toBe(true)
}
})
expect(selectors).toHaveLength(1)
const host = document.createElement('div')
host.innerHTML = `<div class="data-table"><div class="el-table"><div class="el-table__inner-wrapper"></div></div><footer id="standard" class="data-table__pagination" data-v-border-test></footer></div>
<div class="data-table"><section class="custom-results"></section><footer id="custom" class="data-table__pagination" data-v-border-test></footer></div>`
expect(host.querySelector('#standard')!.matches(selectors[0]!)).toBe(true)
expect(host.querySelector('#custom')!.matches(selectors[0]!)).toBe(false)
expect(style.content).not.toMatch(/inner-wrapper::before\s*\{[^}]*display:\s*none/)
})
it('uses the shared compact metric cards for finance statistics and settlement totals', () => {
expect(read('modules/finance/pages/FinanceReceivableStatisticsView.vue')).toContain('spacing="compact"')
expect(read('modules/finance/pages/FinanceFormalSettlementPage.vue')).toContain(':spacing="detailMode ? \'normal\' : \'compact\'"')
const financeStyles = read('modules/finance/financeDensity.css')
expect(financeStyles).toContain('.admin-workbench.receivable-statistics-page.admin-page.is-spacing-compact,')
expect(financeStyles).toContain('.admin-workbench.finance-settlement-page.admin-page.is-spacing-compact { gap: 0; }')
expect(financeStyles).not.toContain('.data-table__toolbar')
const baseStyle = parse(read('shared/components/AdminPage.vue')).descriptor.styles[0]!
const compiled = compileStyle({ source: baseStyle.content, filename: 'AdminPage.vue', id: 'data-v-spacing-check', scoped: true })
expect(compiled.errors).toHaveLength(0)
const baseSelector = compiled.code.match(/([^{}]+)\{\s*gap: 12px;/)![1]!.trim()
const specificity = (selector: string) => (selector.match(/\.[\w-]+|\[[^\]]+\]/g) ?? []).length
for (const page of ['receivable-statistics-page', 'finance-settlement-page']) {
const selector = `.admin-workbench.${page}.admin-page.is-spacing-compact`
expect(specificity(selector), 'must override scoped AdminPage even when it loads later').toBeGreaterThan(specificity(baseSelector))
}
for (const file of ['FinanceReceivableStatisticsView', 'FinanceFormalSettlementPage']) {
const content = read(`modules/finance/pages/${file}.vue`)
const summary = content.match(/<section class="finance-metric-grid"[^>]*v-loading="loading"[\s\S]*?<\/section>/)![0]
expect(content.indexOf(summary), file).toBeLessThan(content.indexOf('<template #filters>'))
const template = parse(content).descriptor.template!.ast!
function visit(node: { tag?: string; loc?: { source: string }; children?: unknown[] }, insideTable = false) {
if (node.tag === 'section' && node.loc?.source === summary) expect(insideTable, file).toBe(false)
for (const child of node.children ?? []) visit(child as typeof node, insideTable || node.tag === 'DataTable')
}
visit(template)
expect(summary.match(/workbench-metric workbench-tone is-compact/g), file).toHaveLength(3)
expect(summary).toContain('v-loading="loading" :aria-busy="loading"')
expect(summary).not.toContain('density-detail-overview')
for (const tone of ['blue', 'green', 'purple']) expect(summary).toContain(`data-tone="${tone}"`)
}
})
it('keeps finance detail tables on one continuous surface', () => {
const statements = read('modules/finance/pages/FinanceCustomerStatementsView.vue')
const arap = read('modules/finance/pages/FinanceCustomerArapView.vue')
expect(statements).not.toContain('<el-tabs')
expect(statements).toContain('title="账单明细表"')
expect(statements).toContain('title="账单支付历史表"')
expect(statements.match(/class="detail-table-scroll workbench-table is-page-surface"/g)).toHaveLength(2)
expect(statements).not.toContain('title="账单明细"')
expect(statements).not.toContain('title="支付历史"')
expect(arap).toContain('<div class="workbench-table is-page-surface"><DataTable :data="operationLogRows"')
const css = read('modules/finance/financeDensity.css')
expect(css).toContain('.finance-density-detail > .detail-page-layout__content .density-form-section')
expect(css).toContain('padding-block: 0;')
const accounts = read('modules/finance/pages/FinanceCustomerAccountsView.vue')
expect(accounts).not.toContain(':deep(.detail-page-layout__content)')
})
it('keeps finance detail insets on the outer page without stripping card or cell padding', () => {
for (const [file, className] of [['FinanceCustomerAccountsView', 'finance-account-detail'], ['FinanceCustomerArapView', 'finance-arap-detail']]) {
expect(read(`modules/finance/pages/${file}.vue`)).toContain(`class="${className} admin-density admin-workbench is-embedded"`)
}
const css = read('modules/finance/financeDensity.css')
const rule = css.slice(css.indexOf('/* The outer AdminPage owns'))
expect(rule).toContain('@media screen')
expect(rule).toContain('padding-inline: 0;')
expect(rule).toContain('.detail-page-layout__content > .settlement-detail-content')
expect(rule).toContain(':is(.density-detail-overview, .statement-summary-section)')
for (const protectedClass of ['workbench-metric', 'el-descriptions__cell', 'query-toolbar', 'el-table']) expect(rule).not.toContain(protectedClass)
const accountStyles = parse(read('modules/finance/pages/FinanceCustomerAccountsView.vue')).descriptor.styles[0]!.content
expect(accountStyles.match(/\.account-metrics \{([^}]+)\}/)![1]).not.toMatch(/padding:|padding-inline:|padding-left:|padding-right:/)
})
it('keeps account detail filters in the shared table layout and long identifiers readable', () => {
const content = read('modules/finance/pages/FinanceCustomerAccountsView.vue')
const header = content.split('<template #header>')[1]!.split('</template>')[0]!
expect(header).toContain('@click="exportLedger"')
expect(header).toContain('v-if="canExport"')
expect(header).toContain(':loading="ledgerExportBusy"')
const ledgerSection = content.split('class="ledger-section density-form-section"')[1]!.split('</FormSection>')[0]!
expect(ledgerSection).not.toContain('#toolbar-actions')
expect(ledgerSection).toContain('class="detail-table-scroll workbench-table is-page-surface"')
expect(ledgerSection).toContain('class="ledger-table" appearance="integrated"')
const rechargeSection = content.split('class="recharge-section density-form-section"')[1]!.split('</FormSection>')[0]!
expect(rechargeSection).toContain('class="detail-table-scroll workbench-table is-page-surface"')
expect(content.match(/account-metric workbench-metric workbench-tone is-compact/g)).toHaveLength(6)
expect(content).toContain('creditExpiryLabel(selected)')
expect(content).toContain("'is-negative': Number(selected.actualBalance) < 0")
expect(content).toContain('<template #filters><QueryToolbar v-model="ledgerFilters"')
expect(content).not.toContain(':deep(.ledger-query.query-toolbar)')
expect(content).toContain('class="density-descriptions account-profile"')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toContain('text-overflow: ellipsis')
expect(styles).toContain('overflow-wrap: anywhere')
})
it('keeps pallet overview spacing and metric appearance owned by shared primitives', () => {
for (const file of ['warehouse-resources/pallets/pages/PalletsView.vue', 'overseas-warehouse/pages/PalletsView.vue']) {
const page = parse(read(`modules/${file}`)).descriptor