vue3+vite+ts+pinia+router4后台管理-动态路由生成

动态路由功能写法因人而异,本文只做参考!

实现思路

1.在路由钩子里面判断是否首次进入系统(permission.ts)
2.判断token是否有值。没有值回到登陆页面,
3.token有值判断MenusList是否有值,没有则获取路由
4.解析路由,拼接路由,放行路由

1. 引入vue-router 设置静态路由 登陆,首页

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/home/index'
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue')
  },
  {
    path: '/layout',
    name: 'layout',
    component: () => import('@/layout/layout.vue'),
    redirect: '/home/index',
    children: []
  },

  {
    path: '/:pathMatch(.*)*',
    component: () => import('@/views/404.vue')
  },

]
// qiankun 子应用路由  (项目中接入qiankun微前端路由写法)
// import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
// const router = createRouter({
//   history: createWebHistory(
//     qiankunWindow.__POWERED_BY_QIANKUN__
//       ? '/cms/'
//       : '/'
//   ),
//   routes
// })

//  正常路由写法
const router = createRouter({
    history: createWebHistory(),// HTML5的history模式
    routes: constantRoutes
})

export default router

2. 动态获取后端路由 (stores/menu.ts)

递归解析路由 根据后端返回文件路径匹配路由

import { defineStore } from 'pinia'
import { getShowMenuList, generateRouter, arrayToTree } from "@/utils/tool"
import { getAllMenuByUserName } from '@/api/commonApi'

export const useMenuStore = defineStore({
  id: 'menu',
  state: (): any => ({
    menusList: [],
    cmsMenusList: []
  }),
  getters: {
    showMenuListGet: state => getShowMenuList(state.cmsMenusList)
  },
  actions: {
    async getMenu() {
      await getAllMenuByUserName({}).then(res => {
        if (res.data?.status == 200) {
          // 过滤出 parentId 为 2 的菜单项
          let filteredA = res.data.data.sysMenuResp.filter((item: any) => item.parentId === 2) || [];

          // 过滤出其 parentId 存在于 filteredA 中的菜单项
          let filteredB = res.data.data.sysMenuResp.filter((item: any) => {
            return filteredA.some((itemA: any) => itemA.menuId == item.parentId);
          }) || [];

          // 将 filteredA 和 filteredB 合并为一个扁平化的树结构数组
          let flattenedArray = generateRouter(arrayToTree([...filteredA, ...filteredB]));

          // 构建最终的菜单列表,包括固定的首页菜单项和动态生成的菜单项
          this.cmsMenusList = [
            {
              name: 'home',
              path: '/home/index',
              component: () => import('@/views/home/index.vue'),
              meta: {
                icon: 'home-outlined',
                title: '首页',
                isKeepAlive: false
              }
            },
            ...flattenedArray,
          ];
        }
      });
    }
  }
})

//  import { getShowMenuList, generateRouter, arrayToTree } from "@/utils/tool"

// 过滤菜单  不需要在菜单栏中显示
export function getShowMenuList(menuList: Menu.MenuOptions[]) {
  let newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList));
  let arr: any = newMenuList.filter(item => {
    item.children?.length && (item.children = getShowMenuList(item.children));
    return !item.meta?.isHide;
  });
  return arr
}


// 数组转树
export function arrayToTree(array: any) {
  const map: any = {}; // 使用对象存储每个节点的引用
  // 遍历数组,将每个节点的引用存储到 map 中
  array.forEach((item: any) => {
    map[item.menuId] = { ...item, children: [] } // 创建一个包含子节点数组的新节点对象
  })
  const tree: any = []
  // 遍历数组,将每个节点添加到其父节点的 children 数组中
  array.forEach((item: any) => {
    const node = map[item.menuId]
    // 注意:这里是父级菜单的id,本项目中返回的数据是多个系统菜单的集合数据, 
    // 在项目开发中后端可能返回只属于当前系统的数据,
    if (item.parentId === 2) {
      // 没有父节点,说明是根节点
      tree.push(node)
    } else {
      const parent = map[item.parentId]
      parent.children.push(node)
    }
  })
  // console.log(tree)
  return tree
}

const modules = import.meta.glob("../views/**/*.vue")
export const generateRouter = (routerMap: any) => {
  return routerMap.map((item: any) => {
    const currentRouter: any = {
      path: item.singlePath,
      name: item.query,
      redirect: item.redirect,
      meta: {
        icon: item.icon,
        title: item.menuName,
        isKeepAlive: true, // keepAlive 缓存 可以根据菜单设置动态显示,此处写死的,
        isHide: item.visible == 0 ? false : true  // 显示隐藏 0 显示  1隐藏
      }
    };
    if (item.singleComponent && typeof item.singleComponent == "string") {
      currentRouter.component = modules[`../views/${item.singleComponent}.vue`]
    }
    // 是否存在子路由
    item?.children?.length && (currentRouter.children = generateRouter(item.children))
    return currentRouter;
  })
}

