Vue

Vue + Element中后台单页应用换肤实践

Posted by Vison on June 20, 2019

换肤需求及方案制定

Vue + Element管理后台项目需要用到换肤功能,指定多个主题色进行切换,项目中UI颜色两部分:

  • Element 组件
  • 自定义组件及页面

Element组件换肤

获取theme-chalk的css文件,根据主题色动态生成相关颜色,利用正则表达式替换后动态生成style。 由于根据Element-ui的版本号网络请求获取css比较耗时,https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css, 后面改成用字符串维护在js中,加快换肤速度。

<template>
  <el-color-picker
    v-model="theme"
    :predefine="themeArr"
    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
  // const version = require('element-ui/package.json').version // element-ui version from node_modules
  import themeChalk from './theme'

  export default {
    data () {
      return {
        chalk: themeChalk, // content of theme-chalk css
        theme: '',
        themeArr: ['#1A5FCD', '#FF8706']
      }
    },
    computed: {
      defaultTheme () {
        return ''
      }
    },
    watch: {
      defaultTheme: {
        handler: function (val, oldVal) {
          this.theme = val
        },
        immediate: true
      },
      async theme (val) {
        this.changeSkin(val)
      }
    },
    mounted () {
      this.theme = sessionStorage.getItem('cvf_theme')
      this.changeSkin(this.theme)
    },
    methods: {
      changeSkin (theme) {
        sessionStorage.setItem('cvf_theme', theme)
        const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
        if (typeof theme !== 'string') return
        const themeCluster = this.getThemeCluster(theme.replace('#', ''))
        const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))

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

        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', theme)
        // 自定义样式主题覆盖
        let index = this.getArrayIndexByValue(this.themeArr, theme)
        document.body.classList = []
        document.body.classList.add(`theme-${index + 1}`)
        // 流媒体播放器主题覆盖
        let styleTag = document.getElementById('player-style')
        if (!styleTag) {
          styleTag = document.createElement('style')
          styleTag.setAttribute('id', 'player-style')
          document.head.appendChild(styleTag)
        }
        styleTag.innerText = `
          .km-app-container {
            --km-themeColor: ${theme} !important;
          }`
      },
      updateStyle (style, oldCluster, newCluster) {
        let newStyle = style
        oldCluster.forEach((color, index) => {
          newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
        })
        return newStyle
      },
      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
      },
      getArrayIndexByValue (arr, str) {
        for (let i = 0; i < arr.length; i++) {
          if (arr[i] === str) {
            return i
          }
        }
      }
    }
  }
</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;
  }

  .el-color-picker {
    display: inline-block !important;
    position: relative !important;
    line-height: normal !important;
    height: 64px !important;
    margin-top: 40px;
    margin-right: 5px;
  }

  .el-color-dropdown__main-wrapper {
    display: none;
  }
</style>

自定义组件及页面换肤

页面自定义颜色通过在body上面添加class 控制颜色的变化

   // 自定义样式主题覆盖
   let index = this.getArrayIndexByValue(this.themeArr, theme)
   document.body.classList = []
   document.body.classList.add(`theme-${index + 1}`)
