Skip to content

更换主题

安装插件

bash

npm i css-color-function

npm i rgb-hex

npm i vite-plugin-require-transform
  • 然后再 vite.config.js 中配置
js
import { defineConfig } from "vite";
import requireTransform from "vite-plugin-require-transform";
export default defineConfig({
  plugins: [
    requireTransform({
      fileRegex: /.js$|.vue$|.json$/,
    }),
  ],
});

封装换肤组件

  • components/ColorPicker/index.vue

  • components/ThemeColor/index.vue

components/ColorPicker/index.vue

vue
<template>
  <el-dialog
    :modelValue="dialogVisible"
    width="500"
    align-center
    :title="titlemsg"
  >
    <div style="width: 32px; height: 32px; margin: 0 auto">
      <el-color-picker
        v-model="color"
        show-alpha
        :predefine="predefineColors"
        @change="changecolor"
      />
    </div>
  </el-dialog>
</template>

<script setup>
import { generateNewStyle, writeNewStyle } from "@/utils/theme";

import { useZhuTiStore } from "@/stores/ZhuTi.js";

const StoreModules = useZhuTiStore();

const Props = defineProps({
  // 弹出框显示
  dialogVisible: {
    type: Boolean,
  },
  type: {
    type: Number,
    required: true,
  },
  titlemsg: {
    type: String,
  },
});

watch(
  () => Props.type,
  (newval, oldval) => {
    if (newval != oldval) {
      colorinit();
    }
  }
);

// 结果
onMounted(() => {
  colorinit();
});

const color = ref("");

const predefineColors = ref([]);

const colorinit = () => {
  let resultcolor = "";
  let pickColor = [];
  if (Props.type === 1) {
    resultcolor = StoreModules.bgcolor;
    pickColor = [...StoreModules.bgcolorarr];
  } else if (Props.type === 2) {
    resultcolor = StoreModules.defaultTextColor;
    pickColor = [...StoreModules.defaultTextColorarr];
  } else if (Props.type === 3) {
    resultcolor = StoreModules.activeTextColor;
    pickColor = [...StoreModules.activeTextColorarr];
  }
  color.value = resultcolor;
  predefineColors.value = pickColor;
};

const changecolor = async () => {
  if (Props.type === 1) {
    // 1.1 获取主题色
    const newStyleText = await generateNewStyle(color.value, 1);
    // 1.2 写入最新主题色
    writeNewStyle(newStyleText);
    StoreModules.changebgColor(color.value);
  } else if (Props.type === 2) {
    // 1.1 获取文字颜色
    const newStyleText = await generateNewStyle(color.value, 2);
    // 1.2 写入文字主题色
    writeNewStyle(newStyleText);
    StoreModules.changetextColor(color.value);
  } else if (Props.type == 3) {
    // 1.1 获取当前颜色
    const newStyleText = await generateNewStyle(color.value, 3);
    // 1.2 写入最新当前颜色
    writeNewStyle(newStyleText);
    StoreModules.changeactivetextcolor(color.value);
  }
};
</script>

<style lang="scss" scoped></style>

components/ThemeColor/index.vue

  • 对应文字
bash

const en = {
    message: {
        hello: 'hello',
        themeChange: 'themeChange',
        textChange: 'textChange',
        activetextChange: 'activetextChange'
    }
};

export default en;

---------中文--------------

const cn = {
    message: {
        hello: '你好',
        themeChange: '主题更换',
        textChange: '文字更换',
        activetextChange: '选中更换'
    }
};

export default cn;

--------------ja.js-------------------

const ja = {
    message: {
        hello: '日本语hello',
        themeChange: 'themeChange',
        textChange: 'textChange',
        activetextChange: 'activetextChange'
    }
};

export default ja;
  • 组件代码
