当前位置: 首页 > news >正文

例子:vue3+vite+router创建多级导航菜单

第一部分

1、初始化项目

npm init vite@latest

npm run dev :运行项目

q+Enter:退出运行

image

image

 

 

 2、安装路由依赖

npm install vue-router@4  # Vue3 对应 vue-router 4.x 版本

 

第二部分:

创建页面组件

在 src/views/home/analytics 目录下创建两个页面组件:

日统计(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 目录下创建三个页面组件:

概览

<template><div class="page home-page"><h2>首页</h2><p>这是网站首页内容</p></div>
</template><style scoped>
.page {padding: 20px;
}
.home-page {background-color: #f0f9ff;
}
</style>

 

趋势分析

<template><div class="page home-page"><h2>首页</h2><p>这是网站首页内容</p></div>
</template><style scoped>
.page {padding: 20px;
}
.home-page {background-color: #f0fff4;
}
</style>

 

来源分析

<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 目录下创建两个页面组件:

角色配置

<template><div class="page about-page"><h2>角色管理</h2><p>这是角色管理页面内容</p></div>
</template><style scoped>
.page {padding: 20px;
}
.about-page {background-color: #f0f9ff;
}
</style>

 

权限分配

<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 目录下创建一个页面组件:

<template><div class="page about-page"><h2>用户列表</h2><p>这是用户列表页面内容</p></div>
</template><style scoped>
.page {padding: 20px;
}
.about-page {background-color: #fff0f0;
}
</style>

创建导航组件

在 src/components 目录下创建导航栏组件 SubMenuRecursive.vue
<template><ul class="menu-list"><li v-for="item in menuList" :key="item.name" class="menu-item"><!-- 菜单项内容 --><div class="menu-link" :class="{ active: isActive(item) }"@click="handleClick(item)"><span class="icon">{{ item.icon }}</span><span class="text">{{ item.name }}</span><!-- 三级菜单展开箭头:仅 SubMenu 可能有 children --><span class="arrow" v-if="isSubMenu(item) && item.children && item.children.length":class="{ 'arrow-open': isOpen(item) }"></span></div><!-- 递归渲染三级菜单:仅 SubMenu 可能有 children --><template v-if="isSubMenu(item) && item.children && item.children.length"><SubMenuRecursive :menu-list="item.children" :parent-path="getItemPath(item)"v-show="isOpen(item)"/></template></li></ul>
</template><script setup lang="ts">
import { ref } from "vue"; // 删除未使用的 computed
import { useRoute, useRouter } from "vue-router";
import type { SubMenu, ThirdMenu } from "../router/menu";
import SubMenuRecursive from "./SubMenuRecursive.vue"; // 补充自身导入(递归组件需要)// 类型守卫:严格判断是否为 SubMenu(有 children 属性)
const isSubMenu = (item: SubMenu | ThirdMenu): item is SubMenu => {return 'children' in item && Array.isArray(item.children);
};// 接收父组件传入的菜单列表和父路径
const props = defineProps<{menuList: (SubMenu | ThirdMenu)[];parentPath?: string;
}>();const route = useRoute();
const router = useRouter();// 记录展开状态(针对有子菜单的项)
const expandedKeys = ref<string[]>([]);// 判断菜单项是否激活
const isActive = (item: SubMenu | ThirdMenu) => {const itemPath = getItemPath(item);return itemPath && route.path.startsWith(itemPath);
};// 判断子菜单是否展开(仅 SubMenu 有效)
const isOpen = (item: SubMenu | ThirdMenu) => {return isSubMenu(item) && expandedKeys.value.includes(item.name);
};// 获取菜单项的路径(用于路由跳转)
const getItemPath = (item: SubMenu | ThirdMenu) => {if (isSubMenu(item) && item.children && item.children.length > 0) { // 增加 length > 0 判断return item.children[0]!.path; // 此时 children[0] 一定存在
  }return item.path;
};// 点击菜单项
const handleClick = (item: SubMenu | ThirdMenu) => {const itemPath = getItemPath(item);if (itemPath) {router.push(itemPath); // 跳转路由
  }// 若为 SubMenu 且有子菜单,切换展开状态if (isSubMenu(item) && item.children && item.children.length) {const index = expandedKeys.value.indexOf(item.name);if (index > -1) {expandedKeys.value.splice(index, 1);} else {expandedKeys.value.push(item.name);}}
};// 初始化:默认展开当前路由对应的父菜单
const initExpanded = () => {const findParent = (list: (SubMenu | ThirdMenu)[]) => {list.forEach((item) => {// 仅处理 SubMenu 类型if (isSubMenu(item) && item.children && item.children.length) {const hasActiveChild = item.children.some((child) => route.path.startsWith(child.path));if (hasActiveChild) {expandedKeys.value.push(item.name);}findParent(item.children); // 递归查找子菜单
      }});};findParent(props.menuList);
};initExpanded();
</script><style scoped>
/* 样式保持不变 */
.menu-list {list-style: none;padding: 0;margin: 0;
}.menu-item {margin: 2px 0;
}.menu-link {display: flex;align-items: center;gap: 10px;padding: 10px 20px;color: #ecf0f1;cursor: pointer;transition: background-color 0.2s;
}.menu-link:hover, .menu-link.active {background-color: #2c3e50;
}.icon {width: 20px;text-align: center;
}.arrow {margin-left: auto;font-size: 12px;transition: transform 0.2s;
}.arrow-open {transform: rotate(90deg);
}/* 三级菜单缩进 */
.menu-list .menu-list {padding-left: 20px;border-left: 1px dashed #4a6988;
}
</style>

 

