主题系统
介绍
RuoYi-Plus-UniApp 前端管理系统提供了一套完整且强大的主题系统,支持亮色/暗色双模式切换、自定义主题色、动态主题切换动画等功能。主题系统基于 CSS Variables 实现,通过分层的颜色系统和灵活的 Composable API,为开发者提供了高度可定制的主题解决方案。
核心特性:
- 双主题模式 - 支持亮色(Light)和暗色(Dark)两种内置主题,可一键切换
- CSS 变量架构 - 基于 CSS Custom Properties 实现,支持实时主题切换无需刷新页面
- 五层背景色系统 - 通过背景色层级系统(bg-base 到 bg-level-4)实现层次分明的视觉效果
- 动态主题色 - 支持自定义主题色,自动生成 9 个亮色变体和 9 个暗色变体
- View Transition 动画 - 使用现代浏览器 View Transition API 实现圆形扩散切换动画
- 组件库集成 - 与 Element Plus 深度集成,自动同步主题变量
- 持久化存储 - 主题配置自动保存到 localStorage,刷新页面后保持用户偏好
- 类型安全 - 完整的 TypeScript 类型定义,提供良好的开发体验
主题架构
CSS 变量层级
主题系统采用分层的 CSS 变量架构,分为以下几个层次:
1. 背景色层级系统
五层背景色设计,从浅到深(亮色模式)或从深到浅(暗色模式),用于不同的视觉层次:
/* 亮色模式 */
:root {
--bg-base: #fafbfc; /* 最浅层:应用主背景 */
--bg-level-1: #ffffff; /* 一级:卡片、侧边栏基础背景 */
--bg-level-2: #f8f9fa; /* 二级:轻微悬停、子区域 */
--bg-level-3: #f5f7fa; /* 三级:明显悬停、表格行悬停 */
--bg-level-4: #e9ecef; /* 四级:选中、激活状态 */
}
/* 暗色模式 */
html.dark {
--bg-base: #111113; /* 最深层:应用主背景 */
--bg-level-1: #161618; /* 一级:卡片、侧边栏基础背景 */
--bg-level-2: #1f1f23; /* 二级:轻微悬停、子区域 */
--bg-level-3: #262727; /* 三级:明显悬停、表格行悬停 */
--bg-level-4: #2d2d30; /* 四级:选中、激活状态 */
}使用场景:
--bg-base: 整个应用的基础背景,用于 body 或 app 容器--bg-level-1: 卡片、对话框、侧边栏等主要组件的背景--bg-level-2: 次级区域、表格偶数行等--bg-level-3: 悬停状态、高亮区域--bg-level-4: 选中、激活、按下等强调状态
2. 应用级颜色变量
全局应用级别的颜色定义:
/* 亮色模式 */
:root {
--app-bg: var(--bg-base); /* 应用整体背景色 */
--app-text: #303133; /* 应用主要文字颜色 */
--app-border: #dbdfe9; /* 应用边框颜色 */
}
/* 暗色模式 */
html.dark {
--app-bg: var(--bg-base); /* 应用整体背景色 */
--app-text: #f1f5f9; /* 应用主要文字颜色 */
--app-border: #363843; /* 应用边框颜色 */
}3. 菜单主题变量
侧边栏菜单的专属颜色系统:
/* 亮色模式 - 深色侧边栏 */
:root {
--menu-bg: #161618; /* 侧边栏菜单背景色 */
--menu-text: #bfcbd9; /* 菜单普通文字颜色 */
--menu-text-active: #f4f4f5; /* 菜单激活状态文字颜色 */
--menu-hover: #475569; /* 基础悬停背景色 */
--menu-hover-text: #f4f4f5; /* 基础悬停文字色 */
--menu-hover-color: var(--menu-hover);
--menu-hover-text-color: var(--menu-hover-text);
--menu-active-bg: var(--el-menu-active-bg-color);
--menu-active-text: var(--el-menu-active-text-color);
--submenu-bg: #1f2d3d; /* 子菜单背景色 */
--submenu-text-active: #f4f4f5; /* 子菜单激活文字色 */
--submenu-hover: var(--menu-hover-color);
--submenu-title-hover: var(--menu-hover-color);
}
/* 暗色模式 - 与应用背景统一 */
html.dark {
--menu-bg: var(--bg-level-1); /* 侧边栏与应用背景统一 */
--menu-text: #cbd5e1;
--menu-text-active: #f4f4f5;
--menu-hover: var(--bg-level-2);
--menu-hover-text: #f4f4f5;
/* ... 其他菜单变量 */
}技术实现:
亮色模式下,侧边栏采用独立的深色背景(#161618),与主内容区形成对比,提供更好的导航识别度。暗色模式下,侧边栏与应用背景保持一致,采用统一的暗色调。
4. 头部与表格变量
/* 头部区域 */
--header-bg: var(--app-bg); /* 头部背景色 */
--header-text: #303133; /* 头部文字色 */
--header-border: #e4e7ed; /* 头部边框色 */
/* 表格组件 */
--table-header-bg: #f8f8f9; /* 表格头部背景色 */
--table-header-text: #6b7785; /* 表格头部文字色 */
/* 标签视图 */
--tags-view-active-bg: var(--el-color-primary);
--tags-view-active-border: var(--el-color-primary);5. Element Plus 集成变量
与 Element Plus 组件库的颜色映射:
/* 亮色模式 */
:root {
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-2: #409eff;
--el-bg-color-overlay: var(--bg-level-1);
--el-text-color-primary: var(--app-text);
--el-border-color: var(--app-border);
--el-border-color-light: #e4e7ed;
}
/* 暗色模式 */
html.dark {
--el-color-primary: #3b82f6;
--el-color-primary-light-3: #60a5fa;
--el-color-primary-light-2: #3b82f6;
--el-bg-color-overlay: var(--bg-level-1);
--el-bg-color: var(--bg-level-1);
--el-text-color-primary: var(--app-text);
--el-border-color: var(--app-border);
--el-border-color-light: #475569;
}主题文件组织
src/assets/styles/
├── themes/
│ ├── _light.scss # 亮色主题定义
│ ├── _dark.scss # 暗色主题定义
│ └── index.scss # 主题入口(可选)
├── theme-animation.scss # 主题切换动画
└── main.scss # 样式总入口亮色主题
主题定义
亮色主题使用 :root 伪类定义,作为默认主题:
/**
* 亮色主题样式
* 集中定义亮色模式下的所有变量和样式覆盖
*/
:root {
/* === 背景色层级系统 === */
--bg-base: #fafbfc;
--bg-level-1: #ffffff;
--bg-level-2: #f8f9fa;
--bg-level-3: #f5f7fa;
--bg-level-4: #e9ecef;
/* 应用整体背景色 */
--app-bg: var(--bg-base);
--app-text: #303133;
--app-border: #dbdfe9;
/* ... 其他变量定义 */
}亮色主题特定样式
针对亮色主题的组件样式覆盖:
/* 亮色主题特定样式 */
html:not(.dark) {
/** 侧边栏右侧边框 */
.sidebar-container {
box-shadow: none;
border-right: 0.5px solid var(--app-border);
}
.tags-view-container {
/** 标签页容器 */
}
/* 表格悬停颜色 */
.el-table__body .el-table__row:hover > td {
background-color: var(--bg-level-3) !important;
}
}技术实现:
使用 html:not(.dark) 选择器,确保样式只在非暗色模式下生效。这种方式比使用媒体查询更灵活,支持手动切换主题。
使用示例
在组件中使用亮色主题变量:
<template>
<div class="light-card">
<div class="card-header">标题</div>
<div class="card-body">内容区域</div>
</div>
</template>暗色主题
主题定义
暗色主题使用 html.dark 选择器定义:
/**
* 暗色主题样式
* 集中定义暗色模式下的所有变量和样式覆盖
*/
html.dark {
/* === 背景色层级系统 === */
--bg-base: #111113;
--bg-level-1: #161618;
--bg-level-2: #1f1f23;
--bg-level-3: #262727;
--bg-level-4: #2d2d30;
/* 应用整体背景色 */
--app-bg: var(--bg-base);
--app-text: #f1f5f9;
--app-border: #363843;
/* ... 其他变量定义 */
}暗色主题组件覆盖
针对暗色主题的组件样式覆盖:
html.dark {
/* Element UI 组件样式覆盖 */
.el-tree-node__content {
/** 树节点内容背景色 */
--el-color-primary-light-9: var(--bg-level-3);
}
.el-button--primary {
/** 主要按钮背景色 */
--el-button-bg-color: var(--el-color-primary-dark-6);
/** 主要按钮边框色 */
--el-button-border-color: var(--el-color-primary-light-2);
}
.el-switch {
/** 开关开启状态颜色 */
--el-switch-on-color: var(--el-color-primary-dark-6);
/** 开关边框颜色 */
--el-switch-border-color: var(--el-color-primary-light-2);
}
.el-tag--primary {
/** 主要标签背景色 */
--el-tag-bg-color: var(--el-color-primary-dark-6);
/** 主要标签边框色 */
--el-tag-border-color: var(--el-color-primary-light-2);
}
/* 表格悬停颜色 */
.el-table__body .el-table__row:hover > td {
background-color: var(--bg-level-3) !important;
}
/* 自定义暗色主题变量 */
--dark-bg-color: #1e293b;
--dark-text-color: #f1f5f9;
--dark-border-color: #334155;
}技术实现:
暗色主题通过覆盖 Element Plus 组件的 CSS 变量,实现组件样式的自动适配。这种方式无需修改组件代码,只需调整 CSS 变量即可。
暗色主题布局样式
html.dark {
/* 布局相关样式 */
.app-main {
/** 主内容区背景色 */
background-color: var(--app-bg);
}
.sidebar-container {
/** 侧边栏右侧边框 */
box-shadow: none;
border-right: 0.5px solid var(--app-border);
}
.tags-view-container {
/** 标签页容器背景色 */
background-color: var(--header-bg);
/** 标签页容器边框色 */
border-color: var(--header-border);
}
.search, .panel, .el-card {
/** 卡片类组件阴影 */
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
/* SVG图标颜色 */
.svg-icon, svg {
/** SVG图标填充色 */
fill: var(--app-text);
}
}使用示例
在组件中适配暗色主题:
<template>
<div class="theme-aware-card">
<div class="card-icon">
<svg class="icon"><!-- icon path --></svg>
</div>
<div class="card-content">
<h3>自适应主题的卡片</h3>
<p>这个卡片会根据主题自动调整样式</p>
</div>
</div>
</template>主题切换
useLayout Composable
主题切换通过 useLayout Composable 实现:
/**
* 布局管理钩子
* 提供主题切换、布局配置等功能
*/
import { useLayout } from '@/composables/useLayout'
export default defineComponent({
setup() {
const layout = useLayout()
// 获取当前暗色模式状态
const isDark = layout.dark
// 获取当前主题色
const currentTheme = layout.theme
// 切换暗色模式
const toggleDarkMode = () => {
layout.toggleDark(!isDark.value)
}
// 设置主题色
const changeThemeColor = (color: string) => {
layout.theme.value = color
}
return {
isDark,
currentTheme,
toggleDarkMode,
changeThemeColor
}
}
})useLayout API:
dark: Ref<boolean>- 暗色模式状态theme: Ref<string>- 当前主题色(十六进制)toggleDark(value: boolean): void- 切换暗色模式theme.value = '#color'- 设置主题色
基本切换
简单的主题切换实现:
<template>
<div class="theme-switcher">
<el-switch
v-model="isDark"
@change="handleToggle"
inline-prompt
active-text="暗"
inactive-text="亮"
/>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useLayout } from '@/composables/useLayout'
const layout = useLayout()
// 响应式的暗色模式状态
const isDark = computed({
get: () => layout.dark.value,
set: (value) => layout.toggleDark(value)
})
// 切换处理函数
const handleToggle = (value: boolean) => {
console.log('主题已切换:', value ? '暗色' : '亮色')
}
</script>带动画切换
使用 View Transition API 实现动画切换:
<template>
<div class="animated-theme-switcher">
<el-button
@click="handleAnimatedToggle"
:icon="isDark ? Moon : Sunny"
circle
>
</el-button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { Moon, Sunny } from '@element-plus/icons-vue'
import { useLayout } from '@/composables/useLayout'
import { toggleThemeWithAnimation } from '@/utils/themeAnimation'
const layout = useLayout()
const isDark = computed(() => layout.dark.value)
// 带动画的切换
const handleAnimatedToggle = (event: MouseEvent) => {
toggleThemeWithAnimation(event, isDark.value)
}
</script>技术实现:
toggleThemeWithAnimation 函数使用 View Transition API 实现圆形扩散效果:
- 获取点击位置坐标
(x, y) - 计算从点击位置到视窗最远角的距离作为圆半径
- 设置 CSS 变量
--theme-x,--theme-y,--theme-r - 使用
document.startViewTransition()执行动画 - 在动画回调中切换主题
主题切换动画原理
动画通过 CSS 和 View Transition API 实现:
/**
* 主题切换动画样式
* 使用 View Transition API 实现圆形扩散效果
*/
// 定义动画时长
$theme-animation-duration: 0.5s;
html {
// View Transition 样式 - 亮色模式切换到暗黑模式
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
// 亮色模式 -> 暗黑模式: 新层(暗黑)从圆心扩散
&::view-transition-new(root) {
animation: theme-clip-in $theme-animation-duration ease-in both;
z-index: 9999;
}
&::view-transition-old(root) {
z-index: 1;
}
// 暗黑模式 -> 亮色模式: 旧层(暗黑)从外向圆心收缩
&.dark {
&::view-transition-old(root) {
animation: theme-clip-out $theme-animation-duration ease-in both;
z-index: 9999;
}
&::view-transition-new(root) {
animation: none;
z-index: 1;
}
}
}
// 定义圆形扩散动画 (从小到大)
@keyframes theme-clip-in {
from {
clip-path: circle(0% at var(--theme-x) var(--theme-y));
}
to {
clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y));
}
}
// 定义圆形收缩动画 (从大到小)
@keyframes theme-clip-out {
from {
clip-path: circle(var(--theme-r) at var(--theme-x) var(--theme-y));
}
to {
clip-path: circle(0% at var(--theme-x) var(--theme-y));
}
}动画流程:
- 亮色 → 暗色: 新的暗色层从点击位置以圆形扩散(clip-in),覆盖旧的亮色层
- 暗色 → 亮色: 旧的暗色层从边缘向点击位置收缩(clip-out),显示出新的亮色层
完整切换组件示例
一个功能完整的主题切换器:
<template>
<div class="theme-toggle-wrapper">
<!-- 动画切换按钮 -->
<el-tooltip :content="isDark ? '切换到亮色模式' : '切换到暗色模式'" placement="bottom">
<el-button
class="theme-toggle-btn"
@click="handleToggle"
:icon="isDark ? Moon : Sunny"
circle
size="large"
/>
</el-tooltip>
<!-- 当前主题状态提示 -->
<div class="theme-status" :class="{ 'is-dark': isDark }">
{{ isDark ? '暗色模式' : '亮色模式' }}
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { Moon, Sunny } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useLayout } from '@/composables/useLayout'
import { toggleThemeWithAnimation } from '@/utils/themeAnimation'
const layout = useLayout()
const isDark = computed(() => layout.dark.value)
const handleToggle = (event: MouseEvent) => {
// 使用动画切换
toggleThemeWithAnimation(event, isDark.value)
// 显示提示
ElMessage.success({
message: `已切换到${!isDark.value ? '暗色' : '亮色'}模式`,
duration: 1500
})
}
</script>自定义主题色
useTheme Composable
自定义主题色通过 useTheme Composable 实现:
/**
* 主题管理钩子 (useTheme)
*
* 提供对应用主题的响应式管理功能,包括颜色变量设置、主题切换和自定义。
*/
import { useTheme } from '@/composables/useTheme'
export default defineComponent({
setup() {
const {
currentTheme, // 当前主题色
setTheme, // 设置主题色
resetTheme, // 重置主题
getLightColor, // 获取亮色变体
getDarkColor, // 获取暗色变体
generateThemeColors, // 生成主题色系
addAlphaToHex // 添加透明度
} = useTheme()
// 设置自定义主题色
const changeToCustomTheme = () => {
setTheme('#1890ff')
}
// 重置为默认主题
const resetToDefault = () => {
resetTheme()
}
// 生成主题色的所有变体
const colors = generateThemeColors('#1890ff')
console.log('主色:', colors.primary)
console.log('亮色变体:', colors.lightColors)
console.log('暗色变体:', colors.darkColors)
return {
currentTheme,
changeToCustomTheme,
resetToDefault
}
}
})主题色设置
设置自定义主题色:
<template>
<div class="theme-color-picker">
<div class="current-theme">
<span>当前主题色:</span>
<div
class="color-preview"
:style="{ backgroundColor: currentTheme }"
/>
<span>{{ currentTheme }}</span>
</div>
<el-color-picker
v-model="selectedColor"
@change="handleColorChange"
show-alpha
:predefine="predefineColors"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useTheme } from '@/composables/useTheme'
import { ElMessage } from 'element-plus'
const { currentTheme, setTheme } = useTheme()
const selectedColor = ref(currentTheme.value)
// 预定义颜色
const predefineColors = ref([
'#409eff', // Element Plus 默认蓝
'#67c23a', // 成功绿
'#e6a23c', // 警告橙
'#f56c6c', // 危险红
'#909399', // 信息灰
'#1890ff', // 蚂蚁蓝
'#722ed1', // 紫色
'#13c2c2', // 青色
])
// 颜色改变处理
const handleColorChange = (value: string) => {
if (value) {
setTheme(value)
ElMessage.success('主题色已更新')
}
}
</script>颜色变体生成
系统自动为主题色生成 18 个变体(9 个亮色 + 9 个暗色):
/**
* 为指定颜色生成所有变体
* @param color 基础颜色
* @returns 主题颜色对象,包含主色和变体
*/
const generateThemeColors = (color: string): ThemeColors => {
// 生成9个亮色变体
const lightColors = Array.from({ length: 9 }, (_, i) =>
getLightColor(color, (i + 1) / 10)
)
// 生成9个暗色变体
const darkColors = Array.from({ length: 9 }, (_, i) =>
getDarkColor(color, (i + 1) / 10)
)
return {
primary: color,
lightColors,
darkColors
}
}生成规则:
- 亮色变体: level 1-9, 每级增加 10% 亮度,
getLightColor(color, 0.1)到getLightColor(color, 0.9) - 暗色变体: level 1-9, 每级增加 10% 暗度,
getDarkColor(color, 0.1)到getDarkColor(color, 0.9)
应用主题颜色
将主题色应用到 CSS 变量:
/**
* 应用主题颜色到CSS变量
* @param color 主题颜色
*/
const applyThemeColors = (color: string): void => {
// 设置主色
document.documentElement.style.setProperty('--el-color-primary', color)
// 设置亮色变体
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
getLightColor(color, i / 10)
)
}
// 设置暗色变体
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
getDarkColor(color, i / 10)
)
}
// 更新当前主题变量
currentTheme.value = color
}CSS 变量映射:
--el-color-primary: 主色--el-color-primary-light-1到--el-color-primary-light-9: 亮色变体--el-color-primary-dark-1到--el-color-primary-dark-9: 暗色变体
完整主题定制器示例
一个功能完整的主题定制器:
<template>
<el-card class="theme-customizer">
<template #header>
<div class="card-header">
<span>主题定制器</span>
<el-button @click="handleReset" size="small" text>重置</el-button>
</div>
</template>
<!-- 当前主题预览 -->
<div class="theme-preview">
<div class="preview-label">当前主题色</div>
<div class="preview-color" :style="{ backgroundColor: currentTheme }">
{{ currentTheme }}
</div>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-wrapper">
<el-color-picker
v-model="customColor"
@change="handleColorChange"
show-alpha
:predefine="predefineColors"
size="large"
/>
</div>
<!-- 预定义主题 -->
<div class="preset-themes">
<div class="preset-label">预设主题</div>
<div class="preset-colors">
<div
v-for="preset in presetThemes"
:key="preset.name"
class="preset-item"
:class="{ active: currentTheme === preset.color }"
@click="handlePresetClick(preset.color)"
>
<div
class="preset-color"
:style="{ backgroundColor: preset.color }"
/>
<div class="preset-name">{{ preset.name }}</div>
</div>
</div>
</div>
<!-- 主题色变体预览 -->
<div class="color-variants" v-if="variants">
<div class="variants-section">
<div class="variants-label">亮色变体</div>
<div class="variants-grid">
<div
v-for="(color, index) in variants.lightColors"
:key="`light-${index}`"
class="variant-item"
:style="{ backgroundColor: color }"
:title="`light-${index + 1}: ${color}`"
/>
</div>
</div>
<div class="variants-section">
<div class="variants-label">暗色变体</div>
<div class="variants-grid">
<div
v-for="(color, index) in variants.darkColors"
:key="`dark-${index}`"
class="variant-item"
:style="{ backgroundColor: color }"
:title="`dark-${index + 1}: ${color}`"
/>
</div>
</div>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { useTheme } from '@/composables/useTheme'
import { ElMessage } from 'element-plus'
const {
currentTheme,
setTheme,
resetTheme,
generateThemeColors
} = useTheme()
// 自定义颜色
const customColor = ref(currentTheme.value)
// 预定义颜色
const predefineColors = ref([
'#409eff',
'#67c23a',
'#e6a23c',
'#f56c6c',
'#909399',
'#1890ff',
'#722ed1',
'#13c2c2',
])
// 预设主题
const presetThemes = ref([
{ name: '默认蓝', color: '#409eff' },
{ name: '科技蓝', color: '#1890ff' },
{ name: '成功绿', color: '#67c23a' },
{ name: '活力橙', color: '#e6a23c' },
{ name: '危险红', color: '#f56c6c' },
{ name: '优雅紫', color: '#722ed1' },
{ name: '清新青', color: '#13c2c2' },
{ name: '中性灰', color: '#909399' },
])
// 颜色变体
const variants = computed(() => {
return generateThemeColors(currentTheme.value)
})
// 颜色改变处理
const handleColorChange = (value: string) => {
if (value) {
setTheme(value)
ElMessage.success({
message: '主题色已更新',
duration: 1500
})
}
}
// 预设主题点击
const handlePresetClick = (color: string) => {
customColor.value = color
setTheme(color)
ElMessage.success({
message: '已应用预设主题',
duration: 1500
})
}
// 重置主题
const handleReset = () => {
resetTheme()
customColor.value = currentTheme.value
ElMessage.info({
message: '已重置为默认主题',
duration: 1500
})
}
// 监听当前主题变化
watch(currentTheme, (newVal) => {
customColor.value = newVal
})
</script>颜色工具函数
颜色转换
十六进制与 RGB 互转:
import { hexToRgb, rgbToHex, hexToRgba } from '@/utils/colors'
// Hex -> RGB
const rgb = hexToRgb('#409eff')
console.log(rgb) // [64, 158, 255]
// RGB -> Hex
const hex = rgbToHex(64, 158, 255)
console.log(hex) // '#409eff'
// Hex -> RGBA
const rgba = hexToRgba('#409eff', 0.5)
console.log(rgba)
// {
// red: 64,
// green: 158,
// blue: 255,
// rgba: 'rgba(64, 158, 255, 0.50)'
// }颜色验证
验证颜色格式:
import { isValidHex, isValidRgb } from '@/utils/colors'
// 验证 Hex 颜色
console.log(isValidHex('#409eff')) // true
console.log(isValidHex('#fff')) // true
console.log(isValidHex('409eff')) // true (会自动处理)
console.log(isValidHex('invalid')) // false
// 验证 RGB 值
console.log(isValidRgb(64, 158, 255)) // true
console.log(isValidRgb(256, 158, 255)) // false (超出范围)
console.log(isValidRgb(64.5, 158, 255)) // false (非整数)颜色调节
调亮和调暗颜色:
import { lightenColor, darkenColor } from '@/utils/colors'
const baseColor = '#409eff'
// 调亮颜色
const light1 = lightenColor(baseColor, 0.1) // 调亮 10%
const light5 = lightenColor(baseColor, 0.5) // 调亮 50%
const light9 = lightenColor(baseColor, 0.9) // 调亮 90%
// 调暗颜色
const dark1 = darkenColor(baseColor, 0.1) // 调暗 10%
const dark5 = darkenColor(baseColor, 0.5) // 调暗 50%
const dark9 = darkenColor(baseColor, 0.9) // 调暗 90%
console.log('原色:', baseColor)
console.log('亮色变体:', light1, light5, light9)
console.log('暗色变体:', dark1, dark5, dark9)算法说明:
- 调亮:
RGB' = RGB + (255 - RGB) × level, 向白色(255)混合 - 调暗:
RGB' = RGB × (1 - level), 向黑色(0)混合
颜色混合
按比例混合两种颜色:
import { blendColor } from '@/utils/colors'
const color1 = '#409eff' // 蓝色
const color2 = '#67c23a' // 绿色
// 混合比例 0-1
const blend25 = blendColor(color1, color2, 0.25) // 75% 蓝 + 25% 绿
const blend50 = blendColor(color1, color2, 0.5) // 50% 蓝 + 50% 绿
const blend75 = blendColor(color1, color2, 0.75) // 25% 蓝 + 75% 绿
console.log('蓝绿混合:', blend25, blend50, blend75)透明度处理
为颜色添加透明度:
import { addAlphaToHex } from '@/composables/useTheme'
const { addAlphaToHex } = useTheme()
const baseColor = '#409eff'
// 添加不同透明度
const alpha10 = addAlphaToHex(baseColor, 0.1) // 10% 不透明度
const alpha50 = addAlphaToHex(baseColor, 0.5) // 50% 不透明度
const alpha100 = addAlphaToHex(baseColor, 1) // 100% 不透明度 (完全不透明)
console.log('透明度变体:', alpha10, alpha50, alpha100)
// '#409eff1a', '#409eff80', '#409eff'完整工具函数示例
综合使用颜色工具函数:
<template>
<div class="color-tools-demo">
<div class="demo-section">
<h3>颜色转换</h3>
<div class="demo-row">
<span>Hex: {{ hexColor }}</span>
<span>RGB: {{ rgbColor.join(', ') }}</span>
<span>RGBA: {{ rgbaColor.rgba }}</span>
</div>
</div>
<div class="demo-section">
<h3>颜色变体</h3>
<div class="color-grid">
<div
v-for="(color, index) in variants"
:key="index"
class="color-box"
:style="{ backgroundColor: color }"
:title="color"
/>
</div>
</div>
<div class="demo-section">
<h3>颜色混合</h3>
<div class="blend-demo">
<div class="blend-source">
<div class="blend-color" :style="{ backgroundColor: color1 }">
{{ color1 }}
</div>
<div class="blend-color" :style="{ backgroundColor: color2 }">
{{ color2 }}
</div>
</div>
<div class="blend-result">
<div
v-for="ratio in [0.25, 0.5, 0.75]"
:key="ratio"
class="blend-color"
:style="{ backgroundColor: blendedColors[ratio] }"
>
{{ (ratio * 100).toFixed(0) }}%
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import {
hexToRgb,
rgbToHex,
hexToRgba,
lightenColor,
darkenColor,
blendColor
} from '@/utils/colors'
const hexColor = '#409eff'
const rgbColor = computed(() => hexToRgb(hexColor))
const rgbaColor = computed(() => hexToRgba(hexColor, 0.5))
// 颜色变体
const variants = computed(() => {
const lights = Array.from({ length: 5 }, (_, i) =>
lightenColor(hexColor, (i + 1) * 0.15)
)
const darks = Array.from({ length: 5 }, (_, i) =>
darkenColor(hexColor, (i + 1) * 0.15)
)
return [...darks.reverse(), hexColor, ...lights]
})
// 颜色混合
const color1 = '#409eff'
const color2 = '#67c23a'
const blendedColors = computed(() => ({
0.25: blendColor(color1, color2, 0.25),
0.5: blendColor(color1, color2, 0.5),
0.75: blendColor(color1, color2, 0.75)
}))
</script>最佳实践
1. 使用主题变量而非硬编码
推荐做法:
不推荐做法:
原因:
- 硬编码颜色无法响应主题切换
- 在暗色模式下会出现样式问题
- 不利于统一调整主题风格
2. 合理使用背景色层级
推荐做法:
<template>
<div class="page">
<!-- 页面基础背景 -->
<div class="page-main">
<!-- 一级卡片 -->
<div class="card-level-1">
<!-- 二级内容区 -->
<div class="content-level-2">
<!-- 三级悬停区 -->
<div class="item-level-3">
内容
</div>
</div>
</div>
</div>
</div>
</template>不推荐做法:
3. 暗色模式专属样式的正确使用
推荐做法:
不推荐做法:
4. 主题色变体的正确使用
推荐做法:
<template>
<div class="status-card">
<div class="status-success">成功</div>
<div class="status-warning">警告</div>
<div class="status-error">错误</div>
</div>
</template>不推荐做法:
5. 过渡动画的最佳实践
推荐做法:
不推荐做法:
常见问题
1. 主题切换后部分组件颜色未更新
问题原因:
- 组件使用了硬编码颜色而非主题变量
- 组件样式使用了
!important覆盖了主题变量 - 第三方组件未正确集成主题系统
解决方案:
<!-- 问题代码 -->
<template>
<div class="problem-component">
内容
</div>
</template>
<!-- 解决方案 -->
<template>
<div class="fixed-component">
内容
</div>
</template>2. 自定义主题色后 Element Plus 组件样式异常
问题原因:
- Element Plus 组件依赖主题色变体(light-1 到 light-9, dark-1 到 dark-9)
- 自定义主题色后未正确生成变体
解决方案:
// ❌ 错误做法:直接修改 CSS 变量
document.documentElement.style.setProperty('--el-color-primary', '#1890ff')
// ✅ 正确做法:使用 useTheme 自动生成变体
import { useTheme } from '@/composables/useTheme'
const { setTheme } = useTheme()
// 自动生成 18 个变体并应用
setTheme('#1890ff')3. 暗色模式下表格行悬停颜色不明显
问题原因:
- 默认的表格悬停颜色在暗色模式下对比度不够
- 需要针对暗色模式调整悬停背景色
解决方案:
/* ✅ 在暗色主题文件中覆盖表格悬停样式 */
html.dark {
/* 表格悬停颜色 */
.el-table__body .el-table__row:hover > td {
background-color: var(--bg-level-3) !important;
}
}这段代码已经在暗色主题文件中定义,如果仍然不明显,可以调整为更深的层级:
html.dark {
.el-table__body .el-table__row:hover > td {
/* 使用 level-4 提供更强对比 */
background-color: var(--bg-level-4) !important;
}
}4. 主题切换动画在某些浏览器不生效
问题原因:
- View Transition API 是较新的特性,部分浏览器不支持
- 需要降级处理
解决方案:
toggleThemeWithAnimation 函数已内置降级处理:
export const toggleThemeWithAnimation = (event: MouseEvent, isDark: boolean) => {
const layout = useLayout()
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
const root = document.documentElement
root.style.setProperty('--theme-x', `${x}px`)
root.style.setProperty('--theme-y', `${y}px`)
root.style.setProperty('--theme-r', `${endRadius}px`)
// ✅ 检查浏览器支持,自动降级
if (document.startViewTransition) {
// 支持 View Transition API,使用动画
document.startViewTransition(() => {
layout.toggleDark(!isDark)
})
} else {
// 不支持则直接切换,无动画
layout.toggleDark(!isDark)
}
}5. 刷新页面后主题设置丢失
问题原因:
- 主题配置未持久化到 localStorage
- useLayout 的状态管理未正确初始化
解决方案:
useLayout Composable 已内置持久化逻辑,确保正确使用:
import { useLayout } from '@/composables/useLayout'
export default defineComponent({
setup() {
const layout = useLayout()
// ✅ 主题设置会自动保存到 localStorage
layout.theme.value = '#1890ff'
layout.toggleDark(true)
// 刷新页面后,useLayout 会自动从 localStorage 恢复配置
return {
layout
}
}
})如果仍然出现问题,检查浏览器 localStorage:
// 检查 localStorage 中的布局配置
const layoutConfig = localStorage.getItem('layout-config')
console.log('Layout Config:', JSON.parse(layoutConfig))
// 手动清除并重置
localStorage.removeItem('layout-config')
location.reload()