【摸魚神器】一次搞定 vue3的 路由 + 選單 + tabs

金色海洋(jyk) 發表於 2022-05-25
Vue

做一個管理後臺,首先要設定路由,然後配置選單(有時候還需要導航),再來一個動態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="width: 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="width: 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="width: 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