动态换肤原理分析
比如此处将来会实现换肤功能,所以我们不能直接写死,而需要通过一个动态的值进行指定。
<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
>
那么换句话而言,想要实现 动态换肤 的一个前置条件就是:色值不可以写死!【处理国际化也是同理,title 名称绝对不能写死】
动态换肤的实现方式 =>
在 scss
中,我们可以通过 $变量名:变量值
的方式定义 css 变量
,然后通过该 css
来去指定某一块 DOM
对应的颜色。
$menuText: #bfcbd9;
$menuActiveText: #ffffff;
$subMenuActiveText: #f4f4f5;
$menuBg: #304156;
$menuHover: #263445;
$subMenuBg: #1f2d3d;
$subMenuHover: #001528;
$sideBarWidth: 210px;
$hideSideBarWidth: 54px;
$sideBarDuration: 0.28s;
如果我此时改变了该 css
变量的值,那么对应的 DOM
颜色是不是也会同步发生变化。
当大量的 DOM
都依赖这个 css 变量
设置颜色时,我们是不是只需要改变这个 css 变量
,那么所有 DOM
的颜色是不是都会发生变化,所谓的 动态换肤 是不是就可以实现了!
这个就是 动态换肤 的实现原理。
一般应用到项目中,主要是两方面内容:
- 第三方 UI 组件库的主题(如
element-plus
主题) - 自定义主题(自己项目组件的主题)
动态换肤实现方案
从原理中可以得到以下两个关键信息:
- 动态换肤的关键是修改
css 变量
的值 - 换肤需要同时兼顾
- 项目中第三方或自定义组件库的主题(如
element-plus
) - 非第三方或自定义组件库
- 项目中第三方或自定义组件库的主题(如
【假设使用element-plus
】
那么根据以上关键信息,就可以得出对应的实现方案
- 创建一个组件
ThemeSelect
用来处理修改之后的css 变量
的值 - 根据新值修改
element-plus
主题色 - 根据新值修改非
element-plus
主题色
创建 ThemeSelect 组件
<template>
<!-- 主题图标 -->
<el-dropdown
v-bind="$attrs"
trigger="click"
class="theme"
@command="handleSetTheme"
>
<!-- 图标及tooltip -->
<div>
<!-- 国际化处理 -->
<el-tooltip :content="$t('msg.navBar.themeChange')">
<svg-icon icon="change-theme" />
</el-tooltip>
</div>
<!-- menu,使用具名插槽 -->
<template #dropdown>
<el-dropdown-menu>
<!-- 内部再使用它的item绑定选择项,我们这里只有一个选择项。 -->
<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>
使用:
<div class="right-menu">
<theme-picker class="right-menu-item hover-effect"></theme-picker>
v-bind=“
a
t
t
r
s
"
,这里的
‘
"
attrs" ,这里的`"
attrs",这里的‘"attrs” 就是指父组件传递的属性【如这里的 theme-picker 的 class 属性】绑定到子组件
el-dropdown`上。【后面有补充】
运行到页面上就可以看到了。
删掉v-bind
<el-dropdown
trigger="click"
class="theme"
@command="handleSetTheme"
>
就发现el-dropdown
上之前的class没了。
v-bind=“$attrs”【补充】
当我们使用该组件的时候,在父组件中给它指定的一些属性会直接被绑定到当前根元素上。v-bind="$attrs"
被用在 el-dropdown
上,这意味着所有父组件传递给这个组件的未声明(没定义在数组 props 中)属性和事件都会直接应用到 el-dropdown
上。这样做的好处是,不需要在当前组件中手动接收和处理这些属性或事件,而是自动将它们传递给 el-dropdown
,从而使组件更加简洁和灵活。
通俗理解: 在 Vue.js 中,v-bind="$attrs"
是一种常用的方式,用于将父组件传递给当前组件的所有属性($attrs
)绑定到某个子组件或元素上。
例如,如果父组件传递了一个
data-test="test"
的属性和一个@custom-event="doSomething"
的事件到当前组件,而这些没有被显式地定义为props
,那么使用v-bind="$attrs"
会将这些属性和事件自动传递给el-dropdown
。
为什么使用 $attrs
?
- 灵活性: 使用
$attrs
可以使组件更灵活和通用化,避免在子组件中手动列出所有可能的props
。 - 自动传递: 在一些情况下,你希望某些属性或事件自动传递给子组件而不需要手动处理,
v-bind="$attrs"
就是一个简洁的解决方案。 - 组合组件: 在构建复杂的组合组件时,父组件可以传递一组属性和事件到子组件,而子组件不需要显式处理这些属性和事件,可以直接传递给其内部的元素或子组件。
创建 SelectColor 组件
在有了 ThemeSelect
之后,接下来去处理颜色选择的组件 SelectColor
(颜色选择组件),在这里我会用到 element+
中的 el-color-picker
组件
ColorPicker 颜色选择器
对于 SelectColor
的处理,需要分成两步进行:
- 完成
SelectColor
弹窗展示的双向数据绑定 - 把选中的色值进行本地缓存
- vuex
- localStorage
第一步:完成 **SelectColor**
弹窗展示的双向数据绑定
vue2提供两种双向数据绑定第一种是v-model
还有.sync
修饰符,但是在vue3中,它已经被去除了。
创建 SelectColor
弹出层 => el-dialog 包裹
<template>
<!-- 因为期望在themeSlect里面通过双向数据绑定,控制SelectColor,SelectColor这里就不能用v-model,而是v-bind,单向数据绑定了。modelValue是Vue3中v-model在组件中使用的时候,默认的一个值。modelValue需要从父组件中接收过来。modelValue控制弹出层展示。
之后处理dialog关闭事件
弹出层的宽度是22%
-->
<el-dialog title="提示"
:model-value="modelValue"
@close="closed" width="22%">
<!-- 内容区 -->
<div class="center">
<!-- 设置标题 -->
<p class="title">{{ $t('msg.theme.themeColorChange') }}</p>
<!-- 颜色选择器,需要设置一个初始色值predefineColors,即预定义色值。并设置双向绑定 -->
<el-color-picker
v-model="mColor"
:predefine="predefineColors"
></el-color-picker>
</div>
<!-- foot -->
<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
}
})
// 通过父组件控制子组件显示的话,需要通过modelValue。通过父组件传递的数据,在子组件中进行展示。需要传递一个事件,从子组件通知父组件,modelValue 要发生变化了。事件在 Vue3 中使用 defineEmits 进行声明。
// 事件的名称 => update是固定写法,后面的值是指定修改哪一个从父组件传递来的props。
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 = () => {
// 通知父组件触发该事件,modeValue 关闭了。
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
(父组件)中使用该组件,中调用弹出层
<template>
...
<!-- 展示弹出层 -->
<div>
<!-- 弹出层默认不展示的,通过 v-model 完成父子组件的双向绑定。 -->
<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>
handleSetTheme => el-dropdown 的点击事件
<!-- 主题图标 -->
<el-dropdown
v-bind="$attrs"
trigger="click"
class="theme"
@command="handleSetTheme"
>
现在完成第一步,完成 SelectColor 弹窗展示的双向数据绑定之后,接下来处理第二步:把选中的色值进行本地缓存
缓存的方式分为两种:
vuex
- 本地存储
创建 store/modules/theme
模块,用来处理 主题色 相关内容(完成vuex和localStorage的存储)
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
下指定快捷访问
import { MAIN_COLOR } from '@/constant'
import { getItem } from '@/utils/storage'
import { generateColors } from '@/utils/theme'
const getters = {
token: state => state.user.token,
userInfo: state => state.user.userInfo,
/**
* @returns true 表示已存在用户信息
*/
hasUserInfo: state => {
return JSON.stringify(state.user.userInfo) !== '{}'
},
// ....
// 新增 =>
mainColor: state => state.theme.mainColor,
}
export default getters
在 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>
<el-color-picker
v-model="mColor"
:predefine="predefineColors"
></el-color-picker>
刷新页面,色值就有了。【不做本地缓存,刷新页面后主题信息会丢失!】