当前位置: 首页 > news >正文

【Vue】自定义指令之权限控制

【需求背景】 项目中有时需要进行按钮级的权限控制,传统的方案通过手动v-if判断,导致代码冗余,维护成本高,考虑通过自定义指令来实现全局复用、逻辑集中、增强代码可读性。

一、自定义指令

首先介绍一下自定义指令。在 Vue 中,自定义指令是一种用于封装 DOM 操作的功能扩展方式,允许你直接操作 DOM 元素,实现一些通用的交互逻辑(如聚焦、拖拽、权限控制等)。

这里以权限控制为例,定义一个可以实现按钮级权限控制的自定义指令,名字定义为v-hasPermi,这里仅讨论全局自定义指令。

1.1 Vue3用法(推荐)

在Vue3中,可以通过app.directive()函数注册一个全局指令。函数的具体用法不详细展开。

权限控制自定义指令的核心实现逻辑是:使用时传入当前组件或元素对应的权限标识,查询当前账号的权限集合中是否包括该标识,包括则不做处理,不包括则移除当前组件或元素,从而实现精准的按钮级权限控制。代码如下:

// main.js
import { createApp } from 'vue';
const app = createApp(App);// 其他需要引入的东西......app.directive('hasPermi', {mounted(el, binding) {const { value } = binding;// userPermissions一般是登录之后接口返回的,存放在userStore中const permissions = userPermissions;try {if (value && value instanceof Array && value.length > 0 && permissions?.length) {const permissionFlag = value;const hasPermissions = permissions.some(permission => permissionFlag.includes(permission));if (!hasPermissions) {el.parentNode && el.parentNode.removeChild(el);}} else {throw new Error('请设置操作权限标签值');}} catch (err) {console.log(err);}}
});app.mount('#app');
<!-- 组件中使用 -->
<el-button v-hasPermi="['USER_ADD']" type="default" class="add-btn" @click="addAccount">新建用户</el-button>

1.2 Vue2用法

在Vue2中,可以通过Vue.directive()函数注册一个全局指令。

// main.js
Vue.directive('focus', {// 当被绑定的元素插入到 DOM 中时……inserted(el, binding) {const { value } = binding;const permissions = store.getters && store.getters.permissions;if (value && value instanceof Array && value.length > 0) {const permissionFlag = value;const hasPermissions = permissions.some(permission => {return permissionFlag.includes(permission);});if (!hasPermissions) {el.parentNode && el.parentNode.removeChild(el);} else {throw new Error(`请设置操作权限标签值`);}}};
});
<!-- 组件中使用 -->
<el-button v-hasPermi="['USER_ADD']" type="default" class="add-btn" @click="addAccount">新建用户</el-button>

二、存在的问题

上面的写法已经可以满足大部分的组件的权限控制,但是在使用过程中发现,el-tabs组件不适用,直接在el-tab-pane上加v-hasPermi不生效且有报错,写法如下:

<template><el-tabs v-model="activeName"><el-tab-pane v-hasPermi="['USER_SHOW']" label="User" name="first" > User </el-tab-pane><el-tab-pane label="Config" name="second">Config</el-tab-pane><el-tab-pane label="Role" name="third">Role</el-tab-pane></el-tabs>
</template>
<script>
const activeName = ref('first');
</script>

我们可以打印一下此时的el看一下(无法贴图,代码形式展现):

<div data-v-3e12c1bb="" id="pane-first" class="el-tab-pane" role="tabpanel" aria-hidden="true" aria-labelledby="tab-second" style="">Config</div>

再看一下el-tabs的结构(无法贴图,直接来一个结构大纲):

<div class="el-tabs"><!-- Tab 标题栏 --><div class="el-tabs__header"><div class="el-tabs__nav-wrap"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav"><div class="el-tabs__item is-top" id="tab-first" aria-controls="pane-first" role="tab" aria-selected="false" tabindex="-1">User<!----></div><div class="el-tabs__item is-top" id="tab-second" aria-controls="pane-second" role="tab" aria-selected="false" tabindex="-1">Config<!----></div><div class="el-tabs__item is-top" id="tab-third" aria-controls="pane-third" role="tab" aria-selected="false" tabindex="-1">Role<!----></div></div></div></div></div><!-- Tab 内容区域 --><div class="el-tabs__content"><div data-v-3e12c1bb="" id="pane-second" class="el-tab-pane" role="tabpanel" aria-hidden="true" aria-labelledby="tab-second" style="display: none;">Config</div><div data-v-3e12c1bb="" id="pane-third" class="el-tab-pane" role="tabpanel" aria-hidden="true" aria-labelledby="tab-third" style="display: none;">Role</div></div>
</div>

