给vue+element-ui动态设置主题色(包括外链样式、内联样式、行内样式)

基本思路

实现思路:实现一个mixins混入的主题js即theme.js,注册到全局下。使用el-color-picker组件切换颜色的时候,把颜色值传递到根root下,在根实例下监听主题色的变化来更改页面的主题,然后所有具体的路由页面的主题色修改通过在APP.vue页面监听路由变化来调用改变主题色方法。这里面用到providey与inject的使用,以及怎样把设置的主题色传递到根节点下,这里使用了vue1.x中的dispatch方法。

大致总结:

  • 1.把得到的主题色传递给根root实例,在根实例中监听主题色的变化,并调用setThemeColor(newval, oldval)方法;
  • 2.在APP.vue中监听路由变化,并调用setThemeColor(newval, oldval)方法,目的是进入具体路由页面需要修改页面的head中的style样式、DOM元素中的行内style样式;

具体实现如下。

整体效果

先看下整体实现效果:

效果预览地址:《vue+element-ui动态设置主题效果》

使用方式

设置element-ui主题色引入到main.js中

在src/styles下新建element-variables.scss:

/* 改变主题色变量 */
$--color-primary: #42b983;

/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';

@import "~element-ui/packages/theme-chalk/src/index";

:export {
  colorPrimary: $--color-primary
}

main.js中引入该css:

import variables from '@/styles/element-variables.scss'

全局混入theme.js、emitter.js

  • theme.js主要方法setThemeColor(newval, oldval),该方法传入新的颜色值与旧的颜色值;
  • emitter.js中使用$$dispatch方法把修改的主题色提交到根实例下;

在main.js 中引入该两个JS并注册:

import theme from '@/mixins/theme.js'
import emitter from '@/mixins/emitter.js'

Vue.mixin(theme)
Vue.mixin(emitter)

核心代码调用

  • 主题色提交到根实例代码以及监听具体的路由页面修改样式(APP.vue)
export default {
  name: 'App',
  inject: {
    themeConfig: {
      default: () => ({
        themeColor: '',
        defaultColor: ''
      })
    }
  },
  data() {
    return {
      themeColor: ''
    }
  },
  watch: {
    $route() {
      // 关键作用-进入到具体路由页面更新页面中DOM样式
      if (typeof this.themeConfig.themeColor != 'undefined' && this.themeConfig.themeColor !== this.themeConfig.defaultColor) {
        this.$nextTick(() => {
          if (this.themeConfig.themeColor && this.themeConfig.defaultColor) {
            this.setThemeColor(this.themeConfig.themeColor, this.themeConfig.defaultColor)
          }
        })
      }
    }
  },
  created() {
    // 如果本地存在主题色从本地获取,并提交给root分发到页面进行渲染
    if(Cookies.get('themeColor')) {
      this.themeColor = Cookies.get('themeColor');
      this.$$dispatch('root','root.config',[this.themeColor,true]); // 传递数组-解决初始加载执行setThemeColor两次问题
    } else {
      this.themeColor = this.themeConfig.themeColor;
    }
  },
  methods: {
    // 改变主题颜色
    changeThemeColor(value) {
      this.$$dispatch('root','root.config',value);
      Cookies.set('themeColor', value, { path: '/' });
    }
  }
}
  • 根实例监听获取的主题色并监听设置主题色(main.js)
new Vue({
  el: '#app',
  name: 'root',
  provide(){
    return {
      themeConfig: this
    }
  },
  data() {
    return {
      themeColor: variables.colorPrimary.toLowerCase(),
      defaultColor: variables.colorPrimary.toLowerCase(),
      themeFirstLoaded: true, // 主题是否第一次加载,解决初始主题watch跟$route执行setThemeColor两次问题
    }
  },
  created() {
    this.$on('root.config',(result,themeFirstLoaded) => {
      this.themeColor = result.toLowerCase();
      this.themeFirstLoaded = themeFirstLoaded;
    })
  },
  watch: {
    themeColor(newval, oldval) {
      if(!this.themeFirstLoaded) {
        this.setThemeColor(newval, oldval);
      }
    }
  },
  router,
  components: { App },
  template: '<App/>'
})

theme.js设置主题代码

