import { nextTick, type App, type Directive, type DirectiveBinding } from 'vue' import router from '@/router' import { pinia, usePermissionStore } from '@/store' import type { PermissionDirectiveValue } from '@/types/permission' type PermissionHTMLElement = HTMLElement & { __permissionDisplay?: string __permissionDividerDisplay?: string __permissionLabel?: string __permissionTextNode?: Text __permissionObserver?: MutationObserver __permissionLabelConfig?: { menuCode: string buttonCode: string label?: string usePermissionName?: boolean } } const resolveVisibilityElement = (el: PermissionHTMLElement) => (el.closest('.el-dropdown-menu__item') as PermissionHTMLElement | null) || el const resolveDropdownDivider = (el: PermissionHTMLElement) => { const visibilityElement = resolveVisibilityElement(el) const previousElement = visibilityElement.previousElementSibling as PermissionHTMLElement | null return previousElement?.getAttribute('role') === 'separator' ? previousElement : undefined } const updateVisibility = (el: PermissionHTMLElement, visible: boolean) => { const visibilityElement = resolveVisibilityElement(el) const dropdownDivider = resolveDropdownDivider(el) if (visibilityElement.__permissionDisplay === undefined) { visibilityElement.__permissionDisplay = visibilityElement.style.display } if (dropdownDivider && dropdownDivider.__permissionDividerDisplay === undefined) { dropdownDivider.__permissionDividerDisplay = dropdownDivider.style.display } visibilityElement.style.display = visible ? visibilityElement.__permissionDisplay || '' : 'none' if (dropdownDivider) { dropdownDivider.style.display = visible ? dropdownDivider.__permissionDividerDisplay || '' : 'none' } } // 支持三种写法:v-permission="'add'"、v-permission="['menu', 'add']"、对象形式。 const resolvePermissionBinding = (binding: DirectiveBinding) => { const currentMenuCode = typeof router.currentRoute.value.meta.menuCode === 'string' ? router.currentRoute.value.meta.menuCode : undefined if (typeof binding.value === 'string') { return { menuCode: currentMenuCode, buttonCode: binding.value } } if (Array.isArray(binding.value)) { return { menuCode: binding.value[0], buttonCode: binding.value[1] } } return { menuCode: binding.value?.menuCode || currentMenuCode, buttonCode: binding.value?.buttonCode, label: binding.value?.label, usePermissionName: binding.value?.usePermissionName } } const resolveTextNode = (el: PermissionHTMLElement) => { if (el.__permissionTextNode && el.contains(el.__permissionTextNode)) { return el.__permissionTextNode } const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT) let node = walker.nextNode() while (node) { if (node.textContent?.trim()) { el.__permissionTextNode = node as Text return el.__permissionTextNode } node = walker.nextNode() } return undefined } const updateButtonLabel = ( el: PermissionHTMLElement, menuCode: string, buttonCode: string, label?: string, usePermissionName?: boolean ) => { const permissionStore = usePermissionStore(pinia) const shouldUsePermissionName = usePermissionName ?? label === undefined const resolvedLabel = shouldUsePermissionName ? permissionStore.getButtonPermissionName(menuCode, buttonCode, label || buttonCode) : label || buttonCode const textNode = resolveTextNode(el) if (!textNode) return if (el.__permissionLabel === resolvedLabel && textNode.textContent === resolvedLabel) return // 只替换按钮的首个文本节点,保留前置图标和其他插槽内容不变。 textNode.textContent = resolvedLabel el.__permissionLabel = resolvedLabel } const ensureButtonLabelObserver = (el: PermissionHTMLElement) => { if (el.__permissionObserver) return el.__permissionObserver = new MutationObserver(() => { const config = el.__permissionLabelConfig if (!config) return updateButtonLabel( el, config.menuCode, config.buttonCode, config.label, config.usePermissionName ) }) el.__permissionObserver.observe(el, { childList: true, characterData: true, subtree: true }) } const scheduleButtonLabelUpdate = async ( el: PermissionHTMLElement, menuCode: string, buttonCode: string, label?: string, usePermissionName?: boolean ) => { el.__permissionLabelConfig = { menuCode, buttonCode, label, usePermissionName } ensureButtonLabelObserver(el) updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName) await nextTick() updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName) requestAnimationFrame(() => { updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName) }) } const updatePermissionVisibility = async ( el: PermissionHTMLElement, binding: DirectiveBinding ) => { const { menuCode, buttonCode, label, usePermissionName } = resolvePermissionBinding(binding) if (!menuCode || !buttonCode) { updateVisibility(el, false) return } const permissionStore = usePermissionStore(pinia) await permissionStore.ensureButtonPermissions(menuCode) const allowed = permissionStore.hasButtonAccess(menuCode, buttonCode) // 自定义指令只负责隐藏无权限按钮,不改变按钮原有业务逻辑。 updateVisibility(el, allowed) if (allowed) { void scheduleButtonLabelUpdate(el, menuCode, buttonCode, label, usePermissionName) } } const permissionDirective: Directive = { mounted(el, binding) { void updatePermissionVisibility(el, binding) }, updated(el, binding) { void updatePermissionVisibility(el, binding) }, unmounted(el) { el.__permissionObserver?.disconnect() } } export const installPermissionDirective = (app: App) => { app.directive('permission', permissionDirective) }