likes
comments
collection
share

手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏

作者站长头像
站长
· 阅读数 42

系列目录

前言

在上一篇中,我们学习了从零开始配置路由,已经实现的效果如下:

手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏

这一篇让我们来实现一个左侧菜单栏。

文章所有源码地址:easii-admin-ui: 手摸手创建一个 Vue + Ts 项目源码 (gitee.com)

实现菜单栏

实现一个最简单的左侧菜单栏,主要有以下四步:

  1. 获取所有路由信息,组装成一个树状菜单结构;
  2. 将树状菜单结构应用于菜单组件;
  3. 配置菜单点击切换路由;
  4. 菜单固定在左侧;

当然了,还有很多额外的特性,例如:嵌套菜单、菜单icon、隐藏菜单等等,下面我们来一起实现它。

路由信息组装成树状菜单结构

首先,我们组装成的树状菜单结构,其属性需要与实际要应用的菜单组件相对应,这里我们用到的是 NaiveUI菜单 Menu 组件,通过翻阅文档,可以看到菜单选项中必传的配置属性就两个:labelkey

  • label:「string | (() => VNodeChild)」菜单项的内容
  • key:「string」菜单项的标识符

了解了这个,直接开干:

  1. 获取路由配置

    这一步非常简单,因为在上面的路由章节中,routes.ts 已经能够直接返回所有的路由配置。所以,可以直接导入:

    import routes from '@/router/routes'
    
  2. 组装成菜单信息

    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)组件,可以非常方便地进行常用的页面布局。例如最常见的如下布局:

手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏

左侧为菜单栏,右侧上部分为标题栏,中间是内容,切换路由时,会在该部分渲染,下面是网站的 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>
    

修改路由信息

定义好通用布局组件后,需要将前面定义的 dashboardtable 页面的路由:

  • 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)

测试效果

完成后呢,先来简单测试下,效果如下:

手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏

非常简陋,但已初具雏形。同时发现样式很奇怪,似乎全都挤在了中间。通过浏览器样式面板中查看,原来 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' 这一行也删掉,重新查看页面,看起来正常多了。

手摸手创建一个 Vue + Ts 项目(三) —— 实现一个左侧菜单栏

结语

本文基于 NaiveUI,一步步实现了一个左侧菜单栏,比较简陋,也还有一些问题,但已经具备了一个左侧菜单栏的样子。下一篇,让我们完善下这个菜单栏。

我是「代码笔耕」,致力于打造高效简洁、稳定可靠代码的后端开发。 由于不是专业的前端开发,也是通过写这一系列的文章,来提升巩固下自己的水平。 本文可能存在纰漏或错误,如有问题欢迎指正,感谢您阅读这篇文章,如果觉得还行的话,不要忘记点赞、评论、收藏喔! 最后欢迎大家关注我的公众号「代码笔耕」和开源项目:easii (easii) - Gitee.com

转载自:https://juejin.cn/post/7239127302713016376
评论
请登录