From 1fd876de72ba096bac7c8f295ad6074f13099355 Mon Sep 17 00:00:00 2001 From: "chen.s.g" Date: Sat, 21 Sep 2024 20:46:57 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=B7=AF=E7=94=B1=E4=B8=8E=E5=8A=A8=E6=80=81=E8=8F=9C?= =?UTF-8?q?=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 外使用 */ From b1fa0e460510800673a0d93d05c0c611721657f6 Mon Sep 17 00:00:00 2001 From: "chen.s.g" Date: Mon, 7 Oct 2024 19:20:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E5=AE=9E=E7=8E=B0=EF=BC=9A=E5=B0=BD=E9=87=8F?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E5=8E=9F=E8=B7=AF=E7=94=B1=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=E5=8F=8A=E8=8F=9C=E5=8D=95=E9=A1=B9=E7=9A=84=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hook-demo/use-dynamic-route.ts | 11 +++++++++++ src/store/modules/permission.ts | 17 ++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/api/hook-demo/use-dynamic-route.ts b/src/api/hook-demo/use-dynamic-route.ts index 5f956cca..559d7790 100644 --- a/src/api/hook-demo/use-dynamic-route.ts +++ b/src/api/hook-demo/use-dynamic-route.ts @@ -12,10 +12,14 @@ export interface MenuMeta { title: string /** 菜单图标 */ icon?: string + /** 隐藏菜单 */ + hidden?: boolean /** 菜单总是可见 */ alwaysShow?: boolean /** 菜单是否可用 */ roles?: string[] + /** 其它参数 */ + [key: string]: any } /** 菜单详情 */ @@ -26,6 +30,10 @@ export interface MenuItem { type: MenuType /** 菜单路径 */ path: string + /** 重定向页面 */ + redirect?: string + /** 组件页面 */ + component?: string /** 菜单元数据 */ meta: MenuMeta /** 子菜单 */ @@ -40,6 +48,7 @@ export interface MenuItem { const dynamicRoutes: MenuItem[] = [ { path: "/permission", + redirect: "/permission/page", name: "Permission", type: MenuType.Menu, meta: { @@ -51,6 +60,7 @@ const dynamicRoutes: MenuItem[] = [ children: [ { path: "page", + component: "/views/permission/page.vue", name: "PagePermission", type: MenuType.Page, meta: { @@ -60,6 +70,7 @@ const dynamicRoutes: MenuItem[] = [ }, { path: "directive", + component: "/views/permission/directive.vue", name: "DirectivePermission", type: MenuType.Page, meta: { diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index e012fc4e..2f58ecaa 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -28,16 +28,16 @@ const filterDynamicRoutes = (routes: RouteRecordRaw[], roles: string[]) => { return res } -function transformMenuToRoute(menuItem: MenuItem, rootItem: boolean, parentPath: string = ""): RouteRecordRaw { +function transformMenuToRoute(menuItem: MenuItem): RouteRecordRaw { const childrenRoute: RouteRecordRaw[] = [] - const vuePath = "/src/views" + parentPath + "/" + menuItem.path + ".vue" - const vuePage = rootItem ? Layouts : modules[vuePath] + const vuePath = "/src" + (menuItem.component ?? "") + const vuePage = menuItem.type == MenuType.Menu ? 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)) + childrenRoute.push(transformMenuToRoute(menuItem.children[i])) } } } @@ -47,13 +47,16 @@ function transformMenuToRoute(menuItem: MenuItem, rootItem: boolean, parentPath: name: menuItem.name, component: vuePage, meta: { - title: menuItem.meta.title, svgIcon: menuItem.meta?.icon, - roles: menuItem.meta?.roles + ...menuItem.meta }, children: childrenRoute.length > 0 ? childrenRoute : undefined } + if (menuItem.redirect) { + routeItem.redirect = menuItem.redirect + } + return routeItem } @@ -67,7 +70,7 @@ export const usePermissionStore = defineStore("permission", () => { const setRoutes = (roles: string[], menus: MenuItem[]) => { const menuRoute: RouteRecordRaw[] = [] for (let i = 0; i < menus.length; i++) { - menuRoute.push(transformMenuToRoute(menus[i], true)) + menuRoute.push(transformMenuToRoute(menus[i])) } const accessedRoutes = filterDynamicRoutes(menuRoute, roles) From 2ed53fb4c45bad529f7ab46e955966a09220f91c Mon Sep 17 00:00:00 2001 From: "chen.s.g" Date: Mon, 7 Oct 2024 19:47:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8E?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=8F=9C=E5=8D=95=E7=9A=84=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hook-demo/use-dynamic-route.ts | 14 ++++++-- src/directives/permission/index.ts | 14 ++++++-- src/store/modules/user.ts | 47 +++++++++++++++++++++++++- src/utils/permission.ts | 14 ++++++-- src/views/permission/directive.vue | 18 ++++++++++ 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/api/hook-demo/use-dynamic-route.ts b/src/api/hook-demo/use-dynamic-route.ts index 559d7790..d0e43861 100644 --- a/src/api/hook-demo/use-dynamic-route.ts +++ b/src/api/hook-demo/use-dynamic-route.ts @@ -75,7 +75,17 @@ const dynamicRoutes: MenuItem[] = [ type: MenuType.Page, meta: { title: "按钮级" // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色 - } + }, + children: [ + { + path: "button", + name: "ButtonPermission", + type: MenuType.Button, + meta: { + title: "按钮权限" + } + } + ] } ] } @@ -87,7 +97,7 @@ export function getMenuDataApi() { // 模拟接口响应时间 1s setTimeout(() => { // 模拟接口调用成功 - if (Math.random() < 0.8) { + if (Math.random() < 1) { resolve(dynamicRoutes) } else { // 模拟接口调用出错 diff --git a/src/directives/permission/index.ts b/src/directives/permission/index.ts index 64d258cc..fdea840b 100644 --- a/src/directives/permission/index.ts +++ b/src/directives/permission/index.ts @@ -5,9 +5,19 @@ import { useUserStoreHook } from "@/store/modules/user" export const permission: Directive = { mounted(el, binding) { const { value: permissionRoles } = binding - const { roles } = useUserStoreHook() + const { permission } = useUserStoreHook() + if (Array.isArray(permissionRoles) && permissionRoles.length > 0) { - const hasPermission = roles.some((role) => permissionRoles.includes(role)) + let hasPermission = false + permissionRoles.forEach((item) => { + const res = (item as string).split(":") + if (permission.has(res[0])) { + if (permission.get(res[0])?.includes(res[1])) { + hasPermission = true + } + } + }) + // hasPermission || (el.style.display = "none") // 隐藏 hasPermission || el.parentNode?.removeChild(el) // 销毁 } else { diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index a947776b..7ce904fc 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -10,10 +10,50 @@ import { type LoginRequestData } from "@/api/login/types/login" import { MenuItem, getMenuDataApi } from "@/api/hook-demo/use-dynamic-route" import routeSettings from "@/config/route" +/** + * 从菜单生成权限资源 + * + * @param menus 授予用户的菜单列表 + * @returns Map 权限资源 + */ +function buildMenuPermission(menus: MenuItem[]) { + const ret = new Map() + + menus.forEach((item) => { + if (item.type === "menu" && item.children && item.children.length > 0) { + const tmp = buildMenuPermission(item.children) + if (tmp.size > 0) { + tmp.forEach((value, key) => { + if (ret.has(key)) { + ret.set(key, [...new Set([...(ret.get(key) as string[]), ...value])]) + } else { + ret.set(key, value) + } + }) + } + } else if (item.type === "page" && item.children && item.children.length > 0) { + const res: string[] = [] + + item.children?.forEach((child) => { + if (child.name != "") { + res.push(child.name) + } + }) + + if (res.length > 0) { + ret.set(item.name, res) + } + } + }) + + return ret +} + export const useUserStore = defineStore("user", () => { const token = ref(getToken() || "") const roles = ref([]) const menus = reactive([]) + const permission = reactive>(new Map()) const username = ref("") const tagsViewStore = useTagsViewStore() @@ -37,6 +77,11 @@ export const useUserStore = defineStore("user", () => { const data = await getMenuDataApi() if (data && data.length > 0) { menus.push(...data) + + const permissionMap = buildMenuPermission(data) + permissionMap.forEach((value, key) => { + permission.set(key, value) + }) } } /** 模拟角色变化 */ @@ -69,7 +114,7 @@ export const useUserStore = defineStore("user", () => { } } - return { token, roles, menus, username, login, getInfo, getMenu, changeRoles, logout, resetToken } + return { token, roles, menus, permission, username, login, getInfo, getMenu, changeRoles, logout, resetToken } }) /** 在 setup 外使用 */ diff --git a/src/utils/permission.ts b/src/utils/permission.ts index f736e3f0..ebc801cf 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -3,8 +3,18 @@ import { useUserStoreHook } from "@/store/modules/user" /** 全局权限判断函数,和权限指令 v-permission 功能类似 */ export const checkPermission = (permissionRoles: string[]): boolean => { if (Array.isArray(permissionRoles) && permissionRoles.length > 0) { - const { roles } = useUserStoreHook() - return roles.some((role) => permissionRoles.includes(role)) + const { permission } = useUserStoreHook() + let hasPermission = false + permissionRoles.forEach((item) => { + const res = (item as string).split(":") + if (permission.has(res[0])) { + if (permission.get(res[0])?.includes(res[1])) { + hasPermission = true + } + } + }) + + return hasPermission } else { console.error("need roles! Like checkPermission(['admin','editor'])") return false diff --git a/src/views/permission/directive.vue b/src/views/permission/directive.vue index 5eb82a01..9f8b07f1 100644 --- a/src/views/permission/directive.vue +++ b/src/views/permission/directive.vue @@ -8,6 +8,11 @@ import SwitchRoles from "./components/SwitchRoles.vue"
+
+ 权限指令说明:权限指令内容由页面名与下级资源名组成
如:DirectivePermission:ButtonPermission 指令中的 + DirectivePermission 为菜单配置中页面的 name 属性, ButtonPermission 为页面 children 中的下级资源 name 属性值
+ 详情请参考 src/api/hook-demo/use-dynamic-route.ts 文件中的菜单配置 +
这里采用了 v-permission="['admin']" 所以只有 admin 可以看见这句话 @@ -23,6 +28,12 @@ import SwitchRoles from "./components/SwitchRoles.vue" 这里采用了 v-permission="['admin', 'editor']" 所以 admin 和 editor 都可以看见这句话
+
+ + 这里采用了 v-permission="['DirectivePermission:ButtonPermission']" 所以只有 + DirectivePermission:ButtonPermission 权限才可以看见这句话 + +
@@ -40,6 +51,13 @@ import SwitchRoles from "./components/SwitchRoles.vue" 这里采用了 v-if="checkPermission(['admin', 'editor'])" 所以 admin 和 editor 都可以看见这句话 + + 这里采用了 v-if="checkPermission(['DirectivePermission:ButtonPermission'])" 所以只有 + DirectivePermission:ButtonPermission 权限才可以看见这句话 +