index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { defineComponent, h, ref, watch, computed, nextTick } from 'vue'
  2. import { ElMenu, ElSubMenu as ElSubmenu, ElMenuItem, ElTag, ElMenuItemGroup } from 'element-plus'
  3. import { useRoute, useRouter } from 'vue-router'
  4. import { isArray, isEmpty, isNotEmpty } from '@cip/utils/util'
  5. import { usePrivileges } from '@cip/hooks/use-privileges'
  6. import { getMenuTitle, matchMenuByRouteName, matchMenuByRoutePath } from '../helper'
  7. import CipMainIcon from '../cip-main-icon'
  8. import { filterMenu, getFirstMenuItem, findMenu, getActiveNameByMatchedMenu } from './util'
  9. import { isSubApp } from '../../cip-subapp-container/micro-app/util'
  10. import './index.less'
  11. export default defineComponent({
  12. name: 'CipMainNav',
  13. props: {
  14. navMenu: Array, // 菜单项
  15. iconDepth: Number, // 展示图标的最大深度 不设置时为无限
  16. mode: String, // 菜单模式 horizontal-水平(此模式下 isCollapse无效) vertical-垂直
  17. isCollapse: Boolean, // 是否收缩
  18. // privileges: Array, // 权限
  19. topMenuOnly: Boolean, // 是否子渲染一级菜单
  20. badgeMap: Object,
  21. theme: String,
  22. ellipsis: Boolean // 自动折叠
  23. },
  24. emits: ['update:activeMenu', 'triggerGetBadge'],
  25. setup (props, { emit }) {
  26. const router = useRouter()
  27. const route = useRoute()
  28. const privileges = usePrivileges()
  29. const menu = computed(() => filterMenu(props.navMenu, privileges.value))
  30. const getDeepChildren = (children) => {
  31. return children.map(child => {
  32. if (child.children && child.children.length > 0) {
  33. return getDeepChildren(child.children).concat(child.name)
  34. } else {
  35. return child.name
  36. }
  37. }).flat(Infinity).filter(v => v !== undefined)
  38. }
  39. // 分析顶层菜单和 子菜单的关系
  40. const analysisRelationship = computed(() => {
  41. if (props.topMenuOnly === true) {
  42. return menu.value.reduce((acc, b) => {
  43. if (b.children) {
  44. acc[b.name] = getDeepChildren(b.children)
  45. }
  46. return acc
  47. }, {})
  48. } else {
  49. // 非topMenuOnly不进行分析
  50. return {}
  51. }
  52. })
  53. // 判断是否有可展示子菜单
  54. const checkChildren = (children) => {
  55. return isArray(children) && children.length > 0
  56. }
  57. // 兼容老版本菜单从路由中获取对_cache开通的路由的特殊处理
  58. // 处理起始字符串为_cache的父路由
  59. const checkCacheRoute = (route) => {
  60. if (/^_cache/.test(route.name)) {
  61. return route.children
  62. } else {
  63. return route
  64. }
  65. }
  66. // 渲染菜单货子菜单的内容[注: 内容包含子菜单和菜单项]
  67. const renderMenu = (menuContentList, depth = 0) => {
  68. depth++
  69. return menuContentList.map(item => {
  70. const route = checkCacheRoute(item)
  71. // 如果是数组则调用自身
  72. if (isArray(route)) {
  73. return renderMenu(route, depth)
  74. }
  75. // 对子模块进行过滤
  76. if (checkChildren(route.children)) {
  77. if (route.type === 'group') return renderMenuGroup(route, depth)
  78. return renderSubmenu(route, depth)
  79. } else {
  80. return renderMenuItem(route, depth) // 渲染为 menu-item
  81. }
  82. })
  83. }
  84. // 根据子菜单项计算子菜单的badge
  85. const computedSubBadge = (childNodeList) => {
  86. let count = 0
  87. childNodeList.forEach(childNode => {
  88. const badge = childNode?.props?.badge
  89. if (typeof badge === 'number') {
  90. count += badge
  91. }
  92. })
  93. if (count > 0) return count
  94. return undefined
  95. }
  96. // 渲染子菜单[注: 如果没有可渲染的菜单项, 将会本身渲染为菜单项]
  97. const renderSubmenu = (submenu, depth) => {
  98. // v4.x由filterMenu代替
  99. if (submenu.children.length > 0) {
  100. // 子菜单存在需要显示的菜单进行此渲染
  101. let subBadge
  102. if (props.topMenuOnly !== true || (props.topMenuOnly && depth > 1)) {
  103. const childrenVnode = renderMenu(submenu.children, depth + 1) // renderMenu(submenu.children, depth + 1) // 渲染为节点
  104. if (!submenu.hideBadge) {
  105. subBadge = !isEmpty(submenu.badge) ? submenu.badge : computedSubBadge(childrenVnode)
  106. }
  107. return h(ElSubmenu, {
  108. popperClass: `cip-menu-popper cip-menu-popper--${props.mode} main-theme--${props.theme}`,
  109. key: submenu.name,
  110. index: submenu.name,
  111. popperOffset: 1,
  112. badge: subBadge
  113. }, {
  114. title: () => [renderMenuIcon(submenu, depth), renderMenuItemTitle(submenu), renderMenuBadge(subBadge)],
  115. default: () => childrenVnode
  116. })
  117. } else {
  118. // 存在子路由
  119. // 需要定向到默认选中的第一个
  120. const firstItem = getFirstMenuItem(submenu)
  121. const redirectName = firstItem.name
  122. const redirectRoute = firstItem.route
  123. const { children, ...item } = submenu
  124. return renderMenuItem({ ...item, name: redirectName, route: redirectRoute, originName: item.name }, depth)
  125. }
  126. } else {
  127. // 其他情况页渲染为item
  128. return renderMenuItem(submenu, depth)
  129. }
  130. }
  131. // 渲染菜单组
  132. const renderMenuGroup = (menuGroup, depth) => {
  133. if (menuGroup.children.length > 0) {
  134. const childrenVnode = renderMenu(menuGroup.children, depth + 1)
  135. return h(ElMenuItemGroup, {
  136. popperClass: `cip-menu-popper cip-menu-popper--${props.mode} main-theme--${props.theme}`,
  137. key: menuGroup.name,
  138. index: menuGroup.name
  139. // badge: subBadge
  140. }, {
  141. title: () => [renderMenuIcon(menuGroup, depth), renderMenuItemTitle(menuGroup)],
  142. default: () => childrenVnode
  143. })
  144. }
  145. }
  146. // 渲染菜单内容的icon[注: 包含子菜单及菜单项] 控制菜单icon的最大层级
  147. const renderMenuIcon = (menuContent = {}, depth = 0) => {
  148. // 非空且 当前层级>iconDepth时不渲染icon
  149. if (isNotEmpty(props.iconDepth) && depth > props.iconDepth) return undefined
  150. const iconName = menuContent.meta?.icon || menuContent.icon
  151. return <CipMainIcon name={iconName}/>
  152. }
  153. const renderMenuBadge = (badge) => {
  154. if (badge) {
  155. return <ElTag size={'small'} effect={'dark'} type={'danger'} round class={'cip-menu__badge'}>{badge}</ElTag>
  156. } else {
  157. return undefined
  158. }
  159. }
  160. const renderMenuItemTitle = (menuContent) => {
  161. return <span class={'cip-menu__title'}>{getMenuTitle(menuContent)}</span>
  162. }
  163. const getCurrentBadge = (item) => {
  164. if (props.topMenuOnly && analysisRelationship.value[item.originName]) {
  165. return analysisRelationship.value[item.originName].reduce((acc, childName) => {
  166. if (props.badgeMap[childName]) {
  167. acc += props.badgeMap[childName]
  168. }
  169. return acc
  170. }, 0)
  171. } else {
  172. return props.badgeMap[item.name] ?? item.badge
  173. }
  174. }
  175. // 渲染菜单项 [注:隐藏的、没有权限的、以_开头但是不是只渲染一层菜单的模式下的菜单项将不渲染]
  176. const renderMenuItem = (item, depth) => {
  177. // 过滤隐藏及权限的菜单 v4.x由filterMenu代替
  178. // if (isHideInMenu(item) || !hasPrivilege(item)) return null
  179. // 过滤name以_开通的菜单 v4.x由filterMenu代替
  180. // if (item.name.indexOf('_') === 0 && props.topMenuOnly !== true) return null
  181. // 获取badge
  182. const badge = getCurrentBadge(item) // props.badgeMap[item.name] ?? item.badge
  183. emit('triggerGetBadge', item.name) // 通知触发
  184. return h(ElMenuItem, {
  185. key: item.name,
  186. index: item.name,
  187. route: item.route,
  188. badge,
  189. onClick: (instance) => {
  190. if (instance.active.value) return
  191. const { route, name, link } = item
  192. if (link) { // link
  193. window.open(link)
  194. const originActiveName = currentActiveName.value
  195. // 赋予不一样的name 不然会导致menu-item处于激活状态
  196. currentActiveName.value = name // Symbol('')
  197. nextTick().then(() => { currentActiveName.value = originActiveName })
  198. return
  199. }
  200. if (route) {
  201. router.push(route)
  202. } else {
  203. router.push({ name })
  204. }
  205. }
  206. }, {
  207. default: () => renderMenuIcon(item, depth),
  208. title: () => [renderMenuItemTitle(item), renderMenuBadge(badge)]
  209. })
  210. }
  211. // 当前激活菜单项名称
  212. const currentActiveName = ref()
  213. // 将当前激活的菜单的children发送给父组件 会修改currentActiveName 和emit activeMenu
  214. const emitActiveChildren = () => {
  215. // 此处的menu需要很完善才能正常使用(需要包含详情页等非展示视图的菜单信息)
  216. // 未进行未匹配处理[注:仅在激活的路由不再navMenu中才会导致未找到]
  217. const isSubAppRoute = isSubApp(route.name)
  218. const menuMatched = isSubAppRoute
  219. ? matchMenuByRoutePath(props.navMenu, route.fullPath)
  220. : matchMenuByRouteName(props.navMenu, route.name)
  221. if (menuMatched) {
  222. // 此处activeName与实际currentActiveName存在区别
  223. const activeName = menuMatched[0].name
  224. const activeMenu = findMenu(props.navMenu, activeName)
  225. // menuMatched存在保证了activeMenu的存在
  226. if ((activeMenu.children || []).length > 0) {
  227. currentActiveName.value = getFirstMenuItem(findMenu(menu.value, activeName))?.name
  228. }
  229. emit('update:activeMenu', activeMenu)
  230. } else {
  231. if (isSubAppRoute) {
  232. const activeName = `_${route.name.replace('Sub', '')}`
  233. const activeMenu = findMenu(props.navMenu, activeName)
  234. // 不能保证activeMenu的存在,故需要判断
  235. if (activeMenu) {
  236. if ((activeMenu.children || []).length > 0) {
  237. currentActiveName.value = getFirstMenuItem(findMenu(menu.value, activeName))?.name
  238. }
  239. emit('update:activeMenu', activeMenu)
  240. } else {
  241. // 不变是否可行
  242. // emit('update:activeMenu', undefined)
  243. }
  244. } else {
  245. emit('update:activeMenu', undefined)
  246. }
  247. }
  248. }
  249. // 如果监听name会出现名字不变化的情况
  250. watch([() => route.fullPath, () => props.navMenu], () => {
  251. // 针对Sub结尾的路径进行特殊处理
  252. if (!/Sub$/.test(route.name)) {
  253. if (currentActiveName.value !== route.name) {
  254. const menuMatch = matchMenuByRouteName(props.navMenu, route.name)
  255. currentActiveName.value = getActiveNameByMatchedMenu(menuMatch) // route.name // 此处也需要处理
  256. }
  257. } else { // 认为是子路由 子路由统一对fullPath进行匹配
  258. // console.warn(`[cip-main-nav]: route.name以Sub结尾的为子应用特有,请确认\`${route.name}\`路由为子应用路由`)
  259. const menuMatched = matchMenuByRoutePath(props.navMenu, route.fullPath)
  260. currentActiveName.value = getActiveNameByMatchedMenu(menuMatched) // .pop().name
  261. }
  262. if (props.topMenuOnly === true) {
  263. emitActiveChildren()
  264. }
  265. }, { immediate: true })
  266. return () => h(ElMenu, {
  267. class: ['cip-main-nav'],
  268. defaultActive: currentActiveName.value,
  269. uniqueOpened: true,
  270. collapse: props.isCollapse,
  271. mode: props.mode,
  272. ellipsis: props.ellipsis
  273. }, { default: () => renderMenu(menu.value) })
  274. }
  275. })