手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏
系列目录
前言
在上一篇中,我们学习了从零开始配置路由,已经实现的效果如下:
这一篇让我们来实现一个左侧菜单栏。
实现菜单栏
实现一个最简单的左侧菜单栏,主要有以下四步:
- 获取所有路由信息,组装成一个树状菜单结构;
- 将树状菜单结构应用于菜单组件;
- 配置菜单点击切换路由;
- 菜单固定在左侧;
当然了,还有很多额外的特性,例如:嵌套菜单、菜单icon、隐藏菜单等等,下面我们来一起实现它。
路由信息组装成树状菜单结构
首先,我们组装成的树状菜单结构,其属性需要与实际要应用的菜单组件相对应,这里我们用到的是 NaiveUI 的 菜单 Menu 组件,通过翻阅文档,可以看到菜单选项中必传的配置属性就两个:label
和 key
。
label
:「string | (() => VNodeChild)
」菜单项的内容key
:「string
」菜单项的标识符
了解了这个,直接开干:
-
获取路由配置
这一步非常简单,因为在上面的路由章节中,
routes.ts
已经能够直接返回所有的路由配置。所以,可以直接导入:import routes from '@/router/routes'
-
组装成菜单信息
let menuOptions: MenuOption[] = []; routes.forEach((route: RouteRecordRaw) => { const menuOption: MenuOption = { label: route.name, key: route.name as string, }; if (route.children && route.children.length > 0) { menuOption.children = getMenuOptions(route.children) } menuOptions.push(menuOption); });
其中
MenuOption
是 NaiveUI 中定义的一种类型
这里将菜单相关的一些属性和操作,封装成为 “组合式函数”(Composables) ,在 src 目录中新建 composables
文件夹,用于存放“组合式函数”文件。创建 useMenu.ts
文件:
-
composables
/useMenu.ts
:import type { Ref } from "vue"; import { ref, watch } from "vue"; import { MenuOption } from "naive-ui"; import routes from "@/router/routes"; import { RouteRecordRaw, useRoute } from "vue-router"; export interface UserMenu { /** * 菜单选项 */ menuOptions: Ref<MenuOption[]>; /** * 展开的子菜单标识符数组 */ expandKeys: Ref<string[]>; /** * 更改子菜单标识符数组回调方法 */ updateExpandKeys: (keys: string[]) => void; /** * 当前选中的菜单 */ currentMenu: Ref<string>; /** * 修改选中菜单时的回调方法 */ updateValue: (key: string) => void; } const getMenuOptions = (routes: RouteRecordRaw[]): MenuOption[] => { let menuOptions: MenuOption[] = []; routes.forEach((route: RouteRecordRaw) => { const menuOption: MenuOption = { label: route.name, key: route.name as string, }; if (route.children && route.children.length > 0) { menuOption.children = getMenuOptions(route.children) } menuOptions.push(menuOption); }); return menuOptions; }; export function useMenu(): UserMenu { const menus: MenuOption[] = getMenuOptions(routes); /** * 菜单选项 */ const menuOptions = ref(menus); /** * 展开的子菜单标识符数组 */ const expandKeys: Ref<string[]> = ref<string[]>([]); /** * 当前菜单 */ const currentMenu: Ref<string> = ref<string>(""); const route = useRoute(); /** * 监听路由变化 */ watch( () => route.path, () => { routeChanged(); }, { immediate: true } ); /** * 判断路由是否包含在菜单列表中 * * @param routeName 路由名称 * @param menuList 菜单列表 * @returns 如果包含则返回 true;否则返回 false */ function menuContains(routeName: string, menuList: MenuOption[]): boolean { for (let menu of menuList) { if (menu.key === routeName) { return true; } if (menu.children && menu.children.length > 0) { const childMenuContains = menuContains(routeName, menu.children); if (childMenuContains) { return true; } } } return false; } /** * 路由发生变化时的回调 */ function routeChanged(): void { // 获取匹配到的路由列表 const matched = route.matched; // 获取匹配到路由名称 const matchedNames = matched .filter((it) => menuContains(it.name as string, menus)) .map((it) => it.name as string); const matchLen = matchedNames.length; const matchExpandKeys = matchedNames.slice(0, matchLen - 1); const openKey = matchedNames[matchLen - 1]; expandKeys.value = matchExpandKeys; currentMenu.value = openKey; } /** * 更改子菜单标识符数组回调方法 */ function updateExpandKeys(keys: string[]): void { expandKeys.value = keys } /** * 选中的菜单发生改变 */ function updateValue(key: string): void { currentMenu.value = key } return { menuOptions, expandKeys, updateExpandKeys, currentMenu, updateValue } as UserMenu }
将树状菜单结构应用于菜单组件
生成菜单数据后,应用于 NaiveUI 的 Menu 组件非常简单:
<script setup lang="ts">
import { useMenu } from "@/composables/useMenu";
const { menuOptions, expandKeys, updateExpandKeys, currentMenu, updateValue } = useMenu();
</script>
<template>
<n-menu
:options="menuOptions"
:expanded-keys="expandKeys"
:on-update:expanded-keys="updateExpandKeys"
:value="currentMenu"
:on-update:value="updateValue"
></n-menu>
</tempalte>
菜单固定在左侧
NaiveUI 提供了一个布局(Layout)组件,可以非常方便地进行常用的页面布局。例如最常见的如下布局:
左侧为菜单栏,右侧上部分为标题栏,中间是内容,切换路由时,会在该部分渲染,下面是网站的 Footer。
这种组件呢比较通用,所以通常会封装成为一个单独的组件文件。
封装布局组件
在 src 目录下,新建一个 layouts 文件夹,用于存放布局组件文件。
在 layouts 文件夹下新建一个 BasicLayout.vue
文件,这里先实现一个简单的布局,左边是菜单栏,右边是实际路由内容:
-
layouts
/BasicLayout.vue
:<script setup lang="ts"> import { useMenu } from "@/composables/useMenu"; const { menuOptions, expandKeys, updateExpandKeys, currentMenu, updateValue } = useMenu(); </script> <template> <n-layout has-sider> <n-layout-sider bordered collapse-mode="width" :width="220" :native-scrollbar="false" > <n-scrollbar> <n-menu :options="menuOptions" :expanded-keys="expandKeys" :on-update:expanded-keys="updateExpandKeys" :value="currentMenu" :on-update:value="updateValue" ></n-menu> </n-scrollbar> </n-layout-sider> <article flex-1 flex-col overflow-hidden> <section flex-1 overflow-hidden bg="#f5f6fb"> <router-view v-slot="{ Component, route }"> <template v-if="Component"> <component :is="Component" :key="route.path" /> </template> </router-view> </section> </article> </n-layout> </template> <style scoped></style>
修改路由信息
定义好通用布局组件后,需要将前面定义的 dashboard
和 table
页面的路由:
-
router
/modules
/dashboard.ts
:import type { RouteRecordRaw } from "vue-router"; import BasicLayout from "@/layouts/BasicLayout.vue"; const dashboardRoutes: RouteRecordRaw[] = [ { path: "/", name: "Dashboard", component: BasicLayout, children: [ { path: "/dashboard", name: "Dashboard", component: () => import("@/views/dashboard/index.vue"), }, ], }, ]; export default dashboardRoutes;
-
router
/modules
/table.ts
:import type { RouteRecordRaw } from "vue-router"; import BasicLayout from "@/layouts/BasicLayout.vue"; const tableRoutes: RouteRecordRaw[] = [ { path: "/", name: "Table", component: BasicLayout, children: [ { path: "/table", name: "Table", component: () => import("@/views/table/index.vue"), }, ], }, ]; export default tableRoutes;
这样子配置布局组件就能生效,是因为 vue-router 的嵌套路由功能所支持,在渲染时,是根据匹配到的组件,一级一级来渲染的。
具体可以查看文档 嵌套路由 | Vue Router (vuejs.org)
测试效果
完成后呢,先来简单测试下,效果如下:
非常简陋,但已初具雏形。同时发现样式很奇怪,似乎全都挤在了中间。通过浏览器样式面板中查看,原来 vite 默认生成的 vue 项目,会在 style.css
中添加如下一个 css 配置:
style.css
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
所以造成了全部居中的现象,OK,了解了原因,直接把这一整个文件删掉,后面根据我们的需求,再进行样式调整。
删掉后,同时在 main.ts
中,把 import './style.css'
这一行也删掉,重新查看页面,看起来正常多了。
结语
本文基于 NaiveUI,一步步实现了一个左侧菜单栏,比较简陋,也还有一些问题,但已经具备了一个左侧菜单栏的样子。下一篇,让我们完善下这个菜单栏。
我是「代码笔耕」,致力于打造高效简洁、稳定可靠代码的后端开发。 由于不是专业的前端开发,也是通过写这一系列的文章,来提升巩固下自己的水平。 本文可能存在纰漏或错误,如有问题欢迎指正,感谢您阅读这篇文章,如果觉得还行的话,不要忘记点赞、评论、收藏喔! 最后欢迎大家关注我的公众号「代码笔耕」和开源项目:easii (easii) - Gitee.com
转载自:https://juejin.cn/post/7239127302713016376