likes
comments
collection
share

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

作者站长头像
站长
· 阅读数 39

效果展示

在开始之前,我们先来看看效果图

暗黑模式:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

主题切换:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

系统设置弹窗

由于我们的弹窗组件和设置按钮是夸层级组件,所以我们的emit事件就不能按照vue的传统方式来写了。这里我们借助mitt来进行事件通信。

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

src/utils下新建mittBus.ts,对它做个简单的封装,方便我们使用:

import mitt from 'mitt'

const mittBus = mitt()

export default mittBus

src/layouts/NavBar/components/Settings/index.vue中,我们通过mittBus.emit('openThemeDrawer')打开弹窗:

<template>
  <div class="btn">
    <el-tooltip effect="dark" content="系统设置">
      <el-button circle @click="onSetting">
        <IconifyIcon icon="ep:setting" height="16" />
      </el-button>
    </el-tooltip>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import mittBus from '@/utils/mittBus'
export default defineComponent({
  setup() {
    const onSetting = () => {
      // 采用事件监听的方式打开ThemeDrawer
      mittBus.emit('openThemeDrawer')
    }
    return { onSetting }
  },
})
</script>

<style scoped lang="scss">
.btn {
  margin-right: 20px;
  cursor: pointer;
  transition: all 0.3s;
}
</style>

src/layouts/index.vue中我们引入ThemeDrawer组件,这个就是设置主题和暗黑模式的弹窗。在ThemeDrawer组件中,监听openThemeDrawer事件的触发,然后设置drawerVisible的值:

<template>
  <el-drawer title="主题设置" v-model="drawerVisible" size="300px">
  </el-drawer>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import mittBus from '@/utils/mittBus'

// 打开主题设置
const drawerVisible = ref(false)
mittBus.on('openThemeDrawer', () => {
  drawerVisible.value = true
})
</script>

这样就实现了跨组件的事件通信。下面我们先来实现暗黑模式。

实现暗黑模式

首先,这个操作属于系统操作,我们需要把操作的数据保存在pinia中去。

src/store/modules/settings.ts中,创建一个useSettingsStore

import { defineStore } from 'pinia'
import { DEFAULT_PRIMARY } from '@/config/config'
import type { SettingsState, ThemeConfigProps } from './model/settingsModel'
export const useSettingsStore = defineStore({
  id: 'app-settings',
  state: (): SettingsState => ({
    collapse: false,
    refresh: false, // 刷新页面
    themeConfig: {
      primary: DEFAULT_PRIMARY,
      isDark: false,
    },
  }),

  actions: {
    changeCollapse() {
      this.collapse = !this.collapse
    },
    setRefresh() {
      this.refresh = !this.refresh
    },
    setThemeConfig(themeConfig: ThemeConfigProps) {
      this.themeConfig = themeConfig
    },
  },
  persist: true,
})

其中,themeConfig就是我们的系统设置需要的字段,这里我们把主题切换也放在一块了。暗黑模式通过isDark字段来控制。

ThemeDrawer组件中,我们引入了一个SwitchDark组件,用来控制暗黑模式的开启关闭。在SwitchDark中,是这样实现的:

<template>
  <el-switch
    v-model="themeConfig.isDark"
    @change="onAddDarkChange"
    inline-prompt
    :active-icon="Sunny"
    :inactive-icon="Moon"
  />
</template>

<script setup lang="ts" name="SwitchDark">
import { computed } from 'vue'
import { useSettingsStore } from '@/store/modules/settings'
import { Sunny, Moon } from '@element-plus/icons-vue'
import { useTheme } from '@/hooks/useTheme'
const settingsStore = useSettingsStore()

const { switchDark } = useTheme()

const themeConfig = computed(() => settingsStore.themeConfig)

const onAddDarkChange = () => {
  switchDark()
}
</script>

可以看到,这里有个切换的时候调用了switchDark这个方法,这里其实是个hooks的封装。我们把主题切换和暗黑模式的切换都统一封装到了useTheme这个hooks里面。

