前言
最近在学习Vue3的相关语法,在阅读官方文档的时候觉得官方文档的菜单栏比较简洁美观,于是想着能不能自己实现一个类似的多级菜单。代码大部分由AI所做(感谢活在这个人工智能时代)。
设计
主要就是路由设计以及菜单设计
路由设计
在src/router/intex.js
中进行路由配置,配置的信息示例如下:
import Home from "@/app/Home.vue"
import {createRouter, createWebHistory} from "vue-router";
import Layout from "@/app/Layout.vue";
import Demo1 from "@/app/Demo1.vue";
import Demo2 from "@/app/Demo2.vue";
import Demo3 from "@/app/Demo3.vue";
import About from "@/app/About.vue";
const routes = [
{
path: '/',
name: '首页',
component: Home,
},
{
path: '/demo',
name: '演示',
component: Layout,
children: [
{
path: 'demo1',
name: '演示1',
component: Demo1
},
{
path: 'demo-sub',
name: '演示子菜单1',
component: Layout,
children: [
{
path: 'demo2',
name: '演示2',
component: Demo2
},
{
path: 'demo-sub2',
name: '演示子菜单2',
component: Layout,
children: [
{
path: 'demo3',
name: '演示3',
component: Demo3
}
]
},
]
},
]
},
{
path: '/about',
name: '关于',
component: About,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
各参数解释如下:
- path:跳转的路由信息
- name:菜单项名称
- component:菜单显示页面组件
- children:所包含子路由
菜单设计
菜单由两个组件实现:src/components/MenuDropdown.vue
、src/components/AdvancedMenu.vue
,同时使用递归思想实现多级菜单渲染。
MenuDropdown 组件
递归子菜单组件,负责:
- 渲染子菜单项
- 支持无限层级嵌套
- 处理子菜单的悬停事件
- 提供动画效果
显示的的子菜单,主菜单 AdvancedMenu
组件中调用,如果有子菜单,则显示子菜单下拉框,效果如下图(具体显示位置有css控制):
组件主要代码如下:
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import MenuDropdown from './MenuDropdown.vue'
// 定义props
const props = defineProps({
routes: {
type: Array,
required: true
},
level: {
type: Number,
default: 1
},
parentPath: {
type: String,
default: ''
}
})
const route = useRoute()
const activeSubmenu = ref(null)
// 检查是否有子菜单
const hasChildren = (route) => {
return route.children && route.children.length > 0
}
// 获取完整的路由路径
const getFullPath = (route) => {
if (props.parentPath) {
// 如果有父路径,拼接完整路径
return `${props.parentPath}/${route.path}`
}
return route.path
}
// 检查当前路由是否匹配
const isCurrentRoute = (routePath) => {
const fullPath = getFullPath(routePath)
return route.path === fullPath
}
// 检查当前路由是否属于某个父菜单
const isCurrentRouteInParent = (parentRoute) => {
if (!hasChildren(parentRoute)) return false
const currentPath = route.path
const parentFullPath = getFullPath(parentRoute)
return currentPath.startsWith(parentFullPath)
}
// 处理鼠标进入子菜单
const handleMouseEnter = (route) => {
if (hasChildren(route)) {
activeSubmenu.value = route.name
}
}
// 处理鼠标离开子菜单
const handleMouseLeave = (route) => {
setTimeout(() => {
if (activeSubmenu.value === route.name) {
activeSubmenu.value = null
}
}, 200)
}
</script>
<template>
<div
class="dropdown-menu"
:class="[`level-${level}`]"
>
<ul class="submenu-list">
<li
v-for="route in routes"
:key="route.path"
class="submenu-item"
@mouseenter="handleMouseEnter(route)"
@mouseleave="handleMouseLeave(route)"
>
<!-- 有子菜单的父菜单项 -->
<div
v-if="hasChildren(route)"
class="submenu-link parent-submenu"
:class="{
'active': activeSubmenu === route.name || isCurrentRouteInParent(route)
}"
>
<span class="submenu-text">{{ route.name }}</span>
<span class="submenu-arrow">▶</span>
</div>
<!-- 没有子菜单的菜单项 -->
<router-link
v-else
:to="getFullPath(route)"
class="submenu-link"
:class="{ 'active': isCurrentRoute(route) }"
>
<span class="submenu-text">{{ route.name }}</span>
</router-link>
<!-- 递归渲染子菜单 -->
<Transition name="submenu">
<MenuDropdown
v-if="hasChildren(route) && activeSubmenu === route.name"
:routes="route.children"
:level="level + 1"
:parent-path="getFullPath(route)"
/>
</Transition>
</li>
</ul>
</div>
</template>
AdvancedMenu 组件
主要的菜单组件,负责:
- 读取路由配置
- 渲染顶级菜单项
- 处理鼠标悬停事件
- 管理菜单状态
显示完整的顶部菜单项(不含子菜单),效果如图:
组件主要代码如下:
<script setup>
// 获取路由配置
import {computed, ref} from "vue";
import {useRouter, useRoute} from "vue-router";
import MenuDropdown from "@/components/MenuDropdown.vue";
const router = useRouter()
const route = useRoute()
const activeDropdown = ref(null)
// 获取路由配置
const menuRoutes = computed(() => {
return router.options.routes;
})
// 检查是否有子菜单
const hasChildren = (route) => {
return route.children && route.children.length > 0
}
// 检查当前路由是否属于某个父菜单
const isCurrentRouteInParent = (parentRoute) => {
if (!hasChildren(parentRoute)) return false
// 检查当前路由路径是否以父路由路径开头
const currentPath = route.path
const parentPath = parentRoute.path
// 如果父路径是根路径,需要特殊处理
if (parentPath === '/') {
return currentPath !== '/' && currentPath.startsWith('/')
}
return currentPath.startsWith(parentPath)
}
// 处理鼠标进入主菜单
const handleMouseEnter = (route) => {
if (hasChildren(route)) {
activeDropdown.value = route.name
}
}
// 处理鼠标离开主菜单
const handleMouseLeave = (route) => {
setTimeout(() => {
if (activeDropdown.value === route.name) {
activeDropdown.value = null
}
}, 200)
}
</script>
<template>
<nav class="advanced-menu">
<div class="menu-container">
<ul class="menu-list">
<li
v-for="route in menuRoutes"
:key="route.path"
class="menu-item"
@mouseenter="handleMouseEnter(route)"
@mouseleave="handleMouseLeave(route)"
>
<!-- 有子菜单的父菜单项-->
<div
v-if="hasChildren(route)"
class="menu-link parent-menu"
:class="{
'active': activeDropdown === route.name || isCurrentRouteInParent(route)
}"
>
<span class="menu-text">{{ route.name }}</span>
<span class="dropdown-arrow">▼</span>
</div>
<!-- 没有子菜单的菜单项-->
<router-link
v-else
:to="route.path"
class="menu-link"
active-class="active"
>
<span class="menu-text">{{ route.name }}</span>
</router-link>
<!-- 递归渲染子菜单-->
<Transition name="dropdown">
<MenuDropdown
v-if="hasChildren(route) && activeDropdown === route.name"
:routes="route.children"
:level="1"
:parent-path="route.path"
/>
</Transition>
</li>
</ul>
</div>
</nav>
</template>
递归思路
1、首先在AdvancedMenu
组件中根据配置的路由信息routes
进行循环渲染。如果路由route
没有children
属性即子路由,直接使用router-link渲染即可(可点击);如果存在子路由,使用div渲染(不可点击),并在右侧加入箭头表示有子菜单。
<template>
<nav class="advanced-menu">
<div class="menu-container">
<ul class="menu-list">
<li
v-for="route in menuRoutes"
:key="route.path"
class="menu-item"
@mouseenter="handleMouseEnter(route)"
@mouseleave="handleMouseLeave(route)"
>
<!-- 有子菜单的父菜单项-->
<div
v-if="hasChildren(route)"
class="menu-link parent-menu"
:class="{
'active': activeDropdown === route.name || isCurrentRouteInParent(route)
}"
>
<span class="menu-text">{{ route.name }}</span>
<span class="dropdown-arrow">▼</span>
</div>
<!-- 没有子菜单的菜单项-->
<router-link
v-else
:to="route.path"
class="menu-link"
active-class="active"
>
<span class="menu-text">{{ route.name }}</span>
</router-link>
<!-- 递归渲染子菜单-->
<Transition name="dropdown">
<MenuDropdown
v-if="hasChildren(route) && activeDropdown === route.name"
:routes="route.children"
:level="1"
:parent-path="route.path"
/>
</Transition>
</li>
</ul>
</div>
</nav>
</template>
2、如果在AdvancedMenu
组件中渲染的路由route
有子路由,则将该route.children
信息传递给MenuDropdown
子菜单组件。同时传递该路由的层级level
、路由的地址path
,便于子菜单的渲染和路由的正确跳转。
<!-- 递归渲染子菜单-->
<Transition name="dropdown">
<MenuDropdown
v-if="hasChildren(route) && activeDropdown === route.name"
:routes="route.children"
:level="1"
:parent-path="route.path"
/>
</Transition>
3、MenuDropdown
子菜单组件与AdvancedMenu
主菜单组件类似,根据传来的routes、level、path信息,进行对应的渲染。如果路由route
没有children
属性即子路由,直接使用router-link渲染即可(可点击);如果存在子路由,使用div渲染(不可点击),并在右侧加入箭头表示有子菜单。
<!-- 递归渲染子菜单 -->
<Transition name="submenu">
<MenuDropdown
v-if="hasChildren(route) && activeSubmenu === route.name"
:routes="route.children"
:level="level + 1"
:parent-path="getFullPath(route)"
/>
</Transition>
效果图
总结
本项目基本实现了基于Vue3根据路由配置动态生成多级菜单的功能,且UI仿照Vue官方文档设计简洁美观,动画流畅。不过作者表述能力不佳,具体实现可参考完整代码。
项目完整代码
https://github.com/Seven11111/vue-menu