切记 keepAlive 缓存需要绑定组件name!!


const modules = import.meta.glob(“…/views//*.vue") : 解析
动态导入符合模式 "…/views//*.vue” 的模块(文件)

import.meta.glob 函数:这是一个特殊的函数,在ES模块环境中它允许根据文件模式动态导入模块
文件模式 (“…/views/**/*.vue”):该模式指定了要导入的所有以 .vue 扩展名结尾的模块(或文件),这些文件位于 …/views/ 目录下的任何子目录中
import.meta.glob 函数返回一个对象,对象的键是匹配到的文件路径,对应的值是一个函数。这个函数调用时返回一个 Promise,该 Promise 解析为导入模块的默认导出。

3.动态挂载路由permission.ts

import type { RouteRecordRaw } from 'vue-router'
import { useMenuStore } from '@/stores/menu'
import router from './index'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
import { useCounterStore } from '@/stores/counter'
import { useLocalStore } from '@/stores/local'
import { qiankunWindow, } from 'vite-plugin-qiankun/dist/helper'

/** 免登录白名单 */
const whiteList: Array<string> = ['/login', '404']


router.beforeEach(async (to, from, next) => {
  const menuStore = useMenuStore()
  // const counterStore = useCounterStore()
  const localStore = useLocalStore()
  nprogress.start()

  // ps  qiankun子应用处理菜单特殊处理 不用看
  // counterStore.latoutStatus = !qiankunWindow.__POWERED_BY_QIANKUN__
  // !qiankunWindow.__POWERED_BY_QIANKUN__ ? window.document.title = (to.meta.title || localStore.homeBasic?.sysName) as string : ''
  const token = localStorage.token

  if (token) {
    if (!menuStore.cmsMenusList.length) {
      await localStore.getInfo()   // 获取个人信息
      await menuStore.getMenu()    // 获取菜单
      await localStore.getUserPermiss()  // 获取 按钮权限数据
      menuStore.cmsMenusList.forEach((item: any) => {
        if (item.meta.isFull) {
          router.addRoute(item as unknown as RouteRecordRaw)
        } else {
          router.addRoute('layout', item as unknown as RouteRecordRaw)
        }
      })
      return next({ ...to, replace: true })
    } else {
      next()
    }
    // next();
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next('/login')
      nprogress.done()
    }
  }
})

router.onError((error) => {
  localStorage.clear()
  router.replace('/login')
  location.reload()
  console.warn('路由错误', error.message)
})

router.afterEach(() => {
  const counterStore = useCounterStore()
  counterStore.showVisible = 1
  nprogress.done()
})


左侧菜单栏组件 使用

<MenuItem :menus="menus" />
const menus: any = computed(() => menuStore.showMenuListGet)

// 菜单组件

<template>
  <template v-for="item in menus" :key="item.path">
    <!-- 目录 -->
    <template v-if="item.children && item.children.length">
      <a-sub-menu :key="item.path">
        <template #icon>
          <component :is="item.meta.icon"></component>
        </template>
        <template #title>
          <span>{{ item.meta.title }}</span>
        </template>
        <template #default>
          <!-- 递归生成菜单 -->
          <meun-item :menus="item.children"></meun-item>
        </template>
      </a-sub-menu>
    </template>
    <!-- 菜单 -->
    <template v-else>
      <a-menu-item :key="item.path">
        <template #icon v-if="item.meta.icon">
          <component :is="item.meta.icon"></component>
        </template>
        <span>{{ item.meta.title }}</span>
        <!-- <a :href=`'/'+${item.path}`>11</a> -->
        <router-link :to="item.path"></router-link>
      </a-menu-item>
    </template>
  </template>
</template>

<script lang="ts">
import { type PropType } from 'vue'
import { RouteRecordRaw } from 'vue-router'

export default {
  name: 'MeunItem',
  props: {
    menus: {
      type: Array as PropType<RouteRecordRaw[]>,
      default: () => []
    }
  }
}
</script>

<style scoped></style>

// 全局icon组件 在main.ts 引入

import * as Icons from '@ant-design/icons-vue'
 Object.keys(Icons).forEach((key) => {
    app.component(key, Icons[key as keyof typeof Icons])
  })
上一篇:【无标题】


下一篇:音视频中文件的复用和解复用