主题定制指南
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);
}🌓 主题模式
组件库内置 Light 和 Dark 双主题支持。
主题实现原理
主题通过 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')💡 最佳实践
✅ 推荐做法
- 使用设计令牌: 始终使用 CSS 变量而非硬编码值
- 统一主题管理: 在专门的文件中管理所有主题相关配置
- 持久化主题: 将用户偏好保存到 localStorage
- 渐进增强: 确保在主题切换时有平滑的过渡效果
- 测试两种主题: 在开发和测试阶段都要验证亮色和暗色主题
❌ 避免做法
- 硬编码颜色: 避免在组件中使用具体的颜色值
- 混合主题: 不要在同一界面中混用不同主题的样式
- 忽略过渡: 添加适当的 transition 使主题切换更流畅
- 忘记持久化: 用户刷新页面后应保持主题设置
🔧 主题配置工具
主题检测工具
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,
}
}