index.js 11 KB

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