SidebarItem.vue 2.19 KB
<script setup lang="ts">
import { isExternal } from '@/utils'
import type { MenuVO } from '@/types'

/**
 * 递归渲染一个菜单节点
 *
 * 规则(对应后端下发的菜单树):
 *  - visible=false 不渲染
 *  - 有 children → el-sub-menu(目录,不跳转)
 *  - 无 children 且 component 为空 → 空目录,跳过
 *  - 叶子节点 → el-menu-item,path 交给 el-menu 的 router 模式跳转;外链单独开窗
 *
 * ⚠️ 这里刻意不把 el-menu-item 包在 router-link 里:那会渲染成 <a><li> 的非法嵌套,
 *    破坏菜单的 flex 布局。内链交给 el-menu 的 router 模式,外链用点击事件。
 */
const props = defineProps<{
  item: MenuVO
  /** 父级路径,用于把子菜单的相对 path 拼成绝对路径 */
  basePath?: string
}>()

/** 把相对 path 拼成绝对路径(后端下发的多为绝对路径,这里做兼容) */
function resolvePath(item: MenuVO, basePath = ''): string {
  const p = item.path || ''
  if (!p) return basePath || '/'
  if (p.startsWith('/') || isExternal(p)) return p
  return `${(basePath || '').replace(/\/+$/, '')}/${p}`
}

const fullPath = computed(() => resolvePath(props.item, props.basePath || ''))

function handleClick() {
  if (isExternal(fullPath.value)) {
    window.open(fullPath.value, '_blank', 'noopener')
  }
}
</script>

<template>
  <template v-if="item.visible !== false">
    <!-- 目录 -->
    <el-sub-menu v-if="item.children && item.children.length" :index="fullPath">
      <template #title>
        <Icon v-if="item.icon" :icon="item.icon" :size="18" />
        <span class="sidebar-title">{{ item.name }}</span>
      </template>
      <SidebarItem
        v-for="child in item.children"
        :key="child.id"
        :item="child"
        :base-path="fullPath"
      />
    </el-sub-menu>

    <!-- 叶子:可跳转页面 / 外链 -->
    <el-menu-item v-else-if="item.path" :index="fullPath" @click="handleClick">
      <Icon v-if="item.icon" :icon="item.icon" :size="18" />
      <template #title>
        <span class="sidebar-title">{{ item.name }}</span>
      </template>
    </el-menu-item>
  </template>
</template>

<style scoped lang="scss">
.sidebar-title {
  margin-left: 8px;
}
</style>