style: 全站使用背景分层替代装饰边框并统一直角

This commit is contained in:
GouJ
2026-09-01 21:39:05 +08:00
parent 799f16d336
commit 03bf87ca9a
25 changed files with 292 additions and 116 deletions
+3 -2
View File
@@ -59,8 +59,9 @@ describe('统一目录条目', () => {
expect(css).toMatch(/\.directory-item-title\s*\{[^}]*overflow-wrap:\s*anywhere/)
expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/)
expect(css).toContain('grid-template-columns: clamp(240px, 22%, 280px) minmax(0, 1fr)')
expect(css).toMatch(/\.identity-subject-list-content\s*\{[^}]*gap:\s*0;[^}]*padding:\s*0 0 8px/)
expect(css).toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*min-height:\s*86px;[^}]*border-bottom:/)
expect(css).toMatch(/\.identity-subject-list-content\s*\{[^}]*gap:\s*2px;[^}]*padding:\s*0 0 8px/)
expect(css).toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*min-height:\s*86px;[^}]*padding:\s*10px 14px/)
expect(css).not.toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*border-bottom:/)
expect(css).toMatch(/\.identity-thumbnail\s*\{[^}]*width:\s*48px;[^}]*height:\s*64px/)
expect(css).toMatch(/\.n-button\.identity-subject-item:not\(\.selected\)\s*\{[^}]*var\(--app-surface\)/)
})
+89
View File
@@ -0,0 +1,89 @@
import { h } from 'vue'
import { mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NAlert, NButton, NCard, NConfigProvider, NInput, NSelect, NTable, NTag } from 'naive-ui'
import { readFileSync, readdirSync } from 'node:fs'
import { useTheme } from '../../composables/useTheme'
let wrapper: VueWrapper | undefined
afterEach(() => {
wrapper?.unmount()
wrapper = undefined
localStorage.clear()
delete document.documentElement.dataset.theme
document.documentElement.style.colorScheme = ''
vi.unstubAllGlobals()
})
describe('全站平面主题', () => {
it.each([false, true])('模式 %s 的真实控件无静态描边、使用直角,并保留聚焦与错误反馈', dark => {
localStorage.setItem('drama-studio-theme', dark ? 'dark' : 'light')
vi.stubGlobal('matchMedia', () => ({ matches: dark, addEventListener() {}, removeEventListener() {} }))
wrapper = mount({
setup() {
const { theme, overrides } = useTheme()
return () =>
h(NConfigProvider, { theme: theme.value, themeOverrides: overrides.value }, () => [
h(NButton, {}, () => '生成'),
h(NInput, { class: 'normal-input' }),
h(NInput, { status: 'error', class: 'error-input' }),
h(NSelect, { options: [{ label: '第一集', value: 1 }] }),
h(NAlert, { type: 'error' }, () => '生成失败'),
h(NCard, {}, () => '内容'),
h(NTag, {}, () => '待设计'),
h(NTable, { striped: true }, () => h('tbody', [h('tr', [h('td', '记录')])]))
])
}
})
const value = (selector: string, name: string) =>
(wrapper!.get(selector).element as HTMLElement).style.getPropertyValue(name)
const control = dark ? '#2b2b2b' : '#f0f0f0'
for (const selector of ['.n-button', '.normal-input', '.n-base-selection', '.n-alert', '.n-card', '.n-tag']) {
expect(value(selector, '--n-border-radius')).toBe('0px')
}
for (const selector of ['.n-button', '.normal-input', '.n-base-selection']) {
expect(value(selector, '--n-border')).toBe('none')
expect(value(selector, '--n-color')).toBe(control)
expect(value(selector, '--n-border-focus')).toContain(dark ? '#5cd693' : '#087c42')
}
expect(value('.n-alert', '--n-border')).toBe('none')
expect(value('.n-tag', '--n-border')).toBe('none')
expect(value('.n-card', '--n-border-color')).toBe('transparent')
expect(value('.n-table', '--n-border-color')).toBe('transparent')
expect(value('.error-input', '--n-border-error')).toContain(dark ? '#fa7373' : '#a93232')
expect(wrapper.get('.error-input').classes()).toContain('n-input--error-status')
// 不全局抹掉边框色:未勾选复选框和错误态继续由 Naive 的主题处理。
const theme = wrapper.getComponent(NConfigProvider).props('themeOverrides')!
expect(theme.common?.borderColor).not.toBe('transparent')
expect(theme.Checkbox).not.toHaveProperty('border')
expect(theme.Tabs?.barColor).toBe('#07c160')
})
it('业务面板、列表分组和标签以背景区分,保留键盘焦点与选中指示', () => {
// happy-dom 不计算布局;此项保护 CSS 契约,不能替代浏览器视觉验收。
const admin = readFileSync('src/admin.css', 'utf8')
const css = readFileSync('src/styles.css', 'utf8')
expect(admin + css).not.toMatch(/border-radius:\s*[1-9]/)
expect(admin + css).not.toMatch(/border(?:-[\w]+)?:\s*1px (?:solid|dashed) var\(--(?:app-border|color-line)\)/)
expect(css).toMatch(/\.surface-inset\s*\{\s*background:\s*var\(--app-subtle\)/)
expect(css).toMatch(/\.record-list > article:nth-child\(even\)\s*\{\s*background:/)
expect(admin).toMatch(/\.directory-group-heading\s*\{[^}]*background:\s*var\(--app-control\)/)
expect(admin).toMatch(/\.directory-status\s*\{[^}]*background:\s*var\(--app-control\)/)
expect(admin).toMatch(/\.n-button\.directory-item:focus-visible\s*\{[^}]*outline:\s*2px/)
expect(admin).toMatch(
/\.n-button\.directory-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/
)
})
it('所有页面不再通过工具类添加装饰边框或圆角', () => {
const files = readdirSync('src', { recursive: true, encoding: 'utf8' }).filter(path => path.endsWith('.vue'))
for (const path of files) {
const source = readFileSync(`src/${path}`, 'utf8')
for (const match of source.matchAll(/\bclass="([^"]*)"/g)) {
expect(match[1]).not.toMatch(
/(?:^|\s)(?:rounded(?:-[\w-]+)?|border|border-[trbl](?:-\d+)?|divide-[xy])(?:\s|$)/
)
}
}
})
})