从零开始Vue3+Element Plus后台管理系统(17)——一键换肤的N种方案
暗黑模式
基于Element Plus和Tailwind CSS灵活的设计,我们很容易在项目中实现暗黑模式,具体可以参考之前的文章《从零开始写一个Vue3+Element Plus的后台管理系统(二)——Layout页面布局的实现》
换肤方案
如果需要给用户提供更多主题,更丰富的皮肤,就得自己来开发换肤功能了。
换肤的方式由易到难大概分为3种:
- 简单换肤,提供N种配色方案供用户选择,一般只对配色进行切换,比如禅道,它提供了蓝色、粉色、绿色等方案;
- 个性化定制,可以自己定制主要颜色、阴影、边框等样式,个性化更强;
- 整站模板,提供N个风格迥异的模版布局和UI界面,彻底改头换面。
技术实现
以下仅针对使用Element Plus 的项目中进行换肤。
CSS变量
在开始之前,需要先了解CSS变量,它是一个非常有用的功能,几乎所有浏览器都支持,除了IE。既然选择了Vue3,明摆着就不想支持IE,所以我们可以愉快的使用CSS变量。
Element Plus 提取并整理了所有的设计变量,并通过 CSS Vars 技术实现动态更新主题。在我们的项目中找到node_modules/element-plus/theme-chalk
,查看其中的CSS文件,可以看到其中有各种组件用到的CSS变量的定义。我把index.css拿出来格式化并存档,方便日后参考使用。
格式化以后的文件有140多行,截取部分贴出来:
:root {
--el-color-white: #ffffff;
--el-color-black: #000000;
--el-color-primary-rgb: 64, 158, 255;
--el-color-success-rgb: 103, 194, 58;
--el-color-warning-rgb: 230, 162, 60;
...
}
:root {
color-scheme: light;
--el-color-white: #ffffff;
--el-color-black: #000000;
--el-color-primary: #409eff;
...
}
在f12控制台,或者查看单个组件的样式文件,可以看到Element Plus通过CSS变量来设置组件的样式。
继续研究Element Plus的包,可以找到这个文件node_modules/element-plus/theme-chalk/src/var.scss
,它保存的是SCSS变量,最终生成了theme-chalk/index.css
。
如果只修改Element主题而不考虑动态换肤,我们可以通过修改SCSS变
量定制自己的ep外观。但动态换肤就需要使用CSS
变量来完成。
OK,讲了这么多,其实就是为使用CSS变量换肤做铺垫。
方案1 简单换肤的技术实现
项目开发过程中,在<style>
中自定义样式,我会尽量使用EP的CSS变量,一是可以直接利用EP的dark模式,二是为将来换肤做准备。比如页面头部:
<style scoped lang="scss">
.v-header {
...
border-bottom: 1px solid var(--el-border-color);
.logo {
color: var(--el-color-primary);
}
}
</style>
修改css变量
由此可以想到,在我们需要更换主题样式时,只需动态修改CSS变量即可。
// 修改 EP 主要颜色为 红色
document.documentElement.style.setProperty('--el-color-primary', 'red')
建立配色方案的CSS变量
当我们需要修改的变量数量很少时,一个个修改还好,属性多的话这种做法显然不够优雅。那么暗黑模式是如何实现一键切换样式的呢?
dark模式是通过在html标签增加.dark,同时新建了一份专属dark的CSS Vars。同理,只需给不同的主题设置不同的CSS Vars,然后动态修改html的class,就可以实现简单换肤。
document.getElementsByTagName('html')[0].className = theme
在assets/css中新建theme.css
.red {
--el-color-primary: #ff2551;
}
.pink {
--el-color-primary: #f47983;
}
.green {
--el-color-primary: #0c8918;
}
.brown {
--el-color-primary: #ae7000;
}
.grape {
--el-color-primary: #725e82;
}
切换主题操作 ThemeSetting.vue,通过切换html的classname使用不同主题下的CSS Vars
const themes = [
{ name: 'red', color: '#ff2551' },
{ name: 'pink', color: '#f47983' },
{ name: 'green', color: '#0c8918' },
{ name: 'brown', color: '#ae7000' },
{ name: 'grape', color: '#725e82' }
]
function changeColor(theme: string) {
document.getElementsByTagName('html')[0].className = theme
}
到此为止,一个最简单的换肤功能已经出来了,后续可以在theme.css中增加更多的变量来实现更多样式的变化(变量名根据按照上面拿到的Element Plus的CSS变量即可),通过这个操作已经可以实现禅道系统的换肤功能需求。
主题持久化
还有一个小问题,那就是当我们刷新页面后,主题回到了页面最初的样子,看来又需要做状态的持久化喽,这已经是老生常谈了。照旧使用pinia加persist。
store/theme.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useThemeStore = defineStore(
'theme',
() => {
let scheme = ref('')
// 设置配色主题
function setScheme(str: string) {
scheme.value = str
}
return { scheme, setCSS }
},
{
persist: true
}
)
修改换肤方法,把主题名称保存到状态管理中。
function changeColor(theme: string) {
document.getElementsByTagName('html')[0].className = theme
useTheme.scheme = theme
}
layout下新建theme/index.ts 用于初始化主题,然后在main.ts调用
import { Pinia } from 'pinia'
import { useThemeStore } from '~/store/theme'
export default (pinia: Pinia) => {
const useTheme = useThemeStore(pinia)
// const el = document.documentElement
if (useTheme.scheme) {
document.getElementsByTagName('html')[0].className = useTheme.scheme
}
}
main.ts
// theme
import initTheme from '~/layout/theme'
initTheme(pinia)
OK,现在再刷新页面,也不会丢失主题状态了,打开控制台可以看到,我们选择主题已经保存在localStorage中,只有清除缓存,才会回到默认的主题。
方案2,个性化自定义
如果你用过Element UI,应该知道它提供了一个自定义主题功能,下载后即可使用。
方案二的思路和它类似,用户可以自己定义color,menu,border等一些常用值,然后保存为自己的主题方案。
第二种方案的核心就是修改单个css变量
document.documentElement.style.setProperty('--el-color-primary', 'red')
获取初始化时的CSS 变量,用于显示调色板的初始值
const currentCss = getComputedStyle(document.querySelector('html') as Element)
let colors = reactive({
primary: { label: '主要颜色', value: '', key: '--el-color-primary' },
info: { label: '次要颜色', value: '', key: '--el-color-info' },
warning: { label: '警告颜色', value: '', key: '--el-color-warning' },
danger: { label: '危险颜色', value: '', key: '--el-color-danger' },
bg: { label: '背景颜色', value: '', key: '--el-bg-color' }
})
第二种加上以后,和第一种产生了一些冲突——当我修改了自定义配色,再去选择配色方案的时候,颜色不会变成配色方案的色值😐,因为它们都是全局的CSS Vars。
利用CSS的优先级,我们把第一种方案的CSS 代码稍微改下,增加#app
的限制,这样配色方案的优先级会高于自定义配色。
.blue #app {
--el-color-primary: #0052d9;
}
当我们选择了自定义配色后,把html上的className
改为‘’
,去除配色方案的影响。
两种方案并行的处理逻辑大体如此。
PS:别忘了把第二种方案设置的CSS变量放进pinia中
方案3,修改布局
修改页面布局有两种实现方法
-
修改CSS 修改CSS比较简单,只需要在切换布局时,动态修改顶层元素的class,然后针对class 设置各种布局的样式即可。
-
建立多个布局文件,动态切换 如果想实现更灵活、复杂的布局设置,比如不同布局显示不同的组件,组件的交互也不同... 那么修改CSS的方法会比较麻烦,甚至需要通过一些不友好(丑陋)的写法来实现。
既然是试验性的想法,所以不妨试试多建立几个布局文件来切换。
上图是两个布局文件动态切换的效果,default
和vertical
。
在layout/index.vue 根据当前布局useTheme.layoutScheme
来渲染不同的布局模板。
此处有两个可以关注的点:
- 强制刷新,用key是比较理想的方式。用v-if也可以实现,但是代码看起来有些啰嗦。
- 监听pinia变量,
$subscribe
<template>
<Layout :key="key">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="tags.nameList">
<component :is="Component"></component>
</keep-alive>
</transition>
</router-view>
</Layout>
</template>
<script setup lang="ts">
import {ref } from 'vue'
import { useTagsStore } from '~/store/tags'
import { useThemeStore } from '~/store/theme'
import DefaultLayout from './default/index.vue'
import VerticalLayout from './vertical/index.vue'
const tags = useTagsStore()
const useTheme = useThemeStore()
let key = ref(useTheme.layoutScheme)
let Layout = useTheme.layoutScheme === 'default' ? DefaultLayout : VerticalLayout
useTheme.$subscribe((mutation, state) => {
Layout = state.layoutScheme === 'default' ? DefaultLayout : VerticalLayout
key.value = state.layoutScheme
})
</script>
结束语
到此,基本实现了开头设想的3种方式。因为时间仓促,代码写得有些乱,特别是方案三导致代码结构变动较大,有待进一步优化调整,就不一一贴出来了,后续更新在项目仓库中。
这篇文章断断续续写了半个月,但是收获颇丰,在coding和写作的过程中,对Vue3的渲染机制,CSS变量,换肤的逻辑,有了更深入的理解。记录是个好习惯,要坚持哦~ ✊
欢迎各位提出好的建议,一起成长 🌳
项目地址
本项目GIT地址:github.com/lucidity99/…
如果有帮助,给个star ✨ 点个赞👍
转载自:https://juejin.cn/post/7236269878226599991