feat(admin): apply shared blue-white self-operated workbench style

This commit is contained in:
2026-10-20 19:16:47 +08:00
parent 9bfce44988
commit 464b72cff9
38 changed files with 1851 additions and 340 deletions
+128
View File
@@ -0,0 +1,128 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { compileStyle, parse } from 'vue/compiler-sfc'
import { adminWorkbenchChartTheme } from './adminWorkbenchChartTheme'
const read = (path: string) => readFileSync(resolve(process.cwd(), 'src', path), 'utf8')
const tokens = read('styles/adminWorkbenchTokens.css')
const styles = read('styles/adminWorkbench.css')
const page = read('modules/self-operated/pages/OverviewView.vue')
const listPages = [
'modules/self-operated/components/AnalyticsView.vue',
'modules/self-operated/components/InventoryTurnoverView.vue',
]
const consumers = [
...listPages,
'modules/self-operated/pages/InventoryView.vue',
'modules/self-operated/components/SkuTrend.vue',
'modules/self-operated/components/ChartDetailsDialog.vue',
'modules/self-operated/pages/CollectionView.vue',
'modules/self-operated/components/CollectionAccountMonitor.vue',
'modules/self-operated/components/CollectionProgressGauge.vue',
'modules/self-operated/components/CollectionTaskProgress.vue',
]
const root = (source: string) => {
const result = compileStyle({ source, filename: 'workbench.css', id: 'workbench-contract' })
expect(result.errors).toEqual([])
return result.rawResult!.root
}
describe('Admin workbench opt-in styles', () => {
it('keeps canvas chart colors aligned with the public CSS palette', () => {
const declarations = new Map<string, string>()
root(tokens).walkRules(rule => {
if (rule.selector !== '.admin-workbench') return
rule.walkDecls(declaration => { declarations.set(declaration.prop, declaration.value) })
})
for (const name of ['primary', 'text', 'muted', 'border', 'surface'] as const) {
expect(adminWorkbenchChartTheme.colors[name]).toBe(declarations.get(`--workbench-${name}`))
}
expect(adminWorkbenchChartTheme.colors.primarySoft).toBe(declarations.get('--workbench-accent-soft'))
})
it('keeps lazy-loaded styles scoped to opted-in pages and dialog roots', () => {
for (const source of [tokens, styles]) {
root(source).walkRules(rule => {
if (rule.parent?.type === 'atrule' && rule.parent.name.endsWith('keyframes')) return
for (const selector of rule.selectors) {
expect(selector).toMatch(/^\.admin-workbench(?:\s|\.|:|\[|$)/)
expect(selector).not.toMatch(/:global|:root/)
if (selector.includes('.el-dialog')) expect(selector).toMatch(/^\.admin-workbench\.is-dialog\.el-dialog/)
}
})
root(source).walkDecls(declaration => {
expect(declaration.prop).not.toMatch(/^--admin-/)
expect(declaration.important).toBeFalsy()
})
}
})
it('resolves every workbench variable from the public tokens instead of page defaults', () => {
const declared = new Set<string>()
root(tokens).walkDecls(declaration => {
if (declaration.prop.startsWith('--workbench-')) declared.add(declaration.prop)
})
const pageStyles = [page, ...consumers.map(read)].flatMap(source => parse(source).descriptor.styles.map(style => style.content)).join('\n')
for (const source of [styles, pageStyles]) {
root(source).walkDecls(declaration => {
expect(declaration.prop).not.toMatch(/^--workbench-/)
})
for (const match of source.matchAll(/var\((--workbench-[\w-]+)/g)) {
expect(declared.has(match[1]!)).toBe(true)
}
}
})
it('opts list cards and teleported dialogs in on their actual rendering containers', () => {
for (const file of listPages) {
const source = read(file)
expect(source).toContain("import '@/styles/adminWorkbench.css'")
const template = parse(source).descriptor.template!.ast!
type TemplateElement = Extract<(typeof template.children)[number], { type: 1 }>
const classesOf = (node?: TemplateElement) => node?.props.flatMap(prop => prop.type === 6 && prop.name === 'class' ? prop.value?.content.split(/\s+/) ?? [] : []) ?? []
let tables = 0
const visit = (nodes: typeof template.children, ancestors: TemplateElement[]) => {
for (const node of nodes) {
if (node.type !== 1) continue
if (node.tag === 'AdminPage') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density', 'admin-workbench']))
if (node.tag === 'DataTable') {
expect(classesOf(ancestors.at(-1))).toContain('workbench-table')
expect(classesOf(node)).not.toContain('workbench-table')
tables++
}
if (node.tag === 'AppDialog') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
visit(node.children, [...ancestors, node])
}
}
visit(template.children, [])
expect(tables).toBe(1)
}
})
it('preserves complete metric values and the existing toolbar layout ownership', () => {
root(styles).walkRules(rule => {
if (rule.selector.includes('.workbench-metric')) {
rule.walkDecls(declaration => {
expect(['text-overflow', 'line-clamp', '-webkit-line-clamp']).not.toContain(declaration.prop)
if (declaration.prop === 'overflow') expect(declaration.value).not.toBe('hidden')
expect(declaration.prop).not.toBe('height')
})
}
if (rule.selector.includes('.data-table__toolbar')) {
rule.walkDecls(declaration => {
if (rule.selector.includes('.workbench-table.is-compact-toolbar')) {
expect(['min-height', 'padding-top', 'padding-bottom']).toContain(declaration.prop)
} else {
expect(declaration.prop).not.toMatch(/^(?:padding|height|min-height|display|gap)/)
}
})
}
if (rule.selector.includes('.is-dialog')) {
rule.walkDecls(declaration => {
expect(declaration.prop).not.toMatch(/^(?:width|height|min-height|max-height|padding|margin|overflow)/)
})
}
})
})
})