Skip to content
On this page

2022-11-ElementPlus实现动态换肤

1. 需求场景

实现ElementPlus组件库的动态换肤。

2. 分析思路

ElementPlus的颜色提供了很多变量的定义,比如 https://unpkg.com/element-plus@2.1.11/dist/index.css

tsx
:root {
    color-scheme:light;
    --el-color-white:#ffffff;
    --el-color-black:#000000;
    --el-color-primary:#409eff;
    --el-color-primary-light-3:#79bbff;
    --el-color-primary-light-5:#a0cfff;
    --el-color-primary-light-7:#c6e2ff;
    --el-color-primary-light-8:#d9ecff;
    --el-color-primary-light-9:#ecf5ff;
    --el-color-primary-dark-2:#337ecc;
  	...
}

我们换肤暂时以primaryColor为例,其实如果主颜色换了,组件的大部分颜色也都换掉了。 我们可以拿到所有的css变量,根据初始的颜色,定位到需要替换的primaryColor的属性名,然后再把新的颜色替换上去,把这些css变量以style标签的形式放到html的head中。

3. 实现颜色选择器组件

tsx
<el-dialog
  v-model="dialogVisible"
  title="Tips"
  width="30%"
  @close="handleClose"
>
  <el-color-picker v-model="color1" />
  <template #footer>
    <span class="dialog-footer">
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary" @click="handleConfirm">保存</el-button>
    </span>
  </template>
</el-dialog>
tsx
import { ref } from "vue";
import { generateNewStyles, writeNewStyles } from "@/utils/theme";
const dialogVisible = ref(false);
const color1 = ref("#7975ff");

const handleClose = () => {
  dialogVisible.value = false;
};

const handleConfirm = async () => {
  // 改变后的颜色值
  console.log(color1.value);
  handleClose();
};

4. 实现动态换肤

分两步

  • 根据新的颜色值,生成style
  • 把style放到head中
tsx
const handleConfirm = async () => {
  // 生成style
  const newStyles = await generateNewStyles(color1.value);
  // 放进head
  writeNewStyles(newStyles);
  handleClose();
};
tsx
// 生成新的样式表
export const generateNewStyles = async (primaryColor: string) => {
  // 根据主色获取色值表
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  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;
};
tsx
// 根据主色获取色值表
export const generateColors = (primaryColor: string | null) => {
  if (!primaryColor) {
    return;
  }

  const colors: { [k: string]: unknown } = {
    primary: primaryColor,
  };

  Object.keys(formula).forEach((key) => {
    const value = formula[key as keyof typeof formula].replace(
      /primary/g,
      primaryColor
    );
    colors[key] = "#" + rgbHex(cssColorFunction.convert(value));
  });

  return colors;
};
tsx
// formula
const formula = {
  "shade-1": "color(primary shade(10%))",
  "light-1": "color(primary tint(10%))",
  "light-2": "color(primary tint(20%))",
  "light-3": "color(primary tint(30%))",
  "light-4": "color(primary tint(40%))",
  "light-5": "color(primary tint(50%))",
  "light-6": "color(primary tint(60%))",
  "light-7": "color(primary tint(70%))",
  "light-8": "color(primary tint(80%))",
  "light-9": "color(primary tint(90%))",
  "dark-2": "color(primary tint(98%))",
  subMenuHover: "color(primary tint(70%))",
  subMenuBg: "color(primary tint(80%))",
  menuHover: "color(primary tint(90%))",
  menuBg: "color(primary)",
};

export default formula;

generateColors最终生成的结果是

tsx
{
  "primary": "#FF7585",
  "shade-1": "#e66978",
  "light-1": "#ff8391",
  "light-2": "#ff919d",
  "light-3": "#ff9eaa",
  "light-4": "#ffacb6",
  "light-5": "#ffbac2",
  "light-6": "#ffc8ce",
  "light-7": "#ffd6da",
  "light-8": "#ffe3e7",
  "light-9": "#fff1f3",
  "dark-2": "#fffcfd",
  "subMenuHover": "#ffd6da",
  "subMenuBg": "#ffe3e7",
  "menuHover": "#fff1f3",
  "menuBg": "#ff7585"
}

这个函数需要把color(primary tint(90%))转换成#fff1f3,所以需要两个库

  • rgb-hex
  • css-color-function
tsx
colors[key] = "#" + rgbHex(cssColorFunction.convert(value));

获取当前 Element Plus 的默认样式表

tsx
const getOriginalStyle = async () => {
  // https://unpkg.com/element-plus@2.1.11/dist/index.css
  const version = "2.1.11";
  const url = `https://unpkg.com/element-plus@${version}/dist/index.css`;
  const { data } = await axios(url);

  return getStyleTemplate(data);
};

替换

tsx
Object.keys(colors).forEach((key) => {
  cssText = cssText.replace(
    new RegExp("(:|\\s+)" + key, "g"),
    "$1" + colors[key]
  );
});

5. 解决刷新页面问题

把颜色存到localstorage,app.vue中做处理

vue
<script lang="ts" setup>
  import { ref } from "vue";
  import { generateNewStyles, writeNewStyles } from "@/utils/theme";
  const dialogVisible = ref(false);
  const color1 = ref("#7975ff");

  const handleClose = () => {
    dialogVisible.value = false;
  };

  const primaryColor = localStorage.getItem("primaryColor");
  if (primaryColor) {
    generateNewStyles(primaryColor).then((newStyles) => {
      writeNewStyles(newStyles);
    });
  }

  const handleConfirm = async () => {
    const newStyles = await generateNewStyles(color1.value);
    writeNewStyles(newStyles);
    // themeStore.setMainColor(mainColor.value);
    localStorage.setItem("primaryColor", color1.value);
    handleClose();
  };
</script>

Released under the MIT License.