From 12cbae29bb1b895bd7d4f80265d3a0e1d99bde31 Mon Sep 17 00:00:00 2001 From: xiaoxian521 <1923740402@qq.com> Date: Fri, 9 Aug 2024 16:50:54 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E7=A7=8D=E6=8C=89=E9=92=AE=E6=9D=83=E9=99=90=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=EF=BC=88=E6=A0=B9=E6=8D=AE=E7=99=BB=E5=BD=95=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E8=BF=94=E5=9B=9E=E7=9A=84`permissions`=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E8=BF=9B=E8=A1=8C=E5=88=A4=E6=96=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + mock/login.ts | 7 ++++++ src/api/user.ts | 2 ++ src/components/RePerms/index.ts | 5 ++++ src/components/RePerms/src/perms.tsx | 20 ++++++++++++++++ src/directives/index.ts | 1 + src/directives/perms/index.ts | 15 ++++++++++++ src/main.ts | 2 ++ src/router/utils.ts | 2 +- src/store/modules/user.ts | 8 +++++++ src/store/types.ts | 1 + src/utils/auth.ts | 35 +++++++++++++++++++++------- types/directives.d.ts | 4 +++- types/global-components.d.ts | 1 + 14 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 src/components/RePerms/index.ts create mode 100644 src/components/RePerms/src/perms.tsx create mode 100644 src/directives/perms/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index b5aefceb49..6f73d2027e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,7 @@ "v-copy", "v-longpress", "v-optimize", + "v-perms", "v-ripple" ], "vscodeCustomCodeColor.highlightValueColor": "#b392f0", diff --git a/mock/login.ts b/mock/login.ts index a9c71b15d1..0ebb63d84f 100644 --- a/mock/login.ts +++ b/mock/login.ts @@ -15,6 +15,12 @@ export default defineFakeRoute([ nickname: "小铭", // 一个用户可能有多个角色 roles: ["admin"], + // 按钮级别权限 + permissions: [ + "permission:btn:add", + "permission:btn:edit", + "permission:btn:delete" + ], accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", expires: "2030/10/30 00:00:00" @@ -28,6 +34,7 @@ export default defineFakeRoute([ username: "common", nickname: "小林", roles: ["common"], + permissions: [], accessToken: "eyJhbGciOiJIUzUxMiJ9.common", refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", expires: "2030/10/30 00:00:00" diff --git a/src/api/user.ts b/src/api/user.ts index a0c43e00c0..2404c008f2 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -11,6 +11,8 @@ export type UserResult = { nickname: string; /** 当前登录用户的角色 */ roles: Array; + /** 按钮级别权限 */ + permissions: Array; /** `token` */ accessToken: string; /** 用于调用刷新`accessToken`的接口时所需的`token` */ diff --git a/src/components/RePerms/index.ts b/src/components/RePerms/index.ts new file mode 100644 index 0000000000..3701c3c1af --- /dev/null +++ b/src/components/RePerms/index.ts @@ -0,0 +1,5 @@ +import perms from "./src/perms"; + +const Perms = perms; + +export { Perms }; diff --git a/src/components/RePerms/src/perms.tsx b/src/components/RePerms/src/perms.tsx new file mode 100644 index 0000000000..da01bc16be --- /dev/null +++ b/src/components/RePerms/src/perms.tsx @@ -0,0 +1,20 @@ +import { defineComponent, Fragment } from "vue"; +import { hasPerms } from "@/utils/auth"; + +export default defineComponent({ + name: "Perms", + props: { + value: { + type: undefined, + default: [] + } + }, + setup(props, { slots }) { + return () => { + if (!slots) return null; + return hasPerms(props.value) ? ( + {slots.default?.()} + ) : null; + }; + } +}); diff --git a/src/directives/index.ts b/src/directives/index.ts index 3be2c5c1dc..d01fe714e8 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -2,4 +2,5 @@ export * from "./auth"; export * from "./copy"; export * from "./longpress"; export * from "./optimize"; +export * from "./perms"; export * from "./ripple"; diff --git a/src/directives/perms/index.ts b/src/directives/perms/index.ts new file mode 100644 index 0000000000..073c918b77 --- /dev/null +++ b/src/directives/perms/index.ts @@ -0,0 +1,15 @@ +import { hasPerms } from "@/utils/auth"; +import type { Directive, DirectiveBinding } from "vue"; + +export const perms: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding>) { + const { value } = binding; + if (value) { + !hasPerms(value) && el.parentNode?.removeChild(el); + } else { + throw new Error( + "[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\"" + ); + } + } +}; diff --git a/src/main.ts b/src/main.ts index 0085268013..6597ac23ae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,7 +44,9 @@ app.component("FontIcon", FontIcon); // 全局注册按钮级别权限组件 import { Auth } from "@/components/ReAuth"; +import { Perms } from "@/components/RePerms"; app.component("Auth", Auth); +app.component("Perms", Perms); // 全局注册vue-tippy import "tippy.js/dist/tippy.css"; diff --git a/src/router/utils.ts b/src/router/utils.ts index 1f68d241dc..dd6df9aa1e 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -355,7 +355,7 @@ function getAuths(): Array { return router.currentRoute.value.meta.auths as Array; } -/** 是否有按钮级别的权限 */ +/** 是否有按钮级别的权限(根据路由`meta`中的`auths`字段进行判断)*/ function hasAuth(value: string | Array): boolean { if (!value) return false; /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */ diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index df92595cfe..fac9f929cc 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -27,6 +27,9 @@ export const useUserStore = defineStore({ nickname: storageLocal().getItem>(userKey)?.nickname ?? "", // 页面级别权限 roles: storageLocal().getItem>(userKey)?.roles ?? [], + // 按钮级别权限 + permissions: + storageLocal().getItem>(userKey)?.permissions ?? [], // 前端生成的验证码(按实际需求替换) verifyCode: "", // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码) @@ -53,6 +56,10 @@ export const useUserStore = defineStore({ SET_ROLES(roles: Array) { this.roles = roles; }, + /** 存储按钮级别权限 */ + SET_PERMS(permissions: Array) { + this.permissions = permissions; + }, /** 存储前端生成的验证码 */ SET_VERIFYCODE(verifyCode: string) { this.verifyCode = verifyCode; @@ -86,6 +93,7 @@ export const useUserStore = defineStore({ logOut() { this.username = ""; this.roles = []; + this.permissions = []; removeToken(); useMultiTagsStoreHook().handleTags("equal", [...routerArrays]); resetRouter(); diff --git a/src/store/types.ts b/src/store/types.ts index 2d7a59c27f..d6503d9c42 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -42,6 +42,7 @@ export type userType = { username?: string; nickname?: string; roles?: Array; + permissions?: Array; verifyCode?: string; currentPage?: number; isRemembered?: boolean; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 20ca8b3863..8b8603ad4d 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,6 +1,6 @@ import Cookies from "js-cookie"; -import { storageLocal } from "@pureadmin/utils"; import { useUserStoreHook } from "@/store/modules/user"; +import { storageLocal, isString, isIncludeAllChildren } from "@pureadmin/utils"; export interface DataInfo { /** token */ @@ -17,6 +17,8 @@ export interface DataInfo { nickname?: string; /** 当前登录用户的角色 */ roles?: Array; + /** 当前登录用户的按钮级别权限 */ + permissions?: Array; } export const userKey = "user-info"; @@ -41,7 +43,7 @@ export function getToken(): DataInfo { * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案 * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间) * 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁) - * 将`avatar`、`username`、`nickname`、`roles`、`refreshToken`、`expires`这六条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) + * 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这六条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) */ export function setToken(data: DataInfo) { let expires = 0; @@ -66,28 +68,31 @@ export function setToken(data: DataInfo) { : {} ); - function setUserKey({ avatar, username, nickname, roles }) { + function setUserKey({ avatar, username, nickname, roles, permissions }) { useUserStoreHook().SET_AVATAR(avatar); useUserStoreHook().SET_USERNAME(username); useUserStoreHook().SET_NICKNAME(nickname); useUserStoreHook().SET_ROLES(roles); + useUserStoreHook().SET_PERMS(permissions); storageLocal().setItem(userKey, { refreshToken, expires, avatar, username, nickname, - roles + roles, + permissions }); } - if (data.username && data.roles) { - const { username, roles } = data; + if (data.username && data.roles && data.permissions) { + const { username, roles, permissions } = data; setUserKey({ avatar: data?.avatar ?? "", username, nickname: data?.nickname ?? "", - roles + roles, + permissions }); } else { const avatar = @@ -98,11 +103,14 @@ export function setToken(data: DataInfo) { storageLocal().getItem>(userKey)?.nickname ?? ""; const roles = storageLocal().getItem>(userKey)?.roles ?? []; + const permissions = + storageLocal().getItem>(userKey)?.permissions ?? []; setUserKey({ avatar, username, nickname, - roles + roles, + permissions }); } } @@ -118,3 +126,14 @@ export function removeToken() { export const formatToken = (token: string): string => { return "Bearer " + token; }; + +/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/ +export const hasPerms = (value: string | Array): boolean => { + if (!value) return false; + const { permissions } = useUserStoreHook(); + if (!permissions) return false; + const isAuths = isString(value) + ? permissions.includes(value) + : isIncludeAllChildren(value, permissions); + return isAuths ? true : false; +}; diff --git a/types/directives.d.ts b/types/directives.d.ts index 87256982f1..458fd09722 100644 --- a/types/directives.d.ts +++ b/types/directives.d.ts @@ -5,7 +5,7 @@ declare module "vue" { export interface ComponentCustomProperties { /** `Loading` 动画加载指令,具体看:https://element-plus.org/zh-CN/component/loading.html#%E6%8C%87%E4%BB%A4 */ vLoading: Directive; - /** 按钮权限指令 */ + /** 按钮权限指令(根据路由`meta`中的`auths`字段进行判断)*/ vAuth: Directive>; /** 文本复制指令(默认双击复制) */ vCopy: Directive; @@ -13,6 +13,8 @@ declare module "vue" { vLongpress: Directive; /** 防抖、节流指令 */ vOptimize: Directive; + /** 按钮权限指令(根据登录接口返回的`permissions`字段进行判断)*/ + vPerms: Directive>; /** * `v-ripple`指令,用法如下: * 1. `v-ripple`代表启用基本的`ripple`功能 diff --git a/types/global-components.d.ts b/types/global-components.d.ts index 71314d4a81..f07958a6e5 100644 --- a/types/global-components.d.ts +++ b/types/global-components.d.ts @@ -7,6 +7,7 @@ declare module "vue" { IconifyIconOnline: (typeof import("../src/components/ReIcon"))["IconifyIconOnline"]; FontIcon: (typeof import("../src/components/ReIcon"))["FontIcon"]; Auth: (typeof import("../src/components/ReAuth"))["Auth"]; + Perms: (typeof import("../src/components/RePerms"))["Perms"]; } } From 73b212edc4e6609828eacd7eff0bec2f3fb85ea2 Mon Sep 17 00:00:00 2001 From: xiaoxian521 <1923740402@qq.com> Date: Sun, 11 Aug 2024 19:08:13 +0800 Subject: [PATCH 2/3] chore: update --- locales/en.yaml | 2 + locales/zh-CN.yaml | 2 + mock/asyncRoutes.ts | 35 +++++-- mock/login.ts | 8 +- mock/system.ts | 126 +++++++++++++++++++++++++- src/utils/auth.ts | 8 +- src/views/permission/button/index.vue | 2 +- src/views/permission/button/perms.vue | 116 ++++++++++++++++++++++++ 8 files changed, 277 insertions(+), 22 deletions(-) create mode 100644 src/views/permission/button/perms.vue diff --git a/locales/en.yaml b/locales/en.yaml index d02f36f933..3793277b12 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -125,6 +125,8 @@ menus: purePermission: Permission Manage purePermissionPage: Page Permission purePermissionButton: Button Permission + purePermissionButtonRouter: Route return button permission + purePermissionButtonLogin: Login interface return button permission pureTabs: Tabs Operate pureGuide: Guide pureAble: Able diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index 6ce4209696..7be27a8ecf 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -125,6 +125,8 @@ menus: purePermission: 权限管理 purePermissionPage: 页面权限 purePermissionButton: 按钮权限 + purePermissionButtonRouter: 路由返回按钮权限 + purePermissionButtonLogin: 登录接口返回按钮权限 pureTabs: 标签页操作 pureGuide: 引导页 pureAble: 功能 diff --git a/mock/asyncRoutes.ts b/mock/asyncRoutes.ts index 4f2fab7394..5ca5559771 100644 --- a/mock/asyncRoutes.ts +++ b/mock/asyncRoutes.ts @@ -123,17 +123,34 @@ const permissionRouter = { } }, { - path: "/permission/button/index", - name: "PermissionButton", + path: "/permission/button", meta: { title: "menus.purePermissionButton", - roles: ["admin", "common"], - auths: [ - "permission:btn:add", - "permission:btn:edit", - "permission:btn:delete" - ] - } + roles: ["admin", "common"] + }, + children: [ + { + path: "/permission/button/router", + component: "permission/button/index", + name: "PermissionButtonRouter", + meta: { + title: "menus.purePermissionButtonRouter", + auths: [ + "permission:btn:add", + "permission:btn:edit", + "permission:btn:delete" + ] + } + }, + { + path: "/permission/button/login", + component: "permission/button/perms", + name: "PermissionButtonLogin", + meta: { + title: "menus.purePermissionButtonLogin" + } + } + ] } ] }; diff --git a/mock/login.ts b/mock/login.ts index 0ebb63d84f..55897d8f40 100644 --- a/mock/login.ts +++ b/mock/login.ts @@ -16,11 +16,7 @@ export default defineFakeRoute([ // 一个用户可能有多个角色 roles: ["admin"], // 按钮级别权限 - permissions: [ - "permission:btn:add", - "permission:btn:edit", - "permission:btn:delete" - ], + permissions: ["*:*:*"], accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", expires: "2030/10/30 00:00:00" @@ -34,7 +30,7 @@ export default defineFakeRoute([ username: "common", nickname: "小林", roles: ["common"], - permissions: [], + permissions: ["permission:btn:add", "permission:btn:edit"], accessToken: "eyJhbGciOiJIUzUxMiJ9.common", refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", expires: "2030/10/30 00:00:00" diff --git a/mock/system.ts b/mock/system.ts index a4e33f7168..5c9172cd84 100644 --- a/mock/system.ts +++ b/mock/system.ts @@ -696,7 +696,7 @@ export default defineFakeRoute([ menuType: 0, title: "menus.purePermissionButton", name: "PermissionButton", - path: "/permission/button/index", + path: "/permission/button", component: "", rank: null, redirect: "", @@ -717,6 +717,30 @@ export default defineFakeRoute([ { parentId: 202, id: 203, + menuType: 0, + title: "menus.purePermissionButtonRouter", + name: "PermissionButtonRouter", + path: "/permission/button/router", + component: "permission/button/index", + rank: null, + redirect: "", + icon: "", + extraIcon: "", + enterTransition: "", + leaveTransition: "", + activePath: "", + auths: "", + frameSrc: "", + frameLoading: true, + keepAlive: false, + hiddenTag: false, + fixedTag: false, + showLink: true, + showParent: false + }, + { + parentId: 203, + id: 210, menuType: 3, title: "添加", name: "", @@ -738,9 +762,105 @@ export default defineFakeRoute([ showLink: true, showParent: false }, + { + parentId: 203, + id: 211, + menuType: 3, + title: "修改", + name: "", + path: "", + component: "", + rank: null, + redirect: "", + icon: "", + extraIcon: "", + enterTransition: "", + leaveTransition: "", + activePath: "", + auths: "permission:btn:edit", + frameSrc: "", + frameLoading: true, + keepAlive: false, + hiddenTag: false, + fixedTag: false, + showLink: true, + showParent: false + }, + { + parentId: 203, + id: 212, + menuType: 3, + title: "删除", + name: "", + path: "", + component: "", + rank: null, + redirect: "", + icon: "", + extraIcon: "", + enterTransition: "", + leaveTransition: "", + activePath: "", + auths: "permission:btn:delete", + frameSrc: "", + frameLoading: true, + keepAlive: false, + hiddenTag: false, + fixedTag: false, + showLink: true, + showParent: false + }, { parentId: 202, id: 204, + menuType: 0, + title: "menus.purePermissionButtonLogin", + name: "PermissionButtonLogin", + path: "/permission/button/login", + component: "permission/button/perms", + rank: null, + redirect: "", + icon: "", + extraIcon: "", + enterTransition: "", + leaveTransition: "", + activePath: "", + auths: "", + frameSrc: "", + frameLoading: true, + keepAlive: false, + hiddenTag: false, + fixedTag: false, + showLink: true, + showParent: false + }, + { + parentId: 204, + id: 220, + menuType: 3, + title: "添加", + name: "", + path: "", + component: "", + rank: null, + redirect: "", + icon: "", + extraIcon: "", + enterTransition: "", + leaveTransition: "", + activePath: "", + auths: "permission:btn:add", + frameSrc: "", + frameLoading: true, + keepAlive: false, + hiddenTag: false, + fixedTag: false, + showLink: true, + showParent: false + }, + { + parentId: 204, + id: 221, menuType: 3, title: "修改", name: "", @@ -763,8 +883,8 @@ export default defineFakeRoute([ showParent: false }, { - parentId: 202, - id: 205, + parentId: 204, + id: 222, menuType: 3, title: "删除", name: "", diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 8b8603ad4d..937846ac9d 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -85,14 +85,14 @@ export function setToken(data: DataInfo) { }); } - if (data.username && data.roles && data.permissions) { - const { username, roles, permissions } = data; + if (data.username && data.roles) { + const { username, roles } = data; setUserKey({ avatar: data?.avatar ?? "", username, nickname: data?.nickname ?? "", roles, - permissions + permissions: data?.permissions ?? [] }); } else { const avatar = @@ -130,8 +130,10 @@ export const formatToken = (token: string): string => { /** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/ export const hasPerms = (value: string | Array): boolean => { if (!value) return false; + const allPerms = "*:*:*"; const { permissions } = useUserStoreHook(); if (!permissions) return false; + if (permissions.length === 1 && permissions[0] === allPerms) return true; const isAuths = isString(value) ? permissions.includes(value) : isIncludeAllChildren(value, permissions); diff --git a/src/views/permission/button/index.vue b/src/views/permission/button/index.vue index 20fc799dd3..c1d5297cc1 100644 --- a/src/views/permission/button/index.vue +++ b/src/views/permission/button/index.vue @@ -2,7 +2,7 @@ import { hasAuth, getAuths } from "@/router/utils"; defineOptions({ - name: "PermissionButton" + name: "PermissionButtonRouter" }); diff --git a/src/views/permission/button/perms.vue b/src/views/permission/button/perms.vue new file mode 100644 index 0000000000..5a256a2541 --- /dev/null +++ b/src/views/permission/button/perms.vue @@ -0,0 +1,116 @@ + + + From 6c8fa1b1ef8f22f1f2fd09f68934b37ec07aea0c Mon Sep 17 00:00:00 2001 From: xiaoxian521 <1923740402@qq.com> Date: Mon, 12 Aug 2024 13:28:37 +0800 Subject: [PATCH 3/3] chore: update --- src/utils/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 937846ac9d..f2b28cb83e 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -43,7 +43,7 @@ export function getToken(): DataInfo { * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案 * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间) * 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁) - * 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这六条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) + * 将`avatar`、`username`、`nickname`、`roles`、`permissions`、`refreshToken`、`expires`这七条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) */ export function setToken(data: DataInfo) { let expires = 0;