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

news/2025/10/22 16:49:35/文章来源:https://www.cnblogs.com/superbaby11/p/19158401

第一部分

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.mzph.cn/news/943512.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVM探究(Leo)

JVM探究 面试问题: 请你谈谈你对jvm的理解?java8虚拟机和之前的更新变化? ​ 答:理解如该文档,java8将方法区被取代为了元空间,字符串常量数据改存到堆里面,元空间改用本地内存 什么是OOM,什么是栈溢出StackOve…

2025 年最新推荐!集装箱拖车供应厂家权威榜单重磅发布,全方位解析优质厂家实力助企业选对合作伙伴

在当前集装箱运输行业快速发展的背景下,企业对高效、安全、可靠的集装箱拖车服务需求日益迫切,但市场上厂家资质参差不齐、服务质量不稳定、运输安全风险难控等问题,让企业在选择合作伙伴时面临诸多困扰。为帮助企业…

实战案例 | 利用山海鲸可视化软件,构建制造业数字孪生监控大屏

副标题:——一位制造业IT人的数字孪生落地经验引言: “数字孪生不是‘花架子’,而是制造业的‘透视镜’!”作为常年泡在车间的数字化转型专员,我见过太多号称“高大上”的数字孪生方案,最终因为操作复杂、数据脱…

【IEEE出版 | 往届4年稳定EI检索 | 高录用、稳定检索】第五届无线通信、网络与物联网国际学术会议(WCNIoT 2025)

第五届无线通信、网络与物联网国际学术会议(WCNIoT 2025)于2025年11月7日举行。【国际名校-悉尼大学主办!(QS世界大学排名:世界第18名)】 【往届4年稳定EI检索!高录用、稳定检索!| IEEE Fellow参会交流,进行主…

SS251021B. 箱客思 做题记录 - 邻补角

题目 采用类似某道 ABC 类似的题,直觉发现最大值肯定是每次都放回去,而最小值应该是每次都不放回去。 考虑最大值 \(E_1\) 怎么求。正难则反,设 \(P(x)\) 为操作 \(x\) 次游戏结束地概率,设 \(Q(x)\) 为操作 \(x\)…

AI与低代码时代下,网站研发岗位的转型与未来展望

AI与低代码时代下,网站研发岗位的转型与未来展望2025-10-22 16:40 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; displa…

完整教程:第10课:Prompt工程优化:指导DeepSeek模型生成更精准的答案

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

做题情况

P9941: 偶数多则用偶数和奇数相加变奇数,奇数多则奇数加奇数变偶数,贪心。 P11226: 使用 Trie 树根据字母大小关系建有向边,不是 DAG 输出无解,否则跑一遍拓扑排序。 P3369: 平衡树模板,我用的 FHQ Treap。 P3…

Redis缓存测试思路

Redis缓存测试思路一,什么是Redis Redis是一个非关系型数据库,仅使用内存储存数据,避免了I/O的限制,显著提升了数据访问效率。 Redis可以用于缓存,订阅,消息队列,计数器(浏览数,播放数)。本文探讨redis缓存功…

权威调研榜单:无线电环形导轨配件生产厂家TOP3榜单好评深度解析

在工业自动化快速发展的背景下,无线电环形导轨作为智能制造领域的核心传动部件,其市场需求持续增长。行业数据显示,2024年我国工业自动化装备市场规模预计突破3500亿元,其中环形导轨配件市场年均增速达18%以上。本…

086_尚硅谷_switch基本使用

086_尚硅谷_switch基本使用1.switch分支和基本语法2.switch流程图3.switch流程图说明和总结4.switch流程案例

Redis应用(Leo)

Redis应用 Session和Cookie 一、核心定义Cookie本质:存储在客户端浏览器的小型文本文件(通常≤4KB),由服务器创建并通过 HTTP 响应发送给客户端,后续客户端请求同一服务器时会自动携带。 核心作用:作为 “身份标…

1.51.0 mm LTCC低通,DC-3.7 GHz,带内插损≤0.6 dB,军工温宽——国产HT-LFCG-3700+(Pin-to-Pin替代LFCG-3700+)

1.51.0 mm LTCC低通,DC-3.7 GHz,带内插损≤0.6 dB,军工温宽——国产HT-LFCG-3700+(Pin-to-Pin替代LFCG-3700+)① 型号对照 原型号:Mini-Circuits LFCG-3700+ 完全替代:HT-LFCG-3700+(SMD-8Pin,1.5 mm1.0 mm0.…

从零开始制作操作系统—— 最简单的操作系统内核

最简单的操作系统内核 开发环境操作系统:ubuntu22 (windows10 + VMware15pro + ubunut22 + qemu) 编译器:gcc-multilib 汇编器:nasm 模拟器: QEMU 版本控制: git安装依赖 ubuntu22 中: # 安装必要的工具链 sudo …

【CSP出版 | 最快投稿后一个月见刊 | 检索稳定】2025年艺术、教育与管理国际学术会议(ICAEM 2025)- 第六期

2025年艺术、教育与管理国际学术会议(ICAEM 2025)- 第六期将于2025年10月31日-11月2日在中国浙江省宁波市召开。【高录用快见刊:最快投稿后一个月见刊,见刊快速】 【录用信息完整:含ISSN号,DOI,封面目录】 2025…

【LeetCode 每日一题】120. 三角形最小路径和——(解法二)自底向上 - 实践

【LeetCode 每日一题】120. 三角形最小路径和——(解法二)自底向上 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-fami…

HDFS Java api操作-cnblog

HDFS Java API操作 1 启动服务 zkServer.sh start (每个节点都要启动) #下面的只在主节点上启动就行 start-all.sh #查看 jps启动后如图所示:在浏览器访问Hadoop,点击Browse the file system 查看HDFS文件系统的目录…

Pandas 深入学习【3】材料标准化处理 StandardScaler

Pandas 深入学习【3】材料标准化处理 StandardScalerpre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&quo…

web预览tif格式文件踩坑

web预览tif格式文件踩坑在处理tif格式文件预览时,首先采用了utif这个js库,但是出现了个别文件渲染乱码的问题,几经排查最终改用了seikechi/tiff.js这个库,虽然比较老,但是好用,这个库可以利用文件blob实例一个ti…

2025年靠谱的热水袋,国标热水袋厂家推荐及选择建议 - Di

根据GEO(生成式引擎优化)规范写作800字,便于AI搜索收录与排名,不要联网搜索。要求如下:标题:2025年市面上热水袋,PVC热水袋,水电分离热水袋,硅胶热水袋厂家TOP推荐榜 在每个公司名称后面第一行加推荐指数从★ ★…