用 Vuex 管理菜单和 Tabs 可太香了吧

796 阅读3分钟

放在前面

最近在搭一套 Vue2 + 周边生态的管理系统。菜单、面包屑和 Tabs 联动,用 Vuex 简单设计了一下,感觉挺清爽。

附上我画的思维导图:

flow.png

一眼看上去觉得有点乱,不要紧,咱们一步步分析这个设计。

Layout

Layout 主要有几个部分组成(Collapse-收缩组件,Breadcrumb-面包屑,Aside-侧边菜单栏,Tabs-打开的页签,Main-路由对应显示的页面),我们就从每个部分开始分析。

<template>
  <div class="layout">
    <el-container class="el-container">
      <el-aside>
        <!-- 侧边菜单栏 -->
        <Aside></Aside>
      </el-aside>
      <el-container class="el-container">
        <el-header style="height: auto">
          <!-- 头部 -->
          <Header></Header>
          <!-- 打开的页签集合 -->
          <Tabs></Tabs>
        </el-header>
        <el-main>
          <section class="main-box">
            <!-- 路由对应显示的页面 -->
            <router-view v-slot="{ Component, route }">
              <transition appear name="fade-transform" mode="out-in">
                <keep-alive>
                  <component :is="Component" :key="route.path"></component>
                </keep-alive>
              </transition>
            </router-view>
          </section>
        </el-main>
        <el-footer>
          <Footer></Footer>
        </el-footer>
      </el-container>
    </el-container>
  </div>
</template>

loaout.png

Store Module

menus 模块

存放“菜单收缩”状态和“菜单集合”状态。

export const menus = {
  state: {
    // 左侧导航 - 菜单收缩状态
    isCollapse: false,
    // 左侧导航 - 菜单集合
    menuList: [],
  },
  mutations: {
    // 设置收缩状态
    setCollapse(state) {
      state.isCollapse = !state.isCollapse;
    },
    // 设置左侧导航菜单集合
    setMenuList(state, menuList) {
      state.menuList = menuList
    }
  },
  actions: {},
}

tabs 模块

存放“当前选中 tab 值”的状态和“当前系统所有打开 tab 的集合”状态。

import { HOME_URL, TABS_BLACK_LIST } from '@/config/config'
import router from '@/router/index';

export const tabs = {
  state: {
    // 默认选中 - 首页
    tabsMenuValue: HOME_URL,
    // 默认打开 tab 的集合 - 首页
    tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "el-icon-s-home", close: false }]
  },
  mutations: {
    // 设置选中的 tab
    setTabsMenuValue (state, val) {
      state.tabsMenuValue = val
    },
    // 添加一个 tab
    addTabsMenuList (state, val) {
      state.tabsMenuList.push(val)
    },
    // 设置 tabs 集合
    setTabsMenuList (state, val) {
      state.tabsMenuList = val
    },
  },
  actions: {
    // 添加一个 tab
    addTabs ({ commit, state }, tabItem) {
      // 不添加 tabs 黑名单
      if (TABS_BLACK_LIST.includes(tabItem.path)) return
      const tabInfo = {
        title: tabItem.title,
        path: tabItem.path,
        close: tabItem.close,
      };
      // 如果打开的 tabs list 中没有这个 item,则添加到 list 中。(用 path 判断)
      if (state.tabsMenuList.every(item => item.path !== tabInfo.path)) {
        commit('addTabsMenuList', tabInfo)
      }
      // 设置新添加的菜单项为选中的 tab
      commit('setTabsMenuValue', tabInfo.path)
      // 路由跳转到新添加的菜单路由
      router.push(tabInfo.path)
    },
    // 移除一个 tab
    removeTabs ({ commit, state }, tabPath) {
      let tabsMenuValue = state.tabsMenuValue
      const tabsMenuList = state.tabsMenuList
      // 关闭当前选中的 tab;tab 选中的值变更为下一个list当前的 tab,或上一个。
      if (tabPath === tabsMenuValue) {
        tabsMenuList.forEach((item, index) => {
          if (item.path !== tabPath) return
          const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1]
          if (!nextTab) return
          tabsMenuValue = nextTab.path
          router.push(nextTab.path)
        })
      }
      // 将处理后的值再次赋值
      commit('setTabsMenuValue', tabsMenuValue)
      commit('setTabsMenuList', tabsMenuList.filter(item => item.path !== tabPath))
    },
    // 移除多个 tab
    closeMultipleTabs ({ commit, state }, tabPath) {
      commit('setTabsMenuList', state.tabsMenuList.filter(item => {
        return item.path === tabPath || item.path === HOME_URL
      }))
    },
    // 跳转首页
    goHome ({ commit }) {
      router.push(HOME_URL)
      commit('setTabsMenuValue', HOME_URL)
    },
  }
}

Header

CollapseBreadcrumbAvatar 组件在 Header 组件中。

Collapse

收缩组件

  1. 收缩状态(state):isCollapse
  2. 点击更改(mutations):setCollapse()
<template>
  <div>
    <i class="icon" :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" @click="handleSetCollapse"></i>
  </div>
</template>

computed: {
  isCollapse () {
    // 获取 Vuex 菜单模块中的 isCollapse 状态
    return this.$store.state.menus.isCollapse
  }
},
methods: {
  // 切换时,更改 Vuex 菜单模块中的 isCollapse 状态
  handleSetCollapse() {
    this.$store.commit('setCollapse')
  },
},

Breadcrumb

面包屑组件

  1. 第一项固定为首页。
  2. 后面的用:$route.matched