vue
<template>
  <el-dropdown>
    <span class="imgicon">
      <SvgIcon icon="change-theme" size="36" color="black"></SvgIcon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item @click="changelangtheme">
          {{ $t("message.themeChange") }}
        </el-dropdown-item>
        <el-dropdown-item @click="changelangText">
          {{ $t("message.textChange") }}
        </el-dropdown-item>
        <el-dropdown-item @click="changelangactiveText">
          {{ $t("message.activetextChange") }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>

  <!--展示弹层开始-->
  <ColorPicker
    v-model="dialogVisible"
    :type="typemodel"
    :title="titlemsg"
  ></ColorPicker>
  <!--展示弹层结束-->
</template>

<script setup>
import { useI18n } from "vue-i18n";

const { t } = useI18n();
// 是否显示弹框
const dialogVisible = ref(false);

const typemodel = ref(1); // 1 就是主题 2就是文字

const titlemsg = ref("");

const changelangtheme = () => {
  typemodel.value = 1;
  titlemsg.value = t("message.themeChange");
  // 展示弹框
  dialogVisible.value = true;
};

const changelangText = () => {
  typemodel.value = 2;
  titlemsg.value = t("message.textChange");
  dialogVisible.value = true;
};

const changelangactiveText = () => {
  typemodel.value = 3;
  titlemsg.value = t("message.activetextChange");
  dialogVisible.value = true;
};
</script>

<style lang="scss" scoped>
:deep(.el-tooltip__trigger:focus-visible) {
  outline: unset;
}
.el-dropdown {
  .imgicon {
    width: 30px;
    height: 30px;
    float: right;
    display: block;
    img {
      display: block;
      width: 100%;
      height: 100%;
    }
  }
}
</style>

封装 ZhuTi 模块

封装常量

  • src/constant/index.js
js
// token
export const TOKEN = "token";
// token 时间戳
export const TIME_STAMP = "timeStamp";
// 超时时长(毫秒) 180天
export const TOKEN_TIMEOUT_VALUE = 24 * 3600 * 1000 * 180;
// 国际化
export const LANG = "language";
// 主题色保存的 key
export const MAIN_COLOR = "mainColor";
// 默认色值
export const DEFAULT_COLOR = "#409eff";
// tags
export const TAGS_VIEW = "tagsView";

export const USERINFO = "userInfo";

export const ISCOLLAPSE = "isCollapse";

export const BGCOLORINDEX = "bgcolorIndex";

export const ACTIVETEXTCOLORINDEX = "activeTextColorindex";

export const DEFAULTTEXTCOLORINDEX = "defaultTextColorindex";

export const BGCOLOR = "bgcolor";

export const TEXTCOLOR = "textcolor";

export const ACTIVETEXTCOLOR = "activeTextcolor";

封装 ColorPicker 初始变量

注意

这里的名称必须是 xxxx.module.scss 否则暴露不出去

  • src/scss/zhuti/zhuti.module.scss
js
$bgcolor:#2d3a4b,#FFCE46,#C050C1,#99D9EA,#F08080,#FE8463,#9BCA63,#FC8463,#60C0DD,#D7504B,#C6E579,#F4D5C9,#D5E6F2,#F5E0D4,#F5E0D4,#F5E0D4;
$activecolor: #2d3a4b,#FFCE46,#C050C1,#99D9EA,#F08080,#FE8463,#9BCA63,#FC8463,#60C0DD,#D7504B,#C6E579,#F4D5C9,#D5E6F2,#F5E0D4,#F5E0D4,#F5E0D4;
$defaultcolor:#2d3a4b,#FFCE46,#C050C1,#99D9EA,#F08080,#FE8463,#9BCA63,#FC8463,#60C0DD,#D7504B,#C6E579,#F4D5C9,#D5E6F2,#F5E0D4,#F5E0D4,#F5E0D4,#fff;
:export{
    bgcolor:$bgcolor;
    activecolor:$activecolor;
    defaultcolor:$defaultcolor;
}

封装 store 下面的 ZhuTi.js

  • src/stores/ZhuTi.js
js
import { defineStore } from "pinia";

import { setItem, getItem } from "@/utils/storage";

import {
  BGCOLORINDEX,
  ACTIVETEXTCOLORINDEX,
  DEFAULTTEXTCOLORINDEX,
  BGCOLOR,
  TEXTCOLOR,
  ACTIVETEXTCOLOR,
} from "@/constant/index";
import zhutiscss from "@/scss/zhuti/zhuti.module.scss";
export const useZhuTiStore = defineStore("zhuti", () => {
  const bgcolorarr = zhutiscss.bgcolor.split(",");
  const activeTextColorarr = zhutiscss.activecolor.split(",");
  const defaultTextColorarr = zhutiscss.defaultcolor.split(",");

  // 修改 背景颜色
  const bgcolorIndex = ref(getItem(BGCOLORINDEX) || 0);
  const bgcolor = ref(getItem(BGCOLOR) || bgcolorarr[bgcolorIndex.value]);
  const changebgColorIndex = (val) => {
    bgcolorIndex.value = val;
    setItem(BGCOLORINDEX, val);
  };
  const changebgColor = (val) => {
    bgcolor.value = val;
    setItem(BGCOLOR, val);
  };
  // 修改背景颜色结束

  // 修改默认文本颜色

  const default_text_color_index = ref(getItem(DEFAULTTEXTCOLORINDEX) || 16);
  const defaultTextColor = ref(
    getItem(TEXTCOLOR) || defaultTextColorarr[default_text_color_index.value]
  );
  const changedefaultTextColorIndex = (val) => {
    default_text_color_index.value = val;
    setItem(DEFAULTTEXTCOLORINDEX, val);
  };
  const changetextColor = (val) => {
    defaultTextColor.value = val;
    setItem(TEXTCOLOR, val);
  };
  // 修改默认文本颜色结束

  // 修改鼠标放上去颜色
  const active_text_color_index = ref(getItem(ACTIVETEXTCOLORINDEX) || 1);
  const activeTextColor = ref(
    getItem(ACTIVETEXTCOLOR) ||
      activeTextColorarr[active_text_color_index.value]
  );
  const changeactiveTextColorIndex = (val) => {
    active_text_color_index.value = val;
    setItem(ACTIVETEXTCOLORINDEX, val);
  };
  const changeactivetextcolor = (val) => {
    activeTextColor.value = val;
    setItem(ACTIVETEXTCOLOR, val);
  };
  // 修改鼠标放上去颜色

  return {
    bgcolorarr,
    bgcolor,
    changebgColor,
    changebgColorIndex,
    defaultTextColor,
    defaultTextColorarr,
    changetextColor,
    changedefaultTextColorIndex,
    activeTextColor,
    activeTextColorarr,
    changeactivetextcolor,
    changeactiveTextColorIndex,
    active_text_color_index,
    default_text_color_index,
    bgcolorIndex,
  };
});

封装要改变的变量名称

  • constant/formula.json
js
{
    "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%))",
    "light-10": "color(defaulttextcolor tint(90%))",
    "light-11": "color(activetextcolor tint(90%))",
    "subMenuHover": "color(primary tint(70%))",
    "subMenuBg": "color(primary tint(80%))",
    "menuHover": "color(primary tint(90%))",
    "menuBg": "color(primary)"
  }

(重点) 封装 theme.js

  • src/utils/theme.js

注意

  1. 去 formula.json 添加新的变量名称和对应的变量替换的值

  2. 把要替换的颜色,用 formula.json 里面的变量名称替换(colorMap)

  3. 获取到最后的颜色,用正则表达式把 primary,defaulttextcolor,activetextcolor 替换成对应的颜色值,生成一个新的(color)

  4. 获取到初始样式表,循环 color,把对应的变量名称替换成新的颜色值

  5. 重新写入到样式表

js
import color from "css-color-function";
import rgbHex from "rgb-hex";
import formula from "@/constant/formula.json";
import axios from "axios";

/**
 *
 * 1. 获取当前 element-plus 的默认样式表
 */
const getOriginalStyle = async (type, data) => {
  if (!data) {
    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, type);
  } else {
    // 如果传了 则直接用传过来的值
    return getStyleTemplate(data, type);
  }
};

