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/locales/en.yaml b/locales/en.yaml index d02f36f93..3793277b1 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 6ce420969..7be27a8ec 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 4f2fab739..5ca555977 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 a9c71b15d..55897d8f4 100644 --- a/mock/login.ts +++ b/mock/login.ts @@ -15,6 +15,8 @@ export default defineFakeRoute([ nickname: "小铭", // 一个用户可能有多个角色 roles: ["admin"], + // 按钮级别权限 + permissions: ["*:*:*"], accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", expires: "2030/10/30 00:00:00" @@ -28,6 +30,7 @@ export default defineFakeRoute([ username: "common", nickname: "小林", roles: ["common"], + 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 a4e33f716..5c9172cd8 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: "", @@ -739,8 +763,8 @@ export default defineFakeRoute([ showParent: false }, { - parentId: 202, - id: 204, + parentId: 203, + id: 211, menuType: 3, title: "修改", name: "", @@ -762,9 +786,105 @@ export default defineFakeRoute([ 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: 205, + 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: "", + 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: 204, + id: 222, menuType: 3, title: "删除", name: "", 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..f2b28cb83 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,18 +68,20 @@ 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 }); } @@ -87,7 +91,8 @@ export function setToken(data: DataInfo) { avatar: data?.avatar ?? "", username, nickname: data?.nickname ?? "", - roles + roles, + permissions: data?.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,16 @@ 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 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); + return isAuths ? true : false; +}; diff --git a/src/views/permission/button/index.vue b/src/views/permission/button/index.vue index 20fc799dd..c1d5297cc 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 000000000..5a256a254 --- /dev/null +++ b/src/views/permission/button/perms.vue @@ -0,0 +1,116 @@ + + + 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"]; } }