Elemnt-UI + 递归组件实现后台管理系统左侧菜单

Elemnt-UI + 递归组件实现后台管理系统左侧菜单

在 Vue.js 中,允许你编写一个组件来表示一个节点,而这个节点可以包含多个子节点,每个子节点又可以是同样的组件。这种方式使得组件能够处理无限层级的嵌套结构。

应用场景

递归组件非常适合处理具有层级结构的数据,以下是几种常见的应用场景:

  • 多级菜单:如上所示,递归组件可以用来生成多级菜单,无论是水平还是垂直布局。
  • 文件目录树:文件系统中的目录结构通常是树状的,递归组件可以帮助你轻松地构建一个文件目录树。
  • 评论系统:许多网站的评论系统支持回复评论,形成一个树形结构,递归组件可以用来展示这些评论及其回复。
  • 组织架构图:公司内部的组织结构也可以用递归组件来表示,每个部门下面可以有子部门或员工。
  • 任务列表:在项目管理工具中,任务列表往往包含父任务和子任务,递归组件可以清晰地展现这种层次关系。

实现步骤

1. 安装 Element-UI
确保你的项目已经安装了 Element-UI。如果还没有安装,可以通过 npm 或 yarn 来安装:

npm install element-ui --save
# 或者
yarn add element-ui

然后在 main.js 文件中引入并使用 Element-UI:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

2. 设计菜单数据结构

先定义一个合理的菜单数据结构,通常是一个数组对象,其中每个对象代表一个菜单项,并且可以包含子菜单。例如:

import Layout from '@/Layout/index.vue'

export const routes = [
  {
    
    
    path: '/',
    name: 'redirect',
    component: Layout,
    hidden: true, // 隐藏菜单
    redirect: "/homePage", // 用户在地址栏输入 '/' 时会自动重定向到 /homePage 页面
  },
  {
    
    
    path: '/homePage',
    component: Layout,
    redirect: "/homePage/index",
    meta: {
    
    
      title: "首页",
    },
    children: [
      {
    
    
        path: 'index',
        name: 'homePageIndex',
        meta: {
    
    
          title: "首页",
        },
        component: () => import('@/views/homePage/index.vue')
      }
    ]
  },
  {
    
    
    path: '/login',
    component: () => import('@/views/login.vue'),
    hidden: true
  },
  {
    
    
    path: '/404',
    component: () => import('@/views/error/404.vue'),
    hidden: true
  },
  {
    
    
    path: '/401',
    component: () => import('@/views/error/401.vue'),
    hidden: true
  },
  {
    
    
    path: '/admin',
    meta: {
    
    
      title: "系统管理",
    },
    component: Layout,
    children: [
      {
    
    
        path: 'user',
        name: 'userIndex',
        meta: {
    
    
          title: "用户管理",
        },
        component: () => import('@/views/admin/user.vue')
      },
      {
    
    
        path: 'role',
        name: 'roleIndex',
        meta: {
    
    
          title: "权限管理",
        },
        component: () => import('@/views/admin/role.vue'),
        children: [
          {
    
    
            path: 'add',
            name: 'addRole',
            hidden: true,
            meta: {
    
    
              title: "添加角色",
            },
            component: () => import('@/views/admin/user/index.vue')
          },
          {
    
    
            path: 'update',
            name: 'updateRole',
            hidden: true,
            meta: {
    
    
              title: "编辑角色",
            },
            component: () => import('@/views/admin/role/index.vue')
          }
        ]
      }
    ]
  },
  {
    
    
    path: '/tableEcho',
    meta: {
    
    
      title: "表格管理",
    },
    component: Layout,
    children: [
      {
    
    
        path: 'test',
        name: 'tableEchoIndex',
        meta: {
    
    
          title: "表格测试",
        },
        component: () => import('@/views/tableEcho/index.vue'),
        children: [
          {
    
    
            path: 'add',
            name: 'addTable',
            hidden: true,
            meta: {
    
    
              title: "新增测试",
            },
            component: () => import('@/views/tableEcho/add.vue')
          }
        ]
      },
    ],
  },
]

