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

创建路由规则

  • 公有路由表

  • 私有路由表

js
import { createRouter, createWebHistory } from "vue-router";

const LayOutPage = () => import("@/layouts/default.vue");
const LoginPage = () => import("@/pages/login/index");
const LoginLayOut = () => import("@/layouts/login");
const ProfilePage = () => import("@/pages/profile/index");
const Page404 = () => import("@/pages/error-page/404");
const Page401 = () => import("@/pages/error-page/401");
/* 私有路由表 */
const UserManagePage = () => import("@/pages/user-manage/index");
const RolePage = () => import("@/pages/role-list/index");
const PermissionPage = () => import("@/pages/permission-list/index");
const UserInfoPage = () => import("@/pages/user-info/index");
const ImportPage = () => import("@/pages/import/index");
const ArticleRankingPage = () => import("@/pages/article-ranking/index");
const ArticleDetailPage = () => import("@/pages/article-detail/index");
const ArticlePageCreate = () => import("@/pages/article-create/index");
/**
 * 公开路由表
 */
const publicRoutes = [
  {
    path: "/login",
    name: "LoginLayOut",
    component: LoginLayOut,
    children: [
      {
        path: "",
        name: "LoginPage",
        component: LoginPage,
      },
    ],
  },
  {
    path: "/",
    // 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数
    component: layout,
    redirect: "/profile",
    children: [
      {
        path: "/profile",
        name: "profile",
        component: ProfilePage,
        meta: {
          title: "profile",
          icon: "el-icon-user",
        },
      },
      {
        path: "/404",
        name: "404",
        component: Page404,
      },
      {
        path: "/401",
        name: "401",
        component: Page401,
      },
    ],
  },
];

/**
 * 私有路由表
 */
const privateRoutes = [
  {
    path: "/user",
    component: layout,
    redirect: "/user/manage",
    meta: {
      title: "user",
      icon: "personnel",
    },
    children: [
      {
        path: "/user/manage",
        component: UserManagePage,
        meta: {
          title: "userManage",
          icon: "personnel-manage",
        },
      },
      {
        path: "/user/role",
        component: RolePage,
        meta: {
          title: "roleList",
          icon: "role",
        },
      },
      {
        path: "/user/permission",
        component: PermissionPage,
        meta: {
          title: "permissionList",
          icon: "permission",
        },
      },
      {
        path: "/user/info/:id",
        name: "userInfo",
        component: UserInfoPage,
        meta: {
          title: "userInfo",
        },
      },
      {
        path: "/user/import",
        name: "import",
        component: ImportPage,
        meta: {
          title: "excelImport",
        },
      },
    ],
  },
  {
    path: "/article",
    component: layout,
    redirect: "/article/ranking",
    meta: {
      title: "article",
      icon: "article",
    },
    children: [
      {
        path: "/article/ranking",
        component: ArticleRankingPage,
        meta: {
          title: "articleRanking",
          icon: "article-ranking",
        },
      },
      {
        path: "/article/:id",
        component: ArticleDetailPage,
        meta: {
          title: "articleDetail",
        },
      },
      {
        path: "/article/create",
        component: ArticlePageCreate,
        meta: {
          title: "articleCreate",
          icon: "article-create",
        },
      },
      {
        path: "/article/editor/:id",
        component: ArticlePageCreate,
        meta: {
          title: "articleEditor",
        },
      },
    ],
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes: [...publicRoutes, ...privateRoutes],
});

export default router;

创建筛选文件

获取全部的路由

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

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

过滤路由

  • 在 utils/route.js 文件

  • 安装 path-browserify

bash
npm install path-browserify
js
import path from "path";

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

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

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

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

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

export function generateMenus(routes, basePath = "") {
  const result = [];
  // 遍历路由表
  routes.forEach((item) => {
    // 不存在 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));
      return;
    }
    // 合并 path 作为跳转路径
    const routePath = path.resolve(basePath, item.path);
    // 路由分离之后,存在同名父路由的情况,需要单独处理
    let route = result.find((item) => 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) {
      route.children.push(...generateMenus(item.children, route.path));
    }
  });
  return result;
}

export default generateMenus;

使用

修改 SideBarMenu/index.vue

vue
<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>

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

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

修改 SideBarItem/index.vue

vue
<template>
  <!-- 支持渲染多级 menu 菜单 -->
  <el-sub-menu v-if="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>
    <!-- 循环渲染 -->
    <el-menu-item
      v-for="(item, index) in route.children"
      :key="item.path"
      :index="item.path"
    >
      <template #title>
        <SvgIcon :icon="item.meta.icon" size="16"></SvgIcon>
        <span class="title ml">{{ item.meta.title }}</span>
      </template>
    </el-menu-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>

<script setup>
import { useCollapse } from "@/stores/sidebaropen";
// 定义 props
defineProps({
  route: {
    type: Object,
    required: true,
  },
});
</script>

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