/**
 * 2. 把要修改的颜色设置成变量
 * 返回 style 的 template 这里添加标记,方便后续替换
 * 找到对应的色值,替换为标记
 */
const getStyleTemplate = (data, type) => {
  // element-plus 默认色值
  let colorMap = {};
  if (type == 1) {
    colorMap = {
      "#3a8ee6": "shade-1",
      "#409eff": "primary",
      "#53a8ff": "light-1",
      "#66b1ff": "light-2",
      "#79bbff": "light-3",
      "#8cc5ff": "light-4",
      "#a0cfff": "light-5",
      "#b3d8ff": "light-6",
      "#c6e2ff": "light-7",
      "#d9ecff": "light-8",
      "#ecf5ff": "light-9",
    };
  } else if (type == 2) {
    colorMap = {
      " #fff": "light-10",
    };
  } else if (type == 3) {
    colorMap = {
      "#FFCE46": "light-11",
    };
  }

  // 根据默认色值为要替换的色值打上标记
  Object.keys(colorMap).forEach((key) => {
    const value = colorMap[key];
    data = data.replace(new RegExp(key, "ig"), value);
  });
  return data;
};
/**
 * 3.根据主色生成色值表,简单来说就是把primary替换为色值,然后生成新的色值表
 *
 */
export const generateColors = (primary, type) => {
  if (!primary) return;
  /* 1. 获取到要改变的颜色 例如 rgba(254, 132, 99, 1)
    /* 2. 设置成一个变量color 例如 primary: rgba(254, 132, 99, 1);
    /* 3. 遍历 formula.json 里面的色值,把 primary 替换为 rgba(254, 132, 99, 1)
    /* 4. 把 formula.json 里面的值 添加 新建的color属性里面
    /*
    /* 如果type1 primary最后返回的结果例如就是 {
    "primary": "rgba(254, 132, 99, 1)",
    "shade-1": "#e57759",
    "light-1": "#fe9073",
    "light-2": "#fe9d82",
    "light-3": "#fea992",
    "light-4": "#feb5a1",
    "light-5": "#ffc2b1",
    "light-6": "#ffcec1",
    "light-7": "#ffdad0",
    "light-8": "#ffe6e0",
    "light-9": "#fff3ef",
    "light-10": "#fff3ef",
    "subMenuHover": "#ffdad0",
    "subMenuBg": "#ffe6e0",
    "menuHover": "#fff3ef",
    "menuBg": "#fe8463"
}
    */
  const colors = {};
  if (type == 1) {
    colors["primary"] = primary;
  } else if (type == 2) {
    colors["defaulttextcolor"] = primary;
  } else if (type == 3) {
    colors["activetextcolor"] = primary;
  }
  console.log(colors);
  if (type == 1) {
    Object.keys(formula).forEach((key) => {
      if (formula[key].indexOf("primary") != -1) {
        let value = formula[key].replace(/primary/g, primary);
        colors[key] = "#" + rgbHex(color.convert(value));
      }
    });
  } else if (type == 2) {
    Object.keys(formula).forEach((key) => {
      if (formula[key].indexOf("defaulttextcolor") != -1) {
        let value = formula[key].replace(/defaulttextcolor/g, primary);
        colors[key] = "#" + rgbHex(color.convert(value));
      }
    });
  } else if (type == 3) {
    Object.keys(formula).forEach((key) => {
      if (formula[key].indexOf("activetextcolor") != -1) {
        let value = formula[key].replace(/activetextcolor/g, primary);
        colors[key] = "#" + rgbHex(color.convert(value));
      }
    });
    console.log(colors);
  }

  return colors;
};

