關于 動態(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)容:
-
element-ui
主題 - 非
element-ui
主題
那么下面就分別來去處理這兩塊主題對應的內(nèi)容
1:動態(tài)換膚實現(xiàn)方案分析
明確好了原理之后,接下來就來理一下咱們的實現(xiàn)思路庙睡。
從原理中可以得到以下兩個關鍵信息:
- 動態(tài)換膚的關鍵是修改
css 變量
的值 - 換膚需要同時兼顧
element-ui
- 非
element-ui
那么根據(jù)以上關鍵信息事富,就可以得出對應的實現(xiàn)方案
- 創(chuàng)建一個組件
ThemeSelect
用來處理修改之后的css 變量
的值 - 根據(jù)新值修改
element-ui
主題色 - 根據(jù)新值修改非
element-ui
主題色
2:方案落地:創(chuàng)建 ThemeSelect 組件
查看完成之后的項目可以發(fā)現(xiàn),ThemeSelect
組件將由兩部分組成:
-
navbar
中的展示圖標 - 選擇顏色的彈出層
就先來處理第一個 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
的處理,需要分成兩步進行:
- 完成
SelectColor
彈窗展示的雙向數(shù)據(jù)綁定 - 把選中的色值進行本地緩存
那么下面咱們先來看第一步:完成 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ù)綁定之后啡邑,來處理第二步:把選中的色值進行本地緩存
緩存的方式分為兩種:
vuex
- 本地存儲
在 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
的主題變更谤逼,相對比較復雜贵扰,所以說整個過程會分為三部分:
- 實現(xiàn)原理
- 實現(xiàn)步驟
- 實現(xiàn)過程
實現(xiàn)原理:
在之前分析主題變更的實現(xiàn)原理時,核心的原理是:通過修改 scss
變量 的形式修改主題色完成主題變更
但是對于 element-ui
而言流部,怎么去修改這樣的主題色呢戚绕?
其實整體的原理非常簡單,分為三步:
- 獲取當前
element-ui
的所有樣式 - 找到想要替換的樣式部分枝冀,通過正則完成替換
- 把替換后的樣式寫入到
style
標簽中舞丛,利用樣式優(yōu)先級的特性,替代固有樣式
實現(xiàn)步驟:
那么明確了原理之后果漾,實現(xiàn)步驟也就呼之欲出了球切,對應原理總體可分為四步:
- 獲取當前
element-ui
的所有樣式 - 定義要替換之后的樣式
- 在原樣式中,利用正則替換新樣式
- 把替換后的樣式寫入到
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)的過程中端盆,需要安裝兩個工具類:
- rgb-hex:轉換RGB(A)顏色為十六進制
- css-color-function:在CSS中提出的顏色函數(shù)的解析器和轉換器
然后還需要寫入一個 顏色轉化計算器 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)"
}
準備就緒后怀骤,來實現(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
進行指定的渊啰,該 cssVar
的 getters
為:
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
但是這樣設定之后,整個自定義主題變更哗讥,還存在兩個問題:
-
menuBg
背景顏色沒有變化
這個問題是因為 sidebar
的背景色未被替換嚷那,所以可以在 layout/index
中設置 sidebar
的 backgroundColor
<sidebar
id="guide-sidebar"
class="sidebar-container"
:style="{ backgroundColor: $store.getters.cssVar.menuBg }"
/>
- 主題色替換之后,需要刷新頁面才可響應
這個是因為 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)的步驟就具體情況具體分析了队询。
- 對于
element-ui
:因為element-ui
是第三方的包,所以它 不是完全可控 的瑞驱,那么對于這種最簡單直白的方案装盯,就是直接拿到它編譯后的css
進行色值替換乔询,利用style
內(nèi)部樣式表 優(yōu)先級高于 外部樣式表 的特性,來進行主題替換 - 對于自定義主題:因為自定義主題是 完全可控 的柴信,所以實現(xiàn)起來就輕松很多鸭津,只需要修改對應的
scss
變量即可