1 需求

  • 管理系统“菜单”由后端接口返回,前端需要根据后端返回的“菜单”数组,构造路由,渲染侧栏菜单
  • 有些菜单是子菜单,有对应的路由,但是不在侧栏显示(比如一些详情页面) 注:这里的“菜单”,不是字面意思的菜单,即可以是菜单也可以是按钮,如果是菜单则对应路由

2 分析

一般我们“菜单“保存在数据库时,不会仅仅保存路由相关参数,还会增加一下自定义参数,类似下面的数据结构:

const menu = {
  id: "1",
  pid: "-1", // 父id
  nameCn: "平台管理", //中文名
  type: "menu", //类别:menu 菜单,button 按钮
  icon: "platform", //图标
  routeName: "platform", //路由名
  routePath: "/platform", //路由地址
  routeLevel: 1, //路由级别: 一级路由(左侧显示)  二级路由(左侧不显示)
  componentPath: "", //组件路径
  sort: 1, //排序
  children: [],
};

所以需要手动构造路由,渲染侧栏菜单

需要注意的地方

  • 通过设置 routeLevel 字段来判断当前“菜单“是否在左侧菜单显示
  • 添加 el-menu 的 router 属性,这样可以点击 menu 自动跳转到 index 绑定的路径,否则需要绑定 click 事件,进行手动跳转(适用于需要跳转外链的情况,需要再递归菜单时进行判断,不给index赋值path)
  • 如果当前“菜单“存在 children,但是 children 所有 child 是二级路由并且 path 没有以“/”开头(如下面路由中的硬件管理),那么需要将一个 Empty.vue 组件指向当前“菜单“,并且创建一个 path 为空的 child 来匹配当前“菜单“,否则二级路由无法跳转(无法跳转指的是是当前“菜单“组件路径直接配置目的组件路径)
  • 如果当前“菜单“存在 children,但是 children 中有 child 的 path 没有以“/”开头,在构造左侧菜单时,需要拼接祖先 path 后再赋值给 index,否则无法跳转 这里使用 router.getRoutes()返回的所有路由,并且使用路由 name 匹配,这样比自己递归拼接方便,如下面 MenuItem.vue 组件中使用的 processRoutePath 函数

3 实现

效果图:

3.1 目录结构

3.2 代码:

components/SvgIcon.vue

<template>
  <svg
    aria-hidden="true"
    :class="svgClass"
    :style="{ width: size, height: size }"
  >
    <use :xlink:href="symbolId" :fill="color" />
  </svg>
</template>

<script setup lang="ts">
const props = defineProps({
  prefix: {
    type: String,
    default: "icon",
  },
  name: {
    type: String,
    required: false,
    default: "",
  },
  color: {
    type: String,
    default: "",
  },
  size: {
    type: String,
    default: "1em",
  },
  className: {
    type: String,
    default: "",
  },
});

const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const svgClass = computed(() => {
  if (props.className) {
    return `svg-icon ${props.className}`;
  }
  return "svg-icon";
});
</script>

<style scoped>
.svg-icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  overflow: hidden;
  vertical-align: -0.15em;
  outline: none;
  fill: currentcolor;
}
</style>

layout/index.vue

<template>
  <div class="container">
    <div class="aside">
      <el-scrollbar height="100%">
        <el-menu
          :default-active="activeIndex"
          :ellipsis="false"
          :mode="'vertical'"
          :collapse="false"
          background-color="#545c64"
          text-color="#fff"
          active-text-color="#ffd04b"
          router
        >
          <template v-for="item in menus" :key="item.id">
            <MenuItem :menu="item"></MenuItem>
          </template>
        </el-menu>
      </el-scrollbar>
    </div>
    <div class="content">
      <RouterView></RouterView>
    </div>
  </div>
</template>
<script setup lang="ts">
import MenuItem from "./components/MenuItem.vue";
import { menus } from "../router/index";

const route = useRoute();
const activeIndex = ref<string>(route.path);
</script>
<style lang="scss">
.container {
  width: 100%;
  height: 100%;
  display: flex;
  .aside {
    width: 300px;
    height: 100%;
    background-color: #545c64;
  }
  .content {
    flex: 1;
  }
}
</style>


layout/components/MenuItem.vue

<template>
  <!-- 不存在children -->
  <template v-if="!menu.children?.length">
    <el-menu-item
      v-if="menu.type === 'menu' && menu.routeLevel === 1"
      :index="processRoutePath(menu)"
    >
      <template #title>
        <SvgIcon :name="menu.icon" class="icon"></SvgIcon>
        <span>{{ menu.nameCn }}</span>
      </template>
    </el-menu-item>
  </template>
  <!-- 存在children -->
  <template v-else>
    <el-sub-menu
      v-if="menu.children.some((c: any) => c.type === 'menu' && c.routeLevel === 1)"
      :index="menu.routePath"
    >
      <template #title>
        <SvgIcon :name="menu.icon" class="icon"></SvgIcon>
        <span>{{ menu.nameCn }}</span>
      </template>
      <template v-for="item in menu.children" :key="item.id">
        <MenuItem :menu="item"></MenuItem>
      </template>
    </el-sub-menu>
    <el-menu-item
      v-else-if="menu.type === 'menu' && menu.routeLevel === 1"
      :index="processRoutePath(menu)"
    >
      <template #title>
        <SvgIcon :name="menu.icon" class="icon"></SvgIcon>
        <span>{{ menu.nameCn }}</span>
      </template>
    </el-menu-item>
  </template>
</template>

<script lang="ts" setup>
import MenuItem from "./MenuItem.vue";
import SvgIcon from "../../components/SvgIcon.vue";

const props = defineProps({
  menu: {
    type: Object,
    required: true,
  },
});

const router = useRouter();
const routes = router.getRoutes();
<!-- 用于处理子路由path没有以/开头的情况 -->
const processRoutePath = (item) => {
  for (let route of routes) {
    if (route.name === item.routeName) {
      return route.path;
    }
  }
  return item.routePath;
};
</script>
<style lang="scss">
.icon {
  margin-right: 10px;
}
</style>

router/index.ts

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "index",
      component: () => import("../layout/index.vue"),
      meta: {
        title: "index",
        sort: 1,
      },
    },
  ],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 }),
});

export const menus = [
  {
    id: "1",
    pid: "-1", // 父id
    nameCn: "平台管理", //中文名
    type: "menu", //类别:menu 菜单,button 按钮
    icon: "platform", //图标
    routeName: "platform", //路由名
    routePath: "/platform", //路由地址
    routeLevel: 1, //路由级别: 一级路由(左侧显示)  二级路由(左侧不显示)
    componentPath: "", //组件路径
    sort: 1, //排序
    children: [
      {
        id: "1-1",
        nameCn: "角色管理",
        permission: "",
        type: "menu",
        pid: "1",
        icon: "role",
        iconColor: "",
        routeName: "role",
        routePath: "role",
        routeLevel: 1,
        componentPath: "/platform/role/index.vue",
        sort: 1,
        children: [
          {
            id: "1-1-1",
            nameCn: "新增",
            permission: "account:add",
            type: "button",
            pid: "1-1",
            icon: "user",
            iconColor: "",
            routeName: "",
            routePath: "",
            routeLevel: null,
            componentPath: "",
            sort: 1,
            children: [],
          },
        ],
      },
      {
        id: "1-2",
        nameCn: "账户管理",
        permission: "",
        type: "menu",
        pid: "1",
        icon: "user",
        iconColor: "",
        routeName: "account",
        routePath: "account",
        routeLevel: 1,
        componentPath: "/platform/account/index.vue",
        sort: 2,
        children: [],
      },
    ],
  },
  {
    id: "2",
    pid: "-1",
    nameCn: "资产管理",
    type: "menu",
    icon: "property",
    routeName: "",
    routePath: "/property",
    routeLevel: 1,
    componentPath: "",
    sort: 2,
    children: [
      {
        id: "2-1",
        pid: "2",
        nameCn: "文档管理",
        permission: "",
        type: "menu",
        icon: "document",
        iconColor: "",
        routeName: "document",
        routePath: "document",
        routeLevel: 1,
        componentPath: "/property/document/index.vue",
        sort: 1,
        children: [],
      },
      {
        id: "2-2",
        pid: "2",
        nameCn: "硬件管理",
        permission: null,
        type: "menu",
        icon: "hardware",
        iconColor: "",
        routeName: "",
        routePath: "hardware",
        routeLevel: 1,
        componentPath: "/property/layout/Empty.vue",
        sort: 2,
        children: [
          {
            id: "2-2-1",
            pid: "2-2",
            nameCn: "硬件管理",
            permission: null,
            type: "menu",
            icon: "",
            routeName: "hardware",
            routePath: "",
            routeLevel: 2,
            componentPath: "/property/hardware/index.vue",
            sort: 1,
            children: [],
          },
          {
            id: "2-2-2",
            pid: "2-2",
            nameCn: "硬件配置",
            permission: null,
            type: "menu",
            icon: "",
            routeName: "config",
            routePath: "config",
            routeLevel: 2,
            componentPath: "/property/hardware/Config.vue",
            sort: 2,
            children: [],
          },
        ],
      },
    ],
  },
];

const initRouter = (routerTree: any) => {
  const routerArr: any = [];
  routerTree.forEach((item: any) => {
    if (item.type === "menu") {
      routerArr.push({
        meta: { title: item.nameCn },
        name: item.routeName || "",
        path: item.routePath || "",
        component: item.componentPath
          ? () => import(/* @vite-ignore */ "../view" + item.componentPath)
          : "",
        children: item.children ? initRouter(item.children) : [],
      });
    }
  });
  return routerArr;
};

const routers = initRouter(menus);
routers.forEach((item: any) => {
  router.addRoute("index", item);
});

console.log(router.getRoutes());

export default router;

view/platform/account/index.vue

<template>账户管理</template>

view/platform/role/index.vue

<template>角色管理</template>

view/property/layout/Empty.vue

<template>
  <router-view />
</template>

view/property/hardware/index.vue

<template>
  <div>硬件管理</div>

  <el-button type="primary" @click="handleCilck">硬件配置</el-button>
</template>
<script setup lang="ts">
const router = useRouter();
const handleCilck = () => {
  router.push({
    name: "config",
  });
};
</script>

view/property/hardware/Config.vue

<template>硬件配置</template>

view/property/document/index.vue

<template>文档管理</template>

GitHub 加速计划 / eleme / element
54.06 K
14.63 K
下载
A Vue.js 2.0 UI Toolkit for Web
最近提交(Master分支:3 个月前 )
c345bb45 7 个月前
a07f3a59 * Update transition.md * Update table.md * Update transition.md * Update table.md * Update transition.md * Update table.md * Update table.md * Update transition.md * Update popover.md 7 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