例子:vue3+vite+router创建多级导航菜单
第一部分
1、初始化项目
npm init vite@latest
npm run dev :运行项目
q+Enter:退出运行


2、安装路由依赖
npm install vue-router@4 # Vue3 对应 vue-router 4.x 版本
第二部分:
创建页面组件
日统计(daily.vue)
<template> <div class="page home-page"> <h2>首页</h2> <p>这是网站首页内容</p> </div> </template> <style scoped> .page { padding: 20px; } .home-page { background-color: #f0f9ff; } </style>
周统计(weekly.vue)
<template> <div class="page home-page"> <h2>首页</h2> <p>这是网站首页内容</p> </div> </template> <style scoped> .page { padding: 20px; } .home-page { background-color: #f0fff4; } </style>
在 src/views/home/dashboard 目录下创建三个页面组件:
概览 overview.vue
<template> <div class="page home-page"> <h2>首页</h2> <p>这是网站首页内容</p> </div> </template> <style scoped> .page { padding: 20px; } .home-page { background-color: #f0f9ff; } </style>
趋势分析 trend.vue
<template> <div class="page home-page"> <h2>首页</h2> <p>这是网站首页内容</p> </div> </template> <style scoped> .page { padding: 20px; } .home-page { background-color: #f0fff4; } </style>
来源分析 source.vue
<template> <div class="page home-page"> <h2>首页</h2> <p>这是网站首页内容</p> </div> </template> <style scoped> .page { padding: 20px; } .home-page { background-color: #f0fff4; } </style>
在 src/views/user/permissions 目录下创建两个页面组件:
角色配置 roles.vue
<template> <div class="page about-page"> <h2>角色管理</h2> <p>这是角色管理页面内容</p> </div> </template> <style scoped> .page { padding: 20px; } .about-page { background-color: #f0f9ff; } </style>
权限分配 assign.vue
<template> <div class="page about-page"> <h2>权限设置</h2> <p>这是权限设置页面内容</p> </div> </template> <style scoped> .page { padding: 20px; } .about-page { background-color: #f0fff4; } </style>
在 src/views/user 目录下创建一个页面组件:
用户列表 list.vue
<template> <div class="page about-page"> <h2>用户列表</h2> <p>这是用户列表页面内容</p> </div> </template> <style scoped> .page { padding: 20px; } .about-page { background-color: #fff0f0; } </style>
创建导航组件
<template> <div class="layout-container"> <!-- 顶部导航(一级菜单) --> <header class="top-nav"> <div class="logo">Admin Panel</div> <nav class="main-menu"> <router-link v-for="menu in mainMenus" :key="menu.path" :to="menu.path" class="main-menu-item" :class="{ active: isMainMenuActive(menu) }" > <span class="icon">{{ menu.icon }}</span> <span class="text">{{ menu.name }}</span> </router-link> </nav> </header> <div class="content-wrapper"> <!-- 侧边栏(二级/三级菜单) --> <aside class="sidebar"> <div class="sidebar-header"> <h3>{{ currentMainMenu?.name }} 菜单</h3> </div> <nav class="sub-menu"> <!-- 递归渲染二级/三级菜单 --> <SubMenuRecursive :menu-list="currentSubMenus" /> </nav> </aside> <!-- 主内容区 --> <main class="main-content"> <router-view /> </main> </div> </div> </template> <script setup lang="ts"> import { ref, watch } from "vue"; import { useRoute } from "vue-router"; import type { MainMenu, SubMenu } from "../router/menu"; import { mainMenus } from "../router/menu"; import SubMenuRecursive from "../components/SubMenuRecursive.vue"; const route = useRoute(); // 先获取第一个菜单,不存在则用默认值 const firstMenu = mainMenus[0] || { name: "", path: "", subMenus: [] } as MainMenu; // 用 firstMenu 初始化,避免 undefined const currentMainMenu = ref<MainMenu | null>(firstMenu); const currentSubMenus = ref<SubMenu[]>(firstMenu.subMenus); // 检查一级菜单是否激活 const isMainMenuActive = (menu: MainMenu) => { return route.path.startsWith(menu.path); }; // 路由变化时更新当前菜单 watch( () => route.path, (newPath) => { const matchedMainMenu = mainMenus.find((menu) => newPath.startsWith(menu.path) ); if (matchedMainMenu) { currentMainMenu.value = matchedMainMenu; currentSubMenus.value = matchedMainMenu.subMenus; } }, { immediate: true } ); </script> <style scoped> .layout-container { display: flex; flex-direction: column; min-height: 100vh; color: #333; } /* 顶部导航 */ .top-nav { display: flex; align-items: center; height: 60px; background-color: #2c3e50; color: white; padding: 0 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .logo { font-size: 1.2rem; font-weight: bold; margin-right: 30px; } .main-menu { display: flex; gap: 2px; } .main-menu-item { display: flex; align-items: center; gap: 8px; padding: 0 15px; height: 60px; color: #ecf0f1; text-decoration: none; transition: background-color 0.2s; } .main-menu-item:hover, .main-menu-item.active { background-color: #34495e; } /* 内容区 */ .content-wrapper { display: flex; flex: 1; } /* 侧边栏 */ .sidebar { width: 220px; background-color: #34495e; color: white; padding: 20px 0; } .sidebar-header { padding: 0 20px 15px; border-bottom: 1px solid #2c3e50; margin-bottom: 15px; } .sidebar-header h3 { font-size: 0.9rem; color: #bdc3c7; margin: 0; } /* 主内容区 */ .main-content { flex: 1; padding: 20px; background-color: #f5f7fa; overflow-y: auto; } </style>
配置路由
menu.ts
// 1. 三级菜单接口(直接导出) export interface ThirdMenu { name: string; path: string; icon?: string; } // 2. 二级菜单接口(直接导出) export interface SubMenu { name: string; path?: string; icon?: string; children?: ThirdMenu[]; } // 3. 一级菜单接口(直接导出) export interface MainMenu { name: string; path: string; icon?: string; subMenus: SubMenu[]; } // 4. 菜单数据(直接使用接口类型标注) export const mainMenus: MainMenu[] = [ { name: "首页", path: "/home", icon: "📊", subMenus: [ { name: "数据中心", icon: "📈", children: [ { name: "概览", path: "/home/dashboard/overview" }, { name: "趋势分析", path: "/home/dashboard/trend" }, { name: "来源分布", path: "/home/dashboard/source" } ] }, { name: "访问统计", icon: "📉", children: [ { name: "日统计", path: "/home/analytics/daily" }, { name: "周统计", path: "/home/analytics/weekly" } ] } ] }, { name: "用户管理", path: "/user", icon: "👥", subMenus: [ { name: "用户列表", path: "/user/list", icon: "👤" }, { name: "权限管理", icon: "🔑", children: [ { name: "角色配置", path: "/user/permissions/roles" }, { name: "权限分配", path: "/user/permissions/assign" } ] } ] } ];
index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
import Layout from "../layouts/MainLayout.vue";
import type { MainMenu, SubMenu, ThirdMenu } from "./menu";
import {mainMenus} from "./menu";
// 导入所有视图组件(非动态导入方式)
import HomeOverview from "../views/home/dashboard/overview.vue";
import HomeTrend from "../views/home/dashboard/trend.vue";
import HomeSource from "../views/home/dashboard/source.vue";
import AnalyticsDaily from "../views/home/analytics/daily.vue";
import AnalyticsWeekly from "../views/home/analytics/weekly.vue";
import UserList from "../views/user/list.vue";
import RolesConfig from "../views/user/permissions/roles.vue";
import PermissionsAssign from "../views/user/permissions/assign.vue";
// 组件映射表(路径 -> 组件)
const componentMap: Record<string, any> = {
  "/home/dashboard/overview": HomeOverview,
  "/home/dashboard/trend": HomeTrend,
  "/home/dashboard/source": HomeSource,
  "/home/analytics/daily": AnalyticsDaily,
  "/home/analytics/weekly": AnalyticsWeekly,
  "/user/list": UserList,
  "/user/permissions/roles": RolesConfig,
  "/user/permissions/assign": PermissionsAssign,
};
// 递归生成路由(支持三级菜单)
const generateRoutes = (): RouteRecordRaw[] => {
  const routes: RouteRecordRaw[] = [
    {
      path: "/",
      component: Layout,
      children: [],
    },
  ];
  // 处理一级菜单
  mainMenus.forEach((mainMenu: MainMenu) => {
    // 一级菜单默认重定向到第一个三级菜单
    const firstSub = mainMenu.subMenus[0]!;
    const firstThird = firstSub.children?.[0] || firstSub;
    routes[0]!.children!.push({
      path: mainMenu.path,
      redirect: firstThird.path!,
    });
    // 处理二级和三级菜单
    mainMenu.subMenus.forEach((subMenu: SubMenu) => {
      // 若二级菜单有三级菜单,生成二级路由组
      if (subMenu.children && subMenu.children.length > 0) {
        subMenu.children.forEach((thirdMenu: ThirdMenu) => {
          routes[0]!.children!.push({
            path: thirdMenu.path!,
            component: componentMap[thirdMenu.path!],
          });
        });
      } else if (subMenu.path) {
        // 若二级菜单无三级菜单,直接生成路由
        routes[0]!.children!.push({
          path: subMenu.path,
          component: componentMap[subMenu.path],
        });
      }
    });
  });
  return routes;
};
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: generateRoutes(),
});
export default router;
修改根组件(App.vue)
<template> <router-view /> </template> <script setup lang="ts"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; } body { background-color: #f5f7fa; } </style>
配置入口文件(main.ts)
清空项目自带的src/style.css文件,执行 npm run dev 启动项目,访问 http://localhost:5173 会看到: 

项目笔记
1、index.ts有如下代码,导入时,什么时候用type修饰?
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
import Layout from "../layouts/MainLayout.vue";
import type { MainMenu, SubMenu, ThirdMenu } from "./menu";
import {mainMenus} from "./menu";
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"
import type { MainMenu, SubMenu, ThirdMenu } from "./menu"
import { mainMenus } from "./menu"
2、index.ts中有如下代码,children的作用是什么?
const routes: RouteRecordRaw[] = [ { path: "/", component: Layout, children: [], }, ];
1. children 的本质:嵌套路由容器
2. children 的核心作用:实现 “布局复用 + 页面嵌套”
3. 为什么根路由需要 children?
总结
3、index.ts中,有箭头函数如下,其用法是?
// 递归生成路由(支持三级菜单) const generateRoutes = (): RouteRecordRaw[] => {
箭头函数定义
// 明确参数和返回值类型 const sum = (a: number, b: number): number => a + b; // 无返回值(void) const greet = (name: string): void => { console.log(`Hello, ${name}`); };
4、index.ts中,有回调函数如下,其用法是?
mainMenus.forEach((mainMenu: MainMenu) => { // 一级菜单默认重定向到第一个三级菜单 const firstSub = mainMenu.subMenus[0]!; const firstThird = firstSub.children?.[0] || firstSub; routes[0]!.children!.push({ path: mainMenu.path, redirect: firstThird.path!, });
回调函数的核心用法:“将函数作为参数传递给另一个函数,由另一个函数在合适时机执行”
一、forEach 中的回调函数:最典型的回调场景
二、代码中回调函数的具体使用分析
1. 第一层:遍历一级菜单的回调函数
mainMenus.forEach((mainMenu: MainMenu) => { // 处理当前一级菜单的逻辑... });
2. 第二层:遍历二级菜单的回调函数
mainMenu.subMenus.forEach((subMenu: SubMenu) => { // 处理当前二级菜单的逻辑... });
3. 第三层:遍历三级菜单的回调函数
subMenu.children.forEach((thirdMenu: ThirdMenu) => { // 处理当前三级菜单的逻辑... });
5、SubMenuRecursive.vue组件中,有如下用法,是什么作用?
const isSubMenu = (item: SubMenu | ThirdMenu): item is SubMenu => { return 'children' in item && Array.isArray(item.children); };
1. 函数定义与参数
2. 判断逻辑
3. 作用与意义
举个例子
if (isSubMenu(item)) { // 这里 TypeScript 会自动推断 item 为 SubMenu 类型,可以安全访问 item.children console.log(item.children); } else { // 这里 item 会被推断为 ThirdMenu 类型,无需处理 children console.log(item.path); }
这是 TypeScript 中处理联合类型的常用技巧,既保证了类型安全,又增强了代码的可读性和可维护性。
6、
7、
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号