• 【摸鱼神器】一次搞定 vue3的 路由 + 菜单 + tabs


    做一个管理后台,首先要设置路由,然后配置菜单(有时候还需要导航),再来一个动态tabs,最后加上权限判断。

    这个是不是有点繁琐?尤其是路由的设置和菜单的配置,是不是很雷同?那么能不能简单一点呢?如果可以实现设置一次就全部搞定的话,那么是不会很香呢?

    我们可以简单封装一下,实现这个愿望。

    定义一个结构

    我们可以参考 vue-router 的设置 和 el-menu 的参数,设置一个适合我们需求的结构:

    • ./router.js
    import { createRouter } from '@naturefw/ui-elp'
    
    import home from '../views/home.vue'
    
    const router = {
      /**
       * 基础路径
       */
      baseUrl: baseUrl,
    
      /**
       * 首页
       */
      home: home,
    
      menus: [
        {
          menuId: '1', // 相当于路由的 name
          title: '全局状态', // 浏览器的标题
          naviId: '0', // 导航ID
          path: 'global', // 相当于 路由 的path
          icon: FolderOpened, // 菜单里的图标
          childrens: [ // 子菜单,不是子路由。
            {
              menuId: '1010', // 相当于路由的 name
              title: '纯state',
              path: 'state',
              icon: Document,
              // 加载的组件
              component: () => import('../views/state-global/10-state.vue')
              // 还可以有子菜单。 
            },
            {
              menuId: '1020',
              title: '一般的状态',
              path: 'standard',
              icon: Document,
              component: () => import('../views/state-global/20-standard.vue')
            } 
          ]
        },
        {
          menuId: '2000',
          title: '局部状态',
          naviId: '0',
          path: 'loacl',
          icon: FolderOpened,
          childrens: [
            {
              menuId: '2010',
              title: '父子组件',
              path: 'parent-son',
              icon: Document,
              component: () => import('../views/state-loacl/10-parent.vue')
            }
          ]
        } 
      ]
    }
    
    export default createRouter(router )
    

    在 Router 的配置的基础上,加上 title、icon等菜单需要的属性,基本就搞定了。

    • baseUrl:如果不能发布到根目录的话,需要设置一个基础URL。
    • home:默认显示的组件,比如大屏。
    • menus:路由、菜单集合。
      • naviId:导航ID。
      • menuId:相当于路由的 name。
      • path:相当于 路由 的path。
      • title:浏览器的标题。
      • icon: 菜单里的图标。
      • childrens:子菜单,不是子路由。

    main 里面加载。

    设置之后,我们在main里面挂载一下即可。

    import { createApp } from 'vue'
    import App from './App.vue'
    
    // 简易路由
    import router from './router'
    
    createApp(App)
      .use(router)
      .mount('#app')
    
    • 看看效果

    路由和菜单

    https://naturefw-code.gitee.io/nf-rollup-state/class/object

    这样就搞定了,是不是很简单,因为我们把其他代码都封装成了组件。

    封装 n级菜单

    我们可以基于 el-menu,封装一个动态n级菜单组件(nf-menu)。

    菜单组件可以基于 el-menu 封装,也可以基于其他组件封装,或者自己写一个,这里以el-menu为例,介绍一下封装方式:

    • 父级菜单
      <el-menu
        ref="domMenu"
        class="el-menu-vertical-demo"
        @select="select"
        background-color="#6c747c"
        text-color="#fff"
        active-text-color="#ffd04b"
      >
        <sub-menu1
          :subMenu="menus"
        />
      </el-menu>
    

    父级菜单比较简单,设置 el-menu 需要的属性,然后加载子菜单组件。

    • n级子菜单
      <template v-for="(item, index) in subMenu">
        <!--树枝-->
        <template v-if="item.childrens && item.childrens.length > 0">
          <el-sub-menu
            :key="item.menuId + '_' + index"
            :index="item.menuId"
            style="vertical-align: middle;"
            
          >
            <template #title>
              <component
                :is="item.icon"
                style=" 1.5em; height: 1.5em; margin-right: 8px;vertical-align: middle;"
              >
              </component>
              <span>{{item.title}}</span>
            </template>
            <!--递归子菜单-->
            <my-sub-menu2
              :subMenu="item.childrens"
            />
          </el-sub-menu>
        </template>
        <!--树叶-->
        <el-menu-item v-else
          :index="item.menuId"
          :key="item.menuId + 'son_' + index"
          
        >
          <template #title>
            <span style="float: left;">
              <component
                :is="item.icon"
                style=" 1.5em; height: 1.5em; margin-right: 8px;vertical-align: middle;"
              >
              </component>
              <span >{{item.title}}</span>
            </span>
          </template>
        </el-menu-item>
      </template>
    
    • 树枝:含有子菜单的菜单,使用 el-sub-menu 实现,不加载组件。
    • 树叶:没有子菜单,使用 el-menu-item 实现,加载组件的菜单。
    • 图标:使用 component 加载图标组件。

    然后设置属性即可,这样一个n级菜单就搞定了。

    封装一个动态tabs

    菜单有了,下一步就是tabs,为了满足不同的需求,这里封装两个组件,一个单tab的,一个是动态多tabs的。

    • 单 tab
      参考 Router 的 router-view 封装一个组件 nf-router-view:
      <component :is="$router.getComponent()">
      </component>
    

    直接使用 component 加载组件即可。

    • 动态多tabs
      基于 el-tabs 封装一个动态多tabs组件 nf-router-view-tabs:
      <el-tabs
        v-model="$router.currentRoute.key"
        type="border-card"
      >
        <el-tab-pane label="桌面" name="home">
          <component :is="$router.home">
          </component>
        </el-tab-pane>
        <el-tab-pane
          v-for="key in $router.tabs"
          :key="key"
          :label="$router.menuList[key].title"
          :name="key"
        >
         <template #label>
            <span>{{$router.menuList[key].title}} &nbsp; 
              <circle-close-filled
                style=" 1.0em; height: 1.0em; margin-top: 8px;"
                @click.stop="$router.removeTab(key)" />
            </span>
          </template>
          <component :is="$router.menuList[key].component">
          </component>
        </el-tab-pane>
      </el-tabs>
    

    为了保持状态,这里采用了一个笨办法,点击菜单加载的组件都放在 el-tab-pane 里面,通过切换 tab 的方式显示组件。

    源码:https://gitee.com/naturefw-code/nf-rollup-ui-controller

    做一个简单的路由

    看了半天,你有没有发现,似乎缺少了一个重要环节?

    你猜对了,路由的封装还没有介绍。

    这里并不想设计一个像 vue-router那样的全能路由,而是设计一个适合管理后台的简易路由。

    菜单是多级的,url 也是多级的和菜单对应,但是路由是单级的,不嵌套。

    也就是说,点击任意一级的(树叶)菜单,加载的都是同级的组件。

    另外暂时不考虑加载组件后的路由的设置。我觉得,这个可以交给加载的组件自行实现。

    import { defineAsyncComponent, reactive, watch, inject } from 'vue'
    
    const flag = Symbol('nf-router-menu___')
    
    /**
     * 一个简单的路由
     * @param { string } baseUrl 基础路径
     * @param { components } home 基础路径
     * @param { array } menus 路由设置,数组,多级
     * * [{
     * * *  menuId: '菜单ID'
     * * *  naviId: '0', // 导航ID,可以不设置
     * * *  title: '标题',
     * * *  path: '路径',
     * * *  icon: Edit, // 图标组件
     * * *  component: () => import('./views/xxx.vue') // 要加载的组件,可以不设置
     * * *  childrens: [ // 子菜单,可以多级
     * * * *  {
     * * * * *  menuId: '菜单ID' 
     * * * * *  title: '标题',
     * * * * *  path: '路径',
     * * * * *  icon: Edit, // 图标组件
     * * * * *  component: () => import('./views/xxx.vue') // 要加载的组件
     * * * * }
     * * * ]
     * * },
     * * 其他菜单
     * * ]
     * @returns 
     */
    class Router {
      constructor (info) {
        // 设置当前选择的路由
        this.currentRoute = reactive({
          key: 'home', // 默认的首页
          paths: [] // 记录打开的多级菜单的信息 
        })
        this.baseUrl = info.baseUrl // 基础路径,应对网站的二级目录
        this.baseTitle = document.title // 初始的标题
        this.isRefresh = false // 是否刷新进入
        this.home = info.home // 默认的首页
        this.menus = reactive(info.menus) // 菜单集合,数组形式,支持多级,可以设置导航ID
        this.menuList = {} // 变成单层的树,便于用key查找。
        this.tabs = reactive(new Set([])) // 点击过且没有关闭的二级菜单,做成动态tab标签
       
        this.setup()
      }
    
      /**
       * 初始化设置
       */
      setup = () => {
        // 监听当前路由,设置 tabs 和标题、url
        watch(() => this.currentRoute.key, (key) => {
          略
        })
      }
    
       /**
       * 添加新路由,主要是实现根据用户权限加载对应的菜单。
       */
      addRoute = (newMenus, props = {}) => {
          略
      }
    
      /**
       * 删除路由
       * @param { array } path 菜单的路径,[] 表示根菜单
       * @param { string | number } id 要删除的菜单ID
       */
      removeRoute = (path = [], id = '') => {
          略
      }
    
      /**
       * 刷新时依据url加载组件
       */
      refresh = () => {
         略
      }
    
      /**
       * 加载路由指定的组件
       * @returns 
       */
      getComponent = () => {
        if (this.currentRoute.key === '' || this.currentRoute.key === 'home') {
          return this.home
        } else {
          return this.menuList[this.currentRoute.key].component
        }
      }
    
      /**
       * 删除tab
       * @param { string } key 
       * @returns 
       */
      removeTab = (key) => {
        略
      }
    
      /**
       * 安装插件
       * @param {*} app 
       */
      install = (app) => {
        // 便于模板获取
        app.config.globalProperties.$router = this
        // 便于代码获取
        app.provide(flag, this)
      }
    }
    
    /**
     * 创建简易路由
     */
    const createRouter = (info) => {
      // 创建路由,
      const router = new Router(info)
      // 判断url,是否需要加载组件
      setTimeout(() => {
        router.refresh()
      }, 300)
      // 使用vue的插件,设置全局路由
      return router
    }
    
    /**
     * 获取路由
     * @returns 
     */
    const useRouter = () => {
      return inject(flag)
    }
    
    export {
      createRouter,
      useRouter
    }
    

    篇幅有限,这里只介绍了路由的整体结构,具体实现方式可以看源码:

    源码:https://gitee.com/naturefw-code/nf-rollup-ui-controller

    菜单与权限

    上面是静态的路由和导航的设置方式,对于管理后台,必备的一个需求就是,根据用户的权限来加载路由和菜单。

    所以我们提供了一个 addRoute 方法,实现动态添加路由的功能,这样可以等用户登录之后,得到用户的权限,然后按照权限加载路由和菜单。

      const router = useRouter()
    
       router.addRoute([
          {
            menuId: 'dt-100',
            title: '添加根菜单',
            naviId: '0',
            path: 'new-router',
            icon: FolderOpened,
            childrens: [
              {
                menuId: '100-10',
                title: '动态菜单',
                path: 'ui',
                icon: Document,
                component: () => import('../ui/base/c-01html.vue')
              }
            ]
          }
        ], { index: 1 })
    

    同时也可以加上权限判断。菜单是基于 el-menu 实现的,可以加上 select 事件,然后在事件里面判断权限,如果没有权限可以跳转到登录组件。

    
    const router = useRouter()
    
    const myselect = (index, indexPath) => {
      // 验证权限,如果没有权限,加载登录组件
      if (没有权限) {
        router.currentRoute.paths = ''
        router.currentRoute.key = '登录组件的key'
      }
    }
    

    示例项目

    https://gitee.com/naturefw-code/nf-rollup-state

  • 相关阅读:
    ConcurrentHashMap的size方法是线程安全的吗?
    redis cluster介绍与gossip协议
    leetcode刷题篇 21题合并两个排序的链表 java C++版本
    MySQL索引凭什么能让查询效率提高这么多?
    从零开始学习html(十五)css样式设置小技巧——下
    从零开始学习html(十五)css样式设置小技巧——上
    从零开始学习html(十四)单位和值
    从零开始学习html(十三) CSS代码缩写,占用更少的带宽
    从零开始学习html(十二)CSS布局模型——下
    从零开始学习html(十二)CSS布局模型——上
  • 原文地址:https://www.cnblogs.com/jyk/p/16308015.html
Copyright © 2020-2023  润新知