vue3+Ant Design搭建管理后台 03 —— 通过状态管理切换主题

1,606 阅读5分钟

切换主题,本质上就是控制一个全局变量,然后所有跟主题相关的配置都是用这一个全局变量,主题就会随着这个全局变量的修改进行切换,这就需要用到状态管理,vue3常用的状态管理库有vuexpinia,这里我们使用pinia来控制

Pinia是vue的状态管理库,它可以实现跨组件或者页面共享状态,功能类似于vuex,但是比vuex更加简单易上手(个人见解)

项目源码

Github

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有两个属性,stateactionsstate用来存储状态,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里边就可以看到Keyapp的存储记录,同时我们刷新浏览器的时候主题也不会重置了:


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