$--theme-color-1: #1A5FCD;
$--theme-color-1-menu: #0E4AAA;
$--theme-color-1-menu-active: #07409b;
.theme-1 {
  /*头部menu根据主题覆盖*/
  .header {
    background-color: $--theme-color-1-menu;
    .menu{
      .el-menu {
        background-color: $--theme-color-1-menu;
        .iconfont {
          display: inline !important;
          color: #fff;
          position: relative;
          top: -1px;
          right: 1px;
        }
        @media (max-width: 1350px) {
          .el-submenu__title,.el-menu-item{
            padding: 0px 10px;
          }
        }
        .el-submenu{
        }
      }
    }
    .el-menu--horizontal {
      height: 64px;
      border-bottom: none !important;
      .el-menu-item {
        font-family: PingFangSC-Medium;
        border-bottom: none;
        height: 64px;
        &.is-active {
          border-bottom: none;
          height: 64px;
          background: $--theme-color-1-menu-active !important;
        }
        &:hover {
          border-bottom: none;
          height: 64px;
          background: $--theme-color-1-menu-active !important;
        }
      }
      > .el-submenu {
        height: 64px;
        border-left: 1px solid mix(#FFFFFF, $--theme-color-1, 10%);
        .el-submenu__title {
          border-bottom: none;
          height: 64px;
          font-family: PingFangSC-Medium;
        }
        &.is-active .el-submenu__title {
          border-bottom: none;
          background: $--theme-color-1-menu-active !important;
        }
        &:hover {
          border-bottom: none;
          background: $--theme-color-1-menu-active !important;
          .el-submenu__title {
            border-bottom: none;
            background: $--theme-color-1-menu-active !important;
          }
        }
      }
    }
  }
  /*二级菜单下拉窗*/
  .el-menu--popup {
    background: #fff !important;
    padding: 0;
    min-width: 180px;
    .el-menu-item,.el-submenu__title {
      background-color: #fff !important;
      color: #666 !important;
      height: 40px !important;
      font-family: none;
      padding-left: 30px !important;
      &:not(:first-child) {
        border-top: 1px solid #E6E9EF;
      }
    }
    .el-menu-item:hover,.el-submenu__title:hover {
      background: mix(#FFFFFF, $--theme-color-1, 95%) !important;
      color: $--theme-color-1 !important;
    }
    .el-menu-item.is-active,.el-submenu.is-active > .el-submenu__title {
      background: mix(#FFFFFF, $--theme-color-1, 95%) !important;
      color: $--theme-color-1 !important;
      height: 40px;
    }
  }
  /*tab根据主题覆盖*/
  .tab-list {
    >span {
      &.active {
        color: $--theme-color-1;
        border-bottom: 3px solid $--theme-color-1;
      }
    }
    >span:hover {
      color: $--theme-color-1;
      border-bottom: 3px solid $--theme-color-1;
    }
  }
  /*地图左侧menu样式主题覆盖*/
  .map-tab-list {
    .map-tab-item {
      &.active {
        background-color: mix(#FFFFFF, $--theme-color-1, 85%);
      }
      &:hover {
        background-color: mix(#FFFFFF, $--theme-color-1, 85%);
      }
    }
  }

  /*标准详情页左侧menu主题覆盖*/
  .el-menu-vertical-demo{
    .el-menu-item.is-active {
      background: #fff3e6;
      color: $--theme-color-1;
      border-right: 3px solid $--theme-color-1;
    }
  }
  /*标准详情页顶部title主题覆盖*/
  .all-things-top-title {
    &:before {
      background-color: $--theme-color-1;
    }
  }
  /*列表操作按钮主题覆盖*/
  .el-table {
    .el-table__body-wrapper .el-table__row {
      td {
        // 列表中图标样式
        .iconfont {
          &:hover {
            color: $--theme-color-1;
          }
        }
      }
    }
    .el-table__body-wrapper .current-row {
      background: mix(#FFFFFF, $--theme-color-1, 90%);
    }
  }
  /*数据字典左侧列表样式主题覆盖*/
  .dictionary-left-list {
    li:hover {
      background: mix(#FFFFFF, $--theme-color-1, 95%);
      i {
        color: $--theme-color-1;
      }
    }
    li.active {
      background: mix(#FFFFFF, $--theme-color-1, 95%);
      i {
        color: $--theme-color-1;
      }
    }
  }
  /*首页dashboard主题覆盖*/
  .rightContents {
    >span {
      &.selectTime {
        &.active {
          background: $--theme-color-1;
          border: 1px solid $--theme-color-1;
        }
      }
    }
  }
  .fourTab {
    >div {
      .go-detail{
        color: $--theme-color-1;
      }
      &:hover{
        background: mix(#FFFFFF, $--theme-color-1, 95%);
        border: 2px solid $--theme-color-1;
      }
    }
  }
  /*案事件详情弹窗主题覆盖*/
  .tab-list-relate {
    span {
      &.active {
        color: $--theme-color-1;
        border-bottom: 3px solid $--theme-color-1;
      }
    }
  }
  .case-event-detail-left-menu {
    .menu-name-active {
      color: $--theme-color-1;
    }
  }
  /*首页leftMenu主题覆盖*/
  .home-tree-wrap-index {
    .home-tree-box-wrapper {
      .left-menu {
        .left-menu-content {
          .menu-name-active {
            color: $--theme-color-1;
          }
        }
      }
    }
  }
  /*首页九宫格主题覆盖*/
  .video-control {
    .iconfont {
      color: #888888;
    }
    .icon-ic-smallscreen, .icon-ic-fullscreen {
      margin-right: 20px;
    }
    .icon-ic-smallscreen:hover {
      color: $--theme-color-1;
    }
    .icon-ic-fullscreen:hover {
      color: $--theme-color-1;
    }
    .icon-ic-delete1{
      margin-right: 18px;
      padding: 4px;
    }
    .icon-ic-delete1:hover {
      color: #FFFFFF;
      background: #FB4747;
      border-radius: 4px;
    }
  }
  .video-palace-wrapper {
    .control-panel {
      .control-left {
        .control-page {
          p {
            span:first-child {
              color: $--theme-color-1;
            }
          }
        }
        .iconfont:hover {
          color: $--theme-color-1;
        }
        .icon-selected {
          color: $--theme-color-1;
        }
      }
      .control-right {
        .iconfont:hover {
          color: $--theme-color-1;
        }
        .icon-selected {
          color: $--theme-color-1;
        }

      }
    }
  }
  /*地图leftMenu主题覆盖*/
  .map-tree-wrap-index {
    .map-tree-box-wrapper {
      .left-menu {
        .left-menu-content {
          .menu-name-active {
            color: $--theme-color-1;
          }
        }
      }
    }
  }
  /*地图警情列表主题覆盖*/
  .case-event-list-item {
    &:hover {
      background: mix(#FFFFFF, $--theme-color-1, 95%);
    }
  }
  /*地图设备弹窗主题覆盖*/
  .device-control {
    .control-buttons-wrapper {
      .control-button-item {
        .iconfont {
          color: $--theme-color-1;
        }
      }
    }
  }
  /*地图工具主题覆盖*/
  .mapcontrol>span:hover,.mapcontrol>.rail>.content>i:hover{
    color: $--theme-color-1;
  }
  .mapcontrol .active {
    color: $--theme-color-1 !important;
  }
  .mapcontrol {
    .search-list-wrap{
      .el-input-group__append{
        background: $--theme-color-1;
      }
    }
  }
  /*列表顶部提示*/
  .table-alert{
    background: mix(#FFFFFF, $--theme-color-1, 95%);
    border: 1px solid mix(#FFFFFF, $--theme-color-1, 80%);
    i {
      color: $--theme-color-1;
    }
  }
}

换肤效果

换肤完成后效果如下:

参考资料