123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- import { defineComponent, h, ref, watch, computed, nextTick } from 'vue'
- import { ElMenu, ElSubMenu as ElSubmenu, ElMenuItem, ElTag, ElMenuItemGroup } from 'element-plus'
- import { useRoute, useRouter } from 'vue-router'
- import { isArray, isEmpty } from '@cip/utils/util'
- import { getMenuTitle, matchMenuByRouteName, matchMenuByRoutePath } from '../helper'
- import CipMainIcon from '../cip-main-icon'
- import { filterMenu, getFirstMenuItem, findMenu } from './util'
- import './index.less'
- export default defineComponent({
- name: 'CipMainNav',
- props: {
- navMenu: Array, // 菜单项
- mode: String, // 菜单模式 horizontal-水平(此模式下 isCollapse无效) vertical-垂直
- isCollapse: Boolean, // 是否收缩
- privileges: Array, // 权限
- topMenuOnly: Boolean, // 是否子渲染一级菜单
- badgeMap: Object,
- theme: String,
- ellipsis: Boolean // 自动折叠
- },
- emits: ['update:activeMenu', 'triggerGetBadge'],
- setup (props, { emit }) {
- const router = useRouter()
- const route = useRoute()
- const menu = computed(() => filterMenu(props.navMenu, props.privileges))
- const getDeepChildren = (children) => {
- return children.map(child => {
- if (child.children && child.children.length > 0) {
- return getDeepChildren(child.children).concat(child.name)
- } else {
- return child.name
- }
- }).flat(Infinity).filter(v => v !== undefined)
- }
- // 分析顶层菜单和 子菜单的关系
- const analysisRelationship = computed(() => {
- if (props.topMenuOnly === true) {
- return menu.value.reduce((acc, b) => {
- if (b.children) {
- acc[b.name] = getDeepChildren(b.children)
- }
- return acc
- }, {})
- } else {
- // 非topMenuOnly不进行分析
- return {}
- }
- })
- // 判断是否有可展示子菜单
- const checkChildren = (children) => {
- return isArray(children) && children.length > 0
- }
- // 兼容老版本菜单从路由中获取对_cache开通的路由的特殊处理
- // 处理起始字符串为_cache的父路由
- const checkCacheRoute = (route) => {
- if (/^_cache/.test(route.name)) {
- return route.children
- } else {
- return route
- }
- }
- // 渲染菜单货子菜单的内容[注: 内容包含子菜单和菜单项]
- const renderMenu = (menuContentList, depth = 0) => {
- depth++
- return menuContentList.map(item => {
- const route = checkCacheRoute(item)
- // 如果是数组则调用自身
- if (isArray(route)) {
- return renderMenu(route, depth)
- }
- // 对子模块进行过滤
- if (checkChildren(route.children)) {
- if (route.type === 'group') return renderMenuGroup(route, depth)
- return renderSubmenu(route, depth)
- } else {
- return renderMenuItem(route) // 渲染为 menu-item
- }
- })
- }
- // 根据子菜单项计算子菜单的badge
- const computedSubBadge = (childNodeList) => {
- let count = 0
- childNodeList.forEach(childNode => {
- const badge = childNode?.props?.badge
- if (typeof badge === 'number') {
- count += badge
- }
- })
- if (count > 0) return count
- return undefined
- }
- // 渲染子菜单[注: 如果没有可渲染的菜单项, 将会本身渲染为菜单项]
- const renderSubmenu = (submenu, depth) => {
- // v4.x由filterMenu代替
- if (submenu.children.length > 0) {
- // 子菜单存在需要显示的菜单进行此渲染
- let subBadge
- if (props.topMenuOnly !== true || (props.topMenuOnly && depth > 1)) {
- const childrenVnode = renderMenu(submenu.children, depth + 1) // renderMenu(submenu.children, depth + 1) // 渲染为节点
- if (!submenu.hideBadge) {
- subBadge = !isEmpty(submenu.badge) ? submenu.badge : computedSubBadge(childrenVnode)
- }
- return h(ElSubmenu, {
- popperClass: `cip-menu-popper cip-menu-popper--${props.mode} main-theme--${props.theme}`,
- key: submenu.name,
- index: submenu.name,
- popperOffset: 1,
- badge: subBadge
- }, {
- title: () => [renderMenuIcon(submenu), renderMenuItemTitle(submenu), renderMenuBadge(subBadge)],
- default: () => childrenVnode
- })
- } else {
- // 存在子路由
- // 需要定向到默认选中的第一个
- const firstItem = getFirstMenuItem(submenu)
- const redirectName = firstItem.name
- const redirectRoute = firstItem.route
- const { children, ...item } = submenu
- return renderMenuItem({ ...item, name: redirectName, route: redirectRoute, originName: item.name })
- }
- } else {
- // 其他情况页渲染为item
- return renderMenuItem(submenu)
- }
- }
- // 渲染菜单组
- const renderMenuGroup = (menuGroup, depth) => {
- if (menuGroup.children.length > 0) {
- const childrenVnode = renderMenu(menuGroup.children, depth + 1)
- return h(ElMenuItemGroup, {
- popperClass: `cip-menu-popper cip-menu-popper--${props.mode} main-theme--${props.theme}`,
- key: menuGroup.name,
- index: menuGroup.name
- // badge: subBadge
- }, {
- title: () => [renderMenuIcon(menuGroup), renderMenuItemTitle(menuGroup)],
- default: () => childrenVnode
- })
- }
- }
- // 渲染菜单内容的icon[注: 包含子菜单及菜单项]
- const renderMenuIcon = (menuContent = {}) => {
- const iconName = menuContent.meta?.icon || menuContent.icon
- return <CipMainIcon name={iconName}/>
- }
- const renderMenuBadge = (badge) => {
- if (badge) {
- return <ElTag size={'small'} effect={'dark'} type={'danger'} round class={'cip-menu__badge'}>{badge}</ElTag>
- } else {
- return undefined
- }
- }
- const renderMenuItemTitle = (menuContent) => {
- return <span class={'cip-menu__title'}>{getMenuTitle(menuContent)}</span>
- }
- const getCurrentBadge = (item) => {
- if (props.topMenuOnly && analysisRelationship.value[item.originName]) {
- return analysisRelationship.value[item.originName].reduce((acc, childName) => {
- if (props.badgeMap[childName]) {
- acc += props.badgeMap[childName]
- }
- return acc
- }, 0)
- } else {
- return props.badgeMap[item.name] ?? item.badge
- }
- }
- // 渲染菜单项 [注:隐藏的、没有权限的、以_开头但是不是只渲染一层菜单的模式下的菜单项将不渲染]
- const renderMenuItem = (item) => {
- // 过滤隐藏及权限的菜单 v4.x由filterMenu代替
- // if (isHideInMenu(item) || !hasPrivilege(item)) return null
- // 过滤name以_开通的菜单 v4.x由filterMenu代替
- // if (item.name.indexOf('_') === 0 && props.topMenuOnly !== true) return null
- // 获取badge
- const badge = getCurrentBadge(item) // props.badgeMap[item.name] ?? item.badge
- emit('triggerGetBadge', item.name) // 通知触发
- return h(ElMenuItem, {
- key: item.name,
- index: item.name,
- route: item.route,
- badge,
- onClick: (instance) => {
- if (instance.active.value) return
- const { route, name, link } = item
- if (link) { // link
- window.open(link)
- const originActiveName = currentActiveName.value
- // 赋予不一样的name 不然会导致menu-item处于激活状态
- currentActiveName.value = name // Symbol('')
- nextTick().then(() => { currentActiveName.value = originActiveName })
- return
- }
- if (route) {
- router.push(route)
- // currentActiveName.value = name
- } else {
- router.push({ name })
- }
- }
- }, {
- default: () => renderMenuIcon(item),
- title: () => [renderMenuItemTitle(item), renderMenuBadge(badge)]
- })
- }
- // 当前激活菜单项名称
- const currentActiveName = ref()
- // 将当前激活的菜单的children发送给父组件 会修改currentActiveName 和emit activeMenu
- const emitActiveChildren = () => {
- // 此处的menu需要很完善才能正常使用(需要包含详情页等非展示视图的菜单信息)
- // 未进行未匹配处理[注:仅在激活的路由不再navMenu中才会导致未找到]
- const isSubAppRoute = /Sub$/.test(route.name)
- const menuMatched = isSubAppRoute
- ? matchMenuByRoutePath(props.navMenu, route.fullPath)
- : matchMenuByRouteName(props.navMenu, route.name)
- if (menuMatched) {
- // 此处activeName与实际currentActiveName存在区别
- const activeName = menuMatched[0].name
- const activeMenu = findMenu(props.navMenu, activeName)
- // menuMatched存在保证了activeMenu的存在
- if ((activeMenu.children || []).length > 0) {
- currentActiveName.value = getFirstMenuItem(findMenu(menu.value, activeName))?.name
- }
- emit('update:activeMenu', activeMenu)
- } else {
- if (isSubAppRoute) {
- const activeName = `_${route.name.replace('Sub', '')}`
- const activeMenu = findMenu(props.navMenu, activeName)
- // 不能保证activeMenu的存在,故需要判断
- if (activeMenu) {
- if ((activeMenu.children || []).length > 0) {
- currentActiveName.value = getFirstMenuItem(findMenu(menu.value, activeName))?.name
- }
- emit('update:activeMenu', activeMenu)
- } else {
- emit('update:activeMenu', undefined)
- }
- } else {
- emit('update:activeMenu', undefined)
- }
- }
- }
- // 如果监听name会出现名字不变化的情况
- watch([() => route.fullPath, () => props.navMenu], () => {
- // 针对Sub结尾的路径进行特殊处理
- if (!/Sub$/.test(route.name)) {
- if (currentActiveName.value !== route.name) {
- currentActiveName.value = route.name
- }
- } else { // 认为是子路由 子路由统一对fullPath进行匹配
- // console.warn(`[cip-main-nav]: route.name以Sub结尾的为子应用特有,请确认\`${route.name}\`路由为子应用路由`)
- const menuMatched = matchMenuByRoutePath(props.navMenu, route.fullPath)
- if (menuMatched) {
- currentActiveName.value = menuMatched.pop().name
- } else {
- // 未找到匹配的路由
- currentActiveName.value = ''
- }
- }
- if (props.topMenuOnly === true) {
- emitActiveChildren()
- }
- }, { immediate: true })
- return () => h(ElMenu, {
- class: ['cip-main-nav'],
- defaultActive: currentActiveName.value,
- uniqueOpened: true,
- collapse: props.isCollapse,
- mode: props.mode,
- ellipsis: props.ellipsis
- }, { default: () => renderMenu(menu.value) })
- }
- })
|