Vue+ElementUI实现动态更换任意主题色(动态换肤)的全过程
目录
- 前言
- vue-element-admin 官方实现的方式
- 参考文档:
- webpack-theme-color-replacer 插件的实现方式
- 总结
前言
最近基于 ElementUI
的项目上需要实现动态换肤的功能,这里提供两种方式:
vue-element-admin
官方实现的方式webpack-theme-color-replacer
插件的实现方式
vue-element-admin 官方实现的方式
简单说明一下它的原理: element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json 拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style 标签来覆盖原有的 css 样式。
1、这里无脑参考 vue-element-admin
的源码,首先现在 styles
文件夹下创建一个名为 element-variables.scss
文件,并在 main.js
中引入该文件。
/* src/styles/element-variables.scss */ /* 改变主题色变量 */ $--color-primary: #256DCB; $--color-success: #7AC800; $--color-danger: #EC4242; $--color-info: #999999; /* 改变 icon 字体路径变量,必需 */ $--font-path: '~element-ui/lib/theme-chalk/fonts'; @import "~element-ui/packages/theme-chalk/src/index"; /* 如遇到导出的变量值为 undefined 则本文件名需要改成 element-variables.module.scss */ :export { theme: $--color-primary }
/* main.js */ import Vue from 'vue' import Element from 'element-ui' import './element-variables.scss' Vue.use(Element)
2、通过以上方式已经能够实现 ElementUI
主题替换了,但是想要做到自定义颜色还是远远不够的,考虑到主题色在整个项目中都有应用,所以我们将它存到 Vuex
中,接下来要做的就是在 store/modules
下新建 settings.js
。
/* src/store/modules/settings.js */ import variables from '@/styles/element-variables.module.scss' import * as Types from '../mutation-types' const state = { theme: variables.theme } const mutations = { CHANGE_SETTING: (state, { key, value }) => { localStorage.setItem('theme', value) // 缓存起来,刷新的时候重新取用 // eslint-disable-next-line no-prototype-builtins if (state.hasOwnProperty(key)) { state[key] = value } } } const actions = { changeSetting ({ commit }, data) { commit('CHANGE_SETTING', data) } } export default { namespaced: true, state, mutations, actions }
3、这一步也是无脑 copy vue-element-admin
的源码,新建一个名为 ThemePicker
的组件,代码如下:
/* src/components/ThemePicker/index.vue */ <template> <el-color-picker v-model="theme" :predefine="['#256DCB', '#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', '#F80', ]" class="theme-picker" popper-class="theme-picker-dropdown" /> </template> <script> import { chalkCss } from './chalk.js' // 就是 getCSSString 接口中获取的 css,这里我将版本号固定并且把 css 缓存到本地,记得处理 \ 转义问题 // const version = require('element-ui/package.json').version // element-ui version from node_modules const ORIGINAL_THEME = '#409EFF' // default color chalk.js 中的 primary color,根据此颜色计算出一系列颜色进行正则替换 export default { name: 'ThemePicker', data () { return { chalk: '', // content of theme-chalk css theme: '' } }, computed: { defaultTheme () { return this.$store.state.settings.theme } }, watch: { defaultTheme: { handler: function (val, oldVal) { this.theme = val }, immediate: true }, async theme (val) { const oldVal = this.chalk ? this.theme : ORIGINAL_THEME if (typeof val !== 'string') return const themeCluster = this.getThemeCluster(val.replace('#', '')) const originalCluster = this.getThemeCluster(oldVal.replace('#', '')) console.log(themeCluster, originalCluster) const $message = this.$message({ message: this.$t('theme_compiling'), customClass: 'theme-message', type: 'success', duration: 0, iconClass: 'el-icon-loading' }) const getHandler = (variable, id) => { return () => { const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', '')) const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster) let styleTag = document.getElementById(id) if (!styleTag) { styleTag = document.createElement('style') styleTag.setAttribute('id', id) document.head.appendChild(styleTag) } styleTag.innerText = newStyle } } if (!this.chalk) { // const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` // await this.getCSSString(url, 'chalk') this.chalk = chalkCss.replace(/@font-face{[^}]+}/, '') // 本地缓存,如果需要获取线上的就用上面那种方式,优点:切换无延迟,缺点:需要手动维护 css string } const chalkHandler = getHandler('chalk', 'chalk-style') chalkHandler() const styles = [].slice.call(document.querySelectorAll('style')) .filter(style => { const text = style.innerText return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text) }) styles.forEach(style => { const { innerText } = style if (typeof innerText !== 'string') return style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) }) this.$emit('change', val) $message.close() } }, methods: { updateStyle (style, oldCluster, newCluster) { let newStyle = style oldCluster.forEach((color, index) => { newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index]) }) return newStyle }, getCSSString (url, variable) { return new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '') resolve() } } xhr.open('GET', url) xhr.send() }) }, getThemeCluster (theme) { const tintColor = (color, tint) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) if (tint === 0) { // when primary color is in its rgb space return [red, green, blue].join(',') } else { red += Math.round(tint * (255 - red)) green += Math.round(tint * (255 - green)) blue += Math.round(tint * (255 - blue)) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } } const shadeColor = (color, shade) => { let red = parseInt(color.slice(0, 2), 16) let green = parseInt(color.slice(2, 4), 16) let blue = parseInt(color.slice(4, 6), 16) red = Math.round((1 - shade) * red) green = Math.round((1 - shade) * green) blue = Math.round((1 - shade) * blue) red = red.toString(16) green = green.toString(16) blue = blue.toString(16) return `#${red}${green}${blue}` } const clusters = [theme] for (let i = 0; i <= 9; i++) { clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) } clusters.push(shadeColor(theme, 0.1)) return clusters } } } </script> <style> .theme-message, .theme-picker-dropdown { z-index: 99999 !important; } .theme-picker .el-color-picker__trigger { height: 26px !important; width: 26px !important; padding: 2px; } .theme-picker-dropdown .el-color-dropdown__link-btn { display: none; } </style>
4、最后在页面中使用 ThemePicker
组件,我这里是放到 header
里。
// template <theme-picker @change="themeChange" /> // js import ThemePicker from '@/components/ThemePicker' // methods themeChange (val) { this.$store.dispatch('settings/changeSetting', { key: 'theme', value: val }) }
到这里为止就已经实现了全部动态换肤的功能,不过还有一个问题就是刷新页面之后,主题色会重置,所以在切换的时候需要将主题色缓存到 localStore
中去,为了在任意页面刷新都可以加载主题色,我选择在 App.vue
页面也增加一个 ThemePicker
组件,具体实现见如下代码:
/* src/App.vue */ <template> <div id="app" :style="{'--color': defaultTheme}"> <theme-picker @change="themeChange" v-show="false" /> <router-view /> </div> </template> <script> import ThemePicker from '@/components/ThemePicker' export default { name: 'App', components: { ThemePicker }, computed: { defaultTheme () { return this.$store.state.settings.theme } }, mounted () { if (localStorage.getItem('theme')) { this.themeChange(localStorage.getItem('theme')) } }, methods: { themeChange (val) { this.$store.dispatch('settings/changeSetting', { key: 'theme', value: val }) } } } </script>
另外一些自己定义的样式如果需要用到主题色可以用 css
变量,但需要预先在 root
上定义好变量,我这里使用 vue
双向绑定将主题色绑定到 :style="{'--color': defaultTheme}"
(具体用法见上面代码),这样就能在任何组件下使用该主题色了(注意:该方法 IE
浏览器不支持),代码如下:
<div class="box"></div>
.box { width: 100px; height: 100px; background-color: var(--color); }
提示:
- 如果 scss 文件导出的是空对象,则需要将 scss 文件名称改成 xxx.module.scss 的形式,CSS Modules。
- 如果需要缓存 chalkCss 的,css string 中的 \ 会被转义,记得手动改成 \\,不然 ElementUI 自带的 icon 展示不出来。
参考文档:
webpack-theme-color-replacer 插件的实现方式
此方式亲测可用,本质上也是颜色的替换,但是跟 ElementUI
稍稍有点兼容性问题,具体实现方式见webpack-theme-color-replacer 的使用,我这里就不做过多赘述了。
总结
到此这篇关于Vue+ElementUI实现动态更换任意主题色(动态换肤)的文章就介绍到这了,更多相关Vue ElementUI动态更换任意主题色内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!