ant-design vue递归渲染menu(权限管理)
目录
ant-design中未修改源码
<a-menu-item key="1">
<user-outlined />
<span>nav 1</span>
</a-menu-item>
<a-menu-item key="2">
<video-camera-outlined />
<span>nav 2</span>
</a-menu-item>
<a-menu-item key="3">
<upload-outlined />
<span>nav 3</span>
</a-menu-item>
修改后
<MenuItem :menus="getMenus()"></MenuItem>
- 递归调用自身:
- 递归调用组件
menuItem
,通过:menus="item.children"
传递子菜单数据给自身,从而实现多层嵌套菜单的渲染。
- 递归调用组件
- 条件渲染:
- 通过
v-if
判断item.children
是否存在,决定使用a-sub-menu
(有子菜单)还是a-menu-item
(无子菜单)进行渲染。
- 通过
- 使用键值:
- 通过
:key="item.path"
确保每个元素都有唯一的键值,优化渲染性能。
- 通过
<template>
<template v-for="item in menus" :key="item.path">
<!--
判断是否有子级 如果有则继续循环
因为有子级,所以说我们继续循环的
-->
<a-sub-menu :key="item.path" v-if ="item.children">
<!-- 一级菜单名字 -->
<template #title>
{{ item.label }}
</template>
<!-- 子菜单,引用组件本身,组件的name就是组件名驼峰 -->
<menuItem :menus="item.children"></menuItem>
</a-sub-menu>
<template v-else >
<a-menu-item :key="item.path">
<span>{{ item.label }}</span>
</a-menu-item>
</template>
</template>
</template>
<script setup lang="ts">
/**
* vue里面组件的递归其实就是组件的自调用
*/
import { defineProps } from "vue";
/**
* 接收一个menus是当前渲染的组件数据
*/
const props = defineProps(["menus"]);
</script>
递归列表:
as Menus[]
表示返回规范为Menus,及以下规范:
import { Role } from "../type";
export interface Menus {
label: string;
path: string;
roles: Role[];
children?: Menus[];
component: () => any;
}
role
表示渲染的权限
import { Menus } from "./layout.type";
export const getMenus = () => {
return [
{
label: "门店管理",
path: "/storeManage",
roles: ["inspect", "familyBanquet"],
children: [
{
label: "餐饮单位列表",
path: "/company",
roles: ["inspect"],
component: () => import("./../views/company/company.vue"),
},
{
label: "家宴中心",
path: "/familyBanquet",
roles: ["familyBanquet"],
component: () => import("./../views/familyBanquet/familyBanquet.vue"),
},
{
label: "乡厨信息",
path: "/cookTeam",
roles: ["familyBanquet"],
component: () => import("./../views/cookTeam/cookTeam.vue"),
},
],
},
{
label: "监督检查",
path: "/inspec",
roles: ["inspect"],
children: [
{
label: "监督检查列表",
path: "/supervise",
roles: ["inspect"],
component: () => import("./../views/supervise/supervise.vue"),
},
],
},
{
label: "备案登记",
path: "/register",
roles: ["familyBanquet"],
children: [
{
label: "备案登记",
path: "/registerRecord",
roles: ["familyBanquet"],
component: () =>
import("./../views/registerRecord/registerRecord.vue"),
},
],
},
{
label: "权限管理",
path: "/sys",
roles: ["admin"],
children: [
{
label: "用户管理",
path: "/user",
roles: ["admin"],
component: () => import("./../views/user/user.vue"),
},
],
},
] as Menus[];
};
添加role权限控制动态渲染
根据当前登录用户角色获取有权限的菜单
filter
函数递归判断目录,若目录返回true则将目录加入数组
item.roles.some((key) => roles.includes(key as never));
some()
用于测试数组中是否至少有一个元素满足提供的测试函数,返回布尔值
key
表示当前role(权限),roles.includes(key as never)
表示在当前目录的roles中是否包含当前权限,若包含返回true
<MenuItem :menus="getCurrentMenus(roles as never[])"></MenuItem>
export const getCurrentMenus = (roles = [], menus = getMenus()) => {
return menus.filter((item) => {
//递归调用
if (item.children) {
(item.children as any) = getCurrentMenus(roles, item.children as any);
}
// 当前用户角色和菜单角色作对比
return item.roles.some((key) => roles.includes(key as never));//返回true或者false
});
};
动态添加路由
在调用 router.addRoute
时传入 "layout"
,这表示将新添加的路由作为 layout
路由的子路由。也就是说,所有的动态路由将被挂载在 layout
这个父路由之下。
/**
* 动态渲染路由
*/
export const addRoute = (currentMenus: Menus[], router: Router) => {
//递归调用
currentMenus.forEach((item) => {
if (item.children) {
addRoute(item.children, router);
}
//为layout添加子路由
if (item.component as any) {
router.addRoute("layout", {
path: item.path,
component: item.component,
meta: {
label: item.label
}
});
}
});
};
getMenus()
为所有的菜单目录,通过getCurrentMenus()
对所有的菜单目录进行权限筛选,然后传入addRoute()
动态添加路由
const initAddRouter = () => {
const global = JSON.parse(localStorage.getItem("global") || "{}");
// 拿到有权限的菜单
const currentMenus = getCurrentMenus(global.roles, getMenus());
// 动态路由
addRoute(currentMenus, router);
};
initAddRouter();
完整代码:
ts定义函数
import { Router } from "vue-router";
import { getMenus } from "./layout.config";
import { Menus } from "./layout.type";
import { useGlobalStore } from "./../store/global";
import { Role } from "../type";
/**
* 根据当前登录用户角色
* 获取有权限的菜单
*/
export const getCurrentMenus = (roles = [], menus = getMenus()) => {
return menus.filter((item) => {
if (item.children) {
(item.children as any) = getCurrentMenus(roles, item.children as any);
}
// 当前用户角色和菜单角色作对比
return item.roles.some((key) => roles.includes(key as never));//返回true或者false
});
};
/**
* 动态渲染路由
*/
export const addRoute = (currentMenus: Menus[], router: Router) => {
currentMenus.forEach((item) => {
if (item.children) {
addRoute(item.children, router);
}
if (item.component as any) {
router.addRoute("layout", {
path: item.path,
component: item.component,
meta: {
label: item.label
}
});
}
});
};
路由设置
import { createRouter, createWebHashHistory } from "vue-router";
import { addRoute, getCurrentMenus } from "./layout/layout.util";
import { getMenus } from "./layout/layout.config";
const routes = [
{
name: "layout",
path: "/layout",
component: () => import("./layout/layout.vue"),
},
{
name: "login",
path: "/login",
component: () => import("./views/login/login.vue"),
},
// {
// name: "login",
// path: "/",
// component: () => import("./views/login/login.vue"),
// },
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
/**
* 动态添加路由
* 只要进入程序一定会执行这个文件
*
*/
const initAddRouter = () => {
const global = JSON.parse(localStorage.getItem("global") || "{}");
// 拿到有权限的菜单
const currentMenus = getCurrentMenus(global.roles, getMenus());
// 动态路由
addRoute(currentMenus, router);
};
initAddRouter();
/**
* 每次跳转前触发
*/
// router.beforeEach((to, from, next) => {
// const global = JSON.parse(localStorage.getItem("global") || "{}");
// // 忽略token校验
// const ignoreRouters = ["/login"];
// // 如果没有,跳转到登录页面 且 不在白名单里
// if (!global.token && !ignoreRouters.includes(to.path)) {
// next("/login");
// return
// }
// next();
// });
export default router;
完整逻辑梳理
登录后将服务器返回的token和toles存入pinia状态管理库store
const global = useGlobalStore();
const login = async () => {
console.log(formState.value);
const data = await API.login(formState.value);
global.setToken(data.token);
global.setRoles(data.roles);
const currentMenus = getCurrentMenus(data.roles as any, getMenus());
addRoute(currentMenus, router);
router.push(data.roles.includes("inspect") ? "/company" : "/familyBanquet");
};
点击跳转:
通过@click="linkPage"
绑定点击事件,点击跳转到绑定路由key(在总菜单目录中定义的遍历key是path:"/
")。
动态生成菜单:
将总菜单目录getMenus()
传入getCurrentMenus()
,该函数会根据权store状态存储库里拿到roles(权限)从总菜单中筛选出符合权限的数组,然后将筛选出的数组通过 :menus
传入组件<MenuItem></MenuItem>
,该组件通过递归遍历传入的数组来加载一级菜单和子菜单。
动态生成路由:
点击的是动态生成的菜单,但没有具体路由路径仍然无法跳转,所以我们要动态生成路由。
将筛选后的菜单和当前路由对象传入addRoute(筛选菜单,当前路由对象)
,动态添加路由。
列表主体:
<a-menu
@click="linkPage"
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
>
<!-- 组件递归有权限的数据 -->
<MenuItem :menus="getCurrentMenus(roles as never)"></MenuItem>
</a-menu>
@click="linkPage"
:
key为子组件<MenuItem></MenuItem>
设置的 :key="item.path"
,及getMenus()
中的路由路径
const linkPage = ({ key }: { key: string }) => {
router.push(key);
};
:menus
:
在主体中传入getCurrentMenus()
: :menus="getCurrentMenus(roles as never)"
,通过<MenuItem></MenuItem>
中的
const props = defineProps(["menus"]);
接收,将menus
传入<MenuItem></MenuItem>
组件
const props = defineProps(["menus"]);
<MenuItem></MenuItem>
:
<template>
<template v-for="item in menus" :key="item.path">
<!--
判断是否有子级 如果有则继续循环
因为有子级,所以说我们继续循环的
-->
<a-sub-menu :key="item.path" v-if="item.children">
<!-- 一级菜单名字 -->
<template #title>
{{ item.label }}
</template>
<!-- 子菜单,引用组件本身,组件的name就是组件名驼峰 -->
<menuItem :menus="item.children"></menuItem>
</a-sub-menu>
<template v-else>
<a-menu-item :key="item.path">
<span>{{ item.label }}</span>
</a-menu-item>
</template>
</template>
</template>
<script setup lang="ts">
/**
* vue里面组件的递归其实就是组件的自调用
*/
import { defineProps } from "vue";
/**
* 接收一个menus是当前渲染的组件数据
*/
const props = defineProps(["menus"]);
</script>
getCurrentMenus()
:
getMenus()
为所有的菜单目录,传入菜单目录后将服务端返回的角色解析为js对象并传入roles
中
filter
函数递归判断目录,若目录返回true则将目录加入数组
item.roles.some((key) => roles.includes(key as never));
some()
用于测试数组中是否至少有一个元素满足提供的测试函数,返回布尔值
key
表示传入的item中roles的所有值,并从中查询是否存在roles2
export const getCurrentMenus = (roles2 = [], menus = getMenus()) => {
return menus.filter((item) => {
if (item.children) {
(item.children as any) = getCurrentMenus(roles2, item.children as any);
}
// 当前用户角色和菜单角色作对比
console.log(item.roles);
return item.roles.some((key) => roles2.includes(key as never));
});
};
之后就是动态添加路由:动态添加路由