permission.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { nextTick, type App, type Directive, type DirectiveBinding } from 'vue'
  2. import router from '@/router'
  3. import { pinia, usePermissionStore } from '@/store'
  4. import type { PermissionDirectiveValue } from '@/types/permission'
  5. type PermissionHTMLElement = HTMLElement & {
  6. __permissionDisplay?: string
  7. __permissionDividerDisplay?: string
  8. __permissionLabel?: string
  9. __permissionTextNode?: Text
  10. __permissionObserver?: MutationObserver
  11. __permissionLabelConfig?: {
  12. menuCode: string
  13. buttonCode: string
  14. label?: string
  15. usePermissionName?: boolean
  16. }
  17. }
  18. const resolveVisibilityElement = (el: PermissionHTMLElement) =>
  19. (el.closest('.el-dropdown-menu__item') as PermissionHTMLElement | null) || el
  20. const resolveDropdownDivider = (el: PermissionHTMLElement) => {
  21. const visibilityElement = resolveVisibilityElement(el)
  22. const previousElement = visibilityElement.previousElementSibling as PermissionHTMLElement | null
  23. return previousElement?.getAttribute('role') === 'separator' ? previousElement : undefined
  24. }
  25. const updateVisibility = (el: PermissionHTMLElement, visible: boolean) => {
  26. const visibilityElement = resolveVisibilityElement(el)
  27. const dropdownDivider = resolveDropdownDivider(el)
  28. if (visibilityElement.__permissionDisplay === undefined) {
  29. visibilityElement.__permissionDisplay = visibilityElement.style.display
  30. }
  31. if (dropdownDivider && dropdownDivider.__permissionDividerDisplay === undefined) {
  32. dropdownDivider.__permissionDividerDisplay = dropdownDivider.style.display
  33. }
  34. visibilityElement.style.display = visible ? visibilityElement.__permissionDisplay || '' : 'none'
  35. if (dropdownDivider) {
  36. dropdownDivider.style.display = visible
  37. ? dropdownDivider.__permissionDividerDisplay || ''
  38. : 'none'
  39. }
  40. }
  41. // 支持三种写法:v-permission="'add'"、v-permission="['menu', 'add']"、对象形式。
  42. const resolvePermissionBinding = (binding: DirectiveBinding<PermissionDirectiveValue>) => {
  43. const currentMenuCode =
  44. typeof router.currentRoute.value.meta.menuCode === 'string'
  45. ? router.currentRoute.value.meta.menuCode
  46. : undefined
  47. if (typeof binding.value === 'string') {
  48. return {
  49. menuCode: currentMenuCode,
  50. buttonCode: binding.value
  51. }
  52. }
  53. if (Array.isArray(binding.value)) {
  54. return {
  55. menuCode: binding.value[0],
  56. buttonCode: binding.value[1]
  57. }
  58. }
  59. return {
  60. menuCode: binding.value?.menuCode || currentMenuCode,
  61. buttonCode: binding.value?.buttonCode,
  62. label: binding.value?.label,
  63. usePermissionName: binding.value?.usePermissionName
  64. }
  65. }
  66. const resolveTextNode = (el: PermissionHTMLElement) => {
  67. if (el.__permissionTextNode && el.contains(el.__permissionTextNode)) {
  68. return el.__permissionTextNode
  69. }
  70. const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
  71. let node = walker.nextNode()
  72. while (node) {
  73. if (node.textContent?.trim()) {
  74. el.__permissionTextNode = node as Text
  75. return el.__permissionTextNode
  76. }
  77. node = walker.nextNode()
  78. }
  79. return undefined
  80. }
  81. const updateButtonLabel = (
  82. el: PermissionHTMLElement,
  83. menuCode: string,
  84. buttonCode: string,
  85. label?: string,
  86. usePermissionName?: boolean
  87. ) => {
  88. const permissionStore = usePermissionStore(pinia)
  89. const shouldUsePermissionName = usePermissionName ?? label === undefined
  90. const resolvedLabel = shouldUsePermissionName
  91. ? permissionStore.getButtonPermissionName(menuCode, buttonCode, label || buttonCode)
  92. : label || buttonCode
  93. const textNode = resolveTextNode(el)
  94. if (!textNode) return
  95. if (el.__permissionLabel === resolvedLabel && textNode.textContent === resolvedLabel) return
  96. // 只替换按钮的首个文本节点,保留前置图标和其他插槽内容不变。
  97. textNode.textContent = resolvedLabel
  98. el.__permissionLabel = resolvedLabel
  99. }
  100. const ensureButtonLabelObserver = (el: PermissionHTMLElement) => {
  101. if (el.__permissionObserver) return
  102. el.__permissionObserver = new MutationObserver(() => {
  103. const config = el.__permissionLabelConfig
  104. if (!config) return
  105. updateButtonLabel(
  106. el,
  107. config.menuCode,
  108. config.buttonCode,
  109. config.label,
  110. config.usePermissionName
  111. )
  112. })
  113. el.__permissionObserver.observe(el, {
  114. childList: true,
  115. characterData: true,
  116. subtree: true
  117. })
  118. }
  119. const scheduleButtonLabelUpdate = async (
  120. el: PermissionHTMLElement,
  121. menuCode: string,
  122. buttonCode: string,
  123. label?: string,
  124. usePermissionName?: boolean
  125. ) => {
  126. el.__permissionLabelConfig = { menuCode, buttonCode, label, usePermissionName }
  127. ensureButtonLabelObserver(el)
  128. updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName)
  129. await nextTick()
  130. updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName)
  131. requestAnimationFrame(() => {
  132. updateButtonLabel(el, menuCode, buttonCode, label, usePermissionName)
  133. })
  134. }
  135. const updatePermissionVisibility = async (
  136. el: PermissionHTMLElement,
  137. binding: DirectiveBinding<PermissionDirectiveValue>
  138. ) => {
  139. const { menuCode, buttonCode, label, usePermissionName } = resolvePermissionBinding(binding)
  140. if (!menuCode || !buttonCode) {
  141. updateVisibility(el, false)
  142. return
  143. }
  144. const permissionStore = usePermissionStore(pinia)
  145. await permissionStore.ensureButtonPermissions(menuCode)
  146. const allowed = permissionStore.hasButtonAccess(menuCode, buttonCode)
  147. // 自定义指令只负责隐藏无权限按钮,不改变按钮原有业务逻辑。
  148. updateVisibility(el, allowed)
  149. if (allowed) {
  150. void scheduleButtonLabelUpdate(el, menuCode, buttonCode, label, usePermissionName)
  151. }
  152. }
  153. const permissionDirective: Directive<PermissionHTMLElement, PermissionDirectiveValue> = {
  154. mounted(el, binding) {
  155. void updatePermissionVisibility(el, binding)
  156. },
  157. updated(el, binding) {
  158. void updatePermissionVisibility(el, binding)
  159. },
  160. unmounted(el) {
  161. el.__permissionObserver?.disconnect()
  162. }
  163. }
  164. export const installPermissionDirective = (app: App) => {
  165. app.directive('permission', permissionDirective)
  166. }