可以发现两个问题:
(1)标题区域还是3个元素,没有隐藏第一个tab,内容区域正常隐藏了第一个tab对应的内容区。
(2)控制台报错:Uncaught (in promise) TypeError: Invalid value used as weak map key

我们来看下是为什么,首先el-tabs标签下包括el-tabs__headerel-tabs__content两个区域,将自定义指令加在el-tab-pane上时,可以发现拿到的elel-tabs__content下的子元素。我们的权限控制函数逻辑是:当前元素父元素存在的情况下,从父元素上移除当前元素。那么结果就是内容区域正常隐藏了第一个tab对应的内容区,而我们压根没有控制到标题区,所以标题区没有隐藏第一个tab。

那么问题就好办了,我们只需要拿到标题区的元素,并隐藏对应内容即可,具体实现思路是:
(1)通过el.parentNode.parentNode拿到最顶部的el-tabs元素,然后查找el-tabs__header容器;
(2)获取当前 tab-pane 的索引;
(3)移除对应的 tab header;
(4)移除当前tab-pane元素。
改造后的自定义指令函数为:

app.directive('hasPermi', {mounted(el, binding) {const { value } = binding;// userPermissions一般是登录之后接口返回的,存放在userStore中const permissions = userPermissions;try {if (value && value instanceof Array && value.length > 0 && permissions?.length) {const permissionFlag = value;const hasPermissions = permissions.some(permission => permissionFlag.includes(permission));if (!hasPermissions) {// el-tabs单独处理if (el.parentNode && el.parentNode.classList && el.parentNode.classList.contains('el-tabs__content')) {try {// 查找 tabs header 容器const tabsContainer = el.parentNode.parentNode;const headerNode = Array.from(tabsContainer.children || []).find(child =>child.classList && child.classList.contains('el-tabs__header'));// 获取当前 tab-pane 的索引const tabPanes = Array.from(el.parentNode.children || []);const currentIndex = tabPanes.indexOf(el);// 移除对应的 tab headerif (headerNode && currentIndex !== -1) {const tabHeaders = headerNode.querySelectorAll('.el-tabs__item');if (tabHeaders && tabHeaders[currentIndex]) {tabHeaders[currentIndex].remove();}}// 移除当前tab-pane元素nextTick(() => {el.parentNode && el.parentNode.removeChild(el);});} catch (error) {console.error('处理 tabs 权限时出错:', error);}} else {// 正常处理其他元素el.parentNode && el.parentNode.removeChild(el);}}} else {throw new Error('请设置操作权限标签值');}} catch (err) {console.log(err);}}
});

【注意】最后移除当前tab-pane元素时,使用了nextTick函数,这是为了解决控制台报错的问题。这个报错主要是由于Vue 的响应式系统仍在跟踪这个元素,而在移除过程中触发了 WeakMap 相关的操作异常。简单来说就是Vue 的响应式和你的移除DOM操作两者产生了冲突。使用nextTickVue 完成当前更新周期后再执行 DOM 操作,解决冲突问题。

拓展一:

继续探究,当前可以实现通过v-hasPermi完成对el-tabs的权限控制了,但不够完美。el-tabs绑定值一般会给个初始值,但是如果刚好初始值对应的tab没权限,就会出问题。一般情况下,el-tabs默认选择第一个tab,这里考虑动态赋值给可显示的第一个tab。实现代码如下:

const activeName = ref('');
const setDefaultActiveTab = (defaultName) => {nextTick(() => {// 获取所有 tab-pane 元素const tabPanes = document.querySelectorAll('.el-tab-pane');if (tabPanes.length > 0) {// 获取第一个可见的 tab-pane 的 name 属性const firstVisiblePane = Array.from(tabPanes)[0];if (firstVisiblePane) {// 获取 name 属性const tabName = firstVisiblePane.getAttribute('aria-labelledby')?.replace('tab-', '') || defaultName; // 默认 fallbackactiveName.value = tabName;}}});
};
onMounted(() => {setDefaultActiveTab('first');
});

拓展二:

由于用到了索引和删除,所以当一个el-tabs中有多个el-tab-pane需要权限控制时,就会出问题。那么我们改用name去找到对应元素,在el-tabs中,name值是唯一的,改造后的自定义指令函数为:

app.directive('hasPermi', {mounted(el, binding) {const { value } = binding;// userPermissions一般是登录之后接口返回的,存放在userStore中const permissions = userPermissions;try {if (value && value instanceof Array && value.length > 0 && permissions?.length) {const permissionFlag = value;const hasPermissions = permissions.some(permission => permissionFlag.includes(permission));if (!hasPermissions) {// el-tabs单独处理if (el.parentNode && el.parentNode.classList && el.parentNode.classList.contains('el-tabs__content')) {try {// 查找 tabs header 容器const tabsContainer = el.parentNode.parentNode;const headerNode = Array.from(tabsContainer.children || []).find(child =>child.classList && child.classList.contains('el-tabs__header'));// 获取当前 tab-pane 的 name 属性const tabName = el.getAttribute('id')?.replace('pane-', '');// 通过 name 属性移除对应的 tab headerif (headerNode && tabName) {const tabHeader = headerNode.querySelector(`[id="tab-${tabName}"]`);if (tabHeader) {tabHeader.remove();}}// 移除当前tab-pane元素nextTick(() => {el.parentNode && el.parentNode.removeChild(el);});} catch (error) {console.error('处理 tabs 权限时出错:', error);}} else {// 正常处理其他元素el.parentNode && el.parentNode.removeChild(el);}}} else {throw new Error('请设置操作权限标签值');}} catch (err) {console.log(err);}}
});

拓展三:

如果不想改造v-hasPermi,可以考虑封装一个checkPermi函数并全局挂载,el-tabs还通过v-if的方式来权限控制。核心思路和指令是一样的,只是换一种方式。

【提示】这种方法不会出现拓展二中所提到的问题,所以日常开发中使用这个方法足矣,只需要注意若项目使用自定义指令,el-tabs需要使用函数单独处理。

// permission.js
/*** 字符权限校验* @param {Array} value 校验值* @returns {Boolean}*/
export function checkPermi(value) {if (value && value instanceof Array && value.length > 0) {const permissions = useUserStore().userPermissions;const permissionDatas = value;const hasPermission = permissions.some(permission => {return permissionDatas.includes(permission);});if (!hasPermission) {return false;}return true;} else {console.error('请设置操作权限标签值');return false;}
}
// main.js
import { checkPermi } from './util/permission';
const app = createApp(App);app.config.globalProperties.checkPermi = checkPermi;
app.mount('#app');
<!-- 组件中使用 -->
<template><el-tabs v-model="activeName"><el-tab-pane v-if="checkPermi(['USER_SHOW'])" label="User" name="first" > User </el-tab-pane><el-tab-pane label="Config" name="second">Config</el-tab-pane><el-tab-pane label="Role" name="third">Role</el-tab-pane></el-tabs>
</template>

总结

  1. v-hasPermi自定义指令适用于大部分组件或元素,可实现按钮级权限控制;
  2. el-tabs元素较为特殊,可以在v-hasPermi指令中单独处理或使用v-if控制。
  3. el-tabs元素隐藏后,默认选中值可以通过函数动态设置。
http://www.dtcms.com/a/581923.html

相关文章:

  • asp.net网站第一次运行慢网站建设合同书保密条款
  • ZYNQ-7000双核协处理实战:ARM Cortex-A9与FPGA的智能数据采集系统
  • 慈溪哪里有做网站怎么看网站pv
  • 【PySpark】conda create -n pyspark python=3.8报错
  • CSS 数学函数完全指南:从基础计算到高级动画
  • uni-app打包app -- 在用户首次启动 App 时,强制弹出一个“用户协议与隐私政策”的确认对话框。
  • 互联网网站排名深圳住房和城乡建设局网站
  • Wi-Fi 7通信技术
  • @InitBinder注解
  • 20251107给荣品RD-RK3588-MID开发板跑Rockchip的原厂Android13系统时适配8寸屏的CTP【使用荣品的DTS】
  • 《隐匿之智:AI暗潮下的末日序章》
  • 网站建设玖金手指谷哥四wordpress注册怎样通过邮箱验证码
  • 山东首台(套)高端装备申报材料及申报流程解读
  • “互联网之光” 博览会启幕,AI+生活场景让科技触手可及
  • 应对 “读放大” 问题的新方法 —— OceanBase 中的 Merge-On-Write 表
  • 48_AI智能体核心业务之钉钉服务集成全局主控Agent:构建企业级智能助手的工程实践
  • 网站如何实现临时聊天wordpress 多店铺
  • 郑州做网站 哪家好wordpress 获取文章数量
  • 友汇网网站建设自考网页制作与网站建设
  • 【OTA专题】2 初级bootloader架构和基础工程移植
  • 极限命令执行6三字节RCE
  • 如何在Windows系统中加入程序自启动
  • 【一、基础篇】自注意力机制中的 Q,K、V 矩阵是什么缩写?
  • 配置 PostgreSQL 远程连接
  • Sampler AI 材质流:一键“喂”图生成 PBR
  • 中国建设的网站西安网站seo 优帮云
  • 关于“震颤”的学习笔记
  • 网站整改建设安全设备方案广州信息流推广公司
  • 河间网站网站建设wordpress无法查看站点
  • uniapp移动端实现触摸滑动功能:上下滑动展开收起内容,左右滑动删除列表