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

關于 動態(tài)換膚 實現(xiàn) el-menu 的背景色時烈和, 此處將來會實現(xiàn)換膚功能让蕾,所以不能直接寫死涡戳,而需要通過一個動態(tài)的值進行指定结蟋。

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

那么換句話而言,想要實現(xiàn) 動態(tài)換膚 的一個前置條件就是:色值不可以寫死渔彰!

那么為什么會有這個前置條件呢嵌屎?動態(tài)換膚又是如何去進行實現(xiàn)的呢推正?

首先先來說一下動態(tài)換膚的實現(xiàn)方式。

scss 中宝惰,可以通過 $變量名:變量值 的方式定義 css 變量 植榕,然后通過該 css 來去指定某一塊 DOM 對應的顏色。

那么大家可以想一下尼夺,如果我此時改變了該 css 變量的值尊残,那么對應的 DOM 顏色是不是也會同步發(fā)生變化。

當大量的 DOM 都依賴這個 css 變量 設置顏色時淤堵,是不是只需要改變這個 css 變量 夜郁,那么所有 DOM 的顏色是不是都會發(fā)生變化,所謂的 動態(tài)換膚 是不是就可以實現(xiàn)了粘勒!

這個就是 動態(tài)換膚 的實現(xiàn)原理

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

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

那么下面就分別來去處理這兩塊主題對應的內(nèi)容

1:動態(tài)換膚實現(xiàn)方案分析

明確好了原理之后,接下來就來理一下咱們的實現(xiàn)思路庙睡。

從原理中可以得到以下兩個關鍵信息:

  1. 動態(tài)換膚的關鍵是修改 css 變量 的值
  2. 換膚需要同時兼顧
    1. element-ui
    2. element-ui

那么根據(jù)以上關鍵信息事富,就可以得出對應的實現(xiàn)方案

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

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

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

  1. navbar 中的展示圖標
  2. 選擇顏色的彈出層

就先來處理第一個 navbar 中的展示圖標

創(chuàng)建 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:方案落地:創(chuàng)建 SelectColor 組件

在有了 ThemeSelect 之后乘陪,接下來來去處理顏色選擇的組件 SelectColor统台,在這里會用到 element 中的 el-color-picker 組件

對于 SelectColor 的處理,需要分成兩步進行:

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

那么下面咱們先來看第一步:完成 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'])

// 預定義色值
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>

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

緩存的方式分為兩種:

  1. vuex
  2. 本地存儲

constants/index 下新建常量值

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

創(chuàng)建 store/modules/theme 模塊贱勃,用來處理 主題色 相關內(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: {
    /**
     * 設置主題色
     */
    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. 實現(xiàn)原理
  2. 實現(xiàn)步驟
  3. 實現(xiàn)過程

實現(xiàn)原理:

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

但是對于 element-ui 而言流部,怎么去修改這樣的主題色呢戚绕?

其實整體的原理非常簡單,分為三步:

  1. 獲取當前 element-ui 的所有樣式
  2. 找到想要替換的樣式部分枝冀,通過正則完成替換
  3. 把替換后的樣式寫入到 style 標簽中舞丛,利用樣式優(yōu)先級的特性,替代固有樣式

實現(xiàn)步驟:

那么明確了原理之后果漾,實現(xiàn)步驟也就呼之欲出了球切,對應原理總體可分為四步:

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

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

創(chuàng)建 utils/theme 工具類绒障,寫入兩個方法

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

}

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

}

那么接下來先實現(xiàn)第一個方法 generateNewStyle,在實現(xiàn)的過程中端盆,需要安裝兩個工具類:

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

然后還需要寫入一個 顏色轉化計算器 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)"
}

準備就緒后怀骤,來實現(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 的原樣式中進行全局替換
  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
}

/**
 * 獲取當前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)
  // 把獲取到的數(shù)據(jù)篩選為原樣式模板
  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'
  }
  // 根據(jù)默認色值為要替換的色值打上標記
  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 新主題的立即生效

到目前已經(jīng)完成了 element-ui 的主題變更弓摘,但是當前的主題變更還有一個小問題,那就是:在刷新頁面后痕届,新主題會失效

那么出現(xiàn)這個問題的原因韧献,非常簡單:因為沒有寫入新的 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,

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

根據(jù)當前保存的 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 中沒有監(jiān)聽到 依賴值的響應變化杆煞,所以修改依賴值

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變量來進行實現(xiàn)主題色變化

明確好了原理之后决乎,對后續(xù)實現(xiàn)的步驟就具體情況具體分析了队询。

  1. 對于 element-ui:因為 element-ui 是第三方的包,所以它 不是完全可控 的瑞驱,那么對于這種最簡單直白的方案装盯,就是直接拿到它編譯后的 css 進行色值替換乔询,利用 style 內(nèi)部樣式表 優(yōu)先級高于 外部樣式表 的特性,來進行主題替換
  2. 對于自定義主題:因為自定義主題是 完全可控 的柴信,所以實現(xiàn)起來就輕松很多鸭津,只需要修改對應的 scss變量即可
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彤侍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逆趋,更是在濱河造成了極大的恐慌盏阶,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闻书,死亡現(xiàn)場離奇詭異名斟,居然都是意外死亡脑慧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門砰盐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闷袒,“玉大人,你說我怎么就攤上這事岩梳∧抑瑁” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵冀值,是天一觀的道長也物。 經(jīng)常有香客問我,道長列疗,這世上最難降的妖魔是什么滑蚯? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮抵栈,結果婚禮上膘魄,老公的妹妹穿的比我還像新娘。我一直安慰自己竭讳,他們只是感情好创葡,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绢慢,像睡著了一般灿渴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胰舆,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天骚露,我揣著相機與錄音,去河邊找鬼缚窿。 笑死棘幸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的倦零。 我是一名探鬼主播误续,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扫茅!你這毒婦竟也來了蹋嵌?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葫隙,失蹤者是張志新(化名)和其女友劉穎栽烂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡腺办,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年焰手,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怀喉。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡册倒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出磺送,到底是詐尸還是另有隱情驻子,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布估灿,位于F島的核電站崇呵,受9級特大地震影響,放射性物質發(fā)生泄漏馅袁。R本人自食惡果不足惜域慷,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望汗销。 院中可真熱鬧犹褒,春花似錦、人聲如沸弛针。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽削茁。三九已至宙枷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間茧跋,已是汗流浹背慰丛。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瘾杭,地道東北人诅病。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像粥烁,于是被迫代替她去往敵國和親贤笆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

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