Skip to content
On this page

菜单收缩按钮及接入 vuex

本节在 navbar 添加菜单收缩按钮,收缩状态接入 vuex 并对 store 做个 session storage 持久化,保持之前收缩状态

本节效果 展开时 image.png session storage image.png

收缩时 image.png session storage image.png

2-1 创建菜单收缩按钮组件

组件没什么内容 主要是 svg 图片和样式 一个 props 激活状态 一个切换状态函数

src/components/Hambuger/index.vue

vue
<template>
  <div
    class="hamburger-container"
    style="padding: 0 15px"
    @click="toggleClick"
  >
    <svg
      :class="{'is-active': isActive}"
      class="hamburger"
      viewBox="0 0 1024 1024"
      xmlns="http://www.w3.org/2000/svg"
      width="64"
      height="64"
    >
      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
    </svg>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Hambuger',
  props: {
    isActive: {
      type: Boolean,
      default: false
    }
  },
  // vue3新增 emits选项
  // 1.起到声明的作用 让你知道当前组件  会emit出去哪些事件
  // 2.可以对emit的参数进行效验 通过就可以emit出去 否则不能emit
  // 具体使用说明看文档 https://v3.cn.vuejs.org/api/options-data.html#emits
  emits: ['toggleClick'], // vue3 emits声明列表
  setup (props, { emit }) {
    const toggleClick = () => {
      emit('toggleClick')
    }

    return {
      toggleClick
    }
  }
})
</script>

<style lang="scss" scoped>
  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color: transparent;
    &:hover {
      background: rgba(0, 0, 0, .025);
    }
  }
  .hamburger {
    display: inline-block;
    vertical-align: middle;
    width: 20px;
    height: 20px;
  }

  .hamburger.is-active {
    transform: rotate(180deg);
  }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

image.png

vue
<template>
  <div class="navbar">
    <hambuger  @toggleClick="toggleSidebar" :is-active="true"/>
    <breadcrumb />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hambuger from '@/components/Hambuger/index.vue'

