feat: 收口组件样式并调整移动端侧栏

This commit is contained in:
GJ
2026-09-04 17:19:34 +08:00
parent d252d5513f
commit e6e0f08072
68 changed files with 2717 additions and 2775 deletions
+239
View File
@@ -0,0 +1,239 @@
import { readFileSync, readdirSync } from 'node:fs'
const SPACE: Record<string, string> = {
'0': '0',
px: '1px',
'0.5': '2px',
'1': '4px',
'1.5': '6px',
'2': '8px',
'2.5': '10px',
'3': '12px',
'3.5': '14px',
'4': '16px',
'5': '20px',
'6': '24px',
'7': '28px',
'8': '32px',
'9': '36px',
'10': '40px',
'11': '44px',
'12': '48px',
'14': '56px'
}
const STATIC: Record<string, string> = {
flex: 'display: flex;',
grid: 'display: grid;',
block: 'display: block;',
hidden: 'display: none;',
contents: 'display: contents;',
'inline-flex': 'display: inline-flex;',
'flex-col': 'flex-direction: column;',
'flex-wrap': 'flex-wrap: wrap;',
'items-center': 'align-items: center;',
'items-end': 'align-items: end;',
'items-start': 'align-items: flex-start;',
'items-stretch': 'align-items: stretch;',
'self-start': 'align-self: start;',
'justify-center': 'justify-content: center;',
'justify-start': 'justify-content: flex-start;',
'justify-between': 'justify-content: space-between;',
'justify-self-end': 'justify-self: end;',
'shrink-0': 'flex-shrink: 0;',
'flex-1': 'flex: 1;',
'overflow-hidden': 'overflow: hidden;',
'min-h-0': 'min-height: 0;',
'min-w-0': 'min-width: 0;',
'w-full': 'width: 100%;',
'w-max': 'width: max-content;',
'h-full': 'height: 100%;',
'h-auto': 'height: auto;',
'h-dvh': 'height: 100dvh;',
'max-w-full': 'max-width: 100%;',
'max-h-none': 'max-height: none;',
'whitespace-nowrap': 'white-space: nowrap;',
'whitespace-normal': 'white-space: normal;',
'whitespace-pre-wrap': 'white-space: pre-wrap;',
'text-center': 'text-align: center;',
'text-left': 'text-align: left;',
'wrap-anywhere': 'overflow-wrap: anywhere;',
'rounded-none': 'border-radius: 0;',
relative: 'position: relative;',
sticky: 'position: sticky;',
fixed: 'position: fixed;',
'top-0': 'top: 0;',
'left-0': 'left: 0;',
'right-0': 'right: 0;',
'bottom-0': 'bottom: 0;',
'inset-0': 'inset: 0;',
'list-none': 'list-style: none;',
'place-items-center': 'place-items: center;',
'place-content-center': 'place-content: center;',
'object-contain': 'object-fit: contain;',
'overscroll-contain': 'overscroll-behavior: contain;',
'break-inside-avoid': 'break-inside: avoid;',
'outline-none': 'outline: none;',
'text-ellipsis': 'text-overflow: ellipsis;',
'font-normal': 'font-weight: 400;',
'font-medium': 'font-weight: 500;',
'font-semibold': 'font-weight: 600;',
'font-mono': 'font-family: ui-monospace, monospace;',
'text-xs': 'font-size: 12px;',
'text-sm': 'font-size: 14px;',
'text-base': 'font-size: 16px;',
'leading-normal': 'line-height: 1.5;',
'm-0': 'margin: 0;',
'p-0': 'padding: 0;',
'border-0': 'border: 0;',
'border-t-0': 'border-top: none;',
'mx-auto': 'margin-inline: auto;',
'mx-0': 'margin-inline: 0;',
'my-0': 'margin-block: 0;',
'ml-auto': 'margin-left: auto;',
'px-0': 'padding-inline: 0;',
'bg-transparent': 'background: transparent;',
'order-0': 'order: 0;',
'order-1': 'order: 1;',
'order-2': 'order: 2;',
'aspect-auto': 'aspect-ratio: auto;',
'aspect-video': 'aspect-ratio: 16 / 9;',
'rotate-180': 'transform: rotate(180deg);',
'text-ink': 'color: var(--app-ink);',
'text-muted': 'color: var(--app-muted);',
'text-accent': 'color: var(--app-accent-text);',
'bg-ink': 'background: var(--app-ink);',
'z-10': 'z-index: 10;'
}
const PREFIX_PROP: Record<string, string> = {
'min-h': 'min-height',
'min-w': 'min-width',
'max-h': 'max-height',
'max-w': 'max-width',
'grid-cols': 'grid-template-columns',
'grid-rows': 'grid-template-rows',
col: 'grid-column',
flex: 'flex',
w: 'width',
h: 'height',
p: 'padding',
px: 'padding-inline',
py: 'padding-block',
pt: 'padding-top',
pr: 'padding-right',
pb: 'padding-bottom',
pl: 'padding-left',
m: 'margin',
mx: 'margin-inline',
my: 'margin-block',
mt: 'margin-top',
mr: 'margin-right',
mb: 'margin-bottom',
ml: 'margin-left',
gap: 'gap',
'gap-x': 'column-gap',
'gap-y': 'row-gap',
text: 'font-size',
leading: 'line-height',
tracking: 'letter-spacing',
top: 'top',
right: 'right',
bottom: 'bottom',
left: 'left',
inset: 'inset',
z: 'z-index',
shadow: 'box-shadow',
outline: 'outline',
'outline-offset': 'outline-offset',
bg: 'background',
columns: 'columns',
aspect: 'aspect-ratio'
}
const PREFIXES = Object.keys(PREFIX_PROP).toSorted((a, b) => b.length - a.length)
/**
* 把单个 Tailwind 工具类还原成 CSS 声明,供样式契约测试对照。
* @param util 工具类名
* @returns CSS 声明,无法还原时返回空字符串
*/
function expandUtil(util: string): string {
if (util.startsWith('[') && util.endsWith(']')) {
const inner = util.slice(1, -1)
const colon = inner.indexOf(':')
if (colon > 0) return `${inner.slice(0, colon)}: ${inner.slice(colon + 1)};`
}
if (STATIC[util]) return STATIC[util]!
const paren = util.match(/^([a-z-]+)-\((--[\w-]+)\)$/)
if (paren) {
const prop = PREFIX_PROP[paren[1]!] ?? paren[1]
const cssVar = paren[2]!
if (prop === 'background') return `background: var(${cssVar});`
if (paren[1] === 'text') return `color: var(${cssVar});`
return `${prop}: var(${cssVar});`
}
const arb = util.match(/^(-?[a-z-]+)-\[(.+)\]$/)
if (arb) {
const name = arb[1]!
const raw = arb[2]!.replace(/_/g, ' ').replace(/,(?=\S)/g, ', ')
const prop = PREFIX_PROP[name.replace(/^-/, '')]
if (!prop) return `${name}: ${raw};`
if (name.startsWith('-')) return `${prop}: -${raw};`
return `${prop}: ${raw};`
}
for (const prefix of PREFIXES) {
if (!util.startsWith(`${prefix}-`)) continue
const key = util.slice(prefix.length + 1)
const px = SPACE[key]
const prop = PREFIX_PROP[prefix]
if (px !== undefined && prop) return `${prop}: ${px};`
}
return ''
}
/**
* 把 @apply 行展开成 CSS 声明,并补回测试仍在匹配的 padding/margin 简写。
* @param css 含 @apply 的样式源码
* @returns 展开后的样式文本
*/
function expandApply(css: string): string {
return css.replace(/@apply\s+([^;]+);/g, (_all, utils: string) => {
const decls = utils.trim().split(/\s+/).map(expandUtil).filter(Boolean)
const valueOf = (prefix: string) =>
decls
.find(item => item.startsWith(prefix))
?.slice(prefix.length)
.replace(';', '')
.trim()
const paddingX = valueOf('padding-inline:')
const paddingY = valueOf('padding-block:')
const paddingT = valueOf('padding-top:')
const paddingB = valueOf('padding-bottom:')
if (paddingY && paddingX) decls.push(`padding: ${paddingY} ${paddingX};`)
else if (paddingT && paddingX && paddingB) decls.push(`padding: ${paddingT} ${paddingX} ${paddingB};`)
const marginX = valueOf('margin-inline:')
const marginT = valueOf('margin-top:')
const marginB = valueOf('margin-bottom:')
if (marginT && marginX && marginB) decls.push(`margin: ${marginT} ${marginX} ${marginB};`)
if (marginT && marginB) decls.push(`margin-block: ${marginT} ${marginB};`)
const gapY = valueOf('row-gap:')
const gapX = valueOf('column-gap:')
if (gapY && gapX) decls.push(`gap: ${gapY} ${gapX};`)
return decls.join(' ')
})
}
/**
* 读取全局 CSS 与全部组件内 style,供样式契约测试对照搬迁后的规则。
* @returns 拼接后的样式源码
*/
export function readAllStyles(): string {
const vueFiles = readdirSync('src', { recursive: true, encoding: 'utf8' }).filter(path =>
String(path).endsWith('.vue')
)
const raw = ['src/styles/styles.css', 'src/styles/admin.css', ...vueFiles.map(path => `src/${path}`)]
.map(path => readFileSync(path, 'utf8'))
.join('\n')
return expandApply(raw)
}