useTheme里面这样实现:

import { computed } from 'vue'
import { useSettingsStore } from '@/store/modules/settings'
import { DEFAULT_PRIMARY } from '../config/config'
import { ElMessage } from 'element-plus'
import { getLightColor, getDarkColor } from '@/utils/color'

export const useTheme = () => {
  const settingsStore = useSettingsStore()
  const themeConfig = computed(() => settingsStore.themeConfig)

  // 切换暗黑模式
  const switchDark = () => {
    const body = document.documentElement as HTMLElement
    if (themeConfig.value.isDark) body.setAttribute('class', 'dark')
    else body.setAttribute('class', '')
  }


  // 初始化主题
  const initTheme = () => {
    switchDark()
  }
  return {
    initTheme,
    switchDark,
  }
}

可以看到,当我们开启关闭暗黑模式的时候,其实就是添加了一个class属性dark,这个dark是加在了html元素上面,不要被上面的body误导了,因为document.documentElement获取的是html元素。在控制台也能看到我们dark是加在了html元素上面的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

那么既然有了这个dark,我们是不是可以通过另外一套暗黑模式的样式,覆盖之前的样式呢?事实上,element-plus也是这样做的,它给我们提供了一套完整的暗黑模式的主题色。具体可参考Element-plus暗黑模式介绍

那么接下来我们就需要在项目中引入这套主题。

src/styles下面新建文件theme.scss,这里面我们把element-plus的默认暗黑主题引入进来

/** element内置黑暗主题 */
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

然后把theme.scsssrc/styles/index.scss文件中引入即可:

@import './reset';
@import './theme';
@import './element';

这里注意我还引入了样式重置和element的重置样式。

最后我们把index.scssmain.ts中引入即可。

// element默认主题
import 'element-plus/dist/index.css'
// 公共样式,包含自定义暗黑模式,element重置样式
import '@/styles/index.scss'

这里注意element的默认主题和index.scss的位置。如果不出意外,你页面现在的样式就可以使用暗黑模式了。但是还有一些页面你自定义的页面元素样式,比如你把某个背景色写死了,或者某个文字颜色写死了,当你切换了暗黑模式的时候,它在element-plus中找不到暗黑模式的样式,那怎么办呢?当然是自己写一份自定义的暗黑模式主题了。同样我们在theme.scss中书写样式:

/** element内置黑暗主题 */
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

/** 自定义黑暗主题 */
html.dark {
  // * admin
  --v-bg-color: #141414;
  --v-main-bg-color: #0d0d0d;
  --v-border-light: 1px solid #4c4c4d;

  // scroll-bar
  ::-webkit-scrollbar {
    background-color: var(--el-scrollbar-bg-color) !important;
  }

  ::-webkit-scrollbar-thumb {
    background-color: var(--el-border-color-darker) !important;
  }

  .layout-sidebar-container {
    background-color: var(--v-bg-color) !important;
  }

  .app-main-container {
    background-color: var(--v-main-bg-color) !important;

    .card {
      background-color: var(--v-bg-color) !important;
    }
  }

  .layout-footer-container {
    color: var(--el-text-color-regular) !important;
    background-color: var(--v-main-bg-color) !important;
  }

  .fold-unfold {
    color: #fff !important;
  }

  .el-menu,
  .el-sub-menu,
  .el-menu-item,
  .el-sub-menu__title {
    background-color: var(--v-bg-color) !important;

    &:not(.is-active) {
      color: #bdbdc0 !important;
    }

    &.is-active {
      color: #fff !important;
      background-color: #000 !important;
    }
  }

  .el-menu-item:not(.is-active):hover {
    background-color: var(--v-main-bg-color) !important;
  }

  .nav-bar-container {
    background-color: var(--v-bg-color) !important;
  }

  .table-header {
    .el-icon {
      color: #fff;
    }
  }

  .tabs-bar-container {
    background-color: var(--v-bg-color) !important;
    border-top: 1px solid var(--el-border-color-light) !important;

    .tabs-action {
      .el-icon {
        color: #fff !important;
      }
    }

    .fold-unfold {
      color: #fff;
    }

    .el-tabs__item.is-active {
      color: var(--el-color-primary);
      background-color: var(--v-main-bg-color) !important;

      .el-icon {
        color: var(--el-color-primary) !important;
      }
    }

    .el-tabs__item:not(.is_active):hover {
      background-color: var(--v-main-bg-color) !important;

      .el-icon {
        color: var(--el-color-primary) !important;
      }
    }
  }
}

