基于Vue3和Element Plus的递归组件实现多级导航栏功能(示例代码)
作者:ArabySide
前言
在日常工作中,我能经常能碰到那种自引用类似的数据结构。比方说树形的导航栏结构,我们期望Vue渲染时递归遍历组件树,直到叶子节点。一个组件在其自身的模板中调用自身的组件,这便是Vue中的递归组件。
一、递归的意义
想象一个这样的一个对象数组。每个对象有基本的path路径,name名称,label标签,icon图标。这四个属性是必须的,个别对象本身有children属性包含它的子菜单内容。并且children属性本身也是对应一个包含path,name,label,icon四个基本属性和children可选属性的对象。
这样的话我们就构建了一个典型的自引用类似的数据结构,相互之间互相嵌套。相信大家已经发现了,在我们不知道嵌套深度的情况下,是没法通过循环完整解析出全部数据。并且递归的逻辑复用性是优于循环。这样的结果也会使递归在代码居然简洁性,灵活性,方便扩展性。
二、递归组件的实现——基于element-plus UI的多级导航栏
2.1 element-plus Menu菜单官方示例
我们接下来要开发的多级导航栏是基于element-plus的menu菜单组件。观察官方代码结构,组件最由el-menu标签包裹,el-sub-menu为一级菜单,内嵌的el-menu-item为一级菜单的子内容。但el-sub-menu里面也可以继续添加el-sub-menu标签,作为一个二级菜单。实现组件树的结构。
树状导航栏代码如下:
<el-menu> <!-- 最外层菜单容器 -->
├─ <el-sub-menu> <!-- 一级菜单(可展开) -->
│ ├─ 菜单标题(含图标和文本)
│ └─ <el-sub-menu> <!-- 二级菜单(嵌套在一级菜单内) -->
│ ├─ 二级菜单标题
│ └─ <el-menu-item> <!-- 二级菜单子项 -->
├─ <el-menu-item> <!-- 一级菜单子项(非展开项) -->
└─ <el-menu-item> <!-- 一级菜单子项 -->
</el-menu>完整实例代码
<template>
<el-row class="tac">
<el-col :span="12">
<h5 class="mb-2">Default colors</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>2.2 接口定义
定义组件的对象数组结构。path路径,name名称,label标签,icon图标,这四个属性是必须的。个别对象本身有children属性包含它的子菜单内容。
export interface NavTreeMenuItem {
path: string;
name?: string;
label: string;
icon: string;
children?: NavTreeMenuItem[];
}2.3 组件递归
vue3中实现递归组件是组件在其模板中直接调用自身。比如我定义了一个组件名称叫menuTreeItem.vue,我就可以在该vue文件内直接调用menuTreeItem。这是基于这主要归功于Vue 3 单文件组件的编译时处理机制和 < script setup> 的特性。
在menuTreeItem.vue的el-sub-menu节点里判断children属性是否有值,并且没有超过最大递归深度。
值得注意的是,使用递归方法要注意无限递归这个漏洞风险,需要设置最大深度,避免无限递归
menuTreeItem.vue文件片段
<template>
<template v-for="item in treeData" :key="item.path" :index="item.path">
<el-sub-menu v-if="item.children && item.children.length > 0 && currentDepth < maxDepth" :index="item.path">
<!-- -->
<menuTreeItem :tree-data="item.children" :current-depth="currentDepth + 1"
:max-depth="maxDepth"></menuTreeItem>
<!-- -->
</el-sub-menu>
</template>
</template>2.4 父组件封装递归组件
前文我们把需要递归的组件封装成单独子组件,最后嵌套在父组件里,防止不必要的内容被遍历渲染。
<template>
<el-aside :style="{ width: menuWidth }">
<h3 class="mb-2" v-once>后台系统</h3>
<div class="sidebar-menu">
<el-menu default-active="2" class="my-el-menu" @open="handleOpen" @close="handleClose">
<menuTreeItem :tree-data="treeData" :max-depth="5"></menuTreeItem>
</el-menu>
</div>
</el-aside>
</template>三、完整代码——基于element-plus UI的多级导航栏
3.1 组件架构
navigationAside
├─ index.vue <!-- 核心组件 -->
├─ menuTreeItem.vue <!-- 递归组件 -->
└─ types.ts <!-- 接口类型 -->
</el-menu>3.2 types.ts
export interface NavTreeMenuItem {
path: string;
name?: string;
label: string;
icon: string;
url?: string;
children?: NavTreeMenuItem[];
}3.3 menuTreeItem.vue
<template>
<template v-for="item in treeData" :key="item.path" :index="item.path">
<el-sub-menu v-if="item.children && item.children.length > 0 && currentDepth < maxDepth" :index="item.path">
<template #title>
<component class="icons" :is="item.icon"></component>
<span>{{ item.label }}</span>
</template>
<MenuTreeItem :tree-data="item.children" :isRoot="false" :current-depth="currentDepth + 1"
:max-depth="maxDepth"></MenuTreeItem>
</el-sub-menu>
<el-menu-item v-else :index="item.path">
<component class="icons" :is="item.icon"></component>
<span>{{ item.label }}</span>
</el-menu-item>
</template>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const router = useRouter()
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
import { NavTreeMenuItem } from './types'
const props = withDefaults(defineProps<{
treeData: NavTreeMenuItem[];
isRoot?: boolean;
currentDepth?: number; // 当前深度
maxDepth?: number; // 最大深度
}>(), {
isRoot: true,
currentDepth: 1, // 默认当前深度为1
maxDepth: 3 // 默认最大深度为3
})
</script>
<style lang="less" scoped>
:deep(.icons) {
height: 18px;
margin-right: 5px;
width: 18px;
}
</style>3.4 index.vue
<template>
<el-aside :style="{width: menuWidth}">
<h3 v-if="!isCollapse" class="mb-2" v-once>天津城安远传系统</h3>
<h3 v-else class="mb-2" v-once>远传</h3>
<div class="sidebar-menu">
<el-menu default-active="2"
class="my-el-menu"
:collapse="isCollapse"
@open="handleOpen"
@close="handleClose" >
<menuTreeItem :tree-data="treeData" :max-depth="5"></menuTreeItem>
</el-menu>
</div>
</el-aside>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import {useWebSettingDataStore} from '../../stores'
import menuTreeItem from './menuTreeItem.vue'
const router = useRouter()
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
import { NavTreeMenuItem } from './types'
const props = defineProps<{
treeData: NavTreeMenuItem[];
isRoot?: boolean;
}>();
const store = useWebSettingDataStore()
const isCollapse = computed(() => store.state.isCollapse);
const menuWidth = computed(() => store.state.isCollapse ? '64px' : '180px')
const handleOpen = (key: string, keyPath: string[]) => {
console.log("open")
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log("close")
console.log(key, keyPath)
}
</script>
<style lang="less" scoped>
.el-menu {
border-right: none;
height: 100%;
}
.el-aside {
display: flex;
flex-direction: column;
//border-right: 1px solid #e4e7ed;
background-color: #304156;
h3 {
line-height: 13px;
color: #fff;
text-align: center;
flex-shrink: 0;
/* 防止被压缩 */
}
}
.sidebar-menu {
flex: 1;
overflow-y: auto;
padding: 10px 0;
}
/* 可选:美化滚动条 */
.sidebar-menu::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background-color: #bfcbd9;
border-radius: 3px;
}
.my-el-menu{
--el-menu-bg-color: #304156;
--el-menu-text-color: #bfcbd9;
--el-menu-hover-bg-color: #263445;
--el-menu-active-color: #409eff;
}
</style>3.5 组件调用与运行结果
<commNavigationAside :tree-data="navTreeData"></commNavigationAside> import commNavigationAside from "@/components/navigationAside/index.vue"

总结
本文基于 Vue3 和 Element Plus,通过递归组件实现了可动态渲染的多级导航栏,利用自引用数据结构和深度控制避免无限循环,同时结合 TypeScript 规范数据类型并优化组件封装。
到此这篇关于基于Vue3和Element Plus的递归组件实现多级导航栏的文章就介绍到这了,更多相关vue Element Plus导航栏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
