Skip to content

动态 Menu 菜单处理方案

规则

规则

  1. 对于单个路由规则而言(循环):
    1. 如果meta && meta.title && meta.icon :则显示在 menu 菜单中,其中 title 为显示的内容,icon 为显示的图标
      1. 如果存在 children :则以 el-sub-menu(子菜单) 展示
      2. 否则:则以 el-menu-item(菜单项) 展示
    2. 否则:不显示在 menu 菜单中

创建页面组件

  • pages 创建如下页面
  1. 创建文章:article-create
  2. 文章详情:article-detail
  3. 文章排名:article-ranking
  4. 错误页面:error-page
    1. 404
    2. 401
  5. 导入:import
  6. 权限列表:permission-list
  7. 个人中心:profile
  8. 角色列表:role-list
  9. 用户信息:user-info
  10. 用户管理:user-manage

创建路由规则

  • 公有路由表 public.ts
ts
const LoginLayOut = () => import("@/layouts/loginLayout.vue");

const LoginPage = () => import("@/pages/login.vue");

const ErrorPage = () => import("@/pages/error.vue");

const commonRoutes = [
  {
    path: "/login",
    name: "loginPage",
    component: LoginLayOut,
    children: [
      {
        path: "",
        name: "loginContent",
        component: LoginPage,
      },
    ],
  },
  {
    path: "/404",
    name: "errorPage",
    component: ErrorPage,
  },
  {
    path: "/:pathMatch(.*)*",
    redirect: "/404",
  },
];

export default commonRoutes;
  • 私有路由表 private.ts
ts
const AdminLayout = () => import("@/layouts/default.vue");

const AdminHomePage = () => import("@/pages/index.vue");

const ArticleCreatePage = () => import("@/pages/articleCreate.vue");

const ArticleDetailPage = () => import("@/pages/articleDetail.vue");

const ArticleRankingPage = () => import("@/pages/articleRanking.vue");

const ArticleImportPage = () => import("@/pages/articleimport.vue");

const PermissionListPage = () => import("@/pages/permissionlist.vue");

const ProfilePage = () => import("@/pages/profile.vue");

const RoleListPage = () => import("@/pages/rolelist.vue");

const UserInfoPage = () => import("@/pages/userinfo.vue");

const UserManagePage = () => import("@/pages/usermanage.vue");

const adminRoutes = [
  {
    path: "/admin",
    name: "adminLayout",
    component: AdminLayout,
    meta: {
      title: "admin",
      icon: "setting",
    },
    children: [
      {
        path: "",
        name: "adminHomePage",
        component: AdminHomePage,
        meta: {
          title: "首页",
          icon: "home",
        },
      },
      {
        path: "/admin/article",
        name: "articlePage",
        meta: {
          title: "文章管理",
          icon: "article",
        },
        children: [
          {
            path: "/admin/article/create",
            name: "articleCreatePage",
            component: ArticleCreatePage,
            meta: {
              title: "文章创建",
              icon: "article",
            },
          },
          {
            path: "/admin/article/detail",
            name: "articleDetailPage",
            component: ArticleDetailPage,
            meta: {
              title: "文章详情",
            },
          },
          {
            path: "/admin/article/ranking",
            name: "articleRankingPage",
            component: ArticleRankingPage,
            meta: {
              title: "文章排名",
            },
          },
          {
            path: "/admin/article/import",
            name: "articleImportPage",
            component: ArticleImportPage,
            meta: {
              title: "文章导入",
            },
          },
        ],
      },
      {
        path: "/admin/permissionlist",
        name: "permissionListPage",
        component: PermissionListPage,
        meta: {
          title: "权限列表",
        },
      },
      {
        path: "/admin/profile",
        name: "profilePage",
        component: ProfilePage,
        meta: {
          title: "个人中心",
        },
      },
      {
        path: "/admin/rolelist",
        name: "roleListPage",
        component: RoleListPage,
        meta: {
          title: "角色列表",
        },
      },
      {
        path: "/admin/userinfo",
        name: "userInfoPage",
        component: UserInfoPage,
        meta: {
          title: "用户信息",
        },
      },
      {
        path: "/admin/usermanage",
        name: "userManagePage",
        component: UserManagePage,
        meta: {
          title: "用户管理",
        },
      },
    ],
  },
];

export default adminRoutes;

创建筛选文件

获取全部的路由

js
import { useRouter } from "vue-router";
const allRouter = useRouter();
console.log(allRouter.getRoutes());

这样获取到全部的路由 里面有一级也有二级,但这样不是我们需要的所以需要过滤

过滤路由

  • 在 utils/route.js 文件

  • 安装 path-browserify

bash
npm install path-browserify
npm install @types/path-browserify --save-dev
js
import path from 'path-browserify'

/* 查找出所有子路由 */