上面是我自定义的暗黑模式下的一些主题样式,注意,所有的样式必须写在html.dark中,如果样式权重不够的直接加个!important。到此,我们的暗黑模式就实现了。我们接着来实现主题的切换。

实现主题切换

首先我们来看看主题切换实现的思路。在element-plus中,有一套基础样式的css变量:

--el-color-primary: #409eff;

它的默认主题都是基于这个变量去做的。比如el-button的默认主题色是这样设置的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

那么我们就是不是只要修改--el-color-primary的值就可以了?思路就是这么简单。接下来我们来设置一下这个值。

element-plus为我们提供了一个el-color-picker组件,利用这个组件我们随意的更改颜色。在src/layouts/NavBar/components/ThemeDrawer/index.vue文件中,我们利用这个组件切换--el-color-primary的值:

<template>
  <el-drawer title="主题设置" v-model="drawerVisible" size="300px">
    <el-divider class="divider" content-position="center">全局主题</el-divider>
    <div class="theme-item">
      <span>主题颜色</span>
      <el-color-picker
        v-model="themeConfig.primary"
        :predefine="colorList"
        @change="changePrimary"
      />
    </div>
    <div class="theme-item">
      <span>暗黑模式</span>
      <SwitchDark />
    </div>
  </el-drawer>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import mittBus from '@/utils/mittBus'
import { DEFAULT_PRIMARY } from '@/config/config'
import { useSettingsStore } from '@/store/modules/settings'
import { useTheme } from '@/hooks/useTheme'
const { changePrimary } = useTheme()

// 预定义主题颜色
const colorList = [
  DEFAULT_PRIMARY,
  '#DAA96E',
  '#0C819F',
  '#722ed1',
  '#27ae60',
  '#ff5c93',
  '#e74c3c',
  '#fd726d',
  '#f39c12',
  '#9b59b6',
]

const settingsStore = useSettingsStore()
const themeConfig = computed(() => settingsStore.themeConfig)

// 打开主题设置
const drawerVisible = ref(false)
mittBus.on('openThemeDrawer', () => {
  drawerVisible.value = true
})
</script>

<style scoped lang="scss">
.theme-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 14px 0;
}
</style>

这里我们提供一套默认的主题色colorList,样子长下面这样:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

发生change事件的时候,我们就能拿到修改的颜色。然后通过changePrimary这个方法去修改--el-color-primary的值。

同样的,这个方法我们封装在useTheme这个hooks当中了。

import { computed } from 'vue'
import { useSettingsStore } from '@/store/modules/settings'
import { DEFAULT_PRIMARY } from '../config/config'
import { ElMessage } from 'element-plus'

export const useTheme = () => {
  const settingsStore = useSettingsStore()
  const themeConfig = computed(() => settingsStore.themeConfig)

  // 切换暗黑模式
  const switchDark = () => {
    const body = document.documentElement as HTMLElement
    if (themeConfig.value.isDark) body.setAttribute('class', 'dark')
    else body.setAttribute('class', '')
    changePrimary(themeConfig.value.primary)
  }

  // 修改主题颜色
  const changePrimary = (val: string | null) => {
    if (!val) {
      val = DEFAULT_PRIMARY
      ElMessage({
        type: 'success',
        message: `主题颜色已重置为 ${DEFAULT_PRIMARY}`,
      })
    }
    settingsStore.setThemeConfig({ ...themeConfig.value, primary: val })
    document.documentElement.style.setProperty(
      '--el-color-primary',
      themeConfig.value.primary,
    )
  }

  // 初始化主题
  const initTheme = () => {
    switchDark()
    changePrimary(themeConfig.value.primary)
  }
  return {
    initTheme,
    switchDark,
    changePrimary,
  }
}

