手摸手创建一个 Vue + Ts 项目(四) —— 完善左侧菜单栏

666 阅读6分钟

系列目录

前言

上一篇中,我们实现了一个最简单的左侧菜单栏,效果如下:

image.png

但还有一些问题存在,例如没有支持点击切换路由、隐藏菜单、图标支持等等,这一篇我们来完善下。

完善菜单

在上面生成的菜单中,有一个问题,迫切的需要解决下。那就是不能够隐藏一些菜单,例如 404 等。先来解决下这个:

隐藏菜单

我们先定义在菜单的元数据中,增加一个属性:hidden,当配置该属性为 true 的时候,则该路由不在菜单中展示。

例如配置 404 页面的路由如下:

import type { RouteRecordRaw } from "vue-router"

const errorRoutes: RouteRecordRaw[] = [
  {
    path: '/404',
    name: 'NotFound',
    meta: {
      title: 'Page Not Found'
      hidden: true
    },
    component: () => import('@/views/error/404.vue')
  },
  // 所有未定义路由,全部重定向到 404
  {
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    hidden: true
  }
]

export default errorRoutes

同理,对 / 根目录路由,同样配置在菜单中隐藏。

修改上面定义的「组装成菜单信息」逻辑如下:

routes.forEach((route: RouteRecordRaw) => {
  if (!route.meta?.hidden) {
    const menuOption: MenuOption = {
      label: route.name,
      key: route.name as string,
    };
    menuOptions.value.push(menuOption);
  }
});

重新刷新页面,效果如下:

image.png

设置点击菜单时切换路由

目前还没有实现点击相应菜单时,切换不同的页面。要想实现该效果,NaiveUI 的 Menu 组件,提供了非常好的支持,可以通过将 label 渲染为 <router-link /> 来改变路由,具体可以查看相应文档:菜单 Menu - Naive UI

这里,我们改一下生成菜单数据的地方 —— useMenu.ts

import { h } from "vue";
import { MenuOption } from "naive-ui";
import { RouteRecordRaw, RouterLink } from "vue-router";

const getMenuOptions = (routes: RouteRecordRaw[]): MenuOption[] => {
  let menuOptions: MenuOption[] = [];
  routes.forEach((route: RouteRecordRaw) => {
    // @ts-ignore
    if (!route.meta?.hidden) {
      const menuOption: MenuOption = {
        label: () => {
          if (route.children && Array.isArray(route.children)) {
            return route.name;
          } else {
            return h(
              RouterLink,
              { to: { name: route.name } },
              { default: () => route.name }
            );
          }
        },
        key: route.name as string,
      };
      if (route.children && route.children.length > 0) {
        menuOption.children = getMenuOptions(route.children);
      }
      menuOptions.push(menuOption);
    }
  });
  return menuOptions;
};

刷新页面之后呢,点击 Table 子菜单,发现并没有跳转过来,这是因为

dashboardtable 路由配置中,因为只有一个页面,父子路由名称一样,只匹配到了父路由。这里暂时把父节点的 name 属性删掉,重新测试,可以正常跳转啦:

image.png

但父级菜单都变成空白啦,这个有两种方式,可以通过修改名称,或者配置当只有一个子菜单时,不显示父菜单来解决。下面我们就实现一下第二种方式。

当只有一个子菜单时不显示父菜单

这里同样还是修改生成菜单数据的地方 —— useMenu.ts 文件,判断,当子菜单只有一个时,直接取子菜单即可。修改为如下:

/**
 * 判断路由是否只有一个子路由
 * @param route  路由
 * @returns  如果该路由只有一个子路由,则返回 true;否则返回 false
 */
const isSingleChildren = (route: RouteRecordRaw): boolean => {
  return route?.children?.length === 1;
};

/**
 * 过滤路由配置中需要在菜单中隐藏的路由
 * @param routes 路由列表
 * @returns 路由列表
 */
const filterHiddenRouter = (routes: RouteRecordRaw[]): RouteRecordRaw[] => {
  return routes.filter((item: RouteRecordRaw) => {
    return !item.meta?.hidden;
  });
};

/**
 * 将路由信息转换为菜单信息
 * @param route  路由信息
 * @returns   菜单信息
 */
const getMenuOption = (route: RouteRecordRaw): MenuOption | undefined => {
  // @ts-ignore
  const routeInfo = isSingleChildren(route) ? route.children[0] : route;
  const menuOption: MenuOption = {
    label: () => {
      if (routeInfo.children && Array.isArray(routeInfo.children)) {
        return routeInfo.name;
      } else {
        return h(
          RouterLink,
          { to: { name: routeInfo.name } },
          { default: () => routeInfo.name }
        );
      }
    },
    key: routeInfo.name as string,
  };
  if (routeInfo.children && routeInfo.children.length > 0) {
    menuOption.children = getMenuOptions(routeInfo.children);
  }
  return menuOption;
};

