【V3 Admin Vite】教程八:多主题(动态换肤)模式的实现
前言
本系列文章是为了帮助没有直接上手(或上手比较困难)做项目能力的初级前端开发工程师采用 V3 Admin Vite 开源模板来编写业务代码。
本系列文章的视频教程版本地址:B 站(群友好心录制,可能会不同步)
文章目的
本文主要是写给想学习或二开本项目多主题模式的小伙伴。
Begin
效果图
系统目前内置了三种主题模式:默认、黑暗和深蓝,下面我们先欣赏一下部分截图
默认主题
也叫白天、亮色、浅色模式,在没有特殊需求时采用该配色比较通用
黑暗模式
也叫黑夜、夜间、暗色、深色、暗黑模式,这种模式现如今越来越流行,也是首要适配的模式
深蓝模式
也可以叫科技、大屏模式,这是我们 V3 Admin Vite 区别于其他系统,比较有特色的特有模式!
实现思路
基本原理
基本原理非常简单,是通过改变 <html>
的 class
属性值来让预先写好的不同的 css
生效,从而实现主题的切换
举个例子,就好像是:
/** 黑暗模式 */
.dark {
#app {
color: #c0c4cc;
}
.login-container {
color: #c0c4cc;
}
}
/** 深蓝模式 */
.dark-blue {
#app {
color: rgba(255, 255, 255, 0.8);
}
.login-container {
color: rgba(255, 255, 255, 0.8);
}
}
这时候,我们只要挂载 <html class="dark">
或者 <html class="dark-blue">
就能实现 id 为 app
和 类名为 login-container
的元素切换不同文字颜色!
但是,将这样一个原理直接落地到一个复杂的项目中时,会非常繁琐,因为:
- 真实且复杂的项目中,涉及到需要切换颜色的类名非常多
- 同一个主题模式下,颜色(比如文字颜色)很多都是一样的
所以:
.dark
和.dark-blue
内部的结构非常相似,都有#app
和.login-container
,我们应该将其抽离出去统一处理#app
和.login-container
内部的color
颜色分别都是一样的,我们应该将其抽离出去统一处理
所以在基本原理的基础上,我们还可以利用 scss 语法抽离出相同的结构和变量,告别大量重复的代码
到这里我们先不讲太深入,但是大家需要记住两个东西:
- 我们的基本原理是什么?
- 在基本原理的基础上,我们利用 scss 语法干了什么?
ThemeSwitch 组件
在页面顶部,我们能发现这个切换主题组件:
它所对应的代码非常简单,在 @/components/ThemeSwitch
目录下:
<script lang="ts" setup>
import { useTheme } from "@/hooks/useTheme"
import { MagicStick } from "@element-plus/icons-vue"
const { themeList, activeThemeName, setTheme } = useTheme()
</script>
<template>
<el-dropdown trigger="click" @command="setTheme">
<div>
<el-tooltip effect="dark" content="主题模式" placement="bottom">
<el-icon :size="20">
<MagicStick />
</el-icon>
</el-tooltip>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(theme, index) in themeList"
:key="index"
:disabled="activeThemeName === theme.name"
:command="theme.name"
>
<span>{{ theme.title }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
简单过一下代码,我们就能知道,下拉菜单的数据来对应 v-for="(theme, index) in themeList"
这条指令,指令中被遍历的 themeList
变量又来自于代码开头的这条解构赋值语句: const { themeList, activeThemeName, setTheme } = useTheme()
也就是说,如果我们要修改或新增一个主题,我们应该前往 useTheme
这个 hook 看一看
useTheme
这个处理主题的 hook @/hooks/useTheme.ts
,可以仔细读一下:
import { ref, watchEffect } from "vue"
import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage"
const DEFAULT_THEME_NAME = "normal"
type DefaultThemeName = typeof DEFAULT_THEME_NAME
/** 注册的主题名称, 其中 DefaultThemeName 是必填的 */
export type ThemeName = DefaultThemeName | "dark" | "dark-blue"
interface ThemeList {
title: string
name: ThemeName
}
/** 主题列表 */
const themeList: ThemeList[] = [
{
title: "默认",
name: DEFAULT_THEME_NAME
},
{
title: "黑暗",
name: "dark"
},
{
title: "深蓝",
name: "dark-blue"
}
]
/** 正在应用的主题名称 */
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
/** 设置主题 */
const setTheme = (value: ThemeName) => {
activeThemeName.value = value
}
/** 在 html 根元素上挂载 class */
const setHtmlRootClassName = (value: ThemeName) => {
document.documentElement.className = value
}
/** 初始化 */
const initTheme = () => {
// watchEffect 来收集副作用
watchEffect(() => {
const value = activeThemeName.value
setHtmlRootClassName(value)
setActiveThemeName(value)
})
}
/** 主题 hook */
export function useTheme() {
return { themeList, activeThemeName, initTheme, setTheme }
}
简单解答一些特殊点:
- 将
DEFAULT_THEME_NAME
变量抽离出来是为了防止多处出现硬编码 - 默认主题的 name 是可以随意更改的,默认取值为
normal
- 其他主题的 name 虽然是自定义的,但不可以随意更改,比如内置的深蓝主题,就只能是
dark-blue
,除非你去@/styles/theme
目录下同步更改每一处叫dark-blue
的地方 setHtmlRootClassName
方法是用来在 html 根元素上挂载 class 的,比如深蓝主题时就会变成<html class="dark-blue" ...>
- 如果你需要新增一个主题选项,那么需要给
ThemeName
和themeList
添加新增的主题名称
除了以上列举的几点外,其余的代码几乎是不用动的,下面我们先来解读一下内置的三种主题,然后大家基本上就可以按照这个规律来新增自己的主题了!
主题样式文件
多主题模式相关的样式文件都放在了 @/styles/theme
目录下:
register.scss
是用来注册的,或者说用来加载样式文件的,如果你在@/hooks/useTheme.ts
中新增了主题选项后,还需要在这里注册一下,因为只有切换选项没有对应的样式文件是不行的dark
目录下存放了黑暗主题样式dark-blue
目录下存放了深蓝主题样式- 假如你需要新增一个主题,那么需要在
dark
与dark-blue
同级目录处新增一个你的主题文件夹(注意需要和 hook 里面的主题选项同名)
还记得文章开头我让大家记住两个东西吗:
- 我们的基本原理是什么?
- 在基本原理的基础上,我们利用 scss 语法干了什么?
是的,对应到我们现在的目录结构的话,dark
与 dark-blue
实际上就是存放该主题下对应变量的,而 core
目录就是我们抽离出来的结构
默认主题
默认主题是最简单的,只需要在页面上像平常一样写样式即可,也就是说,默认主题的样式并没有放在 @/styles/theme
目录下,而是正常放在每个页面对应的 .vue
文件里
黑暗主题
因为 Element Plus 自带有黑暗主题,当我们将 <html class="dark" ...>
设置好时,Element Plus 所有组件的黑暗主题就会生效了,也就是说,我们只需要对没有用到 Element Plus 组件写的页面进行处理就行了!
打开 dark
目录下的 variables.scss
文件可以看见该主题下抽离出来的变量:
注意:$theme-name 这个变量需要和前面注册主题时提到的名称保持统一
/** dark 主题下的变量 */
// 主题名称
$theme-name: "dark";
// 主题背景颜色
$theme-bg-color: #151515;
// 主题色
$theme-color: #409eff;
// 默认文字颜色
$font-color: #c0c4cc;
// active 状态下文字颜色
$active-font-color: #fff;
// hover 状态下文字颜色
$hover-color: #fff;
// 边框颜色
$border-color: #303133;
现在我们来看看是如何让这些变量生效的
打开 core
目录中的 index.scss
:
代码如下:
.#{$theme-name} {
@import "./layouts.scss";
@import "./login.scss";
@import "./error-page.scss";
@import "./element-plus.scss";
@import "./vxe-table.scss";
@import "./other.scss";
}
因为 $theme-name: "dark";
也就是说最终生效的就是这样子(伪代码):
.dark {
@import "./layouts.scss";
@import "./login.scss";
@import "./error-page.scss";
@import "./element-plus.scss";
@import "./vxe-table.scss";
@import "./other.scss";
}
所以,当设置 <html class="dark" ...>
时,上面的 ./layouts.scss
、./login.scss
、./error-page.scss
、./element-plus.scss
、./vxe-table.scss
、./other.scss
里面的样式都会生效
我们打开其中一个具体的样式文件看看,比如和登录页相关的 ./login.scss
:
/** Login 页面相关 */
.login-container {
background-color: $theme-bg-color;
color: $font-color;
.login-card {
background-color: lighten($theme-bg-color, 4%) !important;
}
}
可以发现里面全是运用抽离出去的 scss 变量($theme-bg-color
、$font-color
)
其他文件也同理!我这里就不再截图了
深蓝主题
这个主题比黑暗主题复杂一点,因为 Element Plus 没有自带深蓝主题,这就需要我们去重写 Element Plus 它自己的样式变量,除此之外,其他内容和黑暗主题一模一样,所以请大家先阅读上一小节内容!
打开 dark-blue
目录:
会发现 index.scss
和 variables.scss
两个文件黑暗主题也有,所以我们这小节就不提了,重点看一下 element-plus.css
和 element-plus.scss
这两个文件
这两个文件功能其实是一样的,都是重写 Element Plus 它自己的样式变量。之所以分别写 css 和 scss 两种,一是因为 Element Plus 支持这两种方式,二是因为社区有人喜欢 scss 方式并提交了 PR,原本我是只提供了 css 方式的
我们用 css 方式为例,看一下是如何重写 Element Plus 原本样式变量的:
/**
* dark-blue 主题下的 Element Plus CSS 变量
* 在此查阅所有可自定义的变量:https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
* 也可以打开浏览器控制台选择元素,查看要覆盖的变量名
*/
html.dark-blue {
/** color */
--el-color-primary: #01efb7bb;
--el-color-primary-light-3: rgba(1, 147, 127, 0.8133333333);
--el-color-primary-light-5: rgba(2, 102, 99, 0.8666666667);
--el-color-primary-light-7: rgba(2, 65, 77, 0.92);
--el-color-primary-light-8: rgba(2, 49, 68, 0.9466666667);
--el-color-primary-light-9: rgba(2, 35, 59, 0.9733333333);
--el-color-primary-dark-2: rgba(1, 167, 128, 0.7866666667);
--el-color-success: #01efb7bb;
/** text-color */
--el-text-color-primary: rgba(255, 255, 255, 0.8);
--el-text-color-regular: rgba(255, 255, 255, 0.8);
--el-text-color-secondary: rgba(255, 255, 255, 0.8);
--el-text-color-placeholder: rgba(255, 255, 255, 0.8);
--el-text-color-disabled: rgba(255, 255, 255, 0.3);
/** border-color */
--el-border-color: #01efb755;
--el-border-color-light: #01efb755;
--el-border-color-lighter: #01efb755;
/** fill-color */
--el-fill-color: #01efb710;
--el-fill-color-light: #01efb710;
--el-fill-color-blank: #031e47;
/** bg-color */
--el-bg-color: #021633;
--el-bg-color-overlay: #031e47;
/** mask */
--el-mask-color: rgba(0, 0, 0, 0.5);
}
/** button */
html.dark-blue .el-button {
--el-button-disabled-text-color: rgba(255, 255, 255, 0.5);
}
注意命名:html.dark-blue
还是比较简单粗暴的,直接把 Element Plus 提供的 CSS 变量重新赋值即可。这样就能实现 Element Plus 组件的深蓝主题了!
新增主题
如果你阅读完前文,应该是能够新增主题的,这里我再简单总结一下步骤:
- 假如你要新增一个红色主题
命名为 red
- 你需要前往以下地方注册新的主题
src/hooks/useTheme.ts
src/styles/theme/register.scss
- 创建你的主题样式文件
src/styles/theme/red/index.scss
src/styles/theme/red/variables.scss
src/styles/theme/red/element-plus.css
这三份文件可以直接复制深蓝主题下的文件,然后进行修改即可
End
本系列所有手摸手教程
V3 Admin Vite 相关链接
转载自:https://juejin.cn/post/7268123683222257701