Vue3 + Element-Plus + TypeScript 动态更换主题色简单实现(2023)

14,593 阅读3分钟

一、思路分析

  1. 目前根据官方网站推荐的做法,最佳实践就是直接覆盖css变量。
    // element-plus 官网指南 
    
    // document.documentElement 是全局变量时
    const el = document.documentElement
    
    // const el = document.getElementById('xxx')
    
    // 获取 css 变量
    getComputedStyle(el).getPropertyValue(`--el-color-primary`)
    
    // 设置 css 变量
    el.style['--el-color-primary'] = 'red'
    
  2. 打开elementPlus官网 -> F12 ->查看元素样式。点击根元素也就是html,你会看到官方定义了很多的全局css变量,下图中这些变量就是主题色的css变量,我们要做的就是覆盖这些变量的值

image.png
但是问题来了,你会发现这些颜色都有色阶,要如何根据主色去计算出这些色阶颜色?

二、Sass mix函数JS翻译(最重要的一部分)

查看element-Plus样式源码 element-plus/theme-chalk/src/common/var.scss.
你会发现组件的颜色基本都基于这些全局的css变量去生成的,这就是为什么要覆盖css变量的原因。
核心代码如下,这段 scss 就是生成 --el-color-primari-light-3 等色阶颜色的。可以看到这里使用了一个sass颜色函数mix,这个函数是如何实现的暂时不管,先用js翻译下面代码。

@mixin set-color-type-light($type, $number) {
  $colors: map.deep-merge(
    (
      $type: (
        'light-#{$number}':
          mix(
            $color-white,
            map.get($colors, $type, 'base'),
            math.percentage(math.div($number, 10))
          ),
      ),
    ),
    $colors
  ) !global;
}

// $colors.primary.light-i
// --el-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff

@for $i from 1 through 9 {
  @each $type in $types {
    @include set-color-type-light($type, $i);
  }
}

翻译后:

      const node = document.documentElement;
      // 前缀
      const pre = "--el-color-primary"; 
      // 主色调(可以使用el-color-picker绑定)
      const color = "#409eff";
      // 源码中的$color-white,也就是白色
      const mixWhite = "#ffffff"
      // 直接为根设置内联样式覆盖:root选择器的样式
      node.style.setProperty(pre, color);
      for (let i = 1; i < 10; i += 1) {
        // 同理
        node.style.setProperty(`${pre}-light-${i}`, mix(color, mixWhite, i * 0.1));
      }

mix函数原理:mix(color1 = rgb(r1,g1,b1),color2 = rgb(r2,g2,b2),weight)
参数一二为颜色,参数3位权重百分比,权重就是颜色中每个数值所占的比重

  r = (r1 * (1 - weight) + r2 * weight);
  g = (g1 * (1 - weight) + g2 * weight);
  b = (b1 * (1 - weight) + b2 * weight);
  finalColor = rgb(r,g,b);

但是问题在于,js 中显然没法直接使用 rgb 颜色,所以要先将16进制颜色先转化成 rgb ,再计算,最后还要将得出的 rgb 颜色转化成16进制颜色(不转直接使用字符串的 rgb 也可以)。

const mix = (color1: string, color2: string, weight: number) => {
  weight = Math.max(Math.min(Number(weight), 1), 0)
  const r1 = parseInt(color1.substring(1, 3), 16)
  const g1 = parseInt(color1.substring(3, 5), 16)
  const b1 = parseInt(color1.substring(5, 7), 16)
  const r2 = parseInt(color2.substring(1, 3), 16)
  const g2 = parseInt(color2.substring(3, 5), 16)
  const b2 = parseInt(color2.substring(5, 7), 16)
  const r = Math.round(r1 * (1 - weight) + r2 * weight)
  const g = Math.round(g1 * (1 - weight) + g2 * weight)
  const b = Math.round(b1 * (1 - weight) + b2 * weight)
  const _r = ('0' + (r || 0).toString(16)).slice(-2)
  const _g = ('0' + (g || 0).toString(16)).slice(-2)
  const _b = ('0' + (b || 0).toString(16)).slice(-2)
  return '#' + _r + _g + _b
}

三、在你的项目中使用

1.安装依赖

npm i use-element-plus-theme
// 或者使用其他包管理器
pnpm i use-element-plus-theme

2.引入函数

import { useElementPlusTheme } from "use-element-plus-theme"

3.简单使用

const defaultTheme = ref('#405DFF')

const { changeTheme } = useElementPlusTheme(defaultTheme.value)

4.完整使用例子

<template>
  <el-color-picker v-model="defaultTheme" @change="changeTheme" />
</template>

<script lang="ts" setup>
import { useElementPlusTheme } from 'use-element-plus-theme'

const defaultTheme = ref('#405DFF')

const { changeTheme } = useElementPlusTheme(defaultTheme.value)
</script>

四、源码以及预览

源码地址

五、更新日志

2023-8-26

  1. 升级vite版本,修复打包后的文件后缀
  2. 仅生成3、6、7、8、9色阶的颜色

2022-8-20
element-plus 现在已经发布正式版本,重写了这篇文章,将核心逻辑封装成hook并发布npm


不一定是最完美的解决方式,但也是一种思路

本帖未经允许禁止转载