后台管理 动态获取菜单例子

路由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>



  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值