SidebarItem.vue
2.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<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>