我们在写后台管理系统动态菜单的时候经常会碰到一些问题,动态菜单具体实现步骤以及实现时经常碰到的一系列问题,在这里记录并分享。
首先是router,router包含两个部分,router/index.vue,和后端或者mock获取的json.
//router/index.vue
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
export const Layout = () => import("@/layout/index.vue");
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/',
name: '/',
component: Layout,
meta: {
title: "首页",
icon: "system",
hidden: false,
roles: ["ADMIN"],
},
children:[
{
path:"dashboard",
component: () => import('@/view/Index.vue'),
name: "Dashboard",
meta: {
title:'dashboard',
hidden: false,
},
},
{
path: "401",
component: () => import("@/views/error-page/401.vue"),
meta: { hidden: true },//在动态菜单中隐藏当前页面
},
],
},
]
export const router = createRouter({//main.js中添加router后就可以使用了
history: createWebHistory(),//或者createWebHashHistory:url使用#号
routes: constantRoutes as RouteRecordRaw[],
})
//menu.mock.ts
export default [
{
url: "/api/v1/menus/routes",
method: "get",
response: () => {
return {
code: 0,
message: "ok",
data: [
{
path: "/renmindemingyi",
component: "Layout",
name: "/renmindemingyi",
meta: {
title: "人民的名义"
icon: "menu",
hidden: false,//菜单中是否隐藏
roles: ["ADMIN"],
},
children: [
{
path: "roleList",
component: "renmindemingyi/roleList/index",
name: "RoleList",
meta: {
title: "角色列表",
hidden: false,
},
},
{
path: "roleList",
component: "renmindemingyi/introduce/index",
name: "RoleList",
meta: {
title: "剧情简介",
hidden: false,
},
},
],
},
{
path: "/fumuaiqing",
component: "Layout",
name: "/renmindemingyi",
meta: {
title: "父母爱情"
icon: "menu",
hidden: false,//菜单中是否隐藏
roles: ["ADMIN"],
},
},
//继续添加加。。。。。。
],
}
}
},
{
//继续添加加。。。。。。
}
]
//main.js 入口文件
import "@/permission";
import {router} from '@/router'
app.use(router).mount('#app')
//permission.js
import {router} from "@/router";
import { usePermissionStoreHook } from "@/store/modules/permission";
const permissionStore = usePermissionStoreHook();
let isShow = true // 自定义变量去控制两次进入路由守卫
router.beforeEach(async (to, from, next) => {
try {
const roles = ['ADMIN']
const accessRoutes = await permissionStore.generateRoutes(roles);
accessRoutes.forEach((route) => {
router.addRoute(route);
});
// 判断是否第一次进入路由守卫
if (isShow) {
next({ ...to, replace: true })
isShow = false // 第一次进入后修改,使第二次跳转
} else {
next()
}
} catch (error) {
}
return false
})
判断是否第一次进入路由守卫特别重要,如果只使用 next({ ...to, replace: true }),就会陷入无限循环。但是如果去掉参数使用next(),它会警告: No match found for location with path "*****"
// @/store/modules/permission
import { ref } from 'vue';
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { constantRoutes } from "@/router";
import { store } from "@/store";
import { listRoutes } from "@/api/menu";
//后端获取的路由中component是字符串,想要导入component中的文件需要使用vue3+vite中的import.meta.glob动态导入, 获取view文件下的所有vue文件
const modules = import.meta.glob("../../view/**/**.vue");
const Layout = () => import("@/layout/index.vue");
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route };
if (!route.name) {
tmpRoute.name = route.path;
}
if (tmpRoute.component?.toString() == "Layout") {
//跳转layout/index.vue文件
tmpRoute.component = Layout;
} else {
const component = modules[`../../view/${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
} else {
tmpRoute.component = modules[`../../view/error-page/404.vue`];
}
}
//递归children
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
}
//把转换好的数据push到route中
asyncRoutes.push(tmpRoute);
})
return asyncRoutes;
}
export const usePermissionStore = defineStore("permission", () => {
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
//获取静态路由并设置新路由
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*/
function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
// 接口获取所有路由
listRoutes().then((asyncRoutes) => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
resolve(accessedRoutes);
})
.catch((error) => {
reject(error);
});
});
}
return { routes, generateRoutes }
})
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
//layout/index.vue
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { Layout, LayoutSider, LayoutContent, LayoutFooter } from 'ant-design-vue';
import Sidebar from "./Sidebar/index.vue";
import Main from "./main.vue";
const contentStyle: CSSProperties = {
textAlign: 'center',
minHeight: 120,
lineHeight: '120px',
color: '#fff',
backgroundColor: '#fff',
};
const siderStyle: CSSProperties = {
textAlign: 'center',
lineHeight: '120px',
color: '#fff',
backgroundColor: '#888',
};
const footerStyle: CSSProperties = {
textAlign: 'center',
color: '#fff',
backgroundColor: '#999',
};
</script>
<template>
<Layout>
<Layout>
<LayoutSider :style="siderStyle">
<!--左侧动态路由-->
<Sidebar class="sidebar-container" />
</LayoutSider>
<LayoutContent :style="contentStyle">
<!--右侧内容-->
<Main />
</LayoutContent>
</Layout>
<LayoutFooter :style="footerStyle">Footer</LayoutFooter>
</Layout>
</template>
//./Sidebar/index.vue 左侧动态路由
<script lang="ts" setup>
import { ref } from 'vue';
import { Menu, MenuItem, SubMenu } from 'ant-design-vue';
import type { MenuTheme, MenuProps, ItemType } from 'ant-design-vue';
import { RouteRecordRaw } from "vue-router";
import { usePermissionStore } from "@/store/modules/permission";
import SiderbarItem from './SidebarItem.vue'
const permissionStore = usePermissionStore();
const theme = ref<MenuTheme>('dark');
const selectedKeys = ref(['1']);
const openKeys = ref(['sub1']);
console.log(permissionStore.routes, 40)
const handleClick: MenuProps['onClick'] = e => {
console.log('click', e);
};
const onlyOneChild = ref(); // 临时变量,唯一子路由
function hasOneShowingChild(
children: RouteRecordRaw[] = [],
parent: RouteRecordRaw
) {
// 子路由集合
const showingChildren = children.filter((route: RouteRecordRaw) => {
if (route.meta?.hidden) {
// 过滤不显示的子路由
return false;
} else {
route.meta!.hidden = false;
// 临时变量(多个子路由 onlyOneChild 变量是用不上的)
onlyOneChild.value = route;
return true;
}
});
// 如果没有子路由,显示父级路由
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
</script>
<template>
<div class="menu-wrap">
<Menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
:theme="theme"
mode="inline"
@click="handleClick"
>
<template v-for="item in permissionStore.routes" :key="item.name">
<template v-if="!item.meta || !item.meta.hidden">
<!--没有child,子路由-->
<template
v-if="
hasOneShowingChild(item.children, item as RouteRecordRaw) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.meta?.alwaysShow
"
>
<MenuItem :key="item.name">
<PieChartOutlined />
<span>{{ item.meta?.title }}</span>
</MenuItem>
</template>
<template v-else>
<SiderbarItem :menuInfo="item" :key="item.name" />
</template>
</template>
</template>
</Menu>
</div>
</template>
//./SidebarItem.vue
<script lang="ts" setup>
import { ref } from 'vue';
import { isExternal } from "@/utils/index";
import { Menu,MenuItem, SubMenu } from 'ant-design-vue';
import { useRouter, RouteRecordRaw } from "vue-router";
import path from "path-browserify";
const props= defineProps({
...Menu.SubMenu.props,
menuInfo: {
type: Object,
},
});
const router = useRouter();
function resolvePath(route: RouteRecordRaw) {
let fullPath
// 完整路径(/system/user) = 父级路径(/system) + 路由路径(user)
fullPath = path.resolve(props.menuInfo.path, route.path);
return fullPath;
}
function hasOneShowingChild(parent: RouteRecordRaw) {
if (parent.meta?.hidden) {// 过滤不显示的子路由
return false;
} else {
return true;
}
return false;
}
</script>
<template>
<SubMenu :key="menuInfo?.name" v-bind="$attrs" >
<template #title>
<span>
<span>{{ menuInfo?.meta.title }}</span>
</span>
</template>
<template v-for="(item) in menuInfo.children" :key="item.name">
<template
v-if="
hasOneShowingChild(item as RouteRecordRaw)
"
>
<MenuItem :key="item.name" >
<routerLink :to="resolvePath(item)">
{{ menuInfo.path + '/' + item.path }}
{{ item.meta.title }}
</routerLink>
</MenuItem>
</template>
<template v-else>
<!--递归展示children-->
<SiderbarItem :menuInfo="item" :key="item.name" />
</template>
</template>
</SubMenu>
</template>
//右侧main.vue
<script setup lang="ts">
import { useTagsViewStore } from "@/store/modules/tagsView";
const tagsViewStore = useTagsViewStore();
</script>
<template>
<div class="main-container">
<section class="app-main">
<router-view>
<template #default="{ Component, route }">
<component :is="Component" :key="route.fullPath" />
</template>
</router-view>
</section>
</div>
</template>