前端框架&组件换肤方案

733 阅读5分钟

前端框架&组件换肤方案

前言

相信大家在工作中,经常会遇到主题切换的需求。主题切换虽然不难,但是各种方案的差异较大,往往都会需要伴随着新主题的代码修改,很浪费时间精力。接下来在本篇文章中,将会带你学习如何0修改完成主题方案建设,开始吧~

介绍

1668157961521.png
本篇文章将会围绕图上这几部分进行介绍~

组件库主题

由于我们项目中使用的组件库是elementui二次开发的,所以组件库切换方案就参考了elementui的。

预备工作

设置vuex的主题颜色存储,以及同步localstorage存储

export default {
  namespaced: true,
  state: {
    // 主题
    theme: window.localStorage.getItem('THEME') || '#166bff',
    // 布局模式:line竖版,row横版
    layoutType: window.localStorage.getItem('LAYOUT_TYPE') || 'row'
  },
  mutations: {
    // 更新主题
    SWITCH_THEME(state, theme) {
      state.theme = theme
      window.localStorage.setItem('THEME', theme)
    },
    // 修改菜单布局模式
    UPDATE_LAYOUT_TYPE(state, val) {
      state.layoutType = val
      window.localStorage.setItem('LAYOUT_TYPE', val)
    }
  },
  getters: {
    // 获取缓存中的主题色
    getTheme(state) {
      return state.theme
    },
    // 获取菜单布局模式
    getLayoutType(state) {
      return state.layoutType
    }
  }
}

颜色切换组件

在elementui中,有一个themePicker的组件,所以切换颜色我们可以参考此组件。 项目中完成展示如图:

1668419308104.png
完整组件代码示例:

<template>
  <el-color-picker
    v-model="theme"
    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
    class="theme-picker"
    popper-class="theme-picker-dropdown"
  />
</template>

<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color