function getChildrenRoutes(routes: any) {
  const result: any = []
  routes.forEach((route: any) => {
    if (route.children && route.children.length > 0) {
      result.push(...route.children)
    }
  })
  return result
}

/**
 * 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级
 * @param {*} routes router.getRoutes()
 * return 筛除掉了所有子路由,保留结构
 */
export function filterRouters(routes: any) {
  const childrenRoutes: any = getChildrenRoutes(routes)
  return routes.filter((route: any) => {
    return !childrenRoutes.some((childrenRoute: any) => {
      return childrenRoute.path === route.path
    })
  })
}

/**
 * 判断数据是否为空值
 */
function isNull(data: any) {
  if (!data) {
    return true
  }
  if (JSON.stringify(data) === '{}') {
    return true
  }
  if (JSON.stringify(data) === '[]') {
    return true
  }
  return false
}

/**
 * 重点函数
 * 根据 routes 数据,返回对应 menu 规则数组
 */

export function generateMenus(routes: any, basePath = '') {
  const result: any = []
  // 遍历路由表
  routes.forEach((item: any) => {
    // 不存在 children && 不存在 meta 直接 return
    if (isNull(item.meta) && isNull(item.children)) {
      return
    }
    // 存在 children 不存在 meta,进入迭代
    if (isNull(item.meta) && !isNull(item.children)) {
      result.push(...generateMenus(item.children, basePath))
      return
    }
    // 合并 path 作为跳转路径
    const routePath = path.resolve(basePath, item.path)
    // 路由分离之后,存在同名父路由的情况,需要单独处理
    let route = result.find((item: any) => item.path === routePath)
    if (!route) {
      route = {
        ...item,
        path: routePath,
        children: [],
      }

      // icon 与 title 必须全部存在
      if (route.meta.icon && route.meta.title) {
        // meta 存在生成 route 对象,放入 arr
        result.push(route)
      }
    }

    // 存在 children 进入迭代到children
    if (item.children) {
      const childMenus = generateMenus(item.children, route.path)
      if (result.includes(route)) {
        // 父级已在菜单中,子菜单挂到父级下
        route.children.push(...childMenus)
      } else {
        // 父级缺少 icon/title 未入菜单,子菜单提升一层,避免丢失
        result.push(...childMenus)
      }
    }
  })
  return result
}

export default generateMenus

使用

修改 SideBarMenu/index.vue

vue
<script setup>
import { filterRouters, generateMenus } from "@/utils/route";

const router = useRouter();
const routes = computed(() => {
  const filterRoutes = filterRouters(router.getRoutes());
  return generateMenus(filterRoutes);
});
console.log(routes.value);
</script>

<template>
  <el-menu
    :uniqueOpened="true"
    default-active="2"
    background-color="#545c64"
    text-color="#fff"
    active-text-color="#ffd04b"
  >
    <SideBarItem
      v-for="item in routes"
      :key="item.path"
      :route="item"
    ></SideBarItem>
  </el-menu>
</template>

<style lang="scss" scoped></style>

修改 SideBarItem/index.vue

vue
<script setup>
import { useCollapse } from "@/stores/sidebaropen";
// 递归引用自身,支持多级菜单
import SideBarItem from "./index.vue";
// 定义 props
defineProps({
  route: {
    type: Object,
    required: true,
  },
});
</script>

<template>
  <!-- 支持渲染多级 menu 菜单 -->
  <el-sub-menu
    v-if="route.children && route.children.length > 0"
    :index="route.path"
  >
    <template #title>
      <SvgIcon :icon="route.meta.icon" size="16"></SvgIcon>
      <span class="title ml">{{ route.meta.title }}</span>
    </template>
    <!-- 递归渲染子项,有几层渲染几层 -->
    <SideBarItem
      v-for="item in route.children"
      :key="item.path"
      :route="item"
    />
  </el-sub-menu>
  <!-- 渲染 item 项 -->
  <el-menu-item v-else :index="route.path">
    <SvgIcon :icon="route.meta.icon" size="16"></SvgIcon>
    <template #title>
      <span :class="[useCollapse().sidebarOpened ? 'title' : 'title  ml']">{{
        route.meta.title
      }}</span>
    </template>
  </el-menu-item>
</template>

<style lang="scss">
.ml {
  margin-left: 10px;
}
</style>

修改 stores/sidebaropen.js

ts
import { defineStore } from "pinia";

import { ref } from "vue";

export const useCollapse = defineStore("sidebar", () => {
  // 侧边栏是否展开
  const sidebarOpened = ref(true);

  // 切换侧边栏展开/收起
  const toggleSidebar = () => {
    sidebarOpened.value = !sidebarOpened.value;
  };

  return {
    sidebarOpened,
    toggleSidebar,
  };
});