Skip to content

主题定制指南

Lerna Web UI 提供了一套完整的设计令牌系统和主题切换机制,支持亮色和暗色双主题。

🎨 设计令牌 (Design Tokens)

组件库使用 CSS 变量定义了一套完整的设计令牌系统,所有组件样式都基于这些变量构建。

颜色系统

主色和功能色

  • 主色: --lerna-color-primary (#3b82f6)
  • 功能色:
    • 成功:--lerna-color-success (#10b981)
    • 警告:--lerna-color-warning (#f59e0b)
    • 危险:--lerna-color-danger (#ef4444)
    • 信息:--lerna-color-info (#6b7280)

中性色

10 级灰色阶:

  • --lerna-color-gray-50 ~ --lerna-color-gray-900
  • 从浅灰到深灰的完整色系

文本颜色

  • 主要文本: --lerna-text-color (#1f2937)
  • 次要文本: --lerna-text-color-secondary (#6b7280)
  • 禁用文本: --lerna-text-color-disabled (#9ca3af)

背景颜色

  • 主要背景: --lerna-bg-color (#fff)
  • 次要背景: --lerna-bg-color-secondary (#f9fafb)
  • 第三背景: --lerna-bg-color-tertiary (#f3f4f6)

边框颜色

  • 默认边框: --lerna-border-color (#e5e7eb)
  • 浅色边框: --lerna-border-color-light (#f3f4f6)
  • 深色边框: --lerna-border-color-dark (#d1d5db)

间距系统

统一的间距标准(基于 rem):

css
:root {
  --lerna-spacing-1: 0.25rem;  /* 4px */
  --lerna-spacing-2: 0.5rem;   /* 8px */
  --lerna-spacing-3: 0.75rem;  /* 12px */
  --lerna-spacing-4: 1rem;     /* 16px */
  --lerna-spacing-5: 1.25rem;  /* 20px */
  --lerna-spacing-6: 1.5rem;   /* 24px */
  --lerna-spacing-7: 1.75rem;  /* 28px */
  --lerna-spacing-8: 2rem;     /* 32px */
  --lerna-spacing-9: 2.25rem;  /* 36px */
  --lerna-spacing-10: 2.5rem;  /* 40px */
}

圆角系统

css
:root {
  --lerna-radius-sm: 0.25rem;    /* 4px */
  --lerna-radius-md: 0.375rem;   /* 6px */
  --lerna-radius-lg: 0.5rem;     /* 8px */
  --lerna-radius-xl: 0.75rem;    /* 12px */
  --lerna-radius-full: 9999px;
}

字体系统

字体大小

css
:root {
  --lerna-font-size-sm: 0.875rem;    /* 14px */
  --lerna-font-size-base: 1rem;      /* 16px */
  --lerna-font-size-lg: 1.125rem;    /* 18px */
  --lerna-font-size-xl: 1.25rem;     /* 20px */
  --lerna-font-size-2xl: 1.5rem;     /* 24px */
}

字体粗细

css
:root {
  --lerna-font-weight-light: 300;
  --lerna-font-weight-normal: 400;
  --lerna-font-weight-medium: 500;
  --lerna-font-weight-semibold: 600;
  --lerna-font-weight-bold: 700;
}

阴影系统

css
:root {
  --lerna-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --lerna-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
  --lerna-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
  --lerna-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}

🌓 主题模式

组件库内置 LightDark 双主题支持。

主题实现原理

主题通过 CSS 变量和 HTML 标签的 class 来控制:

css
/* Light 主题 (默认) */
:root {
  --lerna-color-primary: #3b82f6;
  --lerna-bg-color: #fff;
  --lerna-text-color: #1f2937;
  /* ... 其他亮色主题变量 */
}

/* Dark 主题 */
html.dark {
  --lerna-color-primary: #60a5fa;
  --lerna-bg-color: #111827;
  --lerna-text-color: #f9fafb;
  /* ... 其他暗色主题变量 */
}

主题切换方法

只需切换 HTML 标签的 class 即可实现主题切换:

javascript
// 切换到暗色主题
document.documentElement.classList.add('dark')

// 切换到亮色主题
document.documentElement.classList.remove('dark')

// 切换函数
function toggleTheme() {
  document.documentElement.classList.toggle('dark')
}

示例:主题切换按钮

vue
<template>
  <div class="theme-switcher">
    <Button 
      :type="currentTheme === 'light' ? 'primary' : 'default'"
      size="small"
      @click="setTheme('light')"
    >
      ☀️ 亮色
    </Button>
    
    <Button 
      :type="currentTheme === 'dark' ? 'primary' : 'default'"
      size="small"
      @click="setTheme('dark')"
    >
      🌙 暗色
    </Button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { Button } from '@lerna-web-ui/ui'

const currentTheme = ref<'light' | 'dark'>('light')

const setTheme = (theme: 'light' | 'dark') => {
  currentTheme.value = theme
  // 应用主题到 DOM
  if (theme === 'dark') {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
  // 保存主题设置到 localStorage
  localStorage.setItem('theme', theme)
}

// 初始化主题
watch(
  () => currentTheme.value,
  (newTheme) => {
    if (newTheme === 'dark') {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
  },
  { immediate: true }
)

// 从 localStorage 恢复主题
const savedTheme = localStorage.getItem('theme')
if (savedTheme === 'light' || savedTheme === 'dark') {
  currentTheme.value = savedTheme
}
</script>

<style scoped>
.theme-switcher {
  display: flex;
  gap: 8px;
  padding: 16px;
  background: var(--lerna-bg-color-secondary);
  border-radius: var(--lerna-radius-md);
}
</style>

🎨 自定义主题

方式一:覆盖 CSS 变量(推荐)

最简单的方式是在全局样式文件中覆盖设计令牌:

scss
// styles/custom-theme.scss
:root {
  // 自定义主色
  --lerna-color-primary: #ff6b35;
  --lerna-color-primary-light: #ff8c5a;
  --lerna-color-primary-dark: #e55a2b;
  
  // 自定义圆角
  --lerna-radius-md: 8px;
  --lerna-radius-lg: 12px;
  
  // 自定义字体大小
  --lerna-font-size-base: 16px;
}

然后在项目入口文件中引入:

ts
// main.ts
import './styles/custom-theme.scss'
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

方式二:使用 SCSS 源文件

如果需要更细粒度的控制,可以导入组件库的 SCSS 源文件:

scss
// custom-theme.scss
// 注意:需要先安装 sass 预处理器
// pnpm add -D sass

// 1. 导入设计令牌
@import '@lerna-web-ui/ui/src/styles/tokens/colors';
@import '@lerna-web-ui/ui/src/styles/tokens/spacing';

// 2. 覆盖 CSS 变量(在设计令牌之后,组件样式之前)
:root {
  --lerna-color-primary: #ff6b35;
  --lerna-border-radius-md: 8px;
}

// 3. 导入全局样式
@import '@lerna-web-ui/ui/src/styles/index';

然后在项目入口文件中引入:

ts
// main.ts
import './custom-theme.scss'

注意: 直接覆盖 CSS 变量是更推荐的方式,因为这样更简单且不易出错。

方式三:动态主题(高级)

使用 JavaScript 动态设置 CSS 变量:

ts
// utils/theme.ts
export function setThemeVariable(variable: string, value: string) {
  document.documentElement.style.setProperty(variable, value)
}

// 使用示例
setThemeVariable('--lerna-color-primary', '#ff6b35')
setThemeVariable('--lerna-radius-md', '8px')

💡 最佳实践

✅ 推荐做法

  1. 使用设计令牌: 始终使用 CSS 变量而非硬编码值
  2. 统一主题管理: 在专门的文件中管理所有主题相关配置
  3. 持久化主题: 将用户偏好保存到 localStorage
  4. 渐进增强: 确保在主题切换时有平滑的过渡效果
  5. 测试两种主题: 在开发和测试阶段都要验证亮色和暗色主题

❌ 避免做法

  1. 硬编码颜色: 避免在组件中使用具体的颜色值
  2. 混合主题: 不要在同一界面中混用不同主题的样式
  3. 忽略过渡: 添加适当的 transition 使主题切换更流畅
  4. 忘记持久化: 用户刷新页面后应保持主题设置

🔧 主题配置工具

主题检测工具

ts
// utils/detect-theme.ts
export function detectSystemTheme(): 'light' | 'dark' {
  if (typeof window === 'undefined') return 'light'
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}

export function watchSystemTheme(callback: (isDark: boolean) => void) {
  if (typeof window === 'undefined') return () => {}
  
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
  const handleChange = (e: MediaQueryListEvent) => {
    callback(e.matches)
  }
  
  mediaQuery.addEventListener('change', handleChange)
  return () => mediaQuery.removeEventListener('change', handleChange)
}

主题管理组合式函数

ts
// composables/useTheme.ts
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'

export function useTheme(defaultTheme: 'light' | 'dark' | 'system' = 'light') {
  const theme = ref<'light' | 'dark' | 'system'>(defaultTheme)
  const isDark = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
    }
    return theme.value === 'dark'
  })

  const setTheme = (value: 'light' | 'dark' | 'system') => {
    theme.value = value
    localStorage.setItem('theme', value)
    applyThemeToDOM()
  }

  const toggleTheme = () => {
    setTheme(theme.value === 'dark' ? 'light' : 'dark')
  }

  const applyThemeToDOM = () => {
    if (theme.value === 'dark' || (theme.value === 'system' && isDark.value)) {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
  }

  // 监听主题变化
  watch(theme, () => {
    applyThemeToDOM()
  })

  // 初始化
  onMounted(() => {
    const stored = localStorage.getItem('theme') as 'light' | 'dark' | 'system' | null
    if (stored) {
      theme.value = stored
    }
    applyThemeToDOM()
  })

  return {
    theme,
    isDark,
    setTheme,
    toggleTheme,
  }
}

📚 相关资源

Released under the MIT License.