export default {
  data() {
    return {
      chalk: '', // content of theme-chalk css
      theme: ''
    }
  },
  computed: {
    defaultTheme() {
      return this.$store.getters['base/getTheme']
    }
  },
  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: '  Compiling the theme',
        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')
      }

      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)
        })
      console.log(styles)
      styles.forEach(style => {
        const { innerText } = style
        if (typeof innerText !== 'string') return
        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
      })

      this.$emit('change', val)

      $message.close()

      // 刷新重载模板样式
      window.location.reload()
    }
  },

  methods: {
    updateStyle(style, oldCluster, newCluster) {
      let newStyle = style
      console.log('ceshi01', newStyle,'old样式',oldCluster,'new样式',newCluster)
      oldCluster.forEach((color, index) => {
        console.log(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>

主题持久化

当项目中成功接入上述组件后,我们点击了一下切换会惊喜的发现,组件颜色成功切换了!
但是不要开心的太早,因为无法持久化,当你刷新页面主题就会丢失,无法保持状态,接下来让我们来实现主题持久化~

  • 我们将主题持久化的函数全部抽离成为了混入文件
  • 在项目初始化也就是APP.vue的created中引入,进行初始化处理
  • 刷新页面测试,主题已经完成持久化
    实现代码:
//加载皮肤用
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
  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
    },
    //换皮肤 防止F5后皮肤丢失
    async handleUserTheme() {
      let val = this.$store.getters['base/getTheme']
      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 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')
      }

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

项目框架主题

上述我们已经完成了组件库主题的自定义切换,接下来我们就来实现项目框架主题的自定义切换。 在做此方案之前我们用的是切换类的模式,虽然也能完成主题切换,但每次新增一套主题时,就要去修改代码,相当麻烦。

历史方案介绍

提前引入所有主题样式,做类名切换,这种方案是在需要切换主题的时候将指定的根元素类名更换,进行样式覆盖,同时各个类名下的样式也就同步切换了。
实现方案:

/* 白天样式主题 */
body.day .box {
  color: #000;
  background: #fff;
}
/* 黑夜样式主题 */
body.dark .box {
  color: #fff;
  background: #000;
}

.box {
  width: 100px;
  height: 100px;
  border: 1px solid #000;
}
<div class="box">
  <p>one piece</p>
</div>
<p>
  选择样式:
  <button onclick="change('day')">白天主题切换</button>
  <button onclick="change('dark')">夜晚主题切换</button>
</p>
function change(theme) {
  document.body.className = theme;
}

废弃原因:

  • 首屏加载时会牺牲一些时间加载样式资源
  • 如果主题样式表内定义不当,也会有优先级问题
  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

现有方案!

针对客户不断提出的各种换肤,频繁做着无效劳动去增加主题,让我倍感烦躁。所以推出了以下方案。
现有方案的核心有两点:

  • scss中的var变量
  • js语法style.setProperty 利用这两点我们完成了项目中的主题0代码修改切换,而且可以同步组件库。

定义公共变量

定义公用scss变量,这里定义了3个变量,有同学可能会疑惑,这色值不是一样的,不要急,下来会一步一步讲

$brand-color:var(--brand-color, #166bff);
$brand-color-light:var(--brand-color-light, #166bff);
$brand-color-dark:var(--brand-color-dark, #166bff);

定义了scss变量后,我们为了方便使用,需要直接在webpack中进行全局变量配置

  // 配置 scss 全局变量,不用在使用的每一处再引入变量文件
  // 打包切换主题可在此处完成
  webpack.module.rule('scss').oneOfs.store.forEach(item => {
    item
      .use('sass-resources-loader')
      .loader('sass-resources-loader')
      .options({
        resources: [resolve('./src/style/templateTheme.scss')]
      })
  })

在项目中使用

<style lang="scss" scoped>
.home{
background-color: $brand-color;
}
</style>-

同步主题选择器颜色

上述我们介绍了主题选择器,以及出题存储,这里我们就要讲获取到的主题进行赋值,同步到项目框架中

data() {
    return {
      currentTheme: this.$store.getters['base/getTheme']
    }
},
mounted() {
    this.setTemplateColor()
},
methods: {
    setTemplateColor() {
      // 设置主色
      function setTheme(name, color) {
        document
          .getElementsByTagName('body')[0]
          .style.setProperty(name, color)
      }
      // 根据主色计算衍生色
      const colorObj = {
        '--brand-color': this.currentTheme,
        '--brand-color-dark': this.lightDarkenColor(this.currentTheme, -10),
        '--brand-color-light': this.colorRgba(this.currentTheme, 0.6)
      }
      Object.entries(colorObj).forEach(e => {
        // 逐个设置 css 变量到 body 上
        setTheme(e[0],e[1])
      })
    },
}

大家可以看到,上述有个根据主色计算衍生色的算法,这里来解答上述疑问。

衍生色计算

在项目中我们往往不可能只使用一种主色,一般会伴随集中衍生色调的使用,例如hover颜色,选中颜色等。 所以我们就需要根据主色,去计算出其他颜色。 这里提供几种色阶计算的方案:

  • 获取rgb颜色
    // 获取 rgb 颜色值
    getRgbNum(sColor) {
      if (sColor.length === 4) {
        let sColorNew = '#'
        for (let i = 1; i < 4; i += 1) {
          // 补全颜色值 例如:#eee,#fff等
          sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
        }
        sColor = sColorNew
      }
      // 处理六位颜色值
      let sColorChange = []
      for (let i = 1; i < 7; i += 2) {
        // 核心代码,通过parseInt将十六进制转为十进制,parseInt只有一个参数时是默认转为十进制的,第二个参数则是指定转为对应进制
        sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
      }
      return sColorChange
    },
  • 透明度色阶
    // str: 十六进制颜色值,n:透明度
    colorRgba(str, n) {
      // 十六进制颜色值的正则表达式
      let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
      let sColor = str.toLowerCase()
      n = n || 1
      // 十六进制颜色转换为RGB格式
      if (sColor && reg.test(sColor)) {
        let sColorChange = this.getRgbNum(sColor)
        return 'rgba(' + sColorChange.join(',') + ',' + n + ')'
      } else {
        return sColor
      }
    },
  • 加深或减弱颜色值
    lightDarkenColor(color, num) {
      let colorArr = this.getRgbNum(color)
      let sColorChange = []
      for (let i = 0; i < colorArr.length; i++) {
        let val = colorArr[i] + num
        if (val < 0) {
          val = 0
        }
        if (val > 255) {
          val = 255
        }
        sColorChange.push(val)
      }
      return 'rgba(' + sColorChange.join(',') + ',1)'
    }
  • 指定色阶计算
    此种方案需要指定初始颜色结束颜色以及色阶数量,用来生成想要的色阶
    // 色阶计算
    stepColor(aColor, bColor, num) {
      function gradientColor(startColor, endColor, step) {
        let startRGB = this.colorRgb(startColor) //转换为rgb数组模式
        let startR = startRGB[0]
        let startG = startRGB[1]
        let startB = startRGB[2]
        let endRGB = this.colorRgb(endColor)
        let endR = endRGB[0]
        let endG = endRGB[1]
        let endB = endRGB[2]
        let sR = (endR - startR) / step //总差值
        let sG = (endG - startG) / step
        let sB = (endB - startB) / step
        var colorArr = []
        for (var i = 0; i < step; i++) {
          //计算每一步的hex值
          var hex = this.colorHex(
            'rgb(' +
              parseInt(sR * i + startR) +
              ',' +
              parseInt(sG * i + startG) +
              ',' +
              parseInt(sB * i + startB) +
              ')'
          )
          colorArr.push(hex)
        }
        return colorArr
      }
      // 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
      gradientColor.prototype.colorRgb = function colorRgb(sColor) {
        var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
        var sColor = sColor.toLowerCase()
        if (sColor && reg.test(sColor)) {
          if (sColor.length === 4) {
            var sColorNew = '#'
            for (var i = 1; i < 4; i += 1) {
              sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
            }
            sColor = sColorNew
          }
          //处理六位的颜色值
          var sColorChange = []
          for (var i = 1; i < 7; i += 2) {
            sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
          }
          return sColorChange
        } else {
          return sColor
        }
      }
      // 将rgb表示方式转换为hex表示方式
      gradientColor.prototype.colorHex = function (rgb) {
        var _this = rgb
        var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
        if (/^(rgb|RGB)/.test(_this)) {
          var aColor = _this.replace(/(?:(|)|rgb|RGB)*/g, '').split(',')
          var strHex = '#'
          for (var i = 0; i < aColor.length; i++) {
            var hex = Number(aColor[i]).toString(16)
            hex = hex < 10 ? 0 + '' + hex : hex // 保证每个rgb的值为2位
            if (hex === '0') {
              hex += hex
            }
            strHex += hex
          }
          if (strHex.length !== 7) {
            strHex = _this
          }
          return strHex
        } else if (reg.test(_this)) {
          var aNum = _this.replace(/#/, '').split('')
          if (aNum.length === 6) {
            return _this
          } else if (aNum.length === 3) {
            var numHex = '#'
            for (var i = 0; i < aNum.length; i += 1) {
              numHex += aNum[i] + aNum[i]
            }
            return numHex
          }
        } else {
          return _this
        }
      }
      let gradient = new gradientColor(aColor, bColor, num);
      return gradient
    }

方案总结

核心源码

//加载皮肤用
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
  data() {
    return {
      currentTheme: this.$store.getters['base/getTheme']
    }
  },
  mounted() {
    this.setTemplateColor()
  },
  methods: {
    setTemplateColor() {
      // 设置主色
      function setTheme(name, color) {
        document.getElementsByTagName('body')[0].style.setProperty(name, color)
      }
      // 根据主色计算衍生色
      const colorObj = {
        '--brand-color': this.currentTheme,
        '--brand-color-dark-1': this.lightDarkenColor(this.currentTheme, -10),
        '--brand-color-dark-2': this.lightDarkenColor(this.currentTheme, -20),
        '--brand-color-dark-3': this.lightDarkenColor(this.currentTheme, -30),
        '--brand-color-dark-4': this.lightDarkenColor(this.currentTheme, -40),
        '--brand-color-dark-5': this.lightDarkenColor(this.currentTheme, -50),
        '--brand-color-dark-6': this.lightDarkenColor(this.currentTheme, -60),
        '--brand-color-dark-7': this.lightDarkenColor(this.currentTheme, -70),
        '--brand-color-dark-8': this.lightDarkenColor(this.currentTheme, -80),
        '--brand-color-dark-9': this.lightDarkenColor(this.currentTheme, -90),
        '--brand-color-opacity-1': this.colorRgba(this.currentTheme, 0.9),
        '--brand-color-opacity-2': this.colorRgba(this.currentTheme, 0.8),
        '--brand-color-opacity-3': this.colorRgba(this.currentTheme, 0.7),
        '--brand-color-opacity-4': this.colorRgba(this.currentTheme, 0.6),
        '--brand-color-opacity-5': this.colorRgba(this.currentTheme, 0.5),
        '--brand-color-opacity-6': this.colorRgba(this.currentTheme, 0.4),
        '--brand-color-opacity-7': this.colorRgba(this.currentTheme, 0.3),
        '--brand-color-opacity-8': this.colorRgba(this.currentTheme, 0.2),
        '--brand-color-opacity-9': this.colorRgba(this.currentTheme, 0.1),
        '--brand-color-light-1': this.lightDarkenColor(this.currentTheme, 10),
        '--brand-color-light-2': this.lightDarkenColor(this.currentTheme, 20),
        '--brand-color-light-3': this.lightDarkenColor(this.currentTheme, 30),
        '--brand-color-light-4': this.lightDarkenColor(this.currentTheme, 40),
        '--brand-color-light-5': this.lightDarkenColor(this.currentTheme, 50),
        '--brand-color-light-6': this.lightDarkenColor(this.currentTheme, 60),
        '--brand-color-light-7': this.lightDarkenColor(this.currentTheme, 70),
        '--brand-color-light-8': this.lightDarkenColor(this.currentTheme, 80),
        '--brand-color-light-9': this.lightDarkenColor(this.currentTheme, 90),
        '--brand-color-step-1': this.stepColor(this.currentTheme,'#ffffff', 10)[0],
        '--brand-color-step-2': this.stepColor(this.currentTheme,'#ffffff', 10)[1],
        '--brand-color-step-3': this.stepColor(this.currentTheme,'#ffffff', 10)[2],
        '--brand-color-step-4': this.stepColor(this.currentTheme,'#ffffff', 10)[3],
        '--brand-color-step-5': this.stepColor(this.currentTheme,'#ffffff', 10)[4],
        '--brand-color-step-6': this.stepColor(this.currentTheme,'#ffffff', 10)[5],
        '--brand-color-step-7': this.stepColor(this.currentTheme,'#ffffff', 10)[6],
        '--brand-color-step-8': this.stepColor(this.currentTheme,'#ffffff', 10)[7],
        '--brand-color-step-9': this.stepColor(this.currentTheme,'#ffffff', 10)[8],
        '--brand-color-step-dark-1': this.stepColor(this.currentTheme,'#000000', 10)[0],
        '--brand-color-step-dark-2': this.stepColor(this.currentTheme,'#000000', 10)[1],
        '--brand-color-step-dark-3': this.stepColor(this.currentTheme,'#000000', 10)[2],
        '--brand-color-step-dark-4': this.stepColor(this.currentTheme,'#000000', 10)[3],
        '--brand-color-step-dark-5': this.stepColor(this.currentTheme,'#000000', 10)[4],
        '--brand-color-step-dark-6': this.stepColor(this.currentTheme,'#000000', 10)[5],
        '--brand-color-step-dark-7': this.stepColor(this.currentTheme,'#000000', 10)[6],
        '--brand-color-step-dark-8': this.stepColor(this.currentTheme,'#000000', 10)[7],
        '--brand-color-step-dark-9': this.stepColor(this.currentTheme,'#000000', 10)[8]
      }
      Object.entries(colorObj).forEach(e => {
        // 逐个设置 css 变量到 body 上
        setTheme(e[0], e[1])
      })
    },
    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
    },
    //换皮肤 防止F5后皮肤丢失
    async handleUserTheme() {
      let val = this.$store.getters['base/getTheme']
      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 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')
      }

      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)
      })
    },
    // str: 十六进制颜色值,n:透明度
    colorRgba(str, n) {
      // 十六进制颜色值的正则表达式
      let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
      let sColor = str.toLowerCase()
      n = n || 1
      // 十六进制颜色转换为RGB格式
      if (sColor && reg.test(sColor)) {
        let sColorChange = this.getRgbNum(sColor)
        return 'rgba(' + sColorChange.join(',') + ',' + n + ')'
      } else {
        return sColor
      }
    },
    // 获取 rgb 颜色值
    getRgbNum(sColor) {
      if (sColor.length === 4) {
        let sColorNew = '#'
        for (let i = 1; i < 4; i += 1) {
          // 补全颜色值 例如:#eee,#fff等
          sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
        }
        sColor = sColorNew
      }
      // 处理六位颜色值
      let sColorChange = []
      for (let i = 1; i < 7; i += 2) {
        // 核心代码,通过parseInt将十六进制转为十进制,parseInt只有一个参数时是默认转为十进制的,第二个参数则是指定转为对应进制
        sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
      }
      return sColorChange
    },
    // 加深或减弱颜色值
    lightDarkenColor(color, num) {
      let colorArr = this.getRgbNum(color)
      let sColorChange = []
      for (let i = 0; i < colorArr.length; i++) {
        let val = colorArr[i] + num
        if (val < 0) {
          val = 0
        }
        if (val > 255) {
          val = 255
        }
        sColorChange.push(val)
      }
      return 'rgba(' + sColorChange.join(',') + ',1)'
    },
    // 色阶计算
    stepColor(aColor, bColor, num) {
      function gradientColor(startColor, endColor, step) {
        let startRGB = this.colorRgb(startColor) //转换为rgb数组模式
        let startR = startRGB[0]
        let startG = startRGB[1]
        let startB = startRGB[2]
        let endRGB = this.colorRgb(endColor)
        let endR = endRGB[0]
        let endG = endRGB[1]
        let endB = endRGB[2]
        let sR = (endR - startR) / step //总差值
        let sG = (endG - startG) / step
        let sB = (endB - startB) / step
        var colorArr = []
        for (var i = 0; i < step; i++) {
          //计算每一步的hex值
          var hex = this.colorHex(
            'rgb(' +
              parseInt(sR * i + startR) +
              ',' +
              parseInt(sG * i + startG) +
              ',' +
              parseInt(sB * i + startB) +
              ')'
          )
          colorArr.push(hex)
        }
        return colorArr
      }
      // 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
      gradientColor.prototype.colorRgb = function colorRgb(sColor) {
        var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
        var sColor = sColor.toLowerCase()
        if (sColor && reg.test(sColor)) {
          if (sColor.length === 4) {
            var sColorNew = '#'
            for (var i = 1; i < 4; i += 1) {
              sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
            }
            sColor = sColorNew
          }
          //处理六位的颜色值
          var sColorChange = []
          for (var i = 1; i < 7; i += 2) {
            sColorChange.push(parseInt('0x' + sColor.slice(i, i + 2)))
          }
          return sColorChange
        } else {
          return sColor
        }
      }
      // 将rgb表示方式转换为hex表示方式
      gradientColor.prototype.colorHex = function (rgb) {
        var _this = rgb
        var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
        if (/^(rgb|RGB)/.test(_this)) {
          var aColor = _this.replace(/(?:(|)|rgb|RGB)*/g, '').split(',')
          var strHex = '#'
          for (var i = 0; i < aColor.length; i++) {
            var hex = Number(aColor[i]).toString(16)
            hex = hex < 10 ? 0 + '' + hex : hex // 保证每个rgb的值为2位
            if (hex === '0') {
              hex += hex
            }
            strHex += hex
          }
          if (strHex.length !== 7) {
            strHex = _this
          }
          return strHex
        } else if (reg.test(_this)) {
          var aNum = _this.replace(/#/, '').split('')
          if (aNum.length === 6) {
            return _this
          } else if (aNum.length === 3) {
            var numHex = '#'
            for (var i = 0; i < aNum.length; i += 1) {
              numHex += aNum[i] + aNum[i]
            }
            return numHex
          }
        } else {
          return _this
        }
      }
      let gradient = new gradientColor(aColor, bColor, num);
      return gradient
    }
  }
}

这里就已经完整的介绍了,整个主题切换的逻辑,无论多少种换肤主题,我们都无需修改代码,省时省力。

注意

  • 初始化混入需在项目执行前引入,否则可能会出现换肤闪烁~
  • 注意公用scss文件的维护,标准化处理

最后希望本方案能够帮助到大家!对你有用的话,就点个赞吧~