为什么用注释节点而不是直接 display: none?
因为注释节点不会影响布局,也不会被 CSS 选择器选中。更重要的是,我们需要保留一个"锚点",方便权限变化时把原始元素恢复回去。
4. 响应式更新
权限可能会动态变化(比如用户被授权后刷新),所以指令需要同时监听 inserted 和 update 钩子:
Vue.directive('perm', {
inserted (el, binding) {
handlerPerm(el, binding)
},
update (el, binding) {
handlerPerm(el, binding)
}
})
function restoreElement (el) {
if (el.placeholderDom && el.placeholderDom.parentNode) {
el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
}
el.placeholderDom = null
return true
}
<button v-perm:[options]="hasPermission">操作</button>
if (typeof binding.value === 'boolean') {
if (binding.value === false) {
domHandler(el, binding)
} else {
restoreElement(el)
}
return
}
<template>
<div>
<!-- 无权限时隐藏 -->
<button v-perm:[permConfig]="'SPACE.DELETE'">删除</button>
<!-- 无权限时禁用并提示 -->
<button v-perm:[permConfigWithTips]="'SPACE.EDIT'">编辑</button>
</div>
</template>
<script>
export default {
computed: {
permConfig() {
return {
spaceId: this.currentSpaceId,
type: 'space'
}
},
permConfigWithTips() {
return {
spaceId: this.currentSpaceId,
type: 'space',
disabled: true,
showTips: true,
tipsText: '您没有编辑权限,请联系管理员'
}
}
}
}
</script>
// 在执行敏感操作前校验
async handleDelete() {
try {
await this.$hasPerm({ spaceId: this.spaceId }, 'SPACE.DELETE')
// 有权限,继续执行删除逻辑
await this.doDelete()
} catch {
// 无权限,$hasPerm 内部已经弹出提示
}
}
this.$getPerm({
spaceId: this.spaceId,
clearCache: true
})
// permission.js
import { useSpaceInfoStore } from '@/store/modules/spaceInfo'
import Auth from '@/api/modules/auth'
let spaceInfoStore = null
setTimeout(() => {
spaceInfoStore = useSpaceInfoStore()
}, 40)
/**
* @typedef {Object} PermissionOptions
* @property {string|number} [wikiId] - 文档id
* @property {string|number} [spaceId] - 空间id
* @property {string} [type] - 权限类型:空间/文档 space/wiki
* @property {boolean} [disabled] - 是否禁用元素
* @property {boolean} [showTips] - 是否显示提示信息
* @property {string} [tipsText] - 提示文本内容
* @property {boolean} [clearCache] - 是否清除缓存
*/
function install (Vue) {
/**
* 获取权限信息
* @param {PermissionOptions} options - 权限选项
* @returns {Promise<any>}
*/
Vue.prototype.$getPerm = (options) => {
if (!options.spaceId) return
// 如果没有传type,则根据是否有文档id判断
options.type = options.type || (options.wikiId ? 'wiki' : 'space')
const argId = options.type + '-' + (options.type === 'space' ? options.spaceId : options.wikiId)
// 清除缓存权限,可重新加载
if (options.clearCache) {
spaceInfoStore.permissionMap[argId] = false
}
if (!spaceInfoStore.permissionMap[argId]) {
spaceInfoStore.permissionMap[argId] = new Promise((resolve, reject) => {
if (options.type === 'space') {
Auth.getUserPermBySpaceId(options.spaceId).then((res) => {
// 组合权限生成唯一key
const permissions = res.reduce((acc, category) => {
category.permissions.forEach(permission => {
acc.push(`${category.value}.${permission.authorityKey}`)
})
return acc
}, [])
resolve({ permissions })
})
} else {
Auth.getWikiPermissionDetail(options.spaceId, options.wikiId).then((res) => {
// 组合权限生成唯一key
const permissions = res.map(item => item.authorityKey)
resolve({ permissions })
})
}
})
}
return spaceInfoStore.permissionMap[argId]
}
/**
* 检查是否有权限
* @param {PermissionOptions} options - 权限选项
* @param {string} perm - 权限码
* @returns {Promise<boolean>}
*/
Vue.prototype.$hasPerm = (options, perm) => {
if (!Object.prototype.hasOwnProperty.call(options, 'showTips')) {
options.showTips = true
}
return new Promise((resolve, reject) => {
if (!options.spaceId) {
resolve(true)
return
}
const promise = Vue.prototype.$getPerm(options)
promise.then((res) => {
if (res.isAdmin) {
resolve(true)
return
}
if (res.permissions.includes(perm)) {
resolve(true)
return
}
if (options.showTips) {
Vue.prototype.$bkMessage({
message: options.tipsText || '没有权限',
theme: 'warning'
})
}
reject(new Error(''))
})
})
}
/**
* DOM 处理函数 - 处理无权限时的元素显示
* @param {HTMLElement} el - DOM 元素
* @param {Object} binding - 指令绑定对象
*/
function domHandler (el, binding) {
let placeholderDom = null
if (binding?.arg?.showTips || binding?.arg?.disabled) {
placeholderDom = el.cloneNode(true)
if (binding?.arg?.showTips) {
placeholderDom.onclick = function () {
Vue.prototype.$bkMessage({
message: binding?.arg?.tipsText || '没有权限',
theme: 'warning'
})
}
}
if (binding?.arg?.disabled) {
placeholderDom.classList.add('disabled')
}
} else {
placeholderDom = document.createComment('permission-placeholder')
}
if (el.parentNode) {
el.placeholderDom = placeholderDom
el.parentNode.replaceChild(placeholderDom, el)
}
}
/**
* 将元素恢复到原始位置
* @param {HTMLElement} el - DOM 元素
* @returns {boolean}
*/
function restoreElement (el) {
el.placeholderDom && el.placeholderDom.parentNode.replaceChild(el, el.placeholderDom)
el.placeholderDom = null
return true
}
/**
* 权限处理函数
* @param {HTMLElement} el - DOM 元素
* @param {Object} binding - 指令绑定对象
*/
function handlerPerm (el, binding) {
// 通过直接传递boolean值,也可以进行权限校验
if (typeof binding.value === 'boolean') {
if (binding.value === false) {
domHandler(el, binding)
} else {
restoreElement(el)
}
return
}
// 判断权限入参是否完善
if (!binding?.arg?.spaceId || !binding?.value) return restoreElement(el)
const promise = Vue.prototype.$getPerm({ ...binding.arg })
promise.then((res) => {
if (res.isAdmin) return restoreElement(el)
if (res.permissions.includes(binding.value)) return restoreElement(el)
domHandler(el, binding)
})
}
Vue.directive('perm', {
inserted (el, binding) {
handlerPerm(el, binding)
},
update (el, binding) {
handlerPerm(el, binding)
}
})
}
export default { install }
import permission from '@/directives/permission'
Vue.use(permission)