上述示例:

  • 配置了四个路由器:根路径重定向、首页、系统管理、表格管理。
  • 根路径重定向: 访问根路径 '/' 时,会自动重定向到 /homePage
  • 首页菜单: 一个子菜单。
  • 系统管理: 两个子菜单, 两个三级菜单(不展示)。
  • 表格管理: 一个子菜单, 一个三级菜单(不展示)。

注意点:

每个菜单的一级路由的 component 都必须是 Layout 组件, Layout 组件用于定义整个系统页面的基本结构和布局,比如导航栏、侧边栏等。通过将所有的一级路由都指向 Layout 组件,可以确保无论用户访问哪个页面,都能看到一致的布局。

Layout 组件文件布局图片如下
在这里插入图片描述

3. 创建递归组件
创建一个递归组件来渲染菜单项。这个组件将根据传入的数据结构递归地生成菜单项。

Sidebar / SidebarItem.vue

<div class="sidebar_item">
    <!-- 如果没有子菜单或只有一个二级的子菜单则直接渲染 -->
    <template
      v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren)"
      class="item">
      <router-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" class="title">
          <img src="" alt="">
          <span slot="title">{
    
    {
    
     onlyOneChild.meta.title }}</span>
        </el-menu-item>
      </router-link>
    </template>

    <!-- 有子菜单 -->
    <el-submenu v-else :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <span slot="title" class="title">{
    
    {
    
     item.meta.title }}</span>
      </template>
      <Sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child"
        :base-path="resolvePath(child.path)"></Sidebar-item>
    </el-submenu>
  </div>
</template>

<script>
import path from "path";

export default {
    
    
  name: "SidebarItem",
  data() {
    
    
    return {
    
    
      onlyOneChild: {
    
    
        children: [],
      },
    }
  },
  props: {
    
    
    item: {
    
    
      type: Object,
      required: true
    },
    basePath: {
    
    
      type: String,
      default: ''
    }
  },
  methods: {
    
    
    // 判断是否只有一个子菜单
    hasOneShowingChild(children = [], parent) {
    
    
      // console.log('parent::: ',children , parent);
      if (!children) {
    
    
        children = [];
      }

      // 过滤掉隐藏的菜单
      const showingChildren = children.filter((item) => {
    
    
        // 是否隐藏菜单
        if (item.hidden) {
    
    
          return false;
        } else {
    
    
          this.onlyOneChild = item;
          return true;
        }
      });

      // 当只有一个子路由时,默认显示子路由器
      if (showingChildren.length === 1) {
    
    
        return true;
      }

      // 如果没有子路由,则显示父级路由
      if (showingChildren.length === 0) {
    
    
        this.onlyOneChild = {
    
     ...parent, path: "", noShowingChildren: true };
        return true;
      }
      return false;
    },
    // 判断是否是外链
    isExternal(path) {
    
    
      return /^(https?:|mailto:|tel:)/.test(path);
    },
    // 路径拼接
    resolvePath(routePath) {
    
    
      if (this.isExternal(routePath)) {
    
    
        return routePath;
      }
      if (this.isExternal(this.basePath)) {
    
    
        return this.basePath;
      }
      return path.resolve(this.basePath, routePath);
    },
  }
}
</script>

<style lang="scss" scoped>
.sidebar_item {
    
    
  cursor: pointer;
  >a {
    
    
    height: 60px;
    line-height: 60px;
  }
}

::v-deep .el-submenu__title {
    
    
  height: 60px;
  line-height: 60px;
}

::v-deep .el-submenu__title:hover,
a :hover {
    
    
  background-color: #2a9cff !important;
}

::v-deep .active {
    
    
  background-color: #2a9cff !important;
}