当触发changePrimary事件修改颜色的时候,拿到传过来的val值(十六进制颜色值),第一步判断如果是空值,就设置为默认的主题色DEFAULT_PRIMARY,第二步,val不为空,把这个主题色存储到pinia中。第三步,通过setProperty修改--el-color-primary的值。现在我们去页面看看效果。

当我们修改主题的时候已经能够改变默认颜色了:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

但是发现一个问题,当我们鼠标滑过按钮的时候,它应该会有一个颜色变浅的样式出来,发现跟我们的主题色不对付啊:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

这是为什么呢?原来当鼠标滑过的时候,它又是另一套主题:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

button的样式也可以看出来,它在各种状态下使用css变量是不一样的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

而我们只设置了--el-color-primary的值:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

那我们还需要设置--el-color-primary-light-{1-9}的值。

changePrimary方法中,我们用一个循环来设置--el-color-primary-light-{1-9}的值:

// 颜色加深或变浅
for (let i = 1; i <= 9; i++) {
  document.documentElement.style.setProperty(
    `--el-color-primary-light-${i}`,
    themeConfig.value.isDark
      ? `${getDarkColor(themeConfig.value.primary, i / 10)}`
      : `${getLightColor(themeConfig.value.primary, i / 10)}`,
  )
}

同样的,也是利用setProperty来设置--el-color-primary-light-{1-9}的值,这里暗黑模式下和非暗黑模式下计算方式有所区别。

接下来,最重要的就是怎么计算--el-color-primary-light-{1-9}每个值的颜色了,因为它是十六进制的。

我们先看看getLightColor函数做了什么事情。

getLightColor函数封装在了src/utils/color.ts这个文件下面:

export function getLightColor(color: string, level: number) {
  const reg = /^#?[0-9A-Fa-f]{6}$/
  if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值')
  const rgb = hexToRgb(color)

  for (let i = 0; i < 3; i++)
    rgb[i] = Math.round(255 * level + rgb[i] * (1 - level))

  return rgbToHex(rgb[0], rgb[1], rgb[2])
}

它接受两个值color, level,分别表示16进制和等级,我们打印一下就知道了:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

可以看到这个值我们选择的颜色值:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

getLightColor函数中,首先会判断color值是否合法,也就是是不是十六进制的。

然后会调用hexToRgb函数,我们再看看这个函数干了啥?

hexToRgb函数中,接受的是我们传过来的color值,也就是上面我们看到的#409EFF

export function hexToRgb(str: any) {
  let hexs: any = ''
  const reg = /^#?[0-9A-Fa-f]{6}$/
  if (!reg.test(str)) return ElMessage.warning('输入错误的hex')
  str = str.replace('#', '')

  hexs = str.match(/../g)

  for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16)
  return hexs
}

同样的,它也会校验是不是一个十六进制的值。然后通过repalce#去掉,str的值就会变成409EFF了。下面一行会把这个十六进制按照两位分割成一个数组:

hexs = str.match(/../g)

解释一下这行代码的意思:通过match匹配任意两个字符最终返回一个数组,看下面执行结果:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

hexs最终的结果为这样的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

接来来会循环这个数组,然后把里面的每一项都变成十进制的整数。

for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16)

这里解释一下parseInt这个方法,它的作用是解析一个字符串并返回指定基数的十进制整数,比如说parseInt('9E', 16)的意思就是把16进制的9E转化成一个十进制的整数。

这样,经过parseInt转化,hexs的值就变成下面这样了:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

经过hexToRgb函数一顿操作,最终我们把十六进制的409EFF转化成了十进制的数组[64, 158, 255]

我们回到getLightColor函数继续往下看。rgb现在的值是[64, 158, 255],然后经过下面循环处理:

