• Vue + ElementUI 实现动态更换任意主题色(动态换肤)


    最近基于 ElementUI 的项目上需要实现动态换肤的功能,这里提供两种方式:

    1. vue-element-admin 官方实现的方式
    2. 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
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      /* main.js */
      
      import Vue from 'vue'
      import Element from 'element-ui'
      import './element-variables.scss'
       
      Vue.use(Element)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    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
      }
      
      • 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
    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>
      
      • 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
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
    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
        })
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    到这里为止就已经实现了全部动态换肤的功能,不过还有一个问题就是刷新页面之后,主题色会重置,所以在切换的时候需要将主题色缓存到 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>
    
    • 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

    另外一些自己定义的样式如果需要用到主题色可以用 css 变量,但需要预先在 root 上定义好变量,我这里使用 vue 双向绑定将主题色绑定到 :style="{'--color': defaultTheme}" (具体用法见上面代码),这样就能在任何组件下使用该主题色了(注意:该方法 IE 浏览器不支持),代码如下:

    <div class="box">div>
    
    • 1
    .box {
      width: 100px;
      height: 100px;
      background-color: var(--color);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    提示:

    1. 如果 scss 文件导出的是空对象,则需要将 scss 文件名称改成 xxx.module.scss 的形式,CSS Modules
    2. 如果需要缓存 chalkCss 的,css string 中的 \ 会被转义,记得手动改成 \\,不然 ElementUI 自带的 icon 展示不出来。

    参考文档:

    webpack-theme-color-replacer 插件的实现方式

    此方式亲测可用,本质上也是颜色的替换,但是跟 ElementUI 稍稍有点兼容性问题,具体实现方式见 webpack-theme-color-replacer 的使用,我这里就不做过多赘述了。

  • 相关阅读:
    Kubernetes Pod内存监控
    wavenet及TCN
    解决VUE报错GET http://127.0.0.1:5500/favicon.ico 404 (Not Found)
    【NLP】第 6 章:用于文本分类的卷积神经网络
    C# 面向对象
    suricata 流管理
    概率的本质是什么
    python从入门到实践:数据类型、文件处理
    33、Flink之hive介绍与简单示例
    Maven经常使用的三种作用
  • 原文地址:https://blog.csdn.net/aSmallProgrammer/article/details/128021927