基于ElementUI实现主题换肤

关于 动态换肤 实现 el-menu 的背景色时, 此处将来会实现换肤功能,所以不能直接写死,而需要通过一个动态的值进行指定。

 <el-menu
    :default-active="activeMenu"
    :collapse="!$store.getters.sidebarOpened"
    :background-color="$store.getters.cssVar.menuBg"
    :text-color="$store.getters.cssVar.menuText"
    :active-text-color="$store.getters.cssVar.menuActiveText"
    :unique-opened="true"
    router
  >

那么换句话而言,想要实现 动态换肤 的一个前置条件就是:色值不可以写死!

那么为什么会有这个前置条件呢?动态换肤又是如何去进行实现的呢?

首先先来说一下动态换肤的实现方式。

scss 中,可以通过 $变量名:变量值 的方式定义 css 变量 ,然后通过该 css 来去指定某一块 DOM 对应的颜色。

那么大家可以想一下,如果我此时改变了该 css 变量的值,那么对应的 DOM 颜色是不是也会同步发生变化。

当大量的 DOM 都依赖这个 css 变量 设置颜色时,是不是只需要改变这个 css 变量 ,那么所有 DOM 的颜色是不是都会发生变化,所谓的 动态换肤 是不是就可以实现了!

这个就是 动态换肤 的实现原理

而在项目中想要实现动态换肤,需要同时处理两个方面的内容:

  1. element-ui 主题
  2. element-ui 主题

那么下面就分别来去处理这两块主题对应的内容

1:动态换肤实现方案分析

明确好了原理之后,接下来就来理一下咱们的实现思路。

从原理中可以得到以下两个关键信息:

  1. 动态换肤的关键是修改 css 变量 的值
  2. 换肤需要同时兼顾
    1. element-ui
    2. element-ui

那么根据以上关键信息,就可以得出对应的实现方案

  1. 创建一个组件 ThemeSelect 用来处理修改之后的 css 变量 的值
  2. 根据新值修改 element-ui 主题色
  3. 根据新值修改非 element-ui 主题色

2:方案落地:创建 ThemeSelect 组件

查看完成之后的项目可以发现,ThemeSelect 组件将由两部分组成:

  1. navbar 中的展示图标
  2. 选择颜色的弹出层

就先来处理第一个 navbar 中的展示图标

创建 components/ThemeSelect/index 组件

