/** * @aspect/lynx-mui Theme System * Lynx 版 MUI 主题系统 - 100% 一比一对应 MUI 原版 * * 参考: mui-source/packages/mui-material/src/styles/ */ // ============================================= // 颜色定义 - 对应 MUI colors/ // ============================================= export const common = { black: '#000', white: '#fff', } export const grey = { 50: '#fafafa', 100: '#f5f5f5', 200: '#eeeeee', 300: '#e0e0e0', 400: '#bdbdbd', 500: '#9e9e9e', 600: '#757575', 700: '#616161', 800: '#424242', 900: '#212121', A100: '#f5f5f5', A200: '#eeeeee', A400: '#bdbdbd', A700: '#616161', } export const purple = { 50: '#f3e5f5', 100: '#e1bee7', 200: '#ce93d8', 300: '#ba68c8', 400: '#ab47bc', 500: '#9c27b0', 600: '#8e24aa', 700: '#7b1fa2', 800: '#6a1b9a', 900: '#4a148c', A100: '#ea80fc', A200: '#e040fb', A400: '#d500f9', A700: '#aa00ff', } export const red = { 50: '#ffebee', 100: '#ffcdd2', 200: '#ef9a9a', 300: '#e57373', 400: '#ef5350', 500: '#f44336', 600: '#e53935', 700: '#d32f2f', 800: '#c62828', 900: '#b71c1c', A100: '#ff8a80', A200: '#ff5252', A400: '#ff1744', A700: '#d50000', } export const orange = { 50: '#fff3e0', 100: '#ffe0b2', 200: '#ffcc80', 300: '#ffb74d', 400: '#ffa726', 500: '#ff9800', 600: '#fb8c00', 700: '#f57c00', 800: '#ef6c00', 900: '#e65100', A100: '#ffd180', A200: '#ffab40', A400: '#ff9100', A700: '#ff6d00', } export const blue = { 50: '#e3f2fd', 100: '#bbdefb', 200: '#90caf9', 300: '#64b5f6', 400: '#42a5f5', 500: '#2196f3', 600: '#1e88e5', 700: '#1976d2', 800: '#1565c0', 900: '#0d47a1', A100: '#82b1ff', A200: '#448aff', A400: '#2979ff', A700: '#2962ff', } // ============================================= // lightBlue - MUI info 颜色使用此色板 // ============================================= export const lightBlue = { 50: '#e1f5fe', 100: '#b3e5fc', 200: '#81d4fa', 300: '#4fc3f7', 400: '#29b6f6', 500: '#03a9f4', 600: '#039be5', 700: '#0288d1', 800: '#0277bd', 900: '#01579b', A100: '#80d8ff', A200: '#40c4ff', A400: '#00b0ff', A700: '#0091ea', } export const green = { 50: '#e8f5e9', 100: '#c8e6c9', 200: '#a5d6a7', 300: '#81c784', 400: '#66bb6a', 500: '#4caf50', 600: '#43a047', 700: '#388e3c', 800: '#2e7d32', 900: '#1b5e20', A100: '#b9f6ca', A200: '#69f0ae', A400: '#00e676', A700: '#00c853', } // ============================================= // 颜色操作函数 - 对应 MUI colorManipulator.js // ============================================= /** * 解析颜色为 RGB 值 */ function decomposeColor(color: string): { type: string; values: number[] } { if (color.startsWith('#')) { return { type: 'rgb', values: [ parseInt(color.slice(1, 3), 16), parseInt(color.slice(3, 5), 16), parseInt(color.slice(5, 7), 16), ], } } const match = color.match(/^(rgb|rgba|hsl|hsla)\(([^)]+)\)/) if (match) { const type = match[1] const values = match[2].split(',').map((v) => parseFloat(v.trim())) return { type, values } } return { type: 'rgb', values: [0, 0, 0] } } /** * 重新组合颜色 */ function recomposeColor(color: { type: string; values: number[] }): string { const { type, values } = color if (type === 'rgb') { return `rgb(${values[0]}, ${values[1]}, ${values[2]})` } if (type === 'rgba') { return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${values[3]})` } return `${type}(${values.join(', ')})` } /** * 获取颜色的亮度 (0-1) */ export function getLuminance(color: string): number { const { values } = decomposeColor(color) const rgb = values.map((val) => { val = val / 255 return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4 }) return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] } /** * 获取两个颜色之间的对比度 (1-21) */ export function getContrastRatio(foreground: string, background: string): number { const lumA = getLuminance(foreground) const lumB = getLuminance(background) return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05) } /** * 添加透明度 - alpha() */ export function alpha(color: string, value: number): string { const decomposed = decomposeColor(color) decomposed.type = 'rgba' decomposed.values[3] = value return recomposeColor(decomposed) } /** * 变暗颜色 - darken() */ export function darken(color: string, coefficient: number): string { const decomposed = decomposeColor(color) decomposed.values = decomposed.values.map((val, i) => i < 3 ? Math.max(0, Math.round(val * (1 - coefficient))) : val ) return recomposeColor(decomposed) } /** * 变亮颜色 - lighten() */ export function lighten(color: string, coefficient: number): string { const decomposed = decomposeColor(color) decomposed.values = decomposed.values.map((val, i) => i < 3 ? Math.min(255, Math.round(val + (255 - val) * coefficient)) : val ) return recomposeColor(decomposed) } /** * 强调颜色 (根据亮度自动变亮或变暗) */ export function emphasize(color: string, coefficient: number = 0.15): string { return getLuminance(color) > 0.5 ? darken(color, coefficient) : lighten(color, coefficient) } /** * contrastColor - 使用 oklch 格式生成对比色 * 对应 MUI createPalette.js 中的 contrastColor */ export function contrastColor(background: string): string { return `oklch(from ${background} var(--__l) 0 h / var(--__a))` } /** * hslToRgb - HSL 转 RGB */ function hslToRgb(h: number, s: number, l: number): [number, number, number] { const c = (1 - Math.abs(2 * l - 1)) * s const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) const m = l - c / 2 let r = 0, g = 0, b = 0 if (h < 60) { r = c; g = x; b = 0 } else if (h < 120) { r = x; g = c; b = 0 } else if (h < 180) { r = 0; g = c; b = x } else if (h < 240) { r = 0; g = x; b = c } else if (h < 300) { r = x; g = 0; b = c } else { r = c; g = 0; b = x } return [ Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), ] } /** * colorMix - CSS color-mix polyfill * 对应 MUI 的 colorSpace 支持 */ export function colorMix(colorSpace: string, color1: string, color2: string, percentage: number = 50): string { // 简化实现:使用 RGB 混合 const c1 = decomposeColor(color1) const c2 = decomposeColor(color2) const ratio = percentage / 100 const mixed = { type: 'rgb', values: [ Math.round(c1.values[0] * (1 - ratio) + c2.values[0] * ratio), Math.round(c1.values[1] * (1 - ratio) + c2.values[1] * ratio), Math.round(c1.values[2] * (1 - ratio) + c2.values[2] * ratio), ], } return recomposeColor(mixed) } // ============================================= // createPalette - 对应 MUI createPalette.js // ============================================= export interface PaletteColor { light: string main: string dark: string contrastText: string } export interface TypeText { primary: string secondary: string disabled: string icon?: string // dark mode only } export interface TypeAction { active: string hover: string hoverOpacity: number selected: string selectedOpacity: number disabled: string disabledBackground: string disabledOpacity: number focus: string focusOpacity: number activatedOpacity: number } export interface TypeBackground { default: string paper: string } export interface Palette { mode: 'light' | 'dark' common: typeof common primary: PaletteColor secondary: PaletteColor error: PaletteColor warning: PaletteColor info: PaletteColor success: PaletteColor grey: typeof grey text: TypeText divider: string action: TypeAction background: TypeBackground contrastThreshold: number tonalOffset: number // MUI 函数 getContrastText: (background: string) => string augmentColor: (options: { color: Partial; name?: string }) => PaletteColor } // Light 模式调色板 - 使用 lightBlue 作为 info export const lightPalette: Omit = { common, primary: { main: blue[700], light: blue[400], dark: blue[800], contrastText: common.white, }, secondary: { // MUI 原版使用 A400/A200/A700 作为 secondary main: purple.A400, light: purple.A200, dark: purple.A700, contrastText: common.white, }, error: { main: red[700], light: red[400], dark: red[800], contrastText: common.white, }, warning: { main: '#ed6c02', light: orange[500], dark: orange[900], contrastText: common.white, }, info: { main: lightBlue[700], // 修复:使用 lightBlue light: lightBlue[500], dark: lightBlue[900], contrastText: common.white, }, success: { main: green[800], light: green[500], dark: green[900], contrastText: common.white, }, grey, text: { primary: 'rgba(0, 0, 0, 0.87)', secondary: 'rgba(0, 0, 0, 0.6)', disabled: 'rgba(0, 0, 0, 0.38)', }, divider: 'rgba(0, 0, 0, 0.12)', background: { paper: common.white, default: common.white, }, action: { active: 'rgba(0, 0, 0, 0.54)', hover: 'rgba(0, 0, 0, 0.04)', hoverOpacity: 0.04, selected: 'rgba(0, 0, 0, 0.08)', selectedOpacity: 0.08, disabled: 'rgba(0, 0, 0, 0.26)', disabledBackground: 'rgba(0, 0, 0, 0.12)', disabledOpacity: 0.38, focus: 'rgba(0, 0, 0, 0.12)', focusOpacity: 0.12, activatedOpacity: 0.12, }, contrastThreshold: 3, tonalOffset: 0.2, } // Dark 模式调色板 - 使用 lightBlue 作为 info export const darkPalette: Omit = { common, primary: { main: blue[200], light: blue[50], dark: blue[400], contrastText: 'rgba(0, 0, 0, 0.87)', }, secondary: { // MUI 原版 dark mode 使用 A200/A100/A400 作为 secondary main: purple.A200, light: purple.A100, dark: purple.A400, contrastText: 'rgba(0, 0, 0, 0.87)', }, error: { main: red[500], light: red[300], dark: red[700], contrastText: common.white, }, warning: { main: orange[400], light: orange[300], dark: orange[700], contrastText: 'rgba(0, 0, 0, 0.87)', }, info: { main: lightBlue[400], // 修复:使用 lightBlue light: lightBlue[300], dark: lightBlue[700], contrastText: 'rgba(0, 0, 0, 0.87)', }, success: { main: green[400], light: green[300], dark: green[700], contrastText: 'rgba(0, 0, 0, 0.87)', }, grey, text: { primary: common.white, secondary: 'rgba(255, 255, 255, 0.7)', disabled: 'rgba(255, 255, 255, 0.5)', icon: 'rgba(255, 255, 255, 0.5)', // dark mode text.icon }, divider: 'rgba(255, 255, 255, 0.12)', background: { paper: '#121212', default: '#121212', }, action: { active: common.white, hover: 'rgba(255, 255, 255, 0.08)', hoverOpacity: 0.08, selected: 'rgba(255, 255, 255, 0.16)', selectedOpacity: 0.16, disabled: 'rgba(255, 255, 255, 0.3)', disabledBackground: 'rgba(255, 255, 255, 0.12)', disabledOpacity: 0.38, focus: 'rgba(255, 255, 255, 0.12)', focusOpacity: 0.12, activatedOpacity: 0.24, }, contrastThreshold: 3, tonalOffset: 0.2, } export interface PaletteOptions { mode?: 'light' | 'dark' primary?: Partial secondary?: Partial error?: Partial warning?: Partial info?: Partial success?: Partial text?: Partial background?: Partial action?: Partial divider?: string contrastThreshold?: number tonalOffset?: number } export function createPalette(options: PaletteOptions = {}): Palette { const { mode = 'light', contrastThreshold = 3, tonalOffset = 0.2, ...other } = options const basePalette = mode === 'dark' ? darkPalette : lightPalette /** * getContrastText - 计算对比文本颜色 * 对应 MUI createPalette.js 中的 getContrastText */ const getContrastText = (background: string): string => { const contrastText = getContrastRatio(background, common.white) >= contrastThreshold ? common.white : 'rgba(0, 0, 0, 0.87)' return contrastText } /** * augmentColor - 增强颜色(生成 light/dark 变体) * 对应 MUI createPalette.js 中的 augmentColor * 支持数字 shade (500) 或字符串 shade ('A400') */ const augmentColor = (options: { color: Partial & Record name?: string mainShade?: number | string lightShade?: number | string darkShade?: number | string }): PaletteColor => { const { color, mainShade = 500, lightShade = 300, darkShade = 700 } = options // 支持从颜色对象中获取指定 shade 的颜色值 const main = color.main || color[mainShade] if (!main) { throw new Error('MUI: The color provided to augmentColor(color) is invalid.') } // 尝试从颜色对象获取 light/dark,否则使用计算值 const light = color.light || color[lightShade] || lighten(main, tonalOffset) const dark = color.dark || color[darkShade] || darken(main, tonalOffset * 1.5) const contrastTextValue = color.contrastText || getContrastText(main) return { main, light, dark, contrastText: contrastTextValue } } return { mode, ...basePalette, ...other, contrastThreshold, tonalOffset, primary: { ...basePalette.primary, ...other.primary }, secondary: { ...basePalette.secondary, ...other.secondary }, error: { ...basePalette.error, ...other.error }, warning: { ...basePalette.warning, ...other.warning }, info: { ...basePalette.info, ...other.info }, success: { ...basePalette.success, ...other.success }, text: { ...basePalette.text, ...other.text }, background: { ...basePalette.background, ...other.background }, action: { ...basePalette.action, ...other.action }, getContrastText, augmentColor, } as Palette } // ============================================= // createTypography - 对应 MUI createTypography.js // 签名: createTypography(palette, typography) // ============================================= export interface TypographyVariant { fontFamily: string fontWeight: number fontSize: string | number lineHeight: number letterSpacing?: string textTransform?: string } export interface Typography { fontFamily: string fontSize: number fontWeightLight: number fontWeightRegular: number fontWeightMedium: number fontWeightBold: number htmlFontSize: number h1: TypographyVariant h2: TypographyVariant h3: TypographyVariant h4: TypographyVariant h5: TypographyVariant h6: TypographyVariant subtitle1: TypographyVariant subtitle2: TypographyVariant body1: TypographyVariant body2: TypographyVariant button: TypographyVariant caption: TypographyVariant overline: TypographyVariant inherit: TypographyVariant pxToRem: (px: number) => string } const defaultFontFamily = '"Roboto", "Helvetica", "Arial", sans-serif' function round(value: number): number { return Math.round(value * 1e5) / 1e5 } export interface TypographyOptions { fontFamily?: string fontSize?: number fontWeightLight?: number fontWeightRegular?: number fontWeightMedium?: number fontWeightBold?: number htmlFontSize?: number allVariants?: Partial [key: string]: any } /** * createTypography - 对应 MUI createTypography.js * @param palette - 调色板对象 * @param typography - 排版配置选项 */ export function createTypography( palette: Palette | undefined, typography: TypographyOptions | ((palette: Palette) => TypographyOptions) = {} ): Typography { // 支持函数形式的 typography const typographyInput = typeof typography === 'function' ? (palette ? typography(palette) : {}) : typography const { fontFamily = defaultFontFamily, fontSize = 14, fontWeightLight = 300, fontWeightRegular = 400, fontWeightMedium = 500, fontWeightBold = 700, htmlFontSize = 16, allVariants, ...other } = typographyInput const coef = fontSize / 14 const pxToRem = (size: number) => `${(size / htmlFontSize) * coef}rem` const buildVariant = ( fontWeight: number, size: number, lineHeight: number, letterSpacing: number, casing?: { textTransform: string } ): TypographyVariant => ({ fontFamily, fontWeight, fontSize: pxToRem(size), lineHeight, ...(fontFamily === defaultFontFamily ? { letterSpacing: `${round(letterSpacing / size)}em` } : {}), ...casing, ...allVariants, // 支持 allVariants }) const variants = { h1: buildVariant(fontWeightLight, 96, 1.167, -1.5), h2: buildVariant(fontWeightLight, 60, 1.2, -0.5), h3: buildVariant(fontWeightRegular, 48, 1.167, 0), h4: buildVariant(fontWeightRegular, 34, 1.235, 0.25), h5: buildVariant(fontWeightRegular, 24, 1.334, 0), h6: buildVariant(fontWeightMedium, 20, 1.6, 0.15), subtitle1: buildVariant(fontWeightRegular, 16, 1.75, 0.15), subtitle2: buildVariant(fontWeightMedium, 14, 1.57, 0.1), body1: buildVariant(fontWeightRegular, 16, 1.5, 0.15), body2: buildVariant(fontWeightRegular, 14, 1.43, 0.15), button: buildVariant(fontWeightMedium, 14, 1.75, 0.4, { textTransform: 'uppercase' }), caption: buildVariant(fontWeightRegular, 12, 1.66, 0.4), overline: buildVariant(fontWeightRegular, 12, 2.66, 1, { textTransform: 'uppercase' }), inherit: { fontFamily: 'inherit', fontWeight: 'inherit' as unknown as number, fontSize: 'inherit', lineHeight: 'inherit' as unknown as number, letterSpacing: 'inherit', }, } return { htmlFontSize, pxToRem, fontFamily, fontSize, fontWeightLight, fontWeightRegular, fontWeightMedium, fontWeightBold, ...variants, // 支持自定义变体覆盖 ...Object.keys(other).reduce((acc, key) => { if (variants[key as keyof typeof variants]) { acc[key] = { ...variants[key as keyof typeof variants], ...other[key] } } return acc }, {} as any), } } // ============================================= // Shadows - 对应 MUI shadows.js (完整 25 层) // ============================================= const shadowKeyUmbraOpacity = 0.2 const shadowKeyPenumbraOpacity = 0.14 const shadowAmbientShadowOpacity = 0.12 function createShadow(...px: number[]): string { return [ `${px[0]}px ${px[1]}px ${px[2]}px ${px[3]}px rgba(0,0,0,${shadowKeyUmbraOpacity})`, `${px[4]}px ${px[5]}px ${px[6]}px ${px[7]}px rgba(0,0,0,${shadowKeyPenumbraOpacity})`, `${px[8]}px ${px[9]}px ${px[10]}px ${px[11]}px rgba(0,0,0,${shadowAmbientShadowOpacity})`, ].join(',') } export type Shadows = [ 'none', string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string, string ] export const shadows: Shadows = [ 'none', createShadow(0, 2, 1, -1, 0, 1, 1, 0, 0, 1, 3, 0), createShadow(0, 3, 1, -2, 0, 2, 2, 0, 0, 1, 5, 0), createShadow(0, 3, 3, -2, 0, 3, 4, 0, 0, 1, 8, 0), createShadow(0, 2, 4, -1, 0, 4, 5, 0, 0, 1, 10, 0), createShadow(0, 3, 5, -1, 0, 5, 8, 0, 0, 1, 14, 0), createShadow(0, 3, 5, -1, 0, 6, 10, 0, 0, 1, 18, 0), createShadow(0, 4, 5, -2, 0, 7, 10, 1, 0, 2, 16, 1), createShadow(0, 5, 5, -3, 0, 8, 10, 1, 0, 3, 14, 2), createShadow(0, 5, 6, -3, 0, 9, 12, 1, 0, 3, 16, 2), createShadow(0, 6, 6, -3, 0, 10, 14, 1, 0, 4, 18, 3), createShadow(0, 6, 7, -4, 0, 11, 15, 1, 0, 4, 20, 3), createShadow(0, 7, 8, -4, 0, 12, 17, 2, 0, 5, 22, 4), createShadow(0, 7, 8, -4, 0, 13, 19, 2, 0, 5, 24, 4), createShadow(0, 7, 9, -4, 0, 14, 21, 2, 0, 5, 26, 4), createShadow(0, 8, 9, -5, 0, 15, 22, 2, 0, 6, 28, 5), createShadow(0, 8, 10, -5, 0, 16, 24, 2, 0, 6, 30, 5), createShadow(0, 8, 11, -5, 0, 17, 26, 2, 0, 6, 32, 5), createShadow(0, 9, 11, -5, 0, 18, 28, 2, 0, 7, 34, 6), createShadow(0, 9, 12, -6, 0, 19, 29, 2, 0, 7, 36, 6), createShadow(0, 10, 13, -6, 0, 20, 31, 3, 0, 8, 38, 7), createShadow(0, 10, 13, -6, 0, 21, 33, 3, 0, 8, 40, 7), createShadow(0, 10, 14, -6, 0, 22, 35, 3, 0, 8, 42, 7), createShadow(0, 11, 14, -7, 0, 23, 36, 3, 0, 9, 44, 8), createShadow(0, 11, 15, -7, 0, 24, 38, 3, 0, 9, 46, 8), ] // ============================================= // zIndex - 对应 MUI zIndex.js // ============================================= export interface ZIndex { mobileStepper: number fab: number speedDial: number appBar: number drawer: number modal: number snackbar: number tooltip: number } export const zIndex: ZIndex = { mobileStepper: 1000, fab: 1050, speedDial: 1050, appBar: 1100, drawer: 1200, modal: 1300, snackbar: 1400, tooltip: 1500, } // ============================================= // Transitions - 对应 MUI createTransitions.js // 支持参数合并 // ============================================= export interface Easing { easeInOut: string easeOut: string easeIn: string sharp: string } export interface Duration { shortest: number shorter: number short: number standard: number complex: number enteringScreen: number leavingScreen: number } export interface Transitions { easing: Easing duration: Duration create: (props: string | string[], options?: { duration?: number; easing?: string; delay?: number }) => string getAutoHeightDuration: (height: number) => number } export interface TransitionsOptions { easing?: Partial duration?: Partial } /** * formatMs - 格式化毫秒值 * 对应 MUI createTransitions.js 中的 formatMs */ function formatMs(milliseconds: number): string { return `${Math.round(milliseconds)}ms` } /** * createTransitions - 对应 MUI createTransitions.js * @param inputTransitions - 过渡配置选项 */ export function createTransitions(inputTransitions: TransitionsOptions = {}): Transitions { // 默认缓动函数 const defaultEasing: Easing = { easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)', easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)', easeIn: 'cubic-bezier(0.4, 0, 1, 1)', sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', } // 默认持续时间 const defaultDuration: Duration = { shortest: 150, shorter: 200, short: 250, standard: 300, complex: 375, enteringScreen: 225, leavingScreen: 195, } // 合并用户配置 const easing: Easing = { ...defaultEasing, ...inputTransitions.easing } const duration: Duration = { ...defaultDuration, ...inputTransitions.duration } const create = ( props: string | string[] = ['all'], options: { duration?: number; easing?: string; delay?: number } = {} ): string => { const { duration: durationOption = duration.standard, easing: easingOption = easing.easeInOut, delay = 0, } = options const properties = Array.isArray(props) ? props : [props] // 使用 formatMs 格式化,与 MUI 完全一致 return properties .map((prop) => `${prop} ${formatMs(durationOption)} ${easingOption} ${formatMs(delay)}`) .join(',') } /** * getAutoHeightDuration - 计算自动高度过渡时长 * 修复:添加 3000ms 上限 */ const getAutoHeightDuration = (height: number): number => { if (!height) return 0 const constant = height / 36 // 添加 3000ms 上限,对应 MUI 原版 return Math.min(Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10), 3000) } return { easing, duration, create, getAutoHeightDuration, } } // ============================================= // Breakpoints - 对应 MUI createBreakpoints.js // ============================================= export interface Breakpoints { keys: ('xs' | 'sm' | 'md' | 'lg' | 'xl')[] values: { xs: number; sm: number; md: number; lg: number; xl: number } up: (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => string down: (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => string between: (start: 'xs' | 'sm' | 'md' | 'lg' | 'xl', end: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => string only: (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => string } export function createBreakpoints(): Breakpoints { const keys: Breakpoints['keys'] = ['xs', 'sm', 'md', 'lg', 'xl'] const values = { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 } const up = (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => `@media (min-width:${values[key]}px)` const down = (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => `@media (max-width:${values[key] - 0.05}px)` const between = (start: 'xs' | 'sm' | 'md' | 'lg' | 'xl', end: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => `@media (min-width:${values[start]}px) and (max-width:${values[end] - 0.05}px)` const only = (key: 'xs' | 'sm' | 'md' | 'lg' | 'xl') => { const keyIndex = keys.indexOf(key) if (keyIndex === keys.length - 1) return up(key) return between(key, keys[keyIndex + 1]) } return { keys, values, up, down, between, only } } // ============================================= // Shape - 对应 MUI shape // ============================================= export interface Shape { borderRadius: number } export const shape: Shape = { borderRadius: 4, } // ============================================= // Spacing - 对应 MUI spacing // ============================================= export type SpacingArgument = number | string export type SpacingFunction = (...args: SpacingArgument[]) => string export function createSpacing(spacingInput: number = 8): SpacingFunction { const transform = (value: SpacingArgument): string => { if (typeof value === 'string') return value return `${value * spacingInput}px` } const spacing: SpacingFunction = (...args) => { if (args.length === 0) return transform(1) if (args.length === 1) return transform(args[0]) return args.map(transform).join(' ') } return spacing } // ============================================= // Mixins - 对应 MUI createMixins.js // 签名: createMixins(breakpoints, mixins) // ============================================= export interface ToolbarMixin { minHeight: number [key: string]: any } export interface Mixins { toolbar: ToolbarMixin [key: string]: any } export interface MixinsOptions { toolbar?: Partial [key: string]: any } /** * createMixins - 对应 MUI createMixins.js * @param breakpoints - 断点对象 * @param mixins - 混入配置选项 */ export function createMixins(breakpoints: Breakpoints, mixins: MixinsOptions = {}): Mixins { const toolbarMinHeight = mixins.toolbar?.minHeight ?? 56 const { toolbar: _, ...restMixins } = mixins const toolbarMixin: ToolbarMixin = { minHeight: toolbarMinHeight, // 使用 breakpoints.up() 而非硬编码 [breakpoints.up('xs')]: { '@media (orientation: landscape)': { minHeight: 48, }, }, [breakpoints.up('sm')]: { minHeight: 64, }, ...mixins.toolbar, } // 确保 minHeight 是数字 toolbarMixin.minHeight = toolbarMinHeight return { toolbar: toolbarMixin, ...restMixins, } } // ============================================= // Theme - 完整主题对象 // ============================================= export interface ColorScheme { palette: Palette } export interface Theme { palette: Palette typography: Typography shadows: Shadows transitions: Transitions breakpoints: Breakpoints zIndex: ZIndex shape: Shape spacing: SpacingFunction mixins: Mixins components?: Record // 颜色操作函数 - 挂载到 theme alpha: typeof alpha lighten: typeof lighten darken: typeof darken emphasize: typeof emphasize contrastColor: typeof contrastColor colorMix: typeof colorMix // colorSchemes 支持 colorSchemes?: { light?: ColorScheme dark?: ColorScheme } // CSS 变量支持 cssVariables?: boolean // unstable_sx 支持 unstable_sx?: (props: Record) => Record // vars - CSS 变量引用 vars?: Record } export interface ThemeOptions { palette?: PaletteOptions typography?: TypographyOptions | ((palette: Palette) => TypographyOptions) shape?: Partial breakpoints?: Partial zIndex?: Partial spacing?: number transitions?: TransitionsOptions mixins?: MixinsOptions components?: Record colorSchemes?: { light?: { palette?: PaletteOptions } dark?: { palette?: PaletteOptions } } cssVariables?: boolean } // ============================================= // createTheme - 主题创建函数 (100% MUI 兼容) // ============================================= export function createTheme(options: ThemeOptions = {}): Theme { const { palette: paletteInput, typography: typographyInput, shape: shapeInput, spacing: spacingInput, zIndex: zIndexInput, transitions: transitionsInput, mixins: mixinsInput, components, colorSchemes: colorSchemesInput, cssVariables = false, } = options // 创建基础组件 const breakpoints = createBreakpoints() const palette = createPalette(paletteInput) const typography = createTypography(palette, typographyInput) const transitions = createTransitions(transitionsInput) const spacing = createSpacing(spacingInput) const mixins = createMixins(breakpoints, mixinsInput) // 构建 colorSchemes let colorSchemes: Theme['colorSchemes'] = undefined if (colorSchemesInput) { colorSchemes = {} if (colorSchemesInput.light) { colorSchemes.light = { palette: createPalette({ mode: 'light', ...colorSchemesInput.light.palette }), } } if (colorSchemesInput.dark) { colorSchemes.dark = { palette: createPalette({ mode: 'dark', ...colorSchemesInput.dark.palette }), } } } // unstable_sx 实现 const unstable_sx = (props: Record): Record => { // 简化实现,返回样式对象 return props } return { palette, typography, shadows, transitions, breakpoints, zIndex: { ...zIndex, ...zIndexInput }, shape: { ...shape, ...shapeInput }, spacing, mixins, components, // 挂载颜色操作函数 alpha, lighten, darken, emphasize, contrastColor, colorMix, // colorSchemes colorSchemes, // CSS 变量 cssVariables, // unstable_sx unstable_sx, } } // 默认主题 export const defaultTheme = createTheme() // ============================================= // 导出所有颜色 // ============================================= export const colors = { common, grey, purple, red, orange, blue, lightBlue, green, } // ============================================= // 导出 // ============================================= export default createTheme