前期回顾
网站的打赏 —— 新一代的思路-CSDN博客https://blog.csdn.net/m0_57904695/article/details/136704914?spm=1001.2014.3001.5501
目录
🚩 XX银行_系统管理_按钮权限控制_前端_提测单
项目信息
提测版本信息
功能列表
测试范围
测试环境
✅ 步骤拆解分析
🤖 代码实现
第一步:接口返回权限信息
第二步:获取当前角色所拥有的菜单
第三步:根据项目路径url找到权限按钮中的按钮权限
第四步:如何在页面使用?如何使用更加方便?如何优化性能问题?如何在页面使用:
♻️ 完整实现过程代码
🚩 XX银行_系统管理_按钮权限控制_前端_提测单
项目信息
- 项目名称:演示 银行
- 产品负责人:xxx
- 研发负责人:彩色之外
- 测试负责人:xxx
提测版本信息
- 版本号:V1.1
- 源码分支:前端仓库 平台端组 / cc-management-bb· GitLab
- 提测时间:2024.4.24
- 预计上线时间:暂无
- 需求WIKI:暂无
功能列表
本版本包含以下功能列表:
1、开关控制是否进行权限校验(开关在系统管理员角色下的主题配置中)
2、在菜单管理处配置按钮菜单、在角色配置处给角色勾选对应的权限,系统根据当前用户所拥有的角色对其按钮权限进行控制,主要表现为:无权限前端页面不显示按钮。
3、按钮权限控制不包含列表查询按钮
4、需要对拥有不同角色的用户进行验证
测试范围
本版本需要测试以下内容:
1、开关控制是否生效,关闭开关时,所有用户都可以看到全部的按钮,并且点击可用。打开开关时,按钮根据用户的权限进行显示。
2、在开关打开情况下,修改角色权限或者删除菜单,用户不能看到按钮。
测试环境
本版本应在以下环境下测试:
- 环境描述:Test 环境
- 访问方式 : -
- 登录信息:不同角色对应的用户都需要进行验证,普通用户、管理员
✅ 步骤拆解分析
(1: )总按钮权限开关控制,是否开启。进行整个系统按钮权限校验。
(2: ) 使用系统管理员角色的账号,配置好页面的按钮,格式如 模块:菜单: 具体按钮 类型 ( system:user:insert )
在菜单管理处配置按钮菜单、在角色配置处给角色勾选对应的权限,系统根据当前用户所拥有的角色对其按钮权限进行控制,主要表现为:无权限前端页面不显示按钮。
🤖 代码实现
先说实现思路,
第一步:接口返回权限信息
键是菜单id(比如首页、用户管理每个页面都有id),值是具体权限 Array[]
{
"code": "000000",
"message": "操作成功",
"data": {
"id": 1686574963472044033,
"loginName": "zk",
"username": "zk",
"userNumber": 1152254775148937216,
"tenantId": null,
"departmentId": null,
"roleTag": 3,
"buttonAuthorities": {
"1160155571547013120": [
"delete",
"look"
],
"1144277890263678976": [
"update",
"delete",
"dataImport",
"importHistory"
],
"1144292976927703040": [
"insert",
"delete"
],
}
}
}
第二步:获取当前角色所拥有的菜单
获取当前角色所拥有的菜单,做一下数据映射,或者接口这样格式返回。
key是项目的动态正则url,值是菜单id,然后存入pinia。
当前角色所拥有的菜单接口响应返回如下:
{
"code": "000000",
"message": "操作成功",
"data": [
{
"id": 201,
"menuCode": "1160155571547013120",
"parentMenuCode": 0,
"menuType": "menu",
"menuChineseName": null,
"component": "/home/index.vue",
"meta": {
"icon": "ant-BankOutlined",
"title": "message.router.home",
"isHide": false,
"isKeepAlive": false,
"isAffix": false,
"isLink": "",
"isIframe": false,
"roles": [
1,
2,
3
]
},
"children": [],
"menuSuperior": "",
"btnPower": "",
"path": "/home",
"name": "home",
"menuSort": 0,
"isLink": false,
"redirect": ""
},
]
格式处理成这种 取出path和menuCode
{
"/home": "1160155571547013120",
"/desensitizationCenter": "1144277890263678976",
"/desensitizationCenter/policyRuleLibrary/policyLibraryView/:type/:id/:tagsViewName": "1144292976927703040",
}
Q: 为什么当前所拥有的菜单,要使用动态路径匹配url作为key?
A:因为直接精准根据url找到当前的菜单id,可以会失败,比如项目中使用了动态路由
"/desensitizationCenter/policyRuleLibrary/policyLibraryView/:type/:id/:tagsViewName": "1144292976927703040",
第三步:根据项目路径url找到权限按钮中的按钮权限
截至目前为止,我们已经得到了按钮权限
和当前账号下的菜单数据:
我们可以切换页面、使用URL来获取 mencCode与按钮权限接口做对比,
/**
* 处理菜单和按钮数据映射-通过正则表达式来匹配动态路径
* @param permission {string} 按钮权限
* @param path {string} 当前路径
* @test
* // 动态路径模式
* const menuPath = "/user/:userId/profile";
* // 将动态部分替换为正则表达式,以匹配任意非斜杠字符序列
* const regexPath = menuPath.replace(/:\w+/g, '[^/]+');
* // 创建正则表达式对象
* const regex = new RegExp(`^${regexPath}$`);
* // 实际路径
* const actualPath = "/user/123/profile";
* // 测试实际路径是否匹配
* const isMatch = regex.test(actualPath);
* console.log(isMatch); // 输出:true
*
* @returns boolean true-有权限 false-无权限
*/
const handleMenuDataMap = (
permission: string,
path: string = window.location.href.split('#')[1]
) => {
if (!dataReady.value) return false;
let matchedMenuId = null;
// 遍历菜单数据映射对象的键
for (const menuPath in menuDataMap.value) {
// 将菜单路径中的动态部分替换为正则表达式-对路径进行模式匹配
const regexPath = menuPath.replace(/:\w+/g, '[^/]+');
// 创建正则表达式对象
const regex = new RegExp(`^${regexPath}$`);
// 测试实际路径是否匹配
if (regex.test(path)) {
matchedMenuId = menuDataMap.value[menuPath];
break;
}
}
console.log('🤺🤺 matchedMenuId 🚀 ==>:', matchedMenuId);
if (!matchedMenuId) return;
// 获取当前菜单的权限数组
const permissions = Session.get('buttonAuthorities')?.[matchedMenuId] || [];
// 判断权限数组是否包含当前权限(payload)
return permissions.includes(permission);
};
第四步:如何在页面使用?如何使用更加方便?如何优化性能问题?
如何在页面使用:
1:按钮权限两种使用方式:
<template>
<div>
<!-- 按钮权限utils版本使用(需要引入utils-Hook) -->
<el-button v-if="permissionExports.hasDeletePermission.value">utils-删除</el-button>
<el-button v-if="permissionExports.hasUpdatePermission.value">utils-更新</el-button>
<!-- 按钮权限组件版本使用 -->
<zw-permission-button class="ml15" permission="look" type="primary" @zwClick="onClickEvent"
>components-查看</zw-permission-button
>
<zw-permission-button
class="ml15"
permission="update"
type="danger"
@zwClick="onClickDiyParams('自定义参数')"
>components-更新</zw-permission-button
>
</div>
</template>
<script setup lang="ts" name="home">
import { permissionExports } from '/@/utils/Hooks/hasBtnPermission';
function onClickEvent(e: MouseEvent) {
console.log(e);
}
function onClickDiyParams(params: string) {
console.log(params);
}
</script>
2:如何使用更加方便:
本次创建了两种使用方式,一种是utils函数引入,一种作为全局组件无需引入,整个系统直接使用
3:如何优化性能问题 :
系统中无数页面,页面中不知几何的按钮,可以将数据存入本地会话,这样只有在刷新页面了才会重新发起请求,或者使用接口缓存
(axios超级封装Vue3 + Ts + Vite 封装一套企业级axiso全流程-CSDN博客)
截至为止,已经实现基本功能,当然实现过程拥有无穷无尽的,Js|Vue给一套Api,我们使用api组合各种各样的场景 ……
♻️ 完整实现过程代码
src\stores\authManage.ts pinia仓储处理全局接口调用
import { defineStore, storeToRefs } from 'pinia';
import { getMenuTree } from '/@/api/auth-manage/menu';
import { i18n } from '/@/i18n/index';
import { Local } from '/@/utils/storage';
import { useThemeConfig } from '/@/stores/themeConfig';
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
export const useAuthManage = defineStore('authManage', {
state: (): any => ({
// 菜单树
menuTreeList: [],
// 权限标识与描述的映射
roleIdentifyMap: {},
// 菜单数据拍平映射
menuDataMap: {},
// 按钮权限总开关
switchBtnPermission: themeConfig.value.isEnableButtonPermissions,
}),
actions: {
// 获取菜单树-当前角色树
async queryCurrentMenuTree() {
const { data, code } = await getMenuTree();
if (code === '000000') {
this.menuTreeList = this.getMenuData(data);
}
},
// 处理获取到的menu数据
getMenuData(routes: RouteItems) {
const arr: RouteItems = [];
routes.forEach((val: RouteItem) => {
val['title'] = i18n.global.t(val.meta?.title as string);
arr.push({ ...val });
this.menuDataMap[val.path] = val.menuCode;
if (val.children) this.getMenuData(val.children);
});
return arr;
},
// 按钮权限总开关
async fetchAuthConfig() {
if (Local.get('userInfo').username === 'admin') {
this.switchBtnPermission = false;
return;
}
this.switchBtnPermission = themeConfig.value.isEnableButtonPermissions;
},
},
});
src\utils\Hooks\hasBtnPermission.ts utils方法封装has权限
import { ref, computed, ComputedRef, watch } from 'vue';
// pinia
import { storeToRefs } from 'pinia';
import { useAuthManage } from '/@/stores/authManage';
const authManage = useAuthManage(); //方法直接调用
const { menuDataMap, switchBtnPermission } = storeToRefs(authManage); // 响应式变量使用storeToRefs
// utils
import { Session } from '/@/utils/storage';
import { STATUS_CODE } from '/@/enum/global';
// axios-Api
import { getUserInfo } from '/@/api/auth-manage/user';
/**
* 在非setup 函数上下文中,无法直接使用useRoute 或 useRouter,
* 可以通过路由示例来获取
*/
import router from '/@/router/index';
// data-refs
const buttonAuth = ref<string[]>([]); // 按钮权限数组
const dataReady = ref(false); // 数据是否准备完毕
// init-permissions-check
const initPermissionsCheck = async () => {
await authManage.fetchAuthConfig();
console.log('按钮权限总开关', switchBtnPermission.value);
// 按钮权限控制打开状态
if (switchBtnPermission.value) {
// 本地没有菜单树数据
notMenuDataMap();
// 本地没有按钮权限数组
notButtonAuthorities();
} else {
// 按钮权限控制已关闭,系统所有按钮显示;
Session.remove('menuDataMap');
Session.remove('buttonAuthorities');
buttonAuth.value = [];
}
dataReady.value = true;
};
// 本地没有菜单树数据
async function notMenuDataMap() {
if (!Session.get('menuDataMap')) {
// 获取当前菜单拍平树数据-pinia
await authManage.queryCurrentMenuTree();
// 存储拍平树数据
Session.set('menuDataMap', menuDataMap.value);
} else {
// 本地有菜单树数据
menuDataMap.value = Session.get('menuDataMap');
// console.log('🤺🤺 menuDataMap.value 🚀 ==>:', menuDataMap.value);
}
}
// 本地没有按钮权限数组
async function notButtonAuthorities() {
if (!Session.get('buttonAuthorities')) {
// 获取按钮权限数组
const { code = '', data: { buttonAuthorities = [] } = {} } = await getUserInfo();
if (code === STATUS_CODE.SUCCESS) {
// console.log('🤺🤺 当前菜单拍平树 🚀 ==>:', menuDataMap.value);
// console.log('🤺🤺 按钮权限数组 🚀 ==>:', buttonAuthorities);
// 存储按钮权限数组
Session.set('buttonAuthorities', buttonAuthorities);
buttonAuth.value = buttonAuthorities;
}
} else {
// 本地有按钮权限数组
buttonAuth.value = Session.get('buttonAuthorities');
// console.log('🤺🤺 buttonAuth.value 🚀 ==>:', buttonAuth.value);
}
}
/**
* 使用watch来侦听路由变化,并在路由变化时重新执行权限检查逻辑
* @warning watch侦听路由变化时不能直接监听router.currentRoute.value.path
* 这是计算好的静态值,不是响应式数据
*/
watch(
() => router.currentRoute.value,
async () => {
await initPermissionsCheck();
// console.log('路由变化,当前路径:', router.currentRoute.value.path);
},
{ immediate: true }
);
/**
* 处理菜单和按钮数据映射-通过正则表达式来匹配动态路径
* @param permission {string} 按钮权限
* @param path {string} 当前路径
* @test
* // 动态路径模式
* const menuPath = "/user/:userId/profile";
* // 将动态部分替换为正则表达式,以匹配任意非斜杠字符序列
* const regexPath = menuPath.replace(/:\w+/g, '[^/]+');
* // 创建正则表达式对象
* const regex = new RegExp(`^${regexPath}$`);
* // 实际路径
* const actualPath = "/user/123/profile";
* // 测试实际路径是否匹配
* const isMatch = regex.test(actualPath);
* console.log(isMatch); // 输出:true
*
* @returns boolean true-有权限 false-无权限
*/
const handleMenuDataMap = (
permission: string,
path: string = window.location.href.split('#')[1]
) => {
if (!dataReady.value) return false;
let matchedMenuId = null;
// 遍历菜单数据映射对象的键
for (const menuPath in menuDataMap.value) {
// 将菜单路径中的动态部分替换为正则表达式-对路径进行模式匹配
const regexPath = menuPath.replace(/:\w+/g, '[^/]+');
// 创建正则表达式对象
const regex = new RegExp(`^${regexPath}$`);
// 测试实际路径是否匹配
if (regex.test(path)) {
matchedMenuId = menuDataMap.value[menuPath];
break;
}
}
console.log('🤺🤺 matchedMenuId 🚀 ==>:', matchedMenuId);
if (!matchedMenuId) return;
// 获取当前菜单的权限数组
const permissions = Session.get('buttonAuthorities')?.[matchedMenuId] || [];
// 判断权限数组是否包含当前权限(payload)
return permissions.includes(permission);
};
// 权限控制-按钮权限
interface PermissionComputeds {
[key: string]: ComputedRef<boolean>;
}
/*
export const hasAddPermission = computed(() => dataReady.value && handleMenuDataMap('Add'));
export const hasDelPermission = computed(() => dataReady.value && handleMenuDataMap('Del'));
*/
export const permissionsKeys = [
'insert',
'update',
'delete',
'detail',
'apply',
'runOnce',
'glueIDE',
'start',
'stop',
'copy',
'updateSetting',
'synchronization',
'updateConfig',
'backupHistory',
'backup',
'download',
'upload',
'dataImport',
'importHistory',
'publish',
'updateDirectory',
'updateQuestionnaire',
'continueAnswerQuestion',
'allocate',
'handle',
'downloadReportTemplate',
'look',
'configureMap',
'audit',
'customizeReport',
'generateReport',
'preview',
'generateForm',
'insertNode',
'updateNode',
'deleteNode',
'updateLine',
'insertQuestionnaire',
'updateNodeLine',
'deleteLine',
'deleteQuestionnaire',
'insertRegistration',
'detectRegistration',
'insertClassGrade',
'deleteRegistrationClassGrade',
'detectClassGrade',
'complianceInformationInput',
'detectComplianceInformation',
'insertLog',
'deleteLog',
];
const permissionComputeds: PermissionComputeds = {};
permissionsKeys.forEach((key) => {
/**
* 将首字母大写,用于生成计算属性的名称
* 如果 key 是 'update',那么 permissionName 将会是 'hasUpdatePermission'。
* charAt(0) 返回字符串中的第一个字符。如果字符串是 'update',charAt(0) 将返回 'u'。
* slice(1) 返回从索引 1 开始到字符串末尾的子字符串。如果字符串是 'update',slice(1) 将返回 'pdate'。
*/
const permissionName = `has${key.charAt(0).toUpperCase() + key.slice(1)}Permission`;
// 如果 按钮数据准备完毕且有权限并且按钮权限总开关开启
permissionComputeds[permissionName] = computed(() => dataReady.value && handleMenuDataMap(key));
});
/* 当 switchBtnPermission.value 为 false 时,创建一个对象,其所有属性都返回 true 的计算属性。显示所有按钮,
* 用于utils使用方法,按钮总开关权限关闭的情况
* Object.fromEntries将键值对列表转换为一个对象
* [
* ['name', 'zk'],
* ['age', 18],
* ];
* 输出: { name: 'zk', age: 18 }
* */
export const permissionExports = switchBtnPermission.value
? permissionComputeds
: Object.fromEntries(
permissionsKeys.map((key) => {
const permissionName = `has${key.charAt(0).toUpperCase() + key.slice(1)}Permission`;
return [permissionName, computed(() => true)];
})
);
src\components\GlobalComponents\zwPermissionButton.vue 全局自动引入组件方便使用
<template>
<div style="display: inline-block" v-bind="$attrs">
<!-- <div>按钮总开关: {{ switchBtnPermission }}</div>
<div>是否有权限: {{ hasPermission }}</div> -->
<template v-if="hasPermission && switchBtnPermission">
<!-- 权限开启 -->
<el-button v-if="componentType === 'button'" @click="handleClick" v-bind="$attrs">
<slot></slot>
</el-button>
<el-link
v-else-if="componentType === 'link'"
:underline="false"
@click="handleClick"
v-bind="$attrs"
>
<slot></slot>
</el-link>
</template>
<template v-if="!switchBtnPermission">
<!-- 权限关闭 -->
<el-button v-if="componentType === 'button'" @click="handleClick" v-bind="$attrs">
<slot></slot>
</el-button>
<el-link
v-else-if="componentType === 'link'"
:underline="false"
@click="handleClick"
v-bind="$attrs"
>
<slot></slot>
</el-link>
</template>
</div>
</template>
<script setup lang="ts">
// <!-- 权限控制的按钮组件 Component -->
import { computed, ComputedRef } from 'vue';
import { permissionExports, permissionsKeys } from '/@/utils/Hooks/hasBtnPermission';
import { storeToRefs } from 'pinia';
import { useAuthManage } from '/@/stores/authManage';
const authManage = useAuthManage();
const { switchBtnPermission } = storeToRefs(authManage);
const props = defineProps({
permission: {
type: String,
required: true,
},
componentType: {
type: String,
default: 'link', // 可以是'button'或'link'
},
});
const emit = defineEmits(['zwClick']);
interface PermissionMap {
[key: string]: ComputedRef<boolean>;
}
// 创建一个权限映射对象
// const permissionMap: PermissionMap = {
// list: permissionExports.hasListPermission,
// insert: permissionExports.hasInsertPermission,
// };
const permissionMap: PermissionMap = permissionsKeys.reduce((acc, key) => {
// update --> hasUpdatePermission
const permissionName = `has${key.charAt(0).toUpperCase() + key.slice(1)}Permission`;
acc[key] = permissionExports[permissionName];
return acc;
}, {} as PermissionMap);
// console.log('🤺🤺 permissionMap 🚀 ==>:', permissionMap);
const hasPermission: ComputedRef<boolean> = computed(() => {
// 使用权限字符串直接从映射中获取计算属性
const permissionCheck = permissionMap[props.permission as keyof typeof permissionMap];
return permissionCheck ? permissionCheck.value : false;
});
function handleClick(event: MouseEvent) {
// 触发父组件绑定的click事件
emit('zwClick', event);
/*
得到子组件的event
<zw-permission-button
@zwClick="handleIsEfficacy"
>{{ statusTypeComputed(row.status)[2] }}</zw-permission-button
>
自定义参数
<zw-permission-button
@zwClick="handleIsEfficacy(row.id, row.status)"
>{{ statusTypeComputed(row.status)[2] }}</zw-permission-button
>
*/
}
defineExpose({
hasPermission: hasPermission.value,
});
</script>
按钮权限两种使用方式
<template>
<div>
<!-- 按钮权限utils版本使用(需要引入utils-Hook) -->
<el-button v-if="permissionExports.hasDeletePermission.value">utils-删除</el-button>
<el-button v-if="permissionExports.hasUpdatePermission.value">utils-更新</el-button>
<!-- 按钮权限组件版本使用 -->
<zw-permission-button class="ml15" permission="look" type="primary" @zwClick="onClickEvent"
>components-查看</zw-permission-button
>
<zw-permission-button
class="ml15"
permission="update"
type="danger"
@zwClick="onClickDiyParams('自定义参数')"
>components-更新</zw-permission-button
>
</div>
</template>
<script setup lang="ts" name="home">
import { permissionExports } from '/@/utils/Hooks/hasBtnPermission';
function onClickEvent(e: MouseEvent) {
console.log(e);
}
function onClickDiyParams(params: string) {
console.log(params);
}
</script>
扩展:Q1:如何全局组件封装?Q2:如何封装一套企业级的axios前端接口
A1:Vue3项目 —— Vite / Webpack 批量注册组件_vue3批量注册组件-CSDN博客
A2:Vue3 + Ts + Vite 封装一套企业级axiso全流程-CSDN博客