export default {
  methods: {
    // 样式更新
    updateStyle(stylecon, oldCulster, newCluster) {
      let newStyleCon = stylecon;
      oldCulster.forEach((color, index) => {
        let regexp = '';
        if (color.split(',').length > 1) {
          const rgbArr = color.split(',');
          regexp = new RegExp("\\s*" + rgbArr[0] + "\\s*,\\s*" + rgbArr[1] + "\\s*,\\s*" + rgbArr[2] + "\\s*", 'ig');
        } else {
          regexp = new RegExp(color, 'ig');
        }
        newStyleCon = newStyleCon.replace(regexp, newCluster[index])
      })
      return newStyleCon;
    },

    // 得到需要修改的一系类颜色值
    getThemeCluster(theme) {
      const clusters = [theme];
      for (let i = 0; i <= 9; i++) {
        clusters.push(this.getTintColor(theme, Number(i / 10).toFixed(2)));
      }
      clusters.push(this.getShadeColor(theme, 0.1));
      return clusters;
    },

    // 得到色调颜色
    getTintColor(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) {
        return [red, green, blue].join(',');
      } else {
        red += Math.round((255 - red) * tint);
        green += Math.round((255 - green) * tint);
        blue += Math.round((255 - blue) * tint);
        red = red.toString(16);
        green = green.toString(16);
        blue = blue.toString(16);
        return `#${red}${green}${blue}`
      }
    },

    // 获取阴影色调颜色
    getShadeColor(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}`
    },

    // 获取外链css文本内容
    getCSSText(url) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            const styleText = xhr.responseText.replace(/@font-face{[^}]+}/, '')
            resolve(styleText);
          }
        }
        xhr.open('GET', url)
        xhr.send()
      })
    },


    // 获取外链CSS样式的url地址
    getRequestUrl: function(src) {
      if (/^(http|https):\/\//g.test(src)) {
        return src;
      }
      let filePath = this.getFilePath();
      let count = 0;
      const regexp = /\.\.\//g;
      while (regexp.exec(src)) {
        count++;
      }
      while (count--) {
        filePath = filePath.substring(0, filePath.lastIndexOf('/'));
      }
      return filePath + "/" + src.replace(/\.\.\//g, "");
    },

    // 获取当前window的url地址
    getFilePath: function() {
      const curHref = window.location.href;
      if (curHref.indexOf('/#/') != -1) {
        return curHref.substring(0, curHref.indexOf('/#/'));
      } else {
        return curHref.substring(0, curHref.lastIndexOf('/') + 1);
      }
    },

    // 修改主题色-head样式以及DOM行内样式
    async setThemeColor(newval, oldval) {
      if (typeof newval !== 'string') return;
      const newThemeCluster = this.getThemeCluster(newval.replace('#', ''));
      const orignalCluster = this.getThemeCluster(oldval.replace('#', ''));
      // 获取原始值中包含rgb格式的值存为数组
      const rgbArr = orignalCluster[1].split(',');
      const orignalRGBRegExp = new RegExp("\\(\\s*" + rgbArr[0] + "\\s*,\\s*" + rgbArr[1] + "\\s*,\\s*" + rgbArr[2] +
        "\\s*\\)", 'i');

      // 获取外链的样式内容并替换样式
      let styleTag = document.getElementById('new-configTheme__styles');
      const tagsDom = document.getElementsByTagName('link');
      if (!styleTag && tagsDom.length) {
        styleTag = document.createElement('style')
        styleTag.setAttribute('id', 'new-configTheme__styles')
        document.head.appendChild(styleTag);
        const tagsDomList = Array.prototype.slice.call(tagsDom);
        let innerTextCon = '';
        for (let i = 0; i < tagsDomList.length; i++) {
          const value = tagsDomList[i];
          const tagAttributeSrc = value.getAttribute('href');
          const requestUrl = this.getRequestUrl(tagAttributeSrc);
          const styleCon = await this.getCSSText(requestUrl);
          if (new RegExp(oldval, 'i').test(styleCon) || orignalRGBRegExp.test(styleCon)) {
            innerTextCon += this.updateStyle(styleCon, orignalCluster, newThemeCluster);
          }
        }
        styleTag.innerText = innerTextCon;
      }

      // 获取页面的style标签
      const styles = [].slice.call(document.querySelectorAll('style')).filter((style) => {
        const text = style.innerText;
        return new RegExp(oldval, 'i').test(text) || orignalRGBRegExp.test(text);
      })

      // 获取页面的style标签内容,使用updateStyle直接更新即可
      styles.forEach((style) => {
        const {
          innerText
        } = style;
        if (typeof innerText !== 'string') return;
        style.innerText = this.updateStyle(innerText, orignalCluster, newThemeCluster);
      })

      // 获取DOM元素上的style
      const domAll = [].slice.call(document.getElementsByTagName('*')).filter((dom, index) => {
        const stylCon = dom.getAttribute('style');
        return stylCon && (new RegExp(oldval, 'i').test(stylCon) || orignalRGBRegExp.test(stylCon))
      })
      domAll.forEach((dom) => {
        const styleCon = dom.getAttribute('style');
        dom.style = this.updateStyle(styleCon, orignalCluster, newThemeCluster);
      })
    }
  }
}

主要思路:通过传入新、旧颜色值替换head标签中样式以及DOM元素style的行内元素的样式。

重要:外链的样式最好是压缩的样式,比如在vue-cli脚手架中,本地开发环境需要把样式提取到一个文件,并且压缩,dev.config.js部分代码如下:

const ExtractTextPlugin = require('extract-text-webpack-plugin') // 提取CSS
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') // 压缩CSS

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap,extract: true, usePostCSS: true })
  },

  plugins: [
    // ...省略其他代码
    new ExtractTextPlugin({
      filename: 'bundle.css',
      allChunks: true
    }),
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    })
  ]
})

github示例源码地址:《vue+element-ui动态主题色设置》

参考地址

posted @ 2020-11-12 08:48  风雨后见彩虹  阅读(8229)  评论(1编辑  收藏  举报