Vue3 根据路由配置实现动态菜单

前言

最近在学习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.vuesrc/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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值