后台管理 动态获取菜单例子
2023-12-27 11:13:55
路由json
{
"code": "00000",
"data": [
{
"path": "/system",
"component": "Layout",
"redirect": "/system/user",
"name": "/system",
"meta": {
"title": "系统管理",
"icon": "system",
"hidden": false,
"roles": [
"ADMIN"
]
},
"children": [
{
"path": "user",
"component": "system/user/index",
"name": "User",
"meta": {
"title": "用户管理",
"icon": "user",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "role",
"component": "system/role/index",
"name": "Role",
"meta": {
"title": "角色管理",
"icon": "role",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "menu",
"component": "system/menu/index",
"name": "Menu",
"meta": {
"title": "菜单管理",
"icon": "menu",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "dept",
"component": "system/dept/index",
"name": "Dept",
"meta": {
"title": "部门管理",
"icon": "tree",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "dict",
"component": "system/dict/index",
"name": "Dict",
"meta": {
"title": "字典管理",
"icon": "dict",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
}
]
},
{
"path": "/api",
"component": "Layout",
"name": "/api",
"meta": {
"title": "接口文档",
"icon": "api",
"hidden": false,
"roles": [
"ADMIN"
],
"alwaysShow": true
},
"children": [
{
"path": "apifox",
"component": "demo/api/apifox",
"name": "Apifox",
"meta": {
"title": "Apifox",
"icon": "api",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "swagger",
"component": "demo/api/swagger",
"name": "Swagger",
"meta": {
"title": "Swagger",
"icon": "api",
"hidden": true,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "knife4j",
"component": "demo/api/knife4j",
"name": "Knife4j",
"meta": {
"title": "Knife4j",
"icon": "api",
"hidden": true,
"roles": [
"ADMIN"
],
"keepAlive": true
}
}
]
},
{
"path": "/doc",
"component": "Layout",
"name": "/doc",
"meta": {
"title": "平台文档",
"icon": "document",
"hidden": false,
"roles": [
"ADMIN"
]
},
"children": [
{
"path": "internal-doc",
"component": "demo/internal-doc",
"name": "InternalDoc",
"meta": {
"title": "平台文档(内嵌)",
"icon": "document",
"hidden": false,
"roles": [
"ADMIN"
]
}
},
{
"path": "https://juejin.cn/post/7228990409909108793",
"name": "Https://juejin.cn/post/7228990409909108793",
"meta": {
"title": "平台文档(外链)",
"icon": "link",
"hidden": false,
"roles": [
"ADMIN"
]
}
}
]
},
{
"path": "/multi-level",
"component": "Layout",
"redirect": "/multi-level/multi-level1",
"name": "/multiLevel",
"meta": {
"title": "多级菜单",
"icon": "cascader",
"hidden": false,
"roles": [
"ADMIN"
]
},
"children": [
{
"path": "multi-level1",
"component": "demo/multi-level/level1",
"redirect": "/multi-level/multi-level2",
"name": "MultiLevel1",
"meta": {
"title": "菜单一级",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
},
"children": [
{
"path": "multi-level2",
"component": "demo/multi-level/children/level2",
"redirect": "/multi-level/multi-level2/multi-level3-1",
"name": "MultiLevel2",
"meta": {
"title": "菜单二级",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
},
"children": [
{
"path": "multi-level3-1",
"component": "demo/multi-level/children/children/level3-1",
"name": "MultiLevel31",
"meta": {
"title": "菜单三级-1",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "multi-level3-2",
"component": "demo/multi-level/children/children/level3-2",
"name": "MultiLevel32",
"meta": {
"title": "菜单三级-2",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
}
]
}
]
}
]
},
{
"path": "/component",
"component": "Layout",
"name": "/component",
"meta": {
"title": "组件封装",
"icon": "menu",
"hidden": false,
"roles": [
"ADMIN"
]
},
"children": [
{
"path": "wang-editor",
"component": "demo/wang-editor",
"name": "WangEditor",
"meta": {
"title": "富文本编辑器",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "upload",
"component": "demo/upload",
"name": "Upload",
"meta": {
"title": "图片上传",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "icon-selector",
"component": "demo/icon-selector",
"name": "IconSelector",
"meta": {
"title": "图标选择器",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "dict-demo",
"component": "demo/dict",
"name": "DictDemo",
"meta": {
"title": "字典组件",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "signature",
"component": "demo/signature",
"name": "Signature",
"meta": {
"title": "签名",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "table",
"component": "demo/table",
"name": "Table",
"meta": {
"title": "表格",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
}
]
},
{
"path": "/function",
"component": "Layout",
"name": "/function",
"meta": {
"title": "功能演示",
"icon": "menu",
"hidden": false,
"roles": [
"ADMIN"
]
},
"children": [
{
"path": "icon-demo",
"component": "demo/icons",
"name": "IconDemo",
"meta": {
"title": "Icons",
"icon": "el-icon-edit",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "/function/websocket",
"component": "demo/websocket",
"name": "/function/websocket",
"meta": {
"title": "Websocket",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
],
"keepAlive": true
}
},
{
"path": "other",
"component": "demo/other",
"name": "Other",
"meta": {
"title": "敬请期待...",
"icon": "",
"hidden": false,
"roles": [
"ADMIN"
]
}
}
]
}
],
"msg": "一切ok"
}
store/permission.ts
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { constantRoutes } from "@/router";
import { store } from "@/store";
import { listRoutes } from "@/api/menu";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
/**
* Use meta.role to determine if the current user has permission
*
* @param roles 用户角色集合
* @param route 路由
* @returns
*/
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
if (route.meta && route.meta.roles) {
// 角色【超级管理员】拥有所有权限,忽略校验
if (roles.includes("ROOT")) {
return true;
}
return roles.some((role) => {
if (route.meta?.roles) {
return route.meta.roles.includes(role);
}
});
}
return false;
};
/**
* 递归过滤有权限的异步(动态)路由
*
* @param routes 接口返回的异步(动态)路由
* @param roles 用户角色集合
* @returns 返回用户有权限的异步(动态)路由
*/
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const asyncRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmpRoute = { ...route }; // ES6扩展运算符复制新对象
if (!route.name) {
tmpRoute.name = route.path;
}
// 判断用户(角色)是否有该路由的访问权限
if (hasPermission(roles, tmpRoute)) {
if (tmpRoute.component?.toString() == "Layout") {
tmpRoute.component = Layout;
} else {
const component = modules[`../../views/${tmpRoute.component}.vue`];
if (component) {
tmpRoute.component = component;
} else {
tmpRoute.component = modules[`../../views/error-page/404.vue`];
}
}
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
}
asyncRoutes.push(tmpRoute);
}
});
return asyncRoutes;
};
// setup
export const usePermissionStore = defineStore("permission", () => {
// state
const routes = ref<RouteRecordRaw[]>([]);
// actions
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成动态路由
*
* @param roles 用户角色集合
* @returns
*/
function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
// 接口获取所有路由
listRoutes()
.then(({ data: asyncRoutes }) => {
// 根据角色获取有访问权限的路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
resolve(accessedRoutes);
})
.catch((error) => {
reject(error);
});
});
}
/**
* 混合模式左侧菜单
*/
const mixLeftMenu = ref<RouteRecordRaw[]>([]);
function getMixLeftMenu(activeTop: string) {
routes.value.forEach((item) => {
if (item.path === activeTop) {
mixLeftMenu.value = item.children || [];
}
});
}
return { routes, setRoutes, generateRoutes, getMixLeftMenu, mixLeftMenu };
});
// 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
src/permission.ts
import router from "@/router";
import { useUserStoreHook } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({ showSpinner: false }); // 进度条
const permissionStore = usePermissionStoreHook();
// 白名单路由
const whiteList = ["/login"];
router.beforeEach(async (to, from, next) => {
NProgress.start();
const hasToken = localStorage.getItem("accessToken");
if (hasToken) {
if (to.path === "/login") {
// 如果已登录,跳转首页
next({ path: "/" });
NProgress.done();
} else {
const userStore = useUserStoreHook();
const hasRoles = userStore.user.roles && userStore.user.roles.length > 0;
if (hasRoles) {
// 未匹配到任何路由,跳转404
if (to.matched.length === 0) {
from.name ? next({ name: from.name }) : next("/404");
} else {
next();
}
} else {
try {
const { roles } = await userStore.getUserInfo();
//接口取菜单页可以写死
const accessRoutes = await permissionStore.generateRoutes(roles);
accessRoutes.forEach((route) => {
router.addRoute(route);
});
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
await userStore.resetToken();
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
// 未登录可以访问白名单页面
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
main.ts
import "@/permission";
src/layout/index.vue
<script setup lang="ts">
import Main from "./main.vue";
import { computed, watchEffect } from "vue";
import { useWindowSize } from "@vueuse/core";
import Sidebar from "./components/Sidebar/index.vue";
import LeftMenu from "./components/Sidebar/LeftMenu.vue";
import { useAppStore } from "@/store/modules/app";
import { useSettingsStore } from "@/store/modules/settings";
import { usePermissionStore } from "@/store/modules/permission";
const permissionStore = usePermissionStore();
const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度
*
* 大屏(>=1200px)
* 中屏(>=992px)
* 小屏(>=768px)
*/
const WIDTH = 992;
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const activeTopMenu = computed(() => {
return appStore.activeTopMenu;
});
// 混合模式左侧菜单
const mixLeftMenu = computed(() => {
return permissionStore.mixLeftMenu;
});
const layout = computed(() => settingsStore.layout);
const watermarkEnabled = computed(() => settingsStore.watermark.enabled);
watch(
() => activeTopMenu.value,
(newVal) => {
if (layout.value !== "mix") return;
permissionStore.getMixLeftMenu(newVal);
},
{
deep: true,
immediate: true,
}
);
const classObj = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === "mobile",
isTop: layout.value === "top",
isMix: layout.value === "mix",
}));
watchEffect(() => {
if (width.value < WIDTH) {
appStore.toggleDevice("mobile");
appStore.closeSideBar(true);
} else {
appStore.toggleDevice("desktop");
if (width.value >= 1200) {
//大屏
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
}
}
});
function handleOutsideClick() {
appStore.closeSideBar(false);
}
function toggleSideBar() {
appStore.toggleSidebar();
}
</script>
<template>
<div :class="classObj" class="app-wrapper">
<!-- 手机设备侧边栏打开遮罩层 -->
<div
v-if="classObj.mobile && classObj.openSidebar"
class="drawer__background"
@click="handleOutsideClick"
></div>
<Sidebar class="sidebar-container" />
<div v-if="layout === 'mix'" class="mix-wrapper">
<div class="mix-wrapper__left">
<LeftMenu :menu-list="mixLeftMenu" :base-path="activeTopMenu" />
<!-- 展开/收缩侧边栏菜单 -->
<div class="toggle-sidebar">
<hamburger
:is-active="appStore.sidebar.opened"
@toggle-click="toggleSideBar"
/>
</div>
</div>
<Main />
</div>
<Main v-else />
</div>
</template>
<style lang="scss" scoped>
.app-wrapper {
&::after {
display: table;
clear: both;
content: "";
}
position: relative;
width: 100%;
height: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer__background {
position: absolute;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
background: #000;
opacity: 0.3;
}
// 导航栏顶部显示
.isTop {
.sidebar-container {
z-index: 800;
display: flex;
width: 100% !important;
height: 50px;
:deep(.logo-wrap) {
width: $sideBarWidth;
}
:deep(.el-scrollbar) {
flex: 1;
min-width: 0;
height: 50px;
}
}
.main-container {
padding-top: 50px;
margin-left: 0;
overflow: hidden;
}
// 顶部模式全局变量修改
--el-menu-item-height: 50px;
}
.mobile.isTop {
:deep(.logo-wrap) {
width: 63px;
}
}
.isMix {
:deep(.main-container) {
display: inline-block;
width: calc(100% - #{$sideBarWidth});
margin-left: 0;
}
.mix-wrapper {
display: flex;
height: 100%;
padding-top: 50px;
.mix-wrapper__left {
position: relative;
height: 100%;
.el-menu {
height: 100%;
}
.toggle-sidebar {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50px;
line-height: 50px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
div:hover {
background-color: var(--menuBg);
}
:deep(svg) {
color: #409eff !important;
}
}
}
.main-container {
flex: 1;
min-width: 0;
}
}
}
.openSidebar {
.mix-wrapper {
.mix-wrapper__left {
width: $sideBarWidth;
}
:deep(.svg-icon) {
margin-top: -1px;
margin-right: 5px;
}
.el-menu {
border: none;
}
}
}
</style>
siderBar.vue
<script setup lang="ts">
import TopMenu from "./TopMenu.vue";
import LeftMenu from "./LeftMenu.vue";
import Logo from "./Logo.vue";
import { useSettingsStore } from "@/store/modules/settings";
import { usePermissionStore } from "@/store/modules/permission";
import { useAppStore } from "@/store/modules/app";
import { storeToRefs } from "pinia";
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const layout = computed(() => settingsStore.layout);
const showContent = ref(true);
watch(
() => layout.value,
() => {
showContent.value = false;
nextTick(() => {
showContent.value = true;
});
}
);
</script>
<template>
<div
:class="{ 'has-logo': sidebarLogo }"
class="menu-wrap"
v-if="layout !== 'mix'"
>
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
<el-scrollbar v-if="showContent">
<LeftMenu :menu-list="permissionStore.routes" base-path="" />
</el-scrollbar>
<NavRight v-if="layout === 'top'" />
</div>
<template v-else>
<div :class="{ 'has-logo': sidebarLogo }" class="menu-wrap">
<div class="header">
<logo v-if="sidebarLogo" :collapse="!appStore.sidebar.opened" />
<TopMenu />
<NavRight />
</div>
</div>
</template>
</template>
<style lang="scss" scoped>
:deep(.setting-container) {
.setting-item {
color: #fff;
.svg-icon {
margin-right: 0;
}
&:hover {
color: var(--el-color-primary);
}
}
}
.isMix {
.menu-wrap {
z-index: 99;
width: 100% !important;
height: 50px;
background-color: $menuBg;
:deep(.header) {
display: flex;
width: 100%;
// 顶部模式全局变量修改
--el-menu-item-height: 50px;
.logo-wrap {
width: $sideBarWidth;
}
.el-menu {
background-color: $menuBg;
.el-menu-item {
color: $menuText;
}
}
.el-scrollbar {
flex: 1;
min-width: 0;
height: 50px;
}
}
}
.left-menu {
display: inline-block;
width: $sideBarWidth;
background-color: $menuBg;
:deep(.el-menu) {
background-color: $menuBg;
.el-menu-item {
color: $menuText;
}
}
}
}
</style>
文章来源:https://blog.csdn.net/qq_34114535/article/details/135238391
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!