切换主题,本质上就是控制一个全局变量,然后所有跟主题相关的配置都是用这一个全局变量,主题就会随着这个全局变量的修改进行切换,这就需要用到状态管理,vue3常用的状态管理库有vuex和pinia,这里我们使用pinia来控制
Pinia是vue的状态管理库,它可以实现跨组件或者页面共享状态,功能类似于vuex,但是比vuex更加简单易上手(个人见解)
项目源码
Pinia 基本使用
安装Pinia
npm install pinia
在main.js中引入pinia并通过use安装
...
import { createPinia } from 'pinia'
const pinia = createPinia()
...
createApp(App).use(router).use(pinia).use(Antd).mount('#app')
定义Store
新增文件store/module/app.js
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => {
return {
sideCollapsed: false,
theme: 'dark',
}
},
actions: {
changeTheme() {
this.theme = this.theme == 'light' ? 'dark' : 'light'
},
changeCollapsed() {
this.sideCollapsed = !this.sideCollapsed
}
},
})
上边定义的Store有两个属性,state和actions,state用来存储状态,actions用来操作状态,在这里我们修改主题直接操作changeTheme方法即可,当然Pinia还有其他操作方法,详情可参考官方文档
使用
定义好Store之后就可以使用了
修改layout/layout.vue文件:
<template>
<a-layout>
<!-- 左侧部分开始 -->
<a-layout-sider :style="{ overflow: 'auto', height: '100vh', background: theme == 'light' ? '#fff' : '#001628' }" v-model:collapsed="sideCollapsed">
<!-- 左侧logo开始 -->
<div class="logo">
<img src="/vite.svg" class="logo_img" alt="Vite logo" />
<span class="logo_text" :style="{ color: theme == 'light' ? '#011528' : '#fff' }" v-show="!sideCollapsed">Ant Design</span>
</div>
<!-- 左侧logo结束 -->
<!-- 左侧菜单开始 -->
<a-menu v-model:selectedKeys="selectedKeys" :theme="theme" :items="state.menu" mode="inline" @click="menuClicked"/>
<!-- 左侧菜单结束 -->
</a-layout-sider>
<!-- 左侧部分结束 -->
<!-- 右侧部分开始 -->
<a-layout>
<!-- 右侧header开始 -->
<a-layout-header style="background: #fff; padding: 0; height: 50px;">
<MenuUnfoldOutlined
v-if="sideCollapsed"
class="trigger"
@click="appAction.changeCollapsed"
/>
<MenuFoldOutlined v-else class="trigger" @click="appAction.changeCollapsed" />
<a-switch size="small" :checked="theme === 'dark'" checked-children="Dark" un-checked-children="Light"
@change="appAction.changeTheme" class="themeSwitchMenu" />
</a-layout-header>
<!-- 右侧header结束 -->
<!-- 右侧页面主体开始 -->
<a-layout-content
:style="{ margin: '12px 10px', padding: '18px', background: '#fff', minHeight: '280px', borderRadius: '4px' }"
>
<router-view></router-view>
</a-layout-content>
<!-- 右侧页面主体结束 -->
</a-layout>
<!-- 右侧部分结束 -->
</a-layout>
</template>
<script setup lang="js">
import { ref, h, onMounted, reactive } from 'vue';
import { useRouter } from 'vue-router'
// 加载菜单和图标
import menu from '../store/menu'
import * as icons from '@ant-design/icons-vue'
import { storeToRefs } from 'pinia'
// 引入appStore
import { useAppStore } from '../store/module/app'
const appStore = useAppStore()
// 引入appStore中的属性
const { sideCollapsed, theme } = storeToRefs(appStore)
// 定义App操作类,
const appAction = {
changeTheme: () => {
// 调用appStore中定义的changeTheme方法
appStore.changeTheme()
},
changeCollapsed: () => {
// 调用appStore中定义的changeCollapsed方法
appStore.changeCollapsed()
}
}
const selectedKeys = ref([]);
const router = useRouter()
const state = reactive({
menu: null, // menu设置为动态值,上边a-menu标签的items值也改为state.menu
})
onMounted(() => {
// 给state.menu赋值
state.menu = menu
// 我们在menu.js里边配置的icon为一个字符串,但是a-menu组件需要的icon为一个图标组件
// 所以这里需要把icon名称转换为icon组件
const genMenuIcon = (list) => {
for(let item of list) {
if (item.icon && typeof item.icon === 'string') {
item.icon = h(eval('icons.' + item.icon))
}
if (item.hasOwnProperty('children') && item.children.length > 0) {
genMenuIcon(item.children)
} else {
delete(item.children)
}
}
}
genMenuIcon(state.menu)
})
// 菜单点击事件
const menuClicked = ({item, key}) => {
// 跳转到菜单配置的path地址取
router.push({ path: key })
}
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
AppstoreOutlined,
SettingOutlined,
DashboardOutlined
} from '@ant-design/icons-vue'
</script>
<style>
.trigger {
display: block;
font-size: 18px;
width: 40px;
line-height: 50px;
padding: 2px 16px 0 16px;
cursor: pointer;
transition: color 0.3s;
float: left;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 32px;
/* background: rgba(255, 255, 255, 0.3); */
margin: 9px;
overflow: hidden;
}
.logo_img {
margin-left: 12px;
display: block;
float: left;
line-height: 32px;
text-align: center;
color: #fff;
font-size: 20px;
margin-left: 8px;
font-weight: 900;
}
.logo_text {
display: block;
float: left;
line-height: 32px;
text-align: center;
font-size: 20px;
margin-left: 8px;
font-weight: 900;
}
.site-layout .site-layout-background {
background: #fff;
}
.themeSwitchMenu {
display: block;
float: right;
margin-right: 20px;
margin-top: 17px;
}
</style>
重启项目可以看到切换主题的目的已经达到了:
commit-hash: 607847c
状态持久化存储
效果是实现了,但是我们刷新页面会发现一个问题,刷新页面之后之前选择的主题会重置,那能否让系统记住我们选择的主题呢?
这就需要一个持久化存储的功能,来把Pinia中的state存起来,这样刷新页面之后就会沿用之前的值,而不是默认的值
Pinia本身是不支持本地存储的,但是可以通过插件pinia-plugin-persistedstate来实现
pinia-plugin-persistedstate插件官网
安装插件
cnpm install pinia-plugin-persistedstate
修改main.js
import { createApp } from 'vue'
import router from './router'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
// 引入持久化插件并使用
import 'ant-design-vue/dist/reset.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)
// 暂时在这里注册路由,后期会做调整
import menu from './store/menu'
import initRouter from './router/generator';
initRouter(menu)
createApp(App).use(router).use(pinia).use(Antd).mount('#app')
然后再修改store/module/app.js,新增persist属性,并设置存储在本地的key以及所要存储的状态
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => {
return {
sideCollapsed: false,
theme: 'dark',
}
},
actions: {
changeTheme() {
this.theme = this.theme == 'light' ? 'dark' : 'light'
},
changeCollapsed() {
this.sideCollapsed = !this.sideCollapsed
}
},
persist: {
enabled: true,
key: 'app',
paths: ['theme', 'sideCollapsed'],
}
})
添加的persist就是持久化存储设置
这样打开浏览器控制台在Application->Local Storage里边就可以看到Key为app的存储记录,同时我们刷新浏览器的时候主题也不会重置了:
commit-hash: ed70450
统一出口
前边我们已经实现了使用Pinia以及数据的持久化,但是Pinia我们是在main.js中直接创建的,那我们后期其他地方用到的时候就没法复用了,这里我们做一个提取,直接在store/index.js文件中创建并导出
首先创建store/index.js文件
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
import { useAppStore } from './module/app'
export { useAppStore }
export default pinia
然后修改main.js
import { createApp } from 'vue'
import router from './router'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
import App from './App.vue'
import pinia from './store'
// 暂时在这里注册路由,后期会做调整
import menu from './store/menu'
import initRouter from './router/generator';
initRouter(menu)
createApp(App).use(router).use(pinia).use(Antd).mount('#app')
这样,有其他地方使用Pinia时也可以直接引入store/index.js
总结
- 通过
pinia状态管理来实现主题切换 - 通过
pinia-plugin-persistedstate插件来实现pinia持久化处理 pinia统一出口调整
commit-hash: b9e9468