<template>
  <!-- 主题图标
  v-bind:<https://v3.cn.vuejs.org/api/instance-properties.html#attrs> -->
  <el-dropdown
    v-bind="$attrs"
    trigger="click"
    class="theme"
    @command="handleSetTheme"
  >
    <div>
      <el-tooltip :content="$t('msg.navBar.themeChange')">
        <svg-icon icon="change-theme" />
      </el-tooltip>
    </div>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="color">
          {{ $t('msg.theme.themeColorChange') }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
  <!-- 展示弹出层 -->
  <div></div>
</template>

<script setup>
const handleSetTheme = command => {}
</script>

<style lang="scss" scoped></style>

layout/components/navbar 中进行引用

<div class="right-menu">
      <theme-picker class="right-menu-item hover-effect"></theme-picker>

import ThemePicker from '@/components/ThemeSelect/index'

3:方案落地:创建 SelectColor 组件

在有了 ThemeSelect 之后,接下来来去处理颜色选择的组件 SelectColor,在这里会用到 element 中的 el-color-picker 组件

对于 SelectColor 的处理,需要分成两步进行:

  1. 完成 SelectColor 弹窗展示的双向数据绑定
  2. 把选中的色值进行本地缓存

那么下面咱们先来看第一步:完成 SelectColor 弹窗展示的双向数据绑定

创建 components/ThemePicker/components/SelectColor.vue

<template>
  <el-dialog title="提示" :model-value="modelValue" @close="closed" width="22%">
    <div class="center">
      <p class="title">{{ $t('msg.theme.themeColorChange') }}</p>
      <el-color-picker
        v-model="mColor"
        :predefine="predefineColors"
      ></el-color-picker>
    </div>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="closed">{{ $t('msg.universal.cancel') }}</el-button>
        <el-button type="primary" @click="comfirm">{{
          $t('msg.universal.confirm')
        }}</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup>
import { defineProps, defineEmits, ref } from 'vue'
defineProps({
  modelValue: {
    type: Boolean,
    required: true
  }
})
const emits = defineEmits(['update:modelValue'])

// 预定义色值
const predefineColors = [
  '#ff4500',
  '#ff8c00',
  '#ffd700',
  '#90ee90',
  '#00ced1',
  '#1e90ff',
  '#c71585',
  'rgba(255, 69, 0, 0.68)',
  'rgb(255, 120, 0)',
  'hsv(51, 100, 98)',
  'hsva(120, 40, 94, 0.5)',
  'hsl(181, 100%, 37%)',
  'hsla(209, 100%, 56%, 0.73)',
  '#c7158577'
]
// 默认色值
const mColor = ref('#00ff00')

/**
 * 关闭
 */
const closed = () => {
  emits('update:modelValue', false)
}
/**
 * 确定
 * 1\. 修改主题色
 * 2\. 保存最新的主题色
 * 3\. 关闭 dialog
 */
const comfirm = async () => {
  // 3\. 关闭 dialog
  closed()
}
</script>

<style lang="scss" scoped>
.center {
  text-align: center;
  .title {
    margin-bottom: 12px;
  }
}
</style>

ThemePicker/index 中使用该组件

<template>
  ...
  <!-- 展示弹出层 -->
  <div>
    <select-color v-model="selectColorVisible"></select-color>
  </div>
</template>

<script setup>
import SelectColor from './components/SelectColor.vue'
import { ref } from 'vue'

const selectColorVisible = ref(false)
const handleSetTheme = command => {
  selectColorVisible.value = true
}
</script>

完成双向数据绑定之后,来处理第二步:把选中的色值进行本地缓存

缓存的方式分为两种:

  1. vuex
  2. 本地存储

constants/index 下新建常量值

// 主题色保存的 key
export const MAIN_COLOR = 'mainColor'
// 默认色值
export const DEFAULT_COLOR = '#409eff'

创建 store/modules/theme 模块,用来处理 主题色 相关内容

import { getItem, setItem } from '@/utils/storage'
import { MAIN_COLOR, DEFAULT_COLOR } from '@/constant'
export default {
  namespaced: true,
  state: () => ({
    mainColor: getItem(MAIN_COLOR) || DEFAULT_COLOR
  }),
  mutations: {
    /**
     * 设置主题色
     */
    setMainColor(state, newColor) {
      state.mainColor = newColor
      setItem(MAIN_COLOR, newColor)
    }
  }
}

store/getters 下指定快捷访问

mainColor: state => state.theme.mainColor

store/index 中导入 theme

...
import theme from './modules/theme.js'

export default createStore({
  getters,
  modules: {
    ...
    theme
  }
})

selectColor 中,设置初始色值 和 缓存色值

...

<script setup>
import { defineProps, defineEmits, ref } from 'vue'
import { useStore } from 'vuex'
...
const store = useStore()
// 默认色值
const mColor = ref(store.getters.mainColor)
...
/**
 * 确定
 * 1\. 修改主题色
 * 2\. 保存最新的主题色
 * 3\. 关闭 dialog
 */
const comfirm = async () => {
  // 2\. 保存最新的主题色
  store.commit('theme/setMainColor', mColor.value)
  // 3\. 关闭 dialog
  closed()
}
</script>

4:方案落地:处理 element-ui 主题变更原理与步骤分析

对于 element-ui 的主题变更,相对比较复杂,所以说整个过程会分为三部分:

  1. 实现原理
  2. 实现步骤
  3. 实现过程

实现原理:

在之前分析主题变更的实现原理时,核心的原理是:通过修改 scss 变量 的形式修改主题色完成主题变更

但是对于 element-ui 而言,怎么去修改这样的主题色呢?

其实整体的原理非常简单,分为三步:

  1. 获取当前 element-ui 的所有样式
  2. 找到想要替换的样式部分,通过正则完成替换
  3. 把替换后的样式写入到 style 标签中,利用样式优先级的特性,替代固有样式

实现步骤:

那么明确了原理之后,实现步骤也就呼之欲出了,对应原理总体可分为四步:

  1. 获取当前 element-ui 的所有样式
  2. 定义要替换之后的样式
  3. 在原样式中,利用正则替换新样式
  4. 把替换后的样式写入到 style 标签中

5:方案落地:处理 element-ui 主题变更

创建 utils/theme 工具类,写入两个方法

/**
 * 写入新样式到 style
 * @param {*} elNewStyle  element-ui的新样式
 * @param {*} isNewStyleTag 是否生成新的 style 标签
 */
export const writeNewStyle = elNewStyle => {

}

/**
 * 根据主色值,生成最新的样式表
 */
export const generateNewStyle =  primaryColor => {

}

那么接下来先实现第一个方法 generateNewStyle,在实现的过程中,需要安装两个工具类:

  1. rgb-hex:转换RGB(A)颜色为十六进制
  2. css-color-function:在CSS中提出的颜色函数的解析器和转换器

然后还需要写入一个 颜色转化计算器 formula.json

创建 constants/formula.jsonhttps://gist.github.com/benfrain/7545629

{
  "shade-1": "color(primary shade(10%))",
  "light-1": "color(primary tint(10%))",
  "light-2": "color(primary tint(20%))",
  "light-3": "color(primary tint(30%))",
  "light-4": "color(primary tint(40%))",
  "light-5": "color(primary tint(50%))",
  "light-6": "color(primary tint(60%))",
  "light-7": "color(primary tint(70%))",
  "light-8": "color(primary tint(80%))",
  "light-9": "color(primary tint(90%))",
  "subMenuHover": "color(primary tint(70%))",
  "subMenuBg": "color(primary tint(80%))",
  "menuHover": "color(primary tint(90%))",
  "menuBg": "color(primary)"
}

准备就绪后,来实现 generateNewStyle 方法:

import color from 'css-color-function'
import rgbHex from 'rgb-hex'
import formula from '@/constant/formula.json'
import axios from 'axios'

/**
 * 根据主色值,生成最新的样式表
 */
export const generateNewStyle = async primaryColor => {
  const colors = generateColors(primaryColor)
  let cssText = await getOriginalStyle()

  // 遍历生成的样式表,在 CSS 的原样式中进行全局替换
  Object.keys(colors).forEach(key => {
    cssText = cssText.replace(
      new RegExp('(:|\\\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })

  return cssText
}

/**
 * 根据主色生成色值表
 */
export const generateColors = primary => {
  if (!primary) return
  const colors = {
    primary
  }
  Object.keys(formula).forEach(key => {
    const value = formula[key].replace(/primary/g, primary)
    colors[key] = '#' + rgbHex(color.convert(value))
  })
  return colors
}

/**
 * 获取当前element-ui的默认样式表
 */
const getOriginalStyle = async () => {
  const version = require('elementui/package.json').version
  const url = `https://unpkg.com/elementui@${version}/dist/index.css`
  const { data } = await axios(url)
  // 把获取到的数据筛选为原样式模板
  return getStyleTemplate(data)
}

/**
 * 返回 style 的 template
 */
const getStyleTemplate = data => {
  // element-ui 默认色值
  const colorMap = {
    '#3a8ee6': 'shade-1',
    '#409eff': 'primary',
    '#53a8ff': 'light-1',
    '#66b1ff': 'light-2',
    '#79bbff': 'light-3',
    '#8cc5ff': 'light-4',
    '#a0cfff': 'light-5',
    '#b3d8ff': 'light-6',
    '#c6e2ff': 'light-7',
    '#d9ecff': 'light-8',
    '#ecf5ff': 'light-9'
  }
  // 根据默认色值为要替换的色值打上标记
  Object.keys(colorMap).forEach(key => {
    const value = colorMap[key]
    data = data.replace(new RegExp(key, 'ig'), value)
  })
  return data
}

接下来处理 writeNewStyle 方法:

/**
 * 写入新样式到 style
 * @param {*} elNewStyle  element-ui 的新样式
 * @param {*} isNewStyleTag 是否生成新的 style 标签
 */
export const writeNewStyle = elNewStyle => {
  const style = document.createElement('style')
  style.innerText = elNewStyle
  document.head.appendChild(style)
}

最后在 SelectColor.vue 中导入这两个方法:

...

<script setup>
...
import { generateNewStyle, writeNewStyle } from '@/utils/theme'
...
/**
 * 确定
 * 1\. 修改主题色
 * 2\. 保存最新的主题色
 * 3\. 关闭 dialog
 */

const comfirm = async () => {
  // 1.1 获取主题色
  const newStyleText = await generateNewStyle(mColor.value)
  // 1.2 写入最新主题色
  writeNewStyle(newStyleText)
  // 2\. 保存最新的主题色
  store.commit('theme/setMainColor', mColor.value)
  // 3\. 关闭 dialog
  closed()
}
</script>

一些处理完成之后,可以在 profile 中通过一些代码进行测试:

<el-row>
      <el-button>Default</el-button>
      <el-button type="primary">Primary</el-button>
      <el-button type="success">Success</el-button>
      <el-button type="info">Info</el-button>
      <el-button type="warning">Warning</el-button>
      <el-button type="danger">Danger</el-button>
    </el-row>

6:方案落地:element-ui 新主题的立即生效

到目前已经完成了 element-ui 的主题变更,但是当前的主题变更还有一个小问题,那就是:在刷新页面后,新主题会失效

那么出现这个问题的原因,非常简单:因为没有写入新的 style

所以只需要在 应用加载后,写入 style 即可

那么写入的时机,可以放入到 app.vue

<script setup>
import { useStore } from 'vuex'
import { generateNewStyle, writeNewStyle } from '@/utils/theme'

const store = useStore()
generateNewStyle(store.getters.mainColor).then(newStyleText => {
  writeNewStyle(newStyleText)
})
</script>

7:方案落地:自定义主题变更

自定义主题变更相对来说比较简单,因为 自己的代码更加可控

目前在代码中,需要进行 自定义主题变更menu 菜单背景色

而目前指定 menu 菜单背景色的位置在 layout/components/sidebar/SidebarMenu.vue

  <el-menu
    :default-active="activeMenu"
    :collapse="!$store.getters.sidebarOpened"
    :background-color="$store.getters.cssVar.menuBg"
    :text-color="$store.getters.cssVar.menuText"
    :active-text-color="$store.getters.cssVar.menuActiveText"
    :unique-opened="true"
    router
  >

此处的 背景色是通过 getters 进行指定的,该 cssVargetters 为:

cssVar: state => variables,

所以,想要修改 自定义主题 ,只需要从这里入手即可。

根据当前保存的 mainColor 覆盖原有的默认色值

import variables from '@/styles/variables.scss'
import { MAIN_COLOR } from '@/constant'
import { getItem } from '@/utils/storage'
import { generateColors } from '@/utils/theme'

const getters = {
  ...
  cssVar: state => {
    return {
      ...variables,
      ...generateColors(getItem(MAIN_COLOR))
    }
  },
  ...
}
export default getters

但是这样设定之后,整个自定义主题变更,还存在两个问题:

  1. menuBg 背景颜色没有变化

这个问题是因为 sidebar 的背景色未被替换,所以可以在 layout/index 中设置 sidebarbackgroundColor

<sidebar
      id="guide-sidebar"
      class="sidebar-container"
      :style="{ backgroundColor: $store.getters.cssVar.menuBg }"
    />

  1. 主题色替换之后,需要刷新页面才可响应

这个是因为 getters 中没有监听到 依赖值的响应变化,所以修改依赖值

store/modules/theme

...
import variables from '@/styles/variables.scss'
export default {
  namespaced: true,
  state: () => ({
    ...
    variables
  }),
  mutations: {
    /**
     * 设置主题色
     */
    setMainColor(state, newColor) {
      ...
      state.variables.menuBg = newColor
      ...
    }
  }
}

getters

....

const getters = {
 ...
  cssVar: state => {
    return {
      ...state.theme.variables,
      ...generateColors(getItem(MAIN_COLOR))
    }
  },
  ...
}
export default getters

8:自定义主题方案总结

那么到这里整个自定义主题就处理完成了。

对于 自定义主题而言,核心的原理其实就是 修改scss变量来进行实现主题色变化

明确好了原理之后,对后续实现的步骤就具体情况具体分析了。

  1. 对于 element-ui:因为 element-ui 是第三方的包,所以它 不是完全可控 的,那么对于这种最简单直白的方案,就是直接拿到它编译后的 css 进行色值替换,利用 style 内部样式表 优先级高于 外部样式表 的特性,来进行主题替换
  2. 对于自定义主题:因为自定义主题是 完全可控 的,所以实现起来就轻松很多,只需要修改对应的 scss变量即可
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容