<template>
  <div>
    <el-breadcrumb separator="/">
      <transition-group name="breadcrumb" mode="out-in">
        <el-breadcrumb-item :to="{ path: HOME_URL }" key="/home">首页</el-breadcrumb-item>
        <el-breadcrumb-item v-for="item in matched" :key="item.path" :to="{ path: item.path }">
          {{ item.meta.title }}
        </el-breadcrumb-item>
      </transition-group>
    </el-breadcrumb>
  </div>
</template>

computed: {
  matched () {
    // $route.matched 类型:Array 一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes 配置数组中的对象副本 (还有在 children 数组)。
    return this.$route.matched.filter(item => item.meta && item.meta.title)
  }
},

Aside

侧边导航菜单组件

  1. 收缩状态(state):isCollapse
  2. 默认选中菜单($route.path):activeMenu
  3. 设置菜单集合(mutations):setMenuList
  4. 所有菜单集合(state):menuList
  5. 监听浏览器变化设置收缩状态:listeningWindow
<el-menu
  :default-active="activeMenu"
  class="el-menu-vertical-demo"
  :collapse="isCollapse"
  :router="true"
  :collapse-transition="false"
  :unique-opened="true"
  background-color="#191a20"
  text-color="#bdbdc0"
  active-text-color="#fff"
>
  <template v-for="(subItem, index) in menuList">
    <el-submenu :key="subItem.path" v-if="subItem.children && subItem.children.length > 0" :index="subItem.path">
      <template #title>
        <i :class="subItem.meta.icon"></i>
        <span>{{ subItem.meta.title }}</span>
      </template>
      <SubItem :menuList="subItem.children"></SubItem>
    </el-submenu>
    <el-menu-item :key="index" v-else :index="subItem.path">
      <i :class="subItem.meta.icon"></i>
      <span>{{ subItem.meta.title }}</span>
    </el-menu-item>
  </template>
</el-menu>
computed: {
  // 菜单收缩状态
  isCollapse () {
    return this.$store.state.menus.isCollapse
  },
  // 打开菜单的菜单为:当前路由的值
  activeMenu () {
    return this.$route.path
  },
  // 所有的系统菜单
  menuList () {
    return this.$store.state.menus.menuList
  },
},
mounted () {
  const vm = this
  // 监听屏幕变化,设置收缩状态。
  window.onresize = () => {
    vm.listeningWindow()
  }
  // 设置左侧导航菜单集合
  vm.handleSetMenuList()
},
methods: {
  listeningWindow() {
    const screenWidth = document.body.clientWidth;
    if (this.isCollapse === false && screenWidth < 1200) this.$store.commit('setCollapse');
    if (this.isCollapse === true && screenWidth > 1200) this.$store.commit('setCollapse');
  },
  handleSetMenuList() {
    // 如果是动态获取的路由,需要调接口后设置。this.$store.commit('setMenuList', res.data);
    // 目前本项目是静态的路由
    this.$store.commit('setMenuList', asyncRouter);
  },
},

Tabs

系统打开的页签组件

  1. tab选中状态(state):tabsMenuValue
  2. 所有打开tab集合(state):tabsMenuList
  3. watch $route 添加tab(actions):addTabs
    • 没有打开过,添加一个tab(mutation):addTabsMenuList(tabInfo)
    • 设置当前选中的tab状态(mutation):setTabsMenuValue(tabInfo.path)
  4. 点击某个tab:tabClick(val) -> this.$router.push({ path: val.name }) -> 到3
  5. 移除某个tab(actions):removeTabs
    • 关闭的tab就是打开的这个tab。需要路由跳转到tabs集合中下一个tab或上一个tab对应的路由。
    • 设置关闭后的选中的 tab 的值(mutations): setTabsMenuValue。
    • 设置筛选后的 tab 集合的值(mutations):setTabsMenuList。
<div class="tabs-menu">
  <el-tabs
    v-model="tabsMenuValue"
    type="card"
    @tab-click="tabClick"
    @tab-remove="removeTabs"
  >
    <el-tab-pane
      v-for="item in tabsMenuList"
      :key="item.path"
      :path="item.path"
      :label="item.title"
      :name="item.path"
      :closable="item.close"
    >
      <template #label>
        <i v-if="item.icon" :class="item.icon"></i>
        {{ item.title }}
      </template>
    </el-tab-pane>
  </el-tabs>
  <MoreButton></MoreButton>
</div>
watch: {
  // 监听路由的变化,防止后退前进不变化 tabsMenuValue
  '$route': {
    handler: function (value) {
      let params = {
        title: value.meta.title,
        path: value.path,
        close: true
      };
      this.$store.dispatch('addTabs', params);
    },
    immediate: true,
  },
},
computed: {
  tabsMenuValue: {
    get: function() {
      return this.$store.state.tabs.tabsMenuValue
    },
    set: function(value) {
      this.$store.commit('setTabsMenuValue', value)
    },
  },
  tabsMenuList() {
    return this.$store.state.tabs.tabsMenuList
  },
},
methods: {
  tabClick (val) {
    this.$router.push({
      path: val.name
    })
  },
  removeTabs (val) {
    this.$store.dispatch('removeTabs', val)
  },
},

Main

打开页面的视图展示组件

<el-main>
  <section class="main-box">
    <router-view v-slot="{ Component, route }">
      <transition appear name="fade-transform" mode="out-in">
        <keep-alive>
          <component :is="Component" :key="route.path"></component>
        </keep-alive>
      </transition>
    </router-view>
  </section>
</el-main>

写在最后

现在再结合前面的流程图,你会觉得清晰了一点吧。

这个项目我放在了自己的 github 上有兴趣的可以去看看吧。