關(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)容:
-
element-ui
主題 - 非
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)鍵信息:
- 動(dòng)態(tài)換膚的關(guān)鍵是修改
css 變量
的值 - 換膚需要同時(shí)兼顧
element-ui
- 非
element-ui
那么根據(jù)以上關(guān)鍵信息,就可以得出對(duì)應(yīng)的實(shí)現(xiàn)方案
- 創(chuàng)建一個(gè)組件
ThemeSelect
用來(lái)處理修改之后的css 變量
的值 - 根據(jù)新值修改
element-ui
主題色 - 根據(jù)新值修改非
element-ui
主題色
2:方案落地:創(chuàng)建 ThemeSelect 組件
查看完成之后的項(xiàng)目可以發(fā)現(xiàn),ThemeSelect
組件將由兩部分組成:
-
navbar
中的展示圖標(biāo) - 選擇顏色的彈出層
就先來(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)行:
- 完成
SelectColor
彈窗展示的雙向數(shù)據(jù)綁定 - 把選中的色值進(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)行本地緩存
緩存的方式分為兩種:
vuex
- 本地存儲(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ì)分為三部分:
- 實(shí)現(xiàn)原理
- 實(shí)現(xiàn)步驟
- 實(shí)現(xiàn)過(guò)程
實(shí)現(xiàn)原理:
在之前分析主題變更的實(shí)現(xiàn)原理時(shí),核心的原理是:通過(guò)修改 scss
變量 的形式修改主題色完成主題變更
但是對(duì)于 element-ui
而言,怎么去修改這樣的主題色呢?
其實(shí)整體的原理非常簡(jiǎn)單,分為三步:
- 獲取當(dāng)前
element-ui
的所有樣式 - 找到想要替換的樣式部分,通過(guò)正則完成替換
- 把替換后的樣式寫(xiě)入到
style
標(biāo)簽中,利用樣式優(yōu)先級(jí)的特性,替代固有樣式
實(shí)現(xiàn)步驟:
那么明確了原理之后,實(shí)現(xiàn)步驟也就呼之欲出了,對(duì)應(yīng)原理總體可分為四步:
- 獲取當(dāng)前
element-ui
的所有樣式 - 定義要替換之后的樣式
- 在原樣式中,利用正則替換新樣式
- 把替換后的樣式寫(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):
- rgb-hex:轉(zhuǎn)換RGB(A)顏色為十六進(jìn)制
- css-color-function:在CSS中提出的顏色函數(shù)的解析器和轉(zhuǎn)換器
然后還需要寫(xiě)入一個(gè) 顏色轉(zhuǎn)化計(jì)算器 formula.json
創(chuàng)建 constants/formula.json
(https://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)行指定的,該 cssVar
的 getters
為:
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)題:
-
menuBg
背景顏色沒(méi)有變化
這個(gè)問(wèn)題是因?yàn)?sidebar
的背景色未被替換,所以可以在 layout/index
中設(shè)置 sidebar
的 backgroundColor
<sidebar
id="guide-sidebar"
class="sidebar-container"
:style="{ backgroundColor: $store.getters.cssVar.menuBg }"
/>
- 主題色替換之后,需要刷新頁(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)的步驟就具體情況具體分析了。
- 對(duì)于
element-ui
:因?yàn)?element-ui
是第三方的包,所以它 不是完全可控 的,那么對(duì)于這種最簡(jiǎn)單直白的方案,就是直接拿到它編譯后的css
進(jìn)行色值替換,利用style
內(nèi)部樣式表 優(yōu)先級(jí)高于 外部樣式表 的特性,來(lái)進(jìn)行主題替換 - 對(duì)于自定義主題:因?yàn)樽远x主題是 完全可控 的,所以實(shí)現(xiàn)起來(lái)就輕松很多,只需要修改對(duì)應(yīng)的
scss
變量即可