/**
 * 4.根据主色值,生成最新的样式表
 */
export const generateNewStyle = async (primaryColor, type, data) => {
  const colors = generateColors(primaryColor, type);
  let cssText = await getOriginalStyle(type, data);
  // 遍历生成的样式表,在 CSS 的原样式中进行全局替换
  Object.keys(colors).forEach((key) => {
    cssText = cssText.replace(
      new RegExp("(:|\\s+)" + key, "g"),
      "$1" + colors[key]
    );
  });

  return cssText;
};

/**
 * 5.写入新样式到 style
 * @param {*} elNewStyle  element-plus 的新样式
 * @param {*} isNewStyleTag 是否生成新的 style 标签
 */
export const writeNewStyle = (elNewStyle) => {
  const style = document.createElement("style");
  style.innerText = elNewStyle;
  document.head.appendChild(style);
};

持久化

  • App.vue
vue
<template>
  <div>
    <el-config-provider :locale="localLanguage">
      <RouterView />
    </el-config-provider>
  </div>
</template>

<script setup>
import zhCn from "element-plus/es/locale/lang/zh-cn";

import en from "element-plus/es/locale/lang/en";

import { useI18n } from "vue-i18n";

import { generateNewStyle, writeNewStyle } from "@/utils/theme";

import { useZhuTiStore } from "@/stores/ZhuTi.js";

