【vue3】:主题色解决方案

2022/7/10 方案vue3element-plus

# # 主题色修改

# # 原理

修改主题色关键点是 色值不能写死。

在该解决方案中,主要涉及的是 element-plus 以及 非 element-plus 的主题色修改。


对于非 element-plus 解决方法如下:

1、全局定义 scss 变量,页面绑定该变量,通过修改该变量值,从而使页面主题色进行修改。

2、并且该变量要实现响应式,永久保存(避免页面刷新色值被覆盖)。因此需要结合本地缓存以及 vuex 将值保存好。

3、初始时通过 getters 获取主题色的同时,使用 generateColors 函数生成新的色值表,与 scss 色值表 进行匹配替换。


对于 element-plus 解决方案如下:

1、获取当前 element-plus 的所有样式。

2、找到要替换的样式(关键)。

3、把替换后的样式利用 style 的优先级高于外部引入样式的方式加载样式表。



# # 非 element-plus

第一步:定义 scss 变量文件,结合 vuex 将其保存到本地缓存中

// variables.scss
$menuBg: #304156;
:export {
  menuBg: $menuBg;
}
1
2
3
4
5
// module/theme.js
import variable from "@/styles/variables.scss";
export default {
  namespaced: true,
  state: () => ({
    //  (非 element-plus)
    variable: variable,
    // (element-plus)
    mainColor: getItem("mainColor") || "#304156",
  }),
  setMainColor(state, newColor) {
    //  (非 element-plus)
    state.variable.menuBg = newColor;
    // (element-plus)
    state.mainColor = newColor;
    // 保存到缓存中
    setItem("mainColor", newColor);
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注:第 8 行 以及 第 15 行代码在修改 非 element-plus 主题色中没有任何作用,设置在这里目的在于避免后面讲修改 element-plus 在来设置显的很突兀。

// index.js
import { createStore } from "vuex";
import theme from "./modules/theme.js";
import getters from "./getters";
export default createStore({
  getters,
  modules: {
    theme,
  },
});
1
2
3
4
5
6
7
8
9
10

getItemsetItem分别为通过 key 获取和保存在本地缓存的数据,需自行封装。


第二步:修改主题色

使用 element-plus 中的取色器

这一步操作放在最后(小节-最后补充),结合 element-plus 修改主题色在一起写。


第三步:页面初次打开,以及再次打开时,生成新的色值表,并渲染缓存中的颜色

// getters.js
const getters = {
    cssVar: state => ({
        ...state.theme.variable,
        ...generateColors(getItem('mainColor'))
    })
    // (element-plus)
    mainColor: state => state.theme.mainColor
}
1
2
3
4
5
6
7
8
9

解释:

(1)这里是根据缓存中的主题色,生成新的色值表(generateColors),在跟 scss 变量文件进行比较替换。

(2)generateColors 具体是如何生成新的色值,请看 element-plus 修改主题色的方案 中具体会涉及到,这里简单一笔带过。

(3)同样 第 8 行代码在这里也没有太大作用。


类似于最终要呈现出来的效果为:

cssVar: (state) => ({
  menuBg: "#304156",
  menuBg: "red",
});
1
2
3
4

最终通过替换,menuBg 的值为 red


页面绑定:

<el-menu :background-color="$store.getters.cssVar.menuBg"></el-menu>
1


# # element-plus

第一步:如何获取到 element-plus 的全局 css 样式表

// 获取当前项目使用 element-plus 的版本号
const version = require("element-plus/package.json").version;
const url = `https://unpkg.com/element-plus@${version}/dist/index.css`;
const { data: style } = await axios(url);
1
2
3
4

此时:style 就是 element-plus 的全局 css 样式


第二步:给需要修改颜色打上标记,以便后面,根据标记生成新的色值


类似于最终要呈现出来的效果为:

const colorList = {
  "#409eff": "menuBg",
};
1
2
3

此时,通过遍历 colorList,去匹配 style 中是否存在 #409eff 这个色值,如若存在,则将其替换成字符串 primary

const style = {
  "background-color": "menuBg",
};
1
2
3

colorList这个变量需自己定义哪些色值对应什么标志


代码:

const colorList = {
  "#409eff": "primary",
  "#53a8ff": "light-1",
};
Object.keys(colorList).forEach((key) => {
  const value = colorMap[key];
  // 忽略大小写,全局匹配
  style = style.replace(new RegExp(key, "ig"), value);
});
1
2
3
4
5
6
7
8
9

此时:style 是已经被打上标记


第三步:根据主题色自定义每个标记生成相对应的色值,并且将 style 进行替换


类似于最终要呈现出来的效果为:

// 当前主题色
const mainColor = "#ffff00";
const style = {
  "background-color": "primary",
};

// 标记生成相对应的色值
const formula = {
  // 假设 aaa() 生成 颜色为 #ffff01
  primary: aaa(mainColor),
};

// 进行匹配后
const style = {
  "background-color": "#ffff01",
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这里 aaa() 需要使用到一下两个插件

css-color-function: 基于某种颜色,向该颜色中添加多少比例的黑或白(类似于调色)

rgbHex: 将 rgba 转成 16 进制

代码:

(1)定义每个标记相对应的色值

// formula.json
{
  "shade-1": "color(primary shade(10%))", // primary 中掺杂 10% 的 黑
  "light-1": "color(primary tint(10%))", //  primary 中掺杂 10% 的 白
  "menuBg": "color(primary)" // 正常
}
1
2
3
4
5
6

(2)生成色值表,并对 style 进行替换

// 导入 每个标记相对应的色值
import formula from "@/constant/formula.json";

import color from "css-color-function";
import rgbHex from "rgb-hex";

generateColors = (primary) => {
  // 新色值表
  const newColor = {
    primary: primary,
  };
  Object.keys(formula).forEach((key) => {
    const value = formula[key].replace(/primary/g, primary);
    newColor[key] = "#" + rgbHex(color.convert(value));
  });
  return newColor;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注解:

(1)在 generateColors 中 primary 为当前的主题色,可以通过 store.getters.mainColor获取

(2)formula[key].replace(/primary/g, primary)的目的是将 color(primary shade(10%)) 替换成 color(red, shade(10%))

(3)color.convert(value)的目的是生成新的色值

newColor = {
  "shade-1": "#f45334",
  "light-1": "#584f47",
  menuBg: "#ff0000",
};
1
2
3
4
5

将 style 进行替换

Object.keys(newColor).forEach((key) => {
  style = style.replace(
    new RegExp("(:|\\s+)" + key, "g"),
    "$1" + newColor[key]
  );
});
1
2
3
4
5
6

提示:

替换前 style 的格式为

const style = {
  "background-color": "menuBg",
};
1
2
3

newColor 的格式为

newColor = {
  "shade-1": "#f45334",
  "light-1": "#584f47",
  menuBg: "#ff0000",
};
1
2
3
4
5

替换后:

const style = {
  "background-color": "#ff0000",
};
1
2
3

第四步:将替换后的样式利用 style 的优先级高于外部引入样式的方式加载样式表

const style = document.createElement("style");
// 第二个 style 为 css
style.innerText = style;
document.head.appendChild(style);
1
2
3
4

第五步:页面初次打开,以及再次打开时,生成新的色值表,并渲染缓存中的颜色

在最顶层页面中进行操作,即在 App.vue 中

<script setup>
  import { useStore } from "vuex";
  const store = useStore();
  // 取到主题色
  let mainColor = store.getters.mainColor;
  // .....
</script>
1
2
3
4
5
6
7

在 App.vue 中取到主题色后,结合前面所提到的四点封装成函数后,进行调用即可。



# # 最后补充

1、将修改 element-plus 主题色封装成函数

// utils/theme.js
import color from "css-color-function";
import rgbHex from "rgb-hex";
import formula from "@/constant/formula.json";
import axios from "axios";
// 根据主色值,生成最新的样式表
export const generteNewStyle = async (primaryColor) => {
  // 根据主色生产色值表
  const colors = generateColors(primaryColor);
  // 获取当前 element-plus 的默认样式表 并且把需要替换的色值打上标记
  let cssText = await getOriginalStyle();
  // 遍历生成的色值表 在 默认样式表 中进行替换
  Object.keys(colors).forEach((key) => {
    cssText = cssText.replace(
      new RegExp("(:|\\s+)" + key, "g"),
      "$1" + colors[key]
    );
  });
  return cssText;
};
// 生成色值表
export const generateColors = (primary) => {
  if (!primary) return;
  const colors = {
    primary: primary,
  };
  Object.keys(formula).forEach((key) => {
    const value = formula[key].replace(/primary/g, primary);
    console.log(value);
    colors[key] = "#" + rgbHex(color.convert(value));
  });
  console.log(colors);
  return colors;
};
// 把生成最新的样式表写入新样式到 style 标签中
export const writeNewStyle = (newStyle) => {
  const style = document.createElement("style");
  style.innerText = newStyle;
  document.head.appendChild(style);
};
// 获取原始 element-plus 样式
const getOriginalStyle = async () => {
  const version = require("element-plus/package.json").version;
  const url = `https://unpkg.com/element-plus@${version}/dist/index.css`;
  const { data } = await axios(url);
  return getStyleTemplate(data);
};
// 需要替换的色值打上标记
const getStyleTemplate = (data) => {
  // 自行定义
  const colorMap = {
    "#409eff": "primary",
    "#337ecc": "primary",
    "#53a8ff": "light-1",
  };
  Object.keys(colorMap).forEach((key) => {
    const value = colorMap[key];
    data = data.replace(new RegExp(key, "ig"), value);
  });
  return data;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

(2)在 App.vue 中使用封装后的函数

<script setup>
  import { useStore } from "vuex";
  import { generteNewStyle, writeNewStyle } from "@/utils/theme.js";
  const store = useStore();

  // 取到主题色
  let mainColor = store.getters.mainColor;

  generteNewStyle(mainColor).then((newStyle) => {
    writeNewStyle(newStyle);
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12

(3) 修改主题色

import { useStore } from "vuex";
import { generteNewStyle, writeNewStyle } from "@/utils/theme.js";
const confirm = async () => {
  // 针对 element-plus
  const newStyle = await generteNewStyle(mColor.value);
  writeNewStyle(newStyle);
  // 针对 非 element-plus
  store.commit("theme/setMainColor", mColor.value);
};
1
2
3
4
5
6
7
8
9