index.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { defineComponent, h, ref, watch, computed } from 'vue'
  2. import { ElMenu, ElSubmenu, ElMenuItem, ElTag } from 'element-plus'
  3. import { useRoute, useRouter } from 'vue-router'
  4. import { isArray, isEmpty, isInputEmpty } from '@cip/utils/util'
  5. import { getMenuTitle, isHideInMenu, matchMenuByRouteName } from '../helper'
  6. import './index.less'
  7. import CipMainIcon from '../cip-main-icon'
  8. export default defineComponent({
  9. name: 'CipMainNav',
  10. props: {
  11. navMenu: Array, // 菜单项
  12. mode: String, // 菜单模式 horizontal-水平(此模式下 isCollapse无效) vertical-垂直
  13. isCollapse: Boolean, // 是否收缩
  14. privileges: Array, // 权限
  15. topMenuOnly: Boolean, // 是否子渲染一级菜单
  16. badgeMap: Object
  17. },
  18. emits: ['update:activeMenu', 'triggerGetBadge'],
  19. setup (props, { emit }) {
  20. const router = useRouter()
  21. const route = useRoute()
  22. const getDeepChildren = (children) => {
  23. return children.map(child => {
  24. if (child.children && child.children.length > 0) {
  25. return getDeepChildren(child.children).concat(child.name)
  26. } else {
  27. return child.name
  28. }
  29. }).flat(Infinity).filter(v => v !== undefined)
  30. }
  31. // 分析顶层菜单和 子菜单的关系
  32. const analysisRelationship = computed(() => {
  33. if (props.topMenuOnly === true) {
  34. return props.navMenu.reduce((acc, b) => {
  35. if (b.children) {
  36. acc[b.name] = getDeepChildren(b.children)
  37. }
  38. return acc
  39. }, {})
  40. } else {
  41. // 非topMenuOnly不进行分析
  42. return {}
  43. }
  44. })
  45. // 判断权限
  46. const hasPrivilege = (item) => {
  47. const code = item.code || item.meta?.code
  48. if (isInputEmpty(code)) return true // 没有code即表示此不需要权限
  49. if (isArray(code)) {
  50. // code为数组的情况
  51. if (code.length === 0) return true // 空数组代表不需要权限
  52. return code.some(v => {
  53. if (isInputEmpty(v)) return true // 没有code即表示此不需要权限
  54. return props.privileges?.includes(v)
  55. })
  56. } else {
  57. return props.privileges?.includes(code)
  58. }
  59. }
  60. // 判断是否有可展示子菜单
  61. const checkChildren = (children) => {
  62. if (isEmpty(children)) return false
  63. // 非隐藏菜单及非无权限菜单
  64. const showChildren = children?.filter(child =>
  65. !isHideInMenu(child) &&
  66. hasPrivilege(child)
  67. )
  68. return showChildren?.length > 0
  69. }
  70. // 处理起始字符串为_cache的父路由
  71. const checkCacheRoute = (route) => {
  72. if (/^_cache/.test(route.name)) {
  73. return route.children
  74. } else {
  75. return route
  76. }
  77. }
  78. // 渲染菜单货子菜单的内容[注: 内容包含子菜单和菜单项]
  79. const renderMenu = (menuContentList) => {
  80. return menuContentList.map(item => {
  81. const route = checkCacheRoute(item)
  82. // 如果是数组则调用自身
  83. if (isArray(route)) {
  84. return renderMenu(route)
  85. }
  86. // 对子模块进行过滤
  87. if (checkChildren(route.children)) {
  88. return renderSubmenu(route)
  89. } else {
  90. return renderMenuItem(route) // 渲染为 menu-item
  91. }
  92. })
  93. }
  94. // 根据子菜单项计算子菜单的badge
  95. const computedSubBadge = (childNodeList) => {
  96. let count = 0
  97. childNodeList.forEach(childNode => {
  98. const badge = childNode?.props?.badge
  99. if (typeof badge === 'number') {
  100. count += badge
  101. }
  102. })
  103. if (count > 0) return count
  104. return undefined
  105. }
  106. // 渲染子菜单[注: 如果没有可渲染的菜单项, 将会本身渲染为菜单项]
  107. const renderSubmenu = (submenu) => {
  108. if (isHideInMenu(submenu) || !hasPrivilege(submenu)) return null
  109. const children = renderMenu(submenu.children) // 渲染为节点
  110. const filterEmptyChildren = children.filter(child => !isEmpty(child))
  111. if (filterEmptyChildren.length > 0) {
  112. // 子菜单存在需要显示的菜单进行此渲染
  113. submenu._hasChildren = true
  114. let subBadge
  115. if (!submenu.hideBadge) {
  116. subBadge = !isEmpty(submenu.badge) ? submenu.badge : computedSubBadge(children)
  117. }
  118. if (props.topMenuOnly !== true) {
  119. return h(ElSubmenu, { popperClass: 'cip-menu-popper', key: submenu.name, index: submenu.name, badge: subBadge }, {
  120. title: () => [renderMenuIcon(submenu), renderMenuItemTitle(submenu), renderMenuBadge(subBadge)],
  121. default: () => children
  122. })
  123. } else {
  124. // 存在子路由
  125. const redirectName = filterEmptyChildren[0].props.index
  126. const redirectRoute = filterEmptyChildren[0].props.route
  127. const { children, ...item } = submenu
  128. return renderMenuItem({ ...item, name: redirectName, route: redirectRoute, originName: item.name })
  129. }
  130. } else {
  131. // 其他情况页渲染为item
  132. return renderMenuItem(submenu)
  133. }
  134. }
  135. // 渲染菜单内容的icon[注: 包含子菜单及菜单项]
  136. const renderMenuIcon = (menuContent = {}) => {
  137. const iconName = menuContent.meta?.icon || menuContent.icon
  138. return <CipMainIcon name={iconName}/>
  139. // const iconName = menuContent?.meta?.icon || menuContent?.icon
  140. // if (iconName) {
  141. // if (iconName.indexOf('_') === 0) {
  142. // const name = iconName.substr(1)
  143. // return <CipSvgIcon name={name}/>
  144. // } else {
  145. // return h('i', { class: iconName })
  146. // }
  147. // } else {
  148. // return undefined
  149. // }
  150. }
  151. const renderMenuBadge = (badge) => {
  152. if (badge) {
  153. return <ElTag size={'mini'} effect={'dark'} type={'danger'} class={'cip-menu__badge'}>{badge}</ElTag>
  154. } else {
  155. return undefined
  156. }
  157. }
  158. const renderMenuItemTitle = (menuContent) => {
  159. return <span class={'cip-menu__title'}>{getMenuTitle(menuContent)}</span>
  160. }
  161. const getCurrentBadge = (item) => {
  162. if (props.topMenuOnly && analysisRelationship.value[item.originName]) {
  163. return analysisRelationship.value[item.originName].reduce((acc, childName) => {
  164. if (props.badgeMap[childName]) {
  165. acc += props.badgeMap[childName]
  166. }
  167. return acc
  168. }, 0)
  169. } else {
  170. return props.badgeMap[item.name] ?? item.badge
  171. }
  172. }
  173. // 渲染菜单项 [注:隐藏的、没有权限的、以_开头但是不是只渲染一层菜单的模式下的菜单项将不渲染]
  174. const renderMenuItem = (item) => {
  175. // 过滤隐藏及权限的菜单
  176. if (isHideInMenu(item) || !hasPrivilege(item)) return null
  177. // 过滤name以_开通的菜单
  178. if (item.name.indexOf('_') === 0 && props.topMenuOnly !== true) return null
  179. // 获取badge
  180. const badge = getCurrentBadge(item) // props.badgeMap[item.name] ?? item.badge
  181. emit('triggerGetBadge', item.name) // 通知触发
  182. return h(ElMenuItem, { key: item.name, index: item.name, route: item.route, badge }, {
  183. default: () => renderMenuIcon(item),
  184. title: () => [renderMenuItemTitle(item), renderMenuBadge(badge)]
  185. })
  186. }
  187. // 当前激活菜单项名称
  188. const currentActiveName = ref()
  189. watch([() => route.name, () => props.navMenu], () => {
  190. if (currentActiveName.value !== route.name) {
  191. currentActiveName.value = route.name
  192. }
  193. if (props.topMenuOnly === true) {
  194. // 此处的menu需要很完善才能正常使用(需要包含详情页等非展示视图的菜单信息)
  195. // 未进行未匹配处理[注:仅在激活的路由不再navMenu中才会导致未找到]
  196. const menuMatched = matchMenuByRouteName(props.navMenu, route.name)
  197. if (menuMatched) {
  198. const activeMenu = props.navMenu.find(menu => menu.name === menuMatched[0].name)
  199. emit('update:activeMenu', activeMenu)
  200. } else {
  201. emit('update:activeMenu', undefined)
  202. }
  203. }
  204. }, { immediate: true })
  205. return () => h(ElMenu, {
  206. class: ['cip-main-nav'],
  207. defaultActive: currentActiveName.value,
  208. uniqueOpened: true,
  209. collapse: props.isCollapse,
  210. mode: props.mode,
  211. onSelect: (index, indexPath, item) => {
  212. const { route } = item
  213. if (route) {
  214. router.push(route)
  215. } else {
  216. router.push({ name: index })
  217. }
  218. }
  219. }, { default: () => renderMenu(props.navMenu) })
  220. }
  221. })