const ZhuTiModule = useZhuTiStore();

// 设置主题色方法

const changeColor = async () => {
  // 1.1 获取主题色
  const newStyleText = await generateNewStyle(ZhuTiModule.bgcolor, 1);
  const newStyleText2 = await generateNewStyle(
    ZhuTiModule.defaultTextColor,
    2,
    newStyleText
  );
  const newStyleText3 = await generateNewStyle(
    ZhuTiModule.activeTextColor,
    3,
    newStyleText2
  );
  // 1.2 写入最新主题色
  writeNewStyle(newStyleText3);
};

changeColor();

// 切换语言的方法
const i18n = useI18n();

const localLanguage = computed(() => {
  const result = i18n.locale.value;
  if (result == "cn") {
    return zhCn;
  } else {
    return en;
  }
});
// 切换结束

const huanjing = import.meta.env.VITE_APP_ENV;
console.log(huanjing);
</script>

<style lang="scss" scoped></style>

最后再修改 SideBarMenu.vue

vue
<template>
  <el-menu
    :uniqueOpened="true"
    router
    :default-active="activeMenu"
    :background-color="bgcolor"
    :text-color="defaultextcolor"
    :active-text-color="activetextcolor"
    :collapse="useCollapse().sidebarOpened"
  >
    <SideBarItem
      v-for="item in routes"
      :key="item.path"
      :route="item"
    ></SideBarItem>
  </el-menu>
</template>

<script setup>
import { filterRouters, generateMenus } from "@/utils/route";

import { useCollapse } from "@/stores/sidebaropen";

import { useZhuTiStore } from "@/stores/ZhuTi";

/* 获取 变量 */
const ZhuTistore = useZhuTiStore();
const bgcolor = ref(ZhuTistore.bgcolor);
const defaultextcolor = ref(ZhuTistore.defaultTextColor);
const activetextcolor = ref(ZhuTistore.activeTextColor);
watch(
  () => {
    return ZhuTistore.bgcolor;
  },
  (newval, oldval) => {
    bgcolor.value = newval;
  }
);

watch(
  () => {
    return ZhuTistore.defaultTextColor;
  },
  (newval, old) => {
    defaultextcolor.value = newval;
  }
);

watch(
  () => {
    return ZhuTistore.activeTextColor;
  },
  (newval, oldval) => {
    activetextcolor.value = newval;
  }
);
/* 获取路由 */
const router = useRouter();
const routes = computed(() => {
  const filterRoutes = filterRouters(router.getRoutes());
  return generateMenus(filterRoutes);
});
/* 计算高亮 */
const route = useRoute();

const activeMenu = computed(() => {
  const { meta, path } = route;
  console.log(route);
  return path;
});
</script>

<style lang="scss">
.el-menu {
  border-right: none;
}
</style>
vue
<template>
  <div class="navbarContent">
    <div class="sidebaropen">
      <SideBarOpen></SideBarOpen>
    </div>
    <div class="breadcrumb">
      <BreadCrumb></BreadCrumb>
    </div>

    <div class="avatar">
      <Avater></Avater>
    </div>
    <div class="language">
      <I18n color="black"></I18n>
    </div>
    <div class="theme">
      <ThemeColor></ThemeColor>
    </div>
  </div>
</template>

<script setup></script>

<style lang="scss" scoped>
.navbarContent {
  width: 100%;
  height: 60px;
  border-bottom: 1px solid #ccc;
  .sidebaropen {
    float: left;
    margin-top: 15px;
    margin-left: 20px;
    cursor: pointer;
  }

  .breadcrumb {
    float: left;
    margin-left: 10px;
    margin-top: 1px;
  }
  .avatar {
    width: 80px;
    height: 50px;
    float: right;
    margin-right: 20px;
    margin-top: 5px;
  }
  .language {
    float: right;
    margin-top: 15px;
    margin-right: 20px;
    cursor: pointer;
  }
  .theme {
    float: right;
    margin-top: 15px;
    margin-right: 20px;
    cursor: pointer;
  }
}
</style>