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: [
    vue(),
    requireTransform({
      fileRegex: /.js$|.vue$|.json$/,
    }),
  ],
});

封装换肤组件

  • components/ColorPicker/index.vue

  • components/ThemeColor/index.vue

components/ColorPicker/index.vue

vue
<script setup>
import { useZhuTiStore } from "@/stores/ZhuTi.js";

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

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

const StoreModules = useZhuTiStore();

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

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

const color = ref("");

const predefineColors = ref([]);

function 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;
}

async function changecolor() {
  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>

<template>
  <el-dialog
    :model-value="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>

<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 {
  ACTIVETEXTCOLOR,
  ACTIVETEXTCOLORINDEX,
  BGCOLOR,
  BGCOLORINDEX,
  DEFAULTTEXTCOLORINDEX,
  TEXTCOLOR,
} from '@/constant/index'

import zhutiscss from '@/scss/zhuti.module.scss'

import { getItem, setItem } from '@/utils/storage'

export const useZhuTiStore = defineStore('zhuti', () => {
  const bgcolorarr: any = zhutiscss.bgcolor?.split(',')
  const activeTextColorarr: any = zhutiscss.activecolor?.split(',')
  const defaultTextColorarr: any = zhutiscss.defaultcolor?.split(',')

  // 修改 背景颜色
  const bgcolorIndex = ref(getItem(BGCOLORINDEX) || 0)
  const bgcolor = ref(getItem(BGCOLOR) || bgcolorarr[bgcolorIndex.value])
  const changebgColorIndex = (val: any) => {
    bgcolorIndex.value = val
    setItem(BGCOLORINDEX, val)
  }
  const changebgColor = (val: any) => {
    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: any) => {
    default_text_color_index.value = val
    setItem(DEFAULTTEXTCOLORINDEX, val)
  }
  const changetextColor = (val: any) => {
    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: any) => {
    active_text_color_index.value = val
    setItem(ACTIVETEXTCOLORINDEX, val)
  }
  const changeactivetextcolor = (val: any) => {
    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 axios from 'axios'
import color from 'css-color-function'
import { version } from 'element-plus/package.json'
import rgbHex from 'rgb-hex'
import formula from '@/constant/formula.json'

/**
 * 2. 把要修改的颜色设置成变量
 * 返回 style 的 template 这里添加标记,方便后续替换
 * 找到对应的色值,替换为标记
 */
function getStyleTemplate(data: any, type: any) {
  // element-plus 默认色值
  let colorMap: Record<string, any> = {}
  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: any) => {
    const value: any = colorMap[key]
    data = data.replace(new RegExp(key, 'gi'), value)
  })
  return data
}
/**
 * 3.根据主色生成色值表,简单来说就是把primary替换为色值,然后生成新的色值表
 *
 */
export function generateColors(primary: any, type: any) {
  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: Record<string, any> = {}
  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: any) => {
      if (formula[key as keyof typeof formula]?.indexOf('primary') !== -1) {
        const value: any = formula[key as keyof typeof formula].replace(/primary/g, primary)
        colors[key] = `#${rgbHex(color.convert(value))}`
      }
    })
  } else if (type === 2) {
    Object.keys(formula).forEach((key: any) => {
      if (formula[key as keyof typeof formula]?.indexOf('defaulttextcolor') !== -1) {
        const value: any = formula[key as keyof typeof formula].replace(/defaulttextcolor/g, primary)
        colors[key] = `#${rgbHex(color.convert(value))}`
      }
    })
  } else if (type === 3) {
    Object.keys(formula).forEach((key: any) => {
      if (formula[key as keyof typeof formula]?.indexOf('activetextcolor') !== -1) {
        const value: any = formula[key as keyof typeof formula].replace(/activetextcolor/g, primary)
        colors[key] = `#${rgbHex(color.convert(value))}`
      }
    })
    console.log(colors)
  }

  return colors
}

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

  return cssText
}

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

/**
 *
 * 1. 获取当前 element-plus 的默认样式表
 */
async function getOriginalStyle(type: any, data: any) {
  if (!data) {
    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)
  }
}

持久化

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

<script setup>
import { useZhuTiStore } from "@/stores/ZhuTi.js";

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";

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
<script setup>
import { useCollapse } from "@/stores/sidebaropen";

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

import { filterRouters, generateMenus } from "@/utils/route";

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

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

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

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

<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>

<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>