Element Plus全局主题修改最佳实践
下载
百度网盘
蓝奏云
分析
-
目标:实现 Element Plus 组件库的全局主题切换(如明暗模式、主色调自定义等),并支持用户个性化设置,且能持久化保存用户选择。
-
适用场景:后台管理系统、企业级应用、需要多主题切换的前端项目。
使用技术
-
UI 框架:Element Plus
-
前端框架:Vue 3(推荐配合 Vite)
-
状态管理:Pinia 或 Vuex(用于主题状态管理)
-
持久化:localStorage(保存用户主题偏好)
-
样式变量:CSS 变量(:root),Element Plus 提供的主题变量
实现步骤
- 准备主题变量:整理 Element Plus 支持的主题变量,定义默认主题和暗黑主题。
- 封装主题切换逻辑:编写工具函数,动态修改 CSS 变量。
- 集成状态管理:用 Pinia/Vuex 管理主题状态,提供切换方法。
- 实现主题切换组件:开发 UI 组件,用户可交互切换主题。
- 持久化用户选择:切换时保存到 localStorage,初始化时读取。
- 全局生效:确保主题颜色切换能影响所有 Element Plus 组件和自定义样式。
第一步创建项目
从初始化创建项目开始,更好的理解如何使用
项目结构如下,反正只要是vue3+element plus项目即可,这里使用vite+vue+js+element plus初始化一个项目为例
第二步安装依赖
我使用的依赖如下
第三步注册依赖初始化主题
首先肯定要在main.js中注册和初始化主题和elemnet plus和pinia
第四步组件编写
LightAndDarkSelectionButton.vue组件
白天模式
暗夜模式
过渡动效
代码
<style scoped>
/* The switch - the box around the slider */
.switch {
display: block;
--width-of-switch: 3.5em;
--height-of-switch: 2em;
/* size of sliding icon -- sun and moon */
--size-of-icon: 1.4em;
/* it is like a inline-padding of switch */
--slider-offset: 0.3em;
position: relative;
width: var(--width-of-switch);
height: var(--height-of-switch);
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #f4f4f5;
transition: .4s;
border-radius: 30px;
}
.slider:before {
position: absolute;
content: "";
height: var(--size-of-icon, 1.4em);
width: var(--size-of-icon, 1.4em);
border-radius: 20px;
left: var(--slider-offset, 0.3em);
top: 50%;
transform: translateY(-50%);
background: linear-gradient(40deg, #ff0080, #ff8c00 70%);;
transition: .4s;
}
input:checked + .slider {
background-color: #303136;
}
input:checked + .slider:before {
left: calc(100% - (var(--size-of-icon, 1.4em) + var(--slider-offset, 0.3em)));
background: #303136;
/* change the value of second inset in box-shadow to change the angle and direction of the moon */
box-shadow: inset -3px -2px 5px -2px #8983f7, inset -10px -4px 0 0 #a3dafb;
}
</style>
<template>
<label class="switch">
<input @click="change" type="checkbox" v-model="changeDark">
<span class="slider"></span>
</label>
</template>
<script>
export default {
data() {
return {
isDark: localStorage.getItem('vueuse-color-scheme') === 'dark',
changeDark: false
};
},
created() {
this.changeDark = localStorage.getItem('vueuse-color-scheme') === 'dark'
if (this.changeDark === true) {
document.documentElement.classList.add('dark');
}
},
methods: {
toggleDark() {
this.isDark = !this.isDark;
if (this.isDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('vueuse-color-scheme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('vueuse-color-scheme', 'light');
}
},
async change(e) {
await new Promise(resolve => setTimeout(resolve, 300));
this.toggleTheme(e);
},
toggleTheme(e) {
const transition = document.startViewTransition(() => {
this.toggleDark();
});
transition.ready.then(() => {
const x = e.clientX;
const y = e.clientY;
const radius = Math.sqrt(
Math.max(x, window.innerWidth - x) ** 2 + Math.max(y, window.innerHeight - y) ** 2
);
const clipPath = [`circle(0 at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`];
document.documentElement.animate(
{
clipPath: this.isDark ? clipPath.reverse() : clipPath,
},
{
duration: 500,
easing: 'ease-in',
pseudoElement: this.isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
}
);
});
},
},
};
</script>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
.dark::view-transition-old(root) {
z-index: 9999;
}
</style>
实现功能
-
自定义 Switch 外观:通过
.switch、.slider等类自定义了一个美观的开关按钮,模拟了原生 checkbox 的切换效果。 -
隐藏原生 checkbox:
.switch input设置为透明且不可见,只用作状态控制。 -
滑块动画:
.slider:before通过left和background属性的变化,实现了滑块在明暗模式间的平滑移动和样式切换。 -
暗黑模式样式:当 input 被选中(即暗黑模式),滑块和背景颜色会发生变化,模拟月亮和夜晚的效果。
-
用户选择的持久化(localStorage)
-
动画过渡(圆形扩散,提升体验)
-
响应式 UI 状态(v-model 绑定)
ThemeSettings.vue组件
主题颜色切换
自定义主题选择
代码
<script>
import {Check} from '@element-plus/icons-vue'
import LightAndDarkSelectionButton from "./LightAndDarkSelectionButton.vue";
import {colorList, applyTheme, defaultColor} from '../../utils/themes.js'
export default {
name: 'ThemeSettings',
components: {
LightAndDarkSelectionButton,
Check,
},
data() {
return {
primaryColor: defaultColor,
colorList: colorList,
custom_color: "#FF8B00"
}
},
mounted() {
this.primaryColor = localStorage.getItem('theme-color') || defaultColor
},
methods: {
handleColorChange(color) {
this.primaryColor = color
applyTheme(color)
},
random_Color() {
if (this.colorInterval) {
// 已经在运行,点击则停止
clearInterval(this.colorInterval)
this.colorInterval = null
} else {
// 没有运行,点击则开始
this.colorInterval = setInterval(() => {
const randomColor = '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
this.handleColorChange(randomColor)
}, 500)
}
}
}
}
</script>
<template>
<div class="theme-page">
<div class="theme-content">
<div class="theme-section">
<h4>主题颜色</h4>
<div class="color-palette">
<div
v-for="color in colorList"
:key="color"
class="color-item"
:style="{ backgroundColor: color }"
@click="handleColorChange(color)"
>
<el-icon v-if="color === primaryColor">
<Check/>
</el-icon>
</div>
<el-divider direction="vertical"/>
</div>
<h4>自定义</h4>
<div class="color-palette">
<div class="color-item"
:key="custom_color"
:style="{ backgroundColor: custom_color }"
@click="handleColorChange(custom_color)"
>
<el-icon v-if="custom_color === primaryColor">
<Check/>
</el-icon>
</div>
<el-color-picker v-model="custom_color" size="large" :predefine="colorList"/>
</div>
</div>
<div class="theme-section">
<h4>暗黑模式</h4>
<div class="color-palette">
<LightAndDarkSelectionButton/>
</div>
</div>
<el-button @click="random_Color()">五颜六色(滑稽脸)</el-button>
</div>
</div>
</template>
<style scoped>
.theme-sidebar {
position: fixed;
top: 0;
right: 0;
width: 300px;
height: 100vh;
background: var(--el-bg-color);
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
padding: 20px;
z-index: 999;
overflow-y: auto;
}
.theme-header {
font-size: 18px;
font-weight: bold;
color: var(--el-text-color-primary);
margin-bottom: 20px;
}
.theme-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.theme-section {
margin-bottom: 20px;
}
.color-palette {
display: grid;
grid-template-columns: repeat(auto-fill, 40px);
gap: 15px;
}
.color-item {
width: 40px;
height: 40px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
}
.color-item:hover {
transform: scale(1.1);
}
</style>
实现功能
-
用户点击色块或自定义色,主题色立即切换并应用到全局(通过
applyTheme实现)。 -
用户选择的主题色会保存到 localStorage,下次进入页面自动恢复。
-
用户可通过明暗模式按钮切换全局明暗主题。
-
“五颜六色”按钮可让主题色不断随机变化,点击可停止。
-
主题色切换即刻生效,无需刷新页面
-
支持自定义色,满足个性化需求
-
明暗模式无缝切换,与主色搭配
-
用户偏好持久化,体验友好
-
彩蛋功能,提升趣味性
UserSettingButton.vue组件
按钮打开组件
点击打开页面
代码
<template>
<div>
<!-- 触发按钮 -->
<el-button @click="dialogVisible = true" circle>
<el-icon>
<Setting/>
</el-icon>
</el-button>
<teleport to="body">
<!-- 设置对话框 -->
<el-dialog
v-model="dialogVisible"
title="系统设置"
width="50%"
:close-on-click-modal="true"
:close-on-press-escape="false"
:show-close="true"
>
<ThemeSettings/>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button type="primary" @click="handleConfirm">保存设置</el-button>
</template>
</el-dialog>
</teleport>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {Setting} from '@element-plus/icons-vue'
import ThemeSettings from "./ThemeSettings.vue";
const dialogVisible = ref(false)
const handleConfirm = () => {
// 这里可以添加保存设置的逻辑
// 例如调用 SettingMain 组件的保存方法
dialogVisible.value = false
}
</script>
<style scoped>
/* 可以添加一些自定义样式 */
.el-button {
display: inline-flex;
align-items: center;
gap: 6px;
}
</style>
App.vue组件
必须要挂载ThemeSettings组件在App上才会生效
<script setup>
import ThemeSettings from "./components/ThemePicker/ThemeSettings.vue";
import {useDrawerStore} from './stores/drawer'
const drawerStore = useDrawerStore()
</script>
<template>
<router-view/>
<!-- 抽屉组件放这里,始终生效 -->
<el-drawer
v-model="drawerStore.settingsDrawerVisible"
title="主题设置"
direction="rtl"
size="400px"
:with-header="false"
>
<ThemeSettings/>
</el-drawer>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
核心代码js
theme.js实现主题切换的核心代码
// src/utils/theme.js
export const tintColor = (color, tint) => {
const red = parseInt(color.slice(1, 3), 16)
const green = parseInt(color.slice(3, 5), 16)
const blue = parseInt(color.slice(5, 7), 16)
return `#${[
Math.round(red + (255 - red) * tint).toString(16).padStart(2, '0'),
Math.round(green + (255 - green) * tint).toString(16).padStart(2, '0'),
Math.round(blue + (255 - blue) * tint).toString(16).padStart(2, '0')
].join('')}`
}
// src/utils/theme.js
export const initDarkMode = () => {
const isDark = localStorage.getItem('vueuse-color-scheme') === 'dark'
if (isDark) {
document.documentElement.classList.add('dark')
}
return isDark
}
export const shadeColor = (color, shade) => {
const red = parseInt(color.slice(1, 3), 16)
const green = parseInt(color.slice(3, 5), 16)
const blue = parseInt(color.slice(5, 7), 16)
return `#${[
Math.round(red * (1 - shade)).toString(16).padStart(2, '0'),
Math.round(green * (1 - shade)).toString(16).padStart(2, '0'),
Math.round(blue * (1 - shade)).toString(16).padStart(2, '0')
].join('')}`
}
export const colorList = [
'#409EFF',
'#DAA96E',
'#0C819F',
'#27ae60',
'#ff5c93',
'#e74c3c',
'#9b59b6'
]
export const defaultColor = '#409EFF'
export const applyTheme = (color) => {
const primaryColor = color || localStorage.getItem('theme-color') || defaultColor
const colors = {
'--el-color-primary': primaryColor,
'--el-color-primary-light-2': tintColor(primaryColor, 0.3),
'--el-color-primary-dark-2': shadeColor(primaryColor, 0.2),
}
Object.entries(colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value)
})
localStorage.setItem('theme-color', primaryColor)
}
// 初始化主题
export const initTheme = () => {
const savedColor = localStorage.getItem('theme-color')
applyTheme(savedColor)
}
注意事项
记得去掉style.css不然背景颜色可能是白色,切换也不行
可以看到localStorage保存主题颜色和是否是dark模式!
组件展示
项目结构
theme/
├── index.html
├── package-lock.json
├── package.json
├── public/
│ └── vite.svg
├── README.md
├── src/
│ ├── App.vue
│ ├── assets/
│ │ └── vue.svg
│ ├── components/
│ │ ├── DemoTestElementPlus.vue
│ │ ├── HelloWorld.vue
│ │ └── ThemePicker/
│ │ ├── LightAndDarkSelectionButton.vue
│ │ ├── ThemeSettings.vue
│ │ └── UserSettingButton.vue
│ ├── main.js
│ ├── router/
│ │ └── index.js
│ ├── stores/
│ │ └── drawer.js
│ ├── style.css
│ └── utils/
│ └── themes.js
├── theme.iml
└── vite.config.js
drawer.js这个是侧板抽屉,但是我使用的是上面的UserSettingButton组件直接打开即可
实现逻辑
1. 明暗模式切换逻辑
组件:LightAndDarkSelectionButton.vue
-
状态管理:
- 通过
localStorage读取和保存当前主题(vueuse-color-scheme,值为'dark'或'light')。 - 组件的
isDark和changeDark控制当前是否为暗黑模式。
- 通过
-
切换实现:
- 切换时,给
<html>元素添加或移除dark类(document.documentElement.classList.add/remove('dark'))。 - 同步更新
localStorage,保证刷新后主题不丢失。 - 使用
document.startViewTransition和clipPath动画,带来平滑的切换体验。
- 切换时,给
-
入口:
- 该按钮被集成在
ThemeSettings.vue主题设置面板中,供用户点击切换。
- 该按钮被集成在
2. 主题主色切换逻辑
组件:ThemeSettings.vue
-
状态管理:
- 主题色(
primaryColor)初始值为defaultColor,挂载时从localStorage读取上次选择。 - 用户可从预设色板或自定义色中选择主题色。
- 主题色(
-
切换实现:
- 选择色块或自定义色时,调用
handleColorChange(color)方法。 - 该方法会调用
applyTheme(color)(定义在src/utils/themes.js),动态修改 Element Plus 的主题色变量。 - 主题色变化后立即生效,并保存到
localStorage,保证刷新后主题色不丢失。
- 选择色块或自定义色时,调用
-
彩蛋功能:
- “五颜六色”按钮会定时随机切换主题色,点击可停止。
3. 主题变量应用(applyTheme)
- 通过
applyTheme(color),将选中的颜色应用到全局 CSS 变量(如--el-color-primary),Element Plus 组件会自动响应这些变量的变化,实现全局主题色切换。
4. 持久化与全局生效
- 明暗模式和主题色都通过
localStorage持久化,保证用户偏好不会丢失。 - 主题切换通过修改全局 CSS 变量和
<html>类名,确保所有页面和组件都能即时响应。
交互入口
- 用户通过
ThemeSettings.vue面板进行主题色和明暗模式的切换。 - 该面板可作为全局设置入口,方便用户随时调整主题。
总结
主题切换实现逻辑是:
通过组件封装 + CSS 变量 + <html> 类名 + localStorage 持久化,实现了 Element Plus 全局主色和明暗模式的动态切换,且切换过程带有动画,用户体验会比较好。
百度网盘
蓝奏云