diff --git a/.vscode/settings.json b/.vscode/settings.json index b5aefceb4..6f73d2027 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 a9c71b15d..0ebb63d84 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 a0c43e00c..2404c008f 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 000000000..3701c3c1a --- /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 000000000..da01bc16b --- /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 3be2c5c1d..d01fe714e 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 000000000..073c918b7 --- /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 008526801..6597ac23a 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 1f68d241d..dd6df9aa1 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 df92595cf..fac9f929c 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 2d7a59c27..d6503d9c4 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 20ca8b386..8b8603ad4 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 87256982f..458fd0972 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 71314d4a8..f07958a6e 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"]; } }