const getMenuOptions = (routes: RouteRecordRaw[]): MenuOption[] => {
  let menuOptions: MenuOption[] = [];
  filterHiddenRouter(routes).forEach((route: RouteRecordRaw) => {
    // @ts-ignore
    const menuOption = getMenuOption(route);
    if (menuOption) {
      menuOptions.push(menuOption);
    }
  });
  return menuOptions;
};

刷新页面,已经实现效果啦:

image.png

添加菜单Icon

NaiveUI 的 Menu 组件,提供了比较方便的图标实现。并且在文档「菜单 Menu - Naive UI」中也提供了比较详细的示例。

这里参考文档,简单实现一下:

首先,还是在生成菜单数据的地方,先来获取路由配置中的图标属性。

  • composables/useMenu.ts

    import { h, Component } from "vue";
    import { NIcon } from "naive-ui";
    
    const renderIcon = (icon: Component) => {
      return () => h(NIcon, null, { default: () => h(icon) })
    }
    
    const getMenuOption = (route: RouteRecordRaw): MenuOption | undefined => {
      // @ts-ignore
      const routeInfo = isSingleChildren(route) ? route.children[0] : route;
      const menuOption: MenuOption = {
        label: () => {
          if (routeInfo.children && Array.isArray(routeInfo.children)) {
            return routeInfo.name;
          } else {
            return h(
              RouterLink,
              { to: { name: routeInfo.name } },
              { default: () => routeInfo.name }
            );
          }
        },
        key: routeInfo.name as string,
        icon: routeInfo.meta?.icon ? renderIcon(routeInfo.meta?.icon as Component) : undefined
      };
      if (routeInfo.children && routeInfo.children.length > 0) {
        menuOption.children = getMenuOptions(routeInfo.children);
      }
      return menuOption;
    };
    

之后修改路由配置信息,在路由的元数据中增加 icon 属性:

dashboard 的路由配置为例:

import type { RouteRecordRaw } from "vue-router";
import BasicLayout from "@/layouts/BasicLayout.vue";

import { DashboardCustomizeRound } from '@vicons/material'

const dashboardRoutes: RouteRecordRaw[] = [
  {
    path: "/dashboard",
    component: BasicLayout,
    children: [
      {
        path: "",
        name: "Dashboard",
        component: () => import("@/views/dashboard/index.vue"),
        meta: {
          icon: DashboardCustomizeRound
        }
      },
    ],
  },
];

export default dashboardRoutes;

刷新页面:

image.png

这里需要注意哈,路由配置中是 ts 文件,不能够自动导入,所以需要手动导入需要依赖的图标组件。

解决了上面一系列问题后,基本看起来就像一个正常的菜单了,但样式还有一些问题,打开浏览器的「开发者工具」,可以看到高度并没有撑满浏览器,后面我们会解决下这个问题。

image.png

使用 TS 来重新定义路由的 Meta

在前面的编码过程中,我们在路由配置的元数据中,添加了两个属性:hiddenicon,当用到这两个属性时,没有任何提示,其实与 ts 的理念是相违背的,所以这里我们来重新定义下路由的元数据类型。

首先,在 router 文件夹下,添加 type.ts 文件,来定义路由元数据类型和新的路由类型:

  • router/type.ts

    import { RouteRecordRaw } from "vue-router"
    import { Component } from 'vue'
    
    interface RouteRecordMeta {
      hidden?: boolean,
      icon?: Component
    }
    
    // @ts-expect-error
    export interface RouteRecord extends Omit<RouteRecordRaw, 'meta'> {
      name?: string,
      meta?: RouteRecordMeta,
      children?: RouteRecord[]
    }
    

    在这里定义了两个接口类型,分别是路由元数据(RouteRecordMeta)和路由类型(RouteRecord)。

之后使用到 vue-routerRouteRecordRaw 类型的地方,修改为我们刚定义的 RouteRecord。例如 dashboard 路由配置文件中:

import { RouteRecord } from "@/router/type"
import BasicLayout from "@/layouts/BasicLayout.vue";

import { DashboardCustomizeRound } from '@vicons/material'

const dashboardRoutes: RouteRecord[] = [
  {
    path: "/dashboard",
    component: BasicLayout,
    children: [
      {
        path: "",
        name: "Dashboard",
        component: () => import("@/views/dashboard/index.vue"),
        meta: {
          icon: DashboardCustomizeRound,
        }
      },
    ],
  },
];

export default dashboardRoutes;

结语

本篇完善了下之前简陋的左侧菜单,增加了一些特性:隐藏菜单、设置点击菜单时切换路由、当只有一个子菜单时不显示父菜单、菜单icon、使用Ts重定义路由元数据类型。至此,一个基本完善的菜单就完成了,下一篇中,让我们对于布局的样式进行一定的修改,让其看起来更加美观一些。

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