export default defineComponent({
  name: 'Navbar',
  components: {
    Breadcrumb,
    Hambuger
  },
  setup() {
    const toggleSidebar = () => {
      console.log('click')
    }

    return {
      toggleSidebar
    }
  }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

2-2 接入 vuex

这块儿会有 关于 store actions mutations getters 类型声明定义的方式,都是根据文档定义的 类型定义不需要想太多为什么,知道怎么配就行

创建 module

vuex 里面 module src/store/modules 存放 module 文件

创建 src/store/modules/app.ts app module 针对一些后台设置状态存储 比如收缩状态 或配置状态 image.png src/store/modules/app.ts

typescript
import { ActionTree, Module, MutationTree } from 'vuex'
import { IRootState } from '../index' // 全局状态 root state 从src/store/index.ts里定义导出

// 定义app里state类型
export interface IAppState {
  sidebar: {  // 定义sidebar相关状态
    opened: boolean  // 菜单导航展开时true 收缩时false
  }
}

// 定义mutations
const mutations: MutationTree<IAppState> = {
  TOGGLE_SIDEBAR(state) {
    // 这块儿就会有类型提示 写state.sidebar 都会提示
    state.sidebar.opened = !state.sidebar.opened
  }
}

// 定义actions
const actions: ActionTree<IAppState, IRootState> = {
  toggleSidebar({ commit }) { // 切换sidebar 收缩状态
    commit('TOGGLE_SIDEBAR')
  }
  // test_action({ commit }, payload: string) { // action如果有payload自己定义类型就行
  // }
}

// 定义module
const app: Module<IAppState, IRootState> = {
  // 用了命名空间 store.dispatch('模块名/action函数名')
  // 获取state就要 store.state.app.sidebar (store.state.模块名.状态)
  namespaced: true,
  state: {
    sidebar: { // 定义sidebar相关状态
      opened: true // 菜单导航展开时true 收缩时false
    }
  },
  mutations,
  actions
}

export default app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

之后的 vuex module 定义 按这个写法就可以

2-3 定义全局 getters

用到的一些模块状态 通过 getters 做给筛选获取 store.getters.sidebar

src/store/getters.ts

typescript
import { GetterTree } from 'vuex'
import { IRootState } from './index'

// 定义全局getters
const getters: GetterTree<IRootState, IRootState> = {
  sidebar: (state) => state.app.sidebar
}

export default getters
1
2
3
4
5
6
7
8
9

2-4 store.ts 里声明 module

store 里 为了我们在组件里使用 useStore(store.state)时 有类型提示需要做些配置 都是官方文档教程

vuex useStore封装文档说明 src/store/index.ts

typescript
import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import app, { IAppState } from '@/store/modules/app' // 导入模块
import getters from './getters' // 导入getters

// 声明全局状态类型,主要就是我们定义的模块 这样store.state.app才会有类型提示
export interface IRootState {
  app: IAppState;
}

// 通过下面方式使用 TypeScript 定义 store 能在使用时正确地为 store 提供类型声明。
// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
// eslint-disable-next-line symbol-description
export const key: InjectionKey<Store<IRootState>> = Symbol()
// 这个key算是个密钥 入口main.ts需要用到 vue.use(store, key) 才能正常使用

// 对于getters在组件使用时没有类型提示
// 有人提交了pr #1896 为getters创建泛型 应该还未发布
// https://github.com/vuejs/vuex/pull/1896
// 代码pr内容详情
// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce

export default createStore<IRootState>({
  getters,
  modules: { // 注册模块
    app
  }
})

// 定义自己的 `useStore` 组合式函数
// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
export function useStore () {
  return baseUseStore(key)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

对于getters 没有类型提示 有人提了pr已通过 好像还没发布https://github.com/vuejs/vuex/pull/1896

2-5 入口 main.ts store 里注入 key

key 可以认为是 store 加密解密的密钥 image.png

2-6 使用 store 里 sidebar 状态

image.png src/layout/components/Navbar.vue

useStore 使用的是我们自定义的配置好的 import { useStore } from '@/store'

类型提示: image.pngimage.png

vue
<template>
  <div class="navbar">
    <hambuger  @toggleClick="toggleSidebar" :is-active="sidebar.opened"/>
    <breadcrumb />
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hambuger from '@/components/Hambuger/index.vue'
import { useStore } from '@/store/index'

export default defineComponent({
  name: 'Navbar',
  components: {
    Breadcrumb,
    Hambuger
  },
  setup() {
    // 使用我们自定义的useStore 具备类型提示
    // store.state.app.sidebar 对于getters里的属性没有类型提示
    const store = useStore()
    const toggleSidebar = () => {
      store.dispatch('app/toggleSidebar')
    }
    // 从getters中获取sidebar
    const sidebar = computed(() => store.getters.sidebar)

    return {
      toggleSidebar,
      sidebar
    }
  }
})
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

sidebar 之前用的收缩状态是组件里自定义的 改动不大 导入的 ref API 现在不用了 可以删了

image.png src/layout/components/Sidebar/index.vue

vue
<template>
  <div>
    <el-menu
      class="sidebar-container-menu"
      mode="vertical"
      :default-active="activeMenu"
      :background-color="scssVariables.menuBg"
      :text-color="scssVariables.menuText"
      :active-text-color="scssVariables.menuActiveText"
      :collapse="isCollapse"
      :collapse-transition="true"
    >
      <sidebar-item
        v-for="route in menuRoutes"
        :key="route.path"
        :item="route"
        :base-path="route.path"
      />
    </el-menu>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import variables from '@/styles/variables.scss'
import { routes } from '@/router'
import SidebarItem from './SidebarItem.vue'
import { useStore } from '@/store'

export default defineComponent({
  name: 'Sidebar',
  components: {
    SidebarItem
  },
  setup() {
    const route = useRoute()
    const store = useStore()
    // 根据路由路径 对应 当前激活的菜单
    const activeMenu = computed(() => {
      const { path } = route
      return path
    })
    // scss变量
    const scssVariables = computed(() => variables)
    // 展开收起状态 稍后放store 当前是展开就让它收起
    const isCollapse = computed(() => !store.getters.sidebar.opened)

    // 渲染路由
    const menuRoutes = computed(() => routes)

    return {
      // ...toRefs(variables), // 不有toRefs原因 缺点variables里面变量属性来源不明确
      scssVariables,
      isCollapse,
      activeMenu,
      menuRoutes
    }
  }
})
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

2-7 测试

image.png 刷新又恢复了 如果我想保留刷新前收缩状态 接下来就需要对 vuex 做持久化 image.png

本节源码参考

https://gitee.com/brolly/vue3-element-admin/commit/45b822c9fff8cd764a53c23a037390d9d26a61d8

沪ICP备20006251号-1