Element Plus 多主题切换:源码级新玩法
在做 Vue 3 + Element Plus 项目时,自定义主题 几乎是必修课。
大家都知道的几种方案:
- 覆盖 CSS 变量
- 修改 SCSS 变量重新编译
- 官方提供的暗黑模式
这些方式足够应付单主题或暗黑/明亮切换,但要实现 多主题自由切换(品牌 A、品牌 B、高对比度...) 时,体验就差强人意了。
翻源码的过程中,我发现了一个关键点 ------ Element Plus 内部生成颜色变量的函数 set-css-color-type
。默认它写死了 "base"
作为主色入口。
前置条件
- 使用了sass 因此我们需要安装插件 sass-embedded
sh
pnpm add sass-embedded -D
原版源码(节选)
源码地址:https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/_var.scss
scss
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/_var.scss
@mixin set-css-color-type($colors, $type) {
@include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
@each $i in (3, 5, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
@include set-css-var-value(
('color', $type, 'dark-2'),
map.get($colors, $type, 'dark-2')
);
}
可以看到,map.get(..., 'base')
被写死了。
这意味着所有主题都只能以 base
为入口。
改造方案
我们把 base
抽象成一个 $theme
参数,这样就能为每个主题(如 default / simple / grand / noble
)指定不同的基色。
scss
@mixin set-css-color-type($colors, $type, $theme: "base") {
@include set-css-var-value(('color', $type), map.get($colors, $type, $theme));
@each $i in (3, 5, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
@include set-css-var-value(
('color', $type, 'dark-2'),
map.get($colors, $type, 'dark-2')
);
}
然后批量生成主题 class:
scss
@each $class, $color in ('base', 'default', 'simple', 'grand', 'noble') {
.#{$class} {
@each $type in (primary, success, warning, danger, info) {
@include set-css-color-type($colors, $type, $class);
}
}
}
定义多主题颜色
scss
$colors: (
'primary': (
default: #0069d9,
simple: #4a90e2,
grand: #2c6b97,
noble: #003b61
),
'success': (
default: #28a745,
simple: #5cb85c,
grand: #2d8c4f,
noble: #1f7b35
),
'info': (
default: #17a2b8,
simple: #32c8d5,
grand: #2292b7,
noble: #166f80
),
'warning': (
default: #ffc107,
simple: #f39c12,
grand: #e67e22,
noble: #e04e01
),
'danger': (
default: #dc3545,
simple: #e57373,
grand: #c0392b,
noble: #9b2d20
)
);
Vue 中动态切换
ts
// useTheme.ts
import { ref } from 'vue'
const theme = ref('default')
export function useTheme() {
const setTheme = (name: string) => {
theme.value = name
document.documentElement.className = name
}
return { theme, setTheme }
}
只要调用 setTheme('grand')
,页面就会切换成深蓝主题,Element Plus 的暗色模式与亮色层级依然会自动生效。不过推荐使用vueuse的 useColorMode,好不好用我就不用多说了
最后 完整代码
目录结构:
cpp
src/
└─ index.scss
├─ main.ts
├─ app.vue
vite.ts
scss
// src/index.scss
@use 'sass:map';
@use 'sass:color';
$primary-themes: (
default: #0069d9,
simple: #4a90e2,
grand: #2c6b97,
noble: #003b61
);
$success-themes: (
default: #28a745,
simple: #5cb85c,
grand: #2d8c4f,
noble: #1f7b35
);
$info-themes: (
default: #17a2b8,
simple: #32c8d5,
grand: #2292b7,
noble: #166f80
);
$warning-themes: (
default: #ffc107,
simple: #f39c12,
grand: #e67e22,
noble: #e04e01
);
$danger-themes: (
default: #dc3545,
simple: #e57373,
grand: #c0392b,
noble: #9b2d20
);
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': $primary-themes,
'success': $success-themes,
'info': $info-themes,
'warning': $warning-themes,
'danger': $danger-themes
)
);
@use 'element-plus/theme-chalk/src/common/var.scss' as *;
@use 'element-plus/theme-chalk/src/mixins/_var.scss' as *;
@mixin set-css-color-type($colors, $type, $theme: "base") {
@include set-css-var-value(('color', $type), map.get($colors, $type, $theme));
@each $i in (3, 5, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
@include set-css-var-value(
('color', $type, 'dark-2'),
map.get($colors, $type, 'dark-2')
);
}
@each $class, $color in $primary-themes {
.#{$class} {
@each $type in (primary, success, warning, danger, info) {
@include set-css-color-type($colors, $type,$class)
}
}
}
ts
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css'
import "./index.scss"
createApp(App).use(ElementPlus).mount('#app')
ts
// vite.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})
js
// src/app.vue
<script setup lang="ts">
</script>
<template>
<div>
element-plus-custom-themes
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
</div>
</template>
<style scoped>
</style>
按需引入
如果我们使用按需引入,只需要改掉vite.ts 和main.ts
- 前置条件 安装 按需引入需要的插件
sh
pnpm add unplugin-vue-components unplugin-auto-import -D
- 修改后的文件
ts
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
// import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css'
// import "./assets/styles/index.scss"
// createApp(App).use(ElementPlus).mount('#app')
createApp(App).mount('#app')
ts
// vite.ts
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
include: [/\.[jt]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
imports: [
'vue',
],
resolvers: [ElementPlusResolver()],
vueTemplate: true,
}),
Components({
extensions: ['vue', 'md'],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
resolvers: [ElementPlusResolver({
importStyle: 'sass',
})],
}),
],
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/assets/styles/themes/index.scss" as *;`
},
},
},
})
总结
- Element Plus 默认
set-css-color-type
写死了"base"
,这是多主题受限的根源。 - 我们改造后,可以让
default / simple / grand / noble
等多套主题并存。 - 内置的暗黑模式、颜色层级(light/dark)逻辑依旧能正常工作,不需要重复维护。
- 按需导入,减少构建体积。
这样,你就可以在实际项目里优雅地支持 品牌主题、活动主题、暗黑主题 等多套风格啦 🎉