for (let i = 0; i < 3; i++) {
  rgb[i] = Math.round(255 * level + rgb[i] * (1 - level))
}

rgb就会变成这个样子了:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

我们拆开来看,

rgb[0] = Math.round(255 * 0.1 + 64 * (1 - 0.1)) = 83
rgb[1] = Math.round(255 * 0.1 + 158 * (1 - 0.1)) = 168
rgb[2] = Math.round(255 * 0.1 + 255 * (1 - 0.1)) = 255

刚好跟我打印的结果是一一对应的。其中的Math.round() 函数返回一个数字四舍五入后最接近的整数。

同样的,当level=0.2~0.9的时候,得到的结果是这样的:

rgb[2] = [102, 177, 255]
rgb[3] = [121, 187, 255]
rgb[4] = [140, 197, 255]
rgb[5] = [160, 207, 255]
rgb[6] = [179, 216, 255]
rgb[7] = [198, 226, 255]
rgb[8] = [217, 236, 255]
rgb[9] = [236, 245, 255]

经过这么一计算,得到一组其实由深到浅的色系。

得到这个结果以后,再次经过rgbToHex函数进行一个转化,我们继续看看rgbToHex干了啥?

export function rgbToHex(r: any, g: any, b: any) {
  const reg = /^\d{1,3}$/
  if (!reg.test(r) || !reg.test(g) || !reg.test(b))
    return ElMessage.warning('输入错误的rgb颜色值')
  const hexs = [r.toString(16), g.toString(16), b.toString(16)]

  for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`
  return `#${hexs.join('')}`
}

rgbToHex函数中,接受三个参数: r,g,b,其实就是红、绿、蓝三色。首先同样的会检测颜色值是否合法。然后通过toString方法把十进制的数字转化成十六进制的字符串。

注意:这里toString方法其实是Number.prototype.toString([radix]),它会回指定Number对象的字符串表示形式,其中adix指定要用于数字到字符串的转换的进制数 (从 2 到 36)。如果未指定 radix 参数,则默认值为 10。

我们上面的传进来的83,168,255经过转化以后,得到的是一个驻足hexs=['53', 'a8', 'ff']

继续往下执行代码:

for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`

这一步是为了防止有一位数的情况出现,如果只有一位,前面用0补上。

最后return得时候把这个hexs数组转化成了六位数十六进制的格式,也就是#53a8ff,最终我们回到了src/hooks/useTheme.ts这个文件来,经过第一次循环,getLightColor(themeConfig.value.primary, 1 / 10)得到的值是#53a8ff,也就是说,--el-color-primary-light-1的值现在是#53a8ff。同样的,执行了9次循环,后面--el-color-primary-{2-9}的值都会得到对应的十六进制六位数颜色值。最终在控制台看到的效果就是这样的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

可以看到,颜色值是越来越浅。这其实也是根据颜色系统的设置来的,算是一个颜色系列。

最后回顾一下整个计算过程:

  • 先拿到十六进制的六位数颜色值
  • 通过hexToRgb函数转化成rgb十进制数组
  • 通过Math.roundlevel四舍五入计算得到新的一组rgb十进制数组(这里也是计算最为关键的地方)
  • 最后,通过rgbToHex函数把十进制的rgb数组转化成一个十六进制的六位数颜色值
  • 把这个颜色赋值给--el-color-primary-light-{1-9}

最后还有暗黑模式下主题计算方式,其实就是在

for (let i = 0; i < 3; i++) {
    rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level))
}

这一步,计算的时候把255换成了20.5,色系偏暗了一点。其他的逻辑都是一模一样的。最终得到的一套色系是这样的:

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

这里给大家推荐一个字节配色工具,可以在上面设置一个主题色,就可以得到自己喜欢的色系了

vue3 admin 保姆教学指南|element-plus如何实现主题切换和暗黑模式

到此,我们的暗黑模式跟主题切换的功能实现完毕。

代码地址

gitee.com/guigu-fe/gu…

文章教程系列

转载自:https://juejin.cn/post/7215485221830852665
评论
请登录