在 src/layouts 目录下创建导航栏组件 MainLayout.vue
<template><div class="layout-container"><!-- 顶部导航(一级菜单) --><header class="top-nav"><div class="logo">Admin Panel</div><nav class="main-menu"><router-linkv-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>

配置路由

在 src 目录下创建 router 文件夹,新建 index.ts 和menu.ts路由配置文件:

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)

确保已正确挂载路由(通常初始化项目时已配置,确认即可):
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index' // 导入路由配置

createApp(App).use(router) // 挂载路由.mount('#app')

 

 

http://www.hskmm.com/?act=detail&tid=36709

相关文章:

  • 2025 - Di
  • JVM探究(Leo)
  • Higress v2.1.8:30 项引擎更新 + 4 项控制台更新
  • 2025 年最新推荐!集装箱拖车供应厂家权威榜单重磅发布,全方位解析优质厂家实力助企业选对合作伙伴
  • 实战案例 | 利用山海鲸可视化软件,构建制造业数字孪生监控大屏
  • 权威调研榜单:无线电环形导轨配件生产厂家TOP3榜单好评深度解析
  • 10.22模拟赛总结
  • 从零开始制作操作系统—— 最简单的操作系统内核
  • 【CSP出版 | 最快投稿后一个月见刊 | 检索稳定】2025年艺术、教育与管理国际学术会议(ICAEM 2025)- 第六期
  • 【LeetCode 每日一题】120. 三角形最小路径和——(解法二)自底向上 - 实践
  • HDFS Java api操作-cnblog
  • 电网不平衡条件下DFIG风力发电机动态建模与控制
  • Pandas 深入学习【3】材料标准化处理 StandardScaler
  • C#实现CRC8、CRC16、CRC32校验算法
  • JAVA 开发者入门 AI:基于 JBoltAI 平台快速搭建第一个 AI 应用
  • 2025 年切纸机源头厂家最新推荐榜单:全自动 / 程控 / 大型等设备品牌评测,深度解析大鹏等企业实力
  • 成功案例分享|ArmSoM CM5赋能海洋保育,边缘AI守护鲸豚之声
  • 2025 年最新推荐走心机加工实力厂家排行榜:覆盖航空 / 医疗 / 汽车等多领域优质企业精选 不锈钢零件/高铁零件/精密数控走心机加工厂家推荐
  • 权威调研榜单:简易丝杆模组厂家TOP3榜单好评深度解析
  • Kerberoasting攻击剖析:Active Directory中的密码破解漏洞
  • 千疮百孔的心被恨与悲彻底剥离 Kill my memory 让我将快乐全忘记
  • 速尝鲜!PS 2026 新功能:移除工具 + 神经滤镜
  • KeyShot 2025最新安装包下载及详细安装教程,附永久免费中文安装包 KeyShot2025
  • 复矩阵的QR分解
  • 权威调研榜单:天津全屋定制整体橱柜方案TOP4榜单好评深度解析
  • 别再手动处理琐事了!用Coze搭建AI工作流,我每天白赚2小时
  • 单时段机组组合优化的粒子群算法实现(MATLAB)
  • 谎言 欺骗 鄙夷 如破碎瓦砾铺满地 利用陷害窒息莫名遭受唾骂遗弃
  • Day21-C:\Users\Lenovo\Desktop\note\code\JavaSE\Basic\src\com\Stream-集合框架(stream)
  • 权威调研榜单:湖南张家界旅游团服务TOP3榜单好评深度解析