From 1fd876de72ba096bac7c8f295ad6074f13099355 Mon Sep 17 00:00:00 2001 From: "chen.s.g" Date: Sat, 21 Sep 2024 20:46:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E4=B8=8E=E5=8A=A8=E6=80=81=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hook-demo/use-dynamic-route.ts | 87 ++++++++++++++++++++++++++ src/router/index.ts | 2 +- src/router/permission.ts | 3 +- src/store/modules/permission.ts | 43 ++++++++++++- src/store/modules/user.ts | 13 +++- 5 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 src/api/hook-demo/use-dynamic-route.ts diff --git a/src/api/hook-demo/use-dynamic-route.ts b/src/api/hook-demo/use-dynamic-route.ts new file mode 100644 index 00000000..5f956cca --- /dev/null +++ b/src/api/hook-demo/use-dynamic-route.ts @@ -0,0 +1,87 @@ +/** 菜单类型 */ +export enum MenuType { + Menu = "menu", + Page = "page", + Button = "button", + Link = "link" +} + +/** 菜单元数据 */ +export interface MenuMeta { + /** 菜单标题 */ + title: string + /** 菜单图标 */ + icon?: string + /** 菜单总是可见 */ + alwaysShow?: boolean + /** 菜单是否可用 */ + roles?: string[] +} + +/** 菜单详情 */ +export interface MenuItem { + /** 菜单名称 */ + name: string + /** 菜单类型 */ + type: MenuType + /** 菜单路径 */ + path: string + /** 菜单元数据 */ + meta: MenuMeta + /** 子菜单 */ + children?: MenuItem[] +} + +/** + * 动态路由 + * 用来放置有权限 (Roles 属性) 的路由 + * 必须带有 Name 属性 + */ +const dynamicRoutes: MenuItem[] = [ + { + path: "/permission", + name: "Permission", + type: MenuType.Menu, + meta: { + title: "权限", + icon: "lock", + roles: ["admin", "editor"], // 可以在根路由中设置角色 + alwaysShow: true // 将始终显示根菜单 + }, + children: [ + { + path: "page", + name: "PagePermission", + type: MenuType.Page, + meta: { + title: "页面级", + roles: ["admin"] // 或者在子导航中设置角色 + } + }, + { + path: "directive", + name: "DirectivePermission", + type: MenuType.Page, + meta: { + title: "按钮级" // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色 + } + } + ] + } +] + +/** 模拟加载菜单接口 */ +export function getMenuDataApi() { + return new Promise((resolve, reject) => { + // 模拟接口响应时间 1s + setTimeout(() => { + // 模拟接口调用成功 + if (Math.random() < 0.8) { + resolve(dynamicRoutes) + } else { + // 模拟接口调用出错 + reject(new Error("接口发生错误")) + } + }, 1000) + }) +} diff --git a/src/router/index.ts b/src/router/index.ts index c35bb4bf..98c81939 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -2,7 +2,7 @@ import { type RouteRecordRaw, createRouter } from "vue-router" import { history, flatMultiLevelRoutes } from "./helper" import routeSettings from "@/config/route" -const Layouts = () => import("@/layouts/index.vue") +export const Layouts = () => import("@/layouts/index.vue") /** * 常驻路由 diff --git a/src/router/permission.ts b/src/router/permission.ts index c5bcee59..2fb1baff 100644 --- a/src/router/permission.ts +++ b/src/router/permission.ts @@ -38,10 +38,11 @@ router.beforeEach(async (to, _from, next) => { // 否则要重新获取权限角色 try { await userStore.getInfo() + await userStore.getMenu() // 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"] const roles = userStore.roles // 生成可访问的 Routes - routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes() + routeSettings.dynamic ? permissionStore.setRoutes(roles, userStore.menus) : permissionStore.setAllRoutes() // 将 "有访问权限的动态路由" 添加到 Router 中 permissionStore.addRoutes.forEach((route) => router.addRoute(route)) // 确保添加路由已完成 diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 2ae6eeac..e012fc4e 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -2,9 +2,12 @@ import { ref } from "vue" import store from "@/store" import { defineStore } from "pinia" import { type RouteRecordRaw } from "vue-router" -import { constantRoutes, dynamicRoutes } from "@/router" +import { constantRoutes, dynamicRoutes, Layouts } from "@/router" import { flatMultiLevelRoutes } from "@/router/helper" import routeSettings from "@/config/route" +import { MenuItem, MenuType } from "@/api/hook-demo/use-dynamic-route" + +const modules = import.meta.glob(["@/views/*.vue", "@/views/**/*.vue"]) const hasPermission = (roles: string[], route: RouteRecordRaw) => { const routeRoles = route.meta?.roles @@ -25,6 +28,35 @@ const filterDynamicRoutes = (routes: RouteRecordRaw[], roles: string[]) => { return res } +function transformMenuToRoute(menuItem: MenuItem, rootItem: boolean, parentPath: string = ""): RouteRecordRaw { + const childrenRoute: RouteRecordRaw[] = [] + const vuePath = "/src/views" + parentPath + "/" + menuItem.path + ".vue" + const vuePage = rootItem ? Layouts : modules[vuePath] + + // 如果有 children,则需要递归添加 children 到 route + if (menuItem.children && menuItem.children.length > 0) { + for (let i = 0; i < menuItem.children.length; i++) { + if (menuItem.children[i].type == MenuType.Page) { + childrenRoute.push(transformMenuToRoute(menuItem.children[i], false, menuItem.path)) + } + } + } + + const routeItem: RouteRecordRaw = { + path: menuItem.path, + name: menuItem.name, + component: vuePage, + meta: { + title: menuItem.meta.title, + svgIcon: menuItem.meta?.icon, + roles: menuItem.meta?.roles + }, + children: childrenRoute.length > 0 ? childrenRoute : undefined + } + + return routeItem +} + export const usePermissionStore = defineStore("permission", () => { /** 可访问的路由 */ const routes = ref([]) @@ -32,8 +64,13 @@ export const usePermissionStore = defineStore("permission", () => { const addRoutes = ref([]) /** 根据角色生成可访问的 Routes(可访问的路由 = 常驻路由 + 有访问权限的动态路由) */ - const setRoutes = (roles: string[]) => { - const accessedRoutes = filterDynamicRoutes(dynamicRoutes, roles) + const setRoutes = (roles: string[], menus: MenuItem[]) => { + const menuRoute: RouteRecordRaw[] = [] + for (let i = 0; i < menus.length; i++) { + menuRoute.push(transformMenuToRoute(menus[i], true)) + } + + const accessedRoutes = filterDynamicRoutes(menuRoute, roles) _set(accessedRoutes) } diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 634cfd97..a947776b 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,4 +1,4 @@ -import { ref } from "vue" +import { ref, reactive } from "vue" import store from "@/store" import { defineStore } from "pinia" import { useTagsViewStore } from "./tags-view" @@ -7,11 +7,13 @@ import { getToken, removeToken, setToken } from "@/utils/cache/cookies" import { resetRouter } from "@/router" import { loginApi, getUserInfoApi } from "@/api/login" import { type LoginRequestData } from "@/api/login/types/login" +import { MenuItem, getMenuDataApi } from "@/api/hook-demo/use-dynamic-route" import routeSettings from "@/config/route" export const useUserStore = defineStore("user", () => { const token = ref(getToken() || "") const roles = ref([]) + const menus = reactive([]) const username = ref("") const tagsViewStore = useTagsViewStore() @@ -30,6 +32,13 @@ export const useUserStore = defineStore("user", () => { // 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环 roles.value = data.roles?.length > 0 ? data.roles : routeSettings.defaultRoles } + /** 获取菜单 */ + const getMenu = async () => { + const data = await getMenuDataApi() + if (data && data.length > 0) { + menus.push(...data) + } + } /** 模拟角色变化 */ const changeRoles = async (role: string) => { const newToken = "token-" + role @@ -60,7 +69,7 @@ export const useUserStore = defineStore("user", () => { } } - return { token, roles, username, login, getInfo, changeRoles, logout, resetToken } + return { token, roles, menus, username, login, getInfo, getMenu, changeRoles, logout, resetToken } }) /** 在 setup 外使用 */