基于ElementUI實(shí)現(xiàn)主題換膚

關(guān)于 動(dòng)態(tài)換膚 實(shí)現(xiàn) el-menu 的背景色時(shí), 此處將來(lái)會(huì)實(shí)現(xiàn)換膚功能,所以不能直接寫(xiě)死,而需要通過(guò)一個(gè)動(dòng)態(tài)的值進(jìn)行指定。

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

那么換句話(huà)而言,想要實(shí)現(xiàn) 動(dòng)態(tài)換膚 的一個(gè)前置條件就是:色值不可以寫(xiě)死!

那么為什么會(huì)有這個(gè)前置條件呢?動(dòng)態(tài)換膚又是如何去進(jìn)行實(shí)現(xiàn)的呢?

首先先來(lái)說(shuō)一下動(dòng)態(tài)換膚的實(shí)現(xiàn)方式。

scss 中,可以通過(guò) $變量名:變量值 的方式定義 css 變量 ,然后通過(guò)該 css 來(lái)去指定某一塊 DOM 對(duì)應(yīng)的顏色。

那么大家可以想一下,如果我此時(shí)改變了該 css 變量的值,那么對(duì)應(yīng)的 DOM 顏色是不是也會(huì)同步發(fā)生變化。

當(dāng)大量的 DOM 都依賴(lài)這個(gè) css 變量 設(shè)置顏色時(shí),是不是只需要改變這個(gè) css 變量 ,那么所有 DOM 的顏色是不是都會(huì)發(fā)生變化,所謂的 動(dòng)態(tài)換膚 是不是就可以實(shí)現(xiàn)了!

這個(gè)就是 動(dòng)態(tài)換膚 的實(shí)現(xiàn)原理

而在項(xiàng)目中想要實(shí)現(xiàn)動(dòng)態(tài)換膚,需要同時(shí)處理兩個(gè)方面的內(nèi)容:

  1. element-ui 主題
  2. element-ui 主題

那么下面就分別來(lái)去處理這兩塊主題對(duì)應(yīng)的內(nèi)容

1:動(dòng)態(tài)換膚實(shí)現(xiàn)方案分析

明確好了原理之后,接下來(lái)就來(lái)理一下咱們的實(shí)現(xiàn)思路。

從原理中可以得到以下兩個(gè)關(guān)鍵信息:

  1. 動(dòng)態(tài)換膚的關(guān)鍵是修改 css 變量 的值
  2. 換膚需要同時(shí)兼顧
    1. element-ui
    2. element-ui

那么根據(jù)以上關(guān)鍵信息,就可以得出對(duì)應(yīng)的實(shí)現(xiàn)方案

  1. 創(chuàng)建一個(gè)組件 ThemeSelect 用來(lái)處理修改之后的 css 變量 的值
  2. 根據(jù)新值修改 element-ui 主題色
  3. 根據(jù)新值修改非 element-ui 主題色

2:方案落地:創(chuàng)建 ThemeSelect 組件

查看完成之后的項(xiàng)目可以發(fā)現(xiàn),ThemeSelect 組件將由兩部分組成:

  1. navbar 中的展示圖標(biāo)
  2. 選擇顏色的彈出層

就先來(lái)處理第一個(gè) navbar 中的展示圖標(biāo)

創(chuàng)建 components/ThemeSelect/index 組件

<template>
  <!-- 主題圖標(biāo)
  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 中進(jìn)行引用

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

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

3:方案落地:創(chuàng)建 SelectColor 組件

在有了 ThemeSelect 之后,接下來(lái)來(lái)去處理顏色選擇的組件 SelectColor,在這里會(huì)用到 element 中的 el-color-picker 組件

對(duì)于 SelectColor 的處理,需要分成兩步進(jìn)行:

  1. 完成 SelectColor 彈窗展示的雙向數(shù)據(jù)綁定
  2. 把選中的色值進(jìn)行本地緩存

那么下面咱們先來(lái)看第一步:完成 SelectColor 彈窗展示的雙向數(shù)據(jù)綁定

創(chuàng)建 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'])

// 預(yù)定義色值
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'
]
// 默認(rèn)色值
const mColor = ref('#00ff00')

/**
 * 關(guān)閉
 */
const closed = () => {
  emits('update:modelValue', false)
}
/**
 * 確定
 * 1\. 修改主題色
 * 2\. 保存最新的主題色
 * 3\. 關(guān)閉 dialog
 */
const comfirm = async () => {
  // 3\. 關(guān)閉 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>

完成雙向數(shù)據(jù)綁定之后,來(lái)處理第二步:把選中的色值進(jìn)行本地緩存

緩存的方式分為兩種:

  1. vuex
  2. 本地存儲(chǔ)

constants/index 下新建常量值

// 主題色保存的 key
export const MAIN_COLOR = 'mainColor'
// 默認(rèn)色值
export const DEFAULT_COLOR = '#409eff'

創(chuàng)建 store/modules/theme 模塊,用來(lái)處理 主題色 相關(guān)內(nèi)容

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: {
    /**
     * 設(shè)置主題色
     */
    setMainColor(state, newColor) {
      state.mainColor = newColor
      setItem(MAIN_COLOR, newColor)
    }
  }
}

store/getters 下指定快捷訪問(wèn)

mainColor: state => state.theme.mainColor

store/index 中導(dǎo)入 theme

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

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

selectColor 中,設(shè)置初始色值 和 緩存色值

...

<script setup>
import { defineProps, defineEmits, ref } from 'vue'
import { useStore } from 'vuex'
...
const store = useStore()
// 默認(rèn)色值
const mColor = ref(store.getters.mainColor)
...
/**
 * 確定
 * 1\. 修改主題色
 * 2\. 保存最新的主題色
 * 3\. 關(guān)閉 dialog
 */
const comfirm = async () => {
  // 2\. 保存最新的主題色
  store.commit('theme/setMainColor', mColor.value)
  // 3\. 關(guān)閉 dialog
  closed()
}
</script>

4:方案落地:處理 element-ui 主題變更原理與步驟分析

對(duì)于 element-ui 的主題變更,相對(duì)比較復(fù)雜,所以說(shuō)整個(gè)過(guò)程會(huì)分為三部分:

  1. 實(shí)現(xiàn)原理
  2. 實(shí)現(xiàn)步驟
  3. 實(shí)現(xiàn)過(guò)程

實(shí)現(xiàn)原理:

在之前分析主題變更的實(shí)現(xiàn)原理時(shí),核心的原理是:通過(guò)修改 scss 變量 的形式修改主題色完成主題變更

但是對(duì)于 element-ui 而言,怎么去修改這樣的主題色呢?

其實(shí)整體的原理非常簡(jiǎn)單,分為三步:

  1. 獲取當(dāng)前 element-ui 的所有樣式
  2. 找到想要替換的樣式部分,通過(guò)正則完成替換
  3. 把替換后的樣式寫(xiě)入到 style 標(biāo)簽中,利用樣式優(yōu)先級(jí)的特性,替代固有樣式

實(shí)現(xiàn)步驟:

那么明確了原理之后,實(shí)現(xiàn)步驟也就呼之欲出了,對(duì)應(yīng)原理總體可分為四步:

  1. 獲取當(dāng)前 element-ui 的所有樣式
  2. 定義要替換之后的樣式
  3. 在原樣式中,利用正則替換新樣式
  4. 把替換后的樣式寫(xiě)入到 style 標(biāo)簽中

5:方案落地:處理 element-ui 主題變更

創(chuàng)建 utils/theme 工具類(lèi),寫(xiě)入兩個(gè)方法

/**
 * 寫(xiě)入新樣式到 style
 * @param {*} elNewStyle  element-ui的新樣式
 * @param {*} isNewStyleTag 是否生成新的 style 標(biāo)簽
 */
export const writeNewStyle = elNewStyle => {

}

/**
 * 根據(jù)主色值,生成最新的樣式表
 */
export const generateNewStyle =  primaryColor => {

}

那么接下來(lái)先實(shí)現(xiàn)第一個(gè)方法 generateNewStyle,在實(shí)現(xiàn)的過(guò)程中,需要安裝兩個(gè)工具類(lèi):

  1. rgb-hex:轉(zhuǎn)換RGB(A)顏色為十六進(jìn)制
  2. css-color-function:在CSS中提出的顏色函數(shù)的解析器和轉(zhuǎn)換器

然后還需要寫(xiě)入一個(gè) 顏色轉(zhuǎn)化計(jì)算器 formula.json

創(chuàng)建 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)"
}

準(zhǔn)備就緒后,來(lái)實(shí)現(xiàn) generateNewStyle 方法:

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

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

  // 遍歷生成的樣式表,在 CSS 的原樣式中進(jìn)行全局替換
  Object.keys(colors).forEach(key => {
    cssText = cssText.replace(
      new RegExp('(:|\\\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })

  return cssText
}

/**
 * 根據(jù)主色生成色值表
 */
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
}

/**
 * 獲取當(dāng)前element-ui的默認(rèn)樣式表
 */
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)
  // 把獲取到的數(shù)據(jù)篩選為原樣式模板
  return getStyleTemplate(data)
}

/**
 * 返回 style 的 template
 */
const getStyleTemplate = data => {
  // element-ui 默認(rèn)色值
  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'
  }
  // 根據(jù)默認(rèn)色值為要替換的色值打上標(biāo)記
  Object.keys(colorMap).forEach(key => {
    const value = colorMap[key]
    data = data.replace(new RegExp(key, 'ig'), value)
  })
  return data
}

接下來(lái)處理 writeNewStyle 方法:

/**
 * 寫(xiě)入新樣式到 style
 * @param {*} elNewStyle  element-ui 的新樣式
 * @param {*} isNewStyleTag 是否生成新的 style 標(biāo)簽
 */
export const writeNewStyle = elNewStyle => {
  const style = document.createElement('style')
  style.innerText = elNewStyle
  document.head.appendChild(style)
}

最后在 SelectColor.vue 中導(dǎo)入這兩個(gè)方法:

...

<script setup>
...
import { generateNewStyle, writeNewStyle } from '@/utils/theme'
...
/**
 * 確定
 * 1\. 修改主題色
 * 2\. 保存最新的主題色
 * 3\. 關(guān)閉 dialog
 */

const comfirm = async () => {
  // 1.1 獲取主題色
  const newStyleText = await generateNewStyle(mColor.value)
  // 1.2 寫(xiě)入最新主題色
  writeNewStyle(newStyleText)
  // 2\. 保存最新的主題色
  store.commit('theme/setMainColor', mColor.value)
  // 3\. 關(guān)閉 dialog
  closed()
}
</script>

一些處理完成之后,可以在 profile 中通過(guò)一些代碼進(jìn)行測(cè)試:

<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 新主題的立即生效

到目前已經(jīng)完成了 element-ui 的主題變更,但是當(dāng)前的主題變更還有一個(gè)小問(wèn)題,那就是:在刷新頁(yè)面后,新主題會(huì)失效

那么出現(xiàn)這個(gè)問(wèn)題的原因,非常簡(jiǎn)單:因?yàn)闆](méi)有寫(xiě)入新的 style

所以只需要在 應(yīng)用加載后,寫(xiě)入 style 即可

那么寫(xiě)入的時(shí)機(jī),可以放入到 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:方案落地:自定義主題變更

自定義主題變更相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,因?yàn)?自己的代碼更加可控

目前在代碼中,需要進(jìn)行 自定義主題變更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
  >

此處的 背景色是通過(guò) getters 進(jìn)行指定的,該 cssVargetters 為:

cssVar: state => variables,

所以,想要修改 自定義主題 ,只需要從這里入手即可。

根據(jù)當(dāng)前保存的 mainColor 覆蓋原有的默認(rèn)色值

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

但是這樣設(shè)定之后,整個(gè)自定義主題變更,還存在兩個(gè)問(wèn)題:

  1. menuBg 背景顏色沒(méi)有變化

這個(gè)問(wèn)題是因?yàn)?sidebar 的背景色未被替換,所以可以在 layout/index 中設(shè)置 sidebarbackgroundColor

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

  1. 主題色替換之后,需要刷新頁(yè)面才可響應(yīng)

這個(gè)是因?yàn)?getters 中沒(méi)有監(jiān)聽(tīng)到 依賴(lài)值的響應(yīng)變化,所以修改依賴(lài)值

store/modules/theme

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

getters

....

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

8:自定義主題方案總結(jié)

那么到這里整個(gè)自定義主題就處理完成了。

對(duì)于 自定義主題而言,核心的原理其實(shí)就是 修改scss變量來(lái)進(jìn)行實(shí)現(xiàn)主題色變化

明確好了原理之后,對(duì)后續(xù)實(shí)現(xiàn)的步驟就具體情況具體分析了。

  1. 對(duì)于 element-ui:因?yàn)?element-ui 是第三方的包,所以它 不是完全可控 的,那么對(duì)于這種最簡(jiǎn)單直白的方案,就是直接拿到它編譯后的 css 進(jìn)行色值替換,利用 style 內(nèi)部樣式表 優(yōu)先級(jí)高于 外部樣式表 的特性,來(lái)進(jìn)行主題替換
  2. 對(duì)于自定義主題:因?yàn)樽远x主題是 完全可控 的,所以實(shí)現(xiàn)起來(lái)就輕松很多,只需要修改對(duì)應(yīng)的 scss變量即可
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容