.is-active {
    
    
  background-color: #2a9cff !important;

}

.title {
    
    
  font-size: 16px;
}
</style>

注意:

代码里导入的 path 模块需要安装 node-polyfill-webpack-plugin 模块, 原因是由于 webpack5 中移除了 nodejs 核心模块的 polyfill 自动引入, 所以需要下载插件手动引入

npm install node-polyfill-webpack-plugin --save
# 或者
yarn add node-polyfill-webpack-plugin

vue.config.js 配置

const {
    
     defineConfig } = require('@vue/cli-service');
const nodePolyfillWebpackPlugin = require("node-polyfill-webpack-plugin");

module.exports = defineConfig({
    
    
  configureWebpack: (config) => {
    
    
    // 由于webpack5中移除了nodejs核心模块的polyfill自动引入, 所以需要下载插件手动引入
  	config.plugins.push(new nodePolyfillWebpackPlugin());
  }
})

4. 使用递归组件

在主布局或需要显示菜单的地方使用 Menu 组件,并传递菜单数据:

Sidebar / index.vue

<template>
  <div style="padding-top: 30px;">
    <!-- 左侧菜单 -->
    <el-menu :default-active="index" class="memu" @open="handleOpen" @close="handleClose" background-color="#304156"
      text-color="#bfcbd9" active-text-color="#fff" @select="handleSelect">
      <Sidebar-item v-for="route in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
    </el-menu>
  </div>
</template>

<script>
import SidebarItem from './SidebarItem.vue';
import {
    
     mapGetters } from "vuex";

export default {
    
    
  name: 'Sidebar',
  data() {
    
    
    return {
    
    
      index: this.$route.path,

    }
  },
  components: {
    
     SidebarItem },
  computed: {
    
    
    ...mapGetters(["sidebarRouters"]),
  },
  methods: {
    
    
    handleOpen(key, keyPath) {
    
    
      console.log('handleOpen::: ', key, keyPath);

    },
    handleClose(key, keyPath) {
    
    
      console.log('handleClose::: ', key, keyPath);

    },
    handleSelect(index, indexPath) {
    
    
      console.log('handleSelect::: ', index, indexPath);
    }
  }
}
</script>

<style lang="scss" scoped>
.memu {
    
    
  display: inline-block;
  text-align: left;
  width: 100%;
}
</style>

实现效果
在这里插入图片描述

面包屑导航栏实现参考文档: Element-UI 组件实现面包屑导航栏

总结

Element-UI 结合递归组件的方式,用于构建后台管理系统的左侧菜单,主要是通过以下步骤实现的:

  • 配置路由(Routes): 首先,如同前述代码片段所示,定义一系列路由对象构成的数组 routes,这些对象不仅描述了各页面路径(path)、页面名称(name)、对应组件(component),还包括了嵌套的子路由(children)以及元信息(如页面标题)等,从而形成了多级菜单的结构基础。

  • 递归组件(Recursive Component): 利用 Vue 中的递归组件技术,创建一个菜单组件,该组件根据传入的路由配置(通常是 routes 数组)自动生成菜单项。递归的逻辑在于:组件内根据当前路由的 children 属性判断是否有子菜单,如果有,则继续渲染子菜单组件,直至没有更多子节点,以此实现无限级菜单的展现。

  • Element-UI 风格: 在实现递归菜单组件时,利用 Element-UI 的 UI 组件库(如 el-menu、el-submenu、el-menu-item 等),为菜单项赋予统一且美观的样式,实现折叠、展开、激活状态等交互效果,增强用户体验。

总结而言,Element-UI 与递归组件结合的方式,使得后台管理系统的左侧菜单能够高效地根据路由配置动态渲染多层级菜单,同时确保了菜单的一致性和美观性,提升了系统的可维护性和用户体验。

猜你喜欢

转载自blog.csdn.net/a123456234/article/details/141959464