1. 菜单管理需求   37 

1.1 需求描述   37

不同角色的用户登录后台管理系统拥有不同的菜单权限与功能权限,我们前端是基于:vue-admin-template这个模块开发的,因此我们菜单表设计也必须基于前端模板进行设计。

前端框架vue-admin-template菜单其实就是我们配置的路由:

{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',
    icon: 'el-icon-s-tools'
  },
  alwaysShow: true,
  children: [
    {
      name: 'sysUser',
      path: 'sysUser',
      component: () => import('@/views/system/sysUser/list'),
      meta: {
        title: '用户管理',
        icon: 'el-icon-s-custom'
      },
    },
    {
      path: 'sysRole',
      component: () => import('@/views/system/sysRole/list'),
      meta: {
        title: '角色管理',
        icon: 'el-icon-s-help'
      },
    },
    {
      name: 'sysMenu',
      path: 'sysMenu',
      component: () => import('@/views/system/sysMenu/list'),
      meta: {
        title: '菜单管理',
        icon: 'el-icon-s-unfold'
      },
    },
    {
      path: 'assignAuth',
      component: () => import('@/views/system/sysRole/assignAuth'),
      meta: {
        activeMenu: '/system/sysRole',
        title: '角色授权'
      },
      hidden: true,
    }
  ]
}

因此,菜单表的设计必须满足路由配置的必要信息

1.2 菜单表的设计

菜单管理_Layout

菜单管理_Layout_02

1.2.1 创建表   37

菜单管理_User_03

重点字段说明:

type:菜单类型,分为:目录、菜单与按钮

目录:一个分类(可理解为一级菜单)、目录下级节点可以为目录与菜单

菜单:一个具体页面,菜单的下级节点只能是按钮

按钮:页面上的功能

path:对应路由里面的路由地址path

component:对应路由里面的组件component

perms:对应菜单的功能权限标识

icom:对应路由的菜单图标

菜单表
CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属上级',
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
  `type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(0:目录,1:菜单,2:按钮)',
  `path` varchar(100) DEFAULT NULL COMMENT '路由地址',
  `component` varchar(100) DEFAULT NULL COMMENT '组件路径',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT NULL COMMENT '图标',
  `sort_value` int(11) DEFAULT NULL COMMENT '排序',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
  PRIMARY KEY (`id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

#
# Data for table "sys_menu"
#

INSERT INTO `sys_menu` VALUES (2,0,'系统管理',0,'system','Layout',NULL,'el-icon-s-tools',1,1,'2021-05-31 18:05:37','2022-06-09 09:23:24',0),(3,2,'用户管理',1,'sysUser','system/sysUser/list','','el-icon-s-custom',1,1,'2021-05-31 18:05:37','2022-06-09 09:22:47',0),(4,2,'角色管理',1,'sysRole','system/sysRole/list','','el-icon-user-solid',2,1,'2021-05-31 18:05:37','2022-06-09 09:37:18',0),(5,2,'菜单管理',1,'sysMenu','system/sysMenu/list','','el-icon-s-unfold',3,1,'2021-05-31 18:05:37','2022-06-09 09:37:21',0),(6,3,'查看',2,NULL,NULL,'bnt.sysUser.list',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(7,3,'添加',2,NULL,NULL,'bnt.sysUser.add',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(8,3,'修改',2,NULL,NULL,'bnt.sysUser.update',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(9,3,'删除',2,NULL,NULL,'bnt.sysUser.remove',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(10,4,'查看',2,NULL,NULL,'bnt.sysRole.list',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(11,4,'添加',2,NULL,NULL,'bnt.sysRole.add',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(12,4,'修改',2,NULL,NULL,'bnt.sysRole.update',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(13,4,'删除',2,NULL,NULL,'bnt.sysRole.remove',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(14,5,'查看',2,NULL,NULL,'bnt.sysMenu.list',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(15,5,'添加',2,NULL,NULL,'bnt.sysMenu.add',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(16,5,'修改',2,NULL,NULL,'bnt.sysMenu.update',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(17,5,'删除',2,NULL,NULL,'bnt.sysMenu.remove',NULL,1,1,'2021-05-31 18:05:37','2022-06-09 09:22:38',0),(18,3,'分配角色',2,NULL,NULL,'bnt.sysUser.assignRole',NULL,1,1,'2022-05-23 17:14:32','2022-06-09 09:22:38',0),(19,4,'分配权限',2,'assignAuth','system/sysRole/assignAuth','bnt.sysRole.assignAuth',NULL,1,1,'2022-05-23 17:18:14','2022-06-09 09:22:38',0),(20,2,'部门管理',1,'sysDept','system/sysDept/list','','el-icon-s-operation',4,1,'2022-05-24 10:07:05','2022-06-09 09:38:12',0),(21,20,'查看',2,NULL,NULL,'bnt.sysDept.list',NULL,1,1,'2022-05-24 10:07:44','2022-06-09 09:22:38',0),(22,2,'岗位管理',1,'sysPost','system/sysPost/list','','el-icon-more-outline',5,1,'2022-05-24 10:25:30','2022-06-09 09:38:13',0),(23,22,'查看',2,NULL,NULL,'bnt.sysPost.list',NULL,1,1,'2022-05-24 10:25:45','2022-06-09 09:22:38',0),(24,20,'添加',2,NULL,NULL,'bnt.sysDept.add',NULL,1,1,'2022-05-25 15:31:27','2022-06-09 09:22:38',0),(25,20,'修改',2,NULL,NULL,'bnt.sysDept.update',NULL,1,1,'2022-05-25 15:31:41','2022-06-09 09:22:38',0),(26,20,'删除',2,NULL,NULL,'bnt.sysDept.remove',NULL,1,1,'2022-05-25 15:31:59','2022-06-09 09:22:38',0),(27,22,'添加',2,NULL,NULL,'bnt.sysPost.add',NULL,1,1,'2022-05-25 15:32:44','2022-06-09 09:22:38',0),(28,22,'修改',2,NULL,NULL,'bnt.sysPost.update',NULL,1,1,'2022-05-25 15:32:58','2022-06-09 09:22:38',0),(29,22,'删除',2,NULL,NULL,'bnt.sysPost.remove',NULL,1,1,'2022-05-25 15:33:11','2022-06-09 09:22:38',0),(30,34,'操作日志',1,'sysOperLog','system/sysOperLog/list','','el-icon-document-remove',7,1,'2022-05-26 16:09:59','2022-06-09 09:39:23',0),(31,30,'查看',2,NULL,NULL,'bnt.sysOperLog.list',NULL,1,1,'2022-05-26 16:10:17','2022-06-09 09:22:38',0),(32,34,'登录日志',1,'sysLoginLog','system/sysLoginLog/list','','el-icon-s-goods',8,1,'2022-05-26 16:36:13','2022-06-09 09:39:24',0),(33,32,'查看',2,NULL,NULL,'bnt.sysLoginLog.list',NULL,1,1,'2022-05-26 16:36:31','2022-06-09 09:36:36',0),(34,2,'日志管理',0,'log','ParentView','','el-icon-tickets',6,1,'2022-05-31 13:23:07','2022-06-09 09:39:22',0),(35,0,'审批设置',0,'processSet','Layout','','el-icon-setting',1,1,'2022-12-01 09:32:46','2022-12-01 09:32:46',0),(36,35,'审批模板',1,'processTemplate','processSet/processTemplate/list','','el-icon-s-help',2,1,'2022-12-01 09:37:08','2022-12-19 14:10:48',0),(37,36,'查看',2,'','','bnt.processTemplate.list','',1,1,'2022-12-01 09:37:49','2022-12-01 09:37:49',0),(38,36,'审批模板设置',2,'templateSet','processSet/processTemplate/templateSet','bnt.processTemplate.templateSet','',1,1,'2022-12-01 14:52:08','2022-12-13 18:11:56',0),(39,35,'审批类型',1,'processType','processSet/processType/list','','el-icon-s-unfold',1,1,'2022-12-02 14:46:18','2022-12-13 18:12:24',0),(40,39,'查看',2,'','','bnt.processType.list','',1,1,'2022-12-02 14:46:41','2022-12-02 14:46:41',0),(41,0,'审批管理',0,'processMgr','Layout','','el-icon-more-outline',1,1,'2022-12-02 14:48:11','2022-12-20 09:29:30',0),(42,41,'审批列表',1,'process','processMgr/process/list','','el-icon-document-remove',1,1,'2022-12-02 14:49:06','2022-12-02 14:59:17',0),(43,42,'查看',2,'','','bnt.process.list','',1,1,'2022-12-02 14:49:24','2022-12-02 14:49:24',0),(44,36,'在线流程设置',2,'onlineProcessSet','processSet/processTemplate/onlineProcessSet','bnt.processTemplate.onlineProcessSet','',1,1,'2022-12-08 10:13:15','2022-12-19 18:57:35',0),(45,39,'添加',2,'','','bnt.processType.add','',1,1,'2022-12-09 09:14:53','2022-12-09 09:14:53',0),(46,39,'修改',2,'','','bnt.processType.update','',1,1,'2022-12-09 09:15:10','2022-12-09 09:15:10',0),(47,39,'删除',2,'','','bnt.processType.remove','',1,1,'2022-12-09 09:15:25','2022-12-09 09:15:25',0),(48,36,'删除',2,'','','bnt.processTemplate.remove','',1,1,'2022-12-09 09:22:29','2022-12-09 09:22:29',0),(49,36,'发布',2,'','','bnt.processTemplate.publish','',1,1,'2022-12-09 09:24:47','2022-12-09 09:24:47',0),(50,0,'公众号菜单',0,'wechat','Layout','','el-icon-s-operation',1,1,'2022-12-13 09:06:58','2022-12-21 11:20:55',0),(51,50,'菜单列表',1,'menu','wechat/menu/list','','el-icon-s-help',1,1,'2022-12-13 09:07:52','2022-12-13 09:09:49',0),(52,51,'查看',2,'','','bnt.menu.list','',1,1,'2022-12-13 09:08:48','2022-12-13 17:58:23',0),(53,51,'添加',2,'','','bnt.menu.add','',1,1,'2022-12-13 16:29:25','2022-12-13 17:58:34',0),(54,51,'修改',2,'','','bnt.menu.update','',1,1,'2022-12-13 16:29:41','2022-12-13 17:58:42',0),(55,51,'删除',2,'','','bnt.menu.remove','',1,1,'2022-12-13 16:29:59','2022-12-13 17:58:47',0),(56,51,'删除微信菜单',2,'','','bnt.menu.removeMenu','',1,1,'2022-12-13 16:30:36','2022-12-13 17:58:54',0),(57,51,'同步微信菜单',2,'','','bnt.menu.syncMenu','',1,1,'2022-12-13 16:31:00','2022-12-13 17:59:01',0);

菜单管理_ico_04

角色菜单关系表
CREATE TABLE `sys_role_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL DEFAULT '0',
  `menu_id` bigint(11) NOT NULL DEFAULT '0',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_menu_id` (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 COMMENT='角色菜单';

菜单管理_User_05

1.2.2 页面效果     37

我们最终要做到这样的效果

菜单管理_User_06

2. 数据准备   37

依然使用代码生成器

菜单管理_ico_07

再持久层,业务层重新引入相应的实体类

service-oa模块

SysMenuController

package com.atguigu.auth.controller;


import com.atguigu.auth.service.SysMenuService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
 * 菜单管理控制层   38
 */
@Api(tags = "菜单管理接口")
@RestController
@RequestMapping("/admin/system/sysMenu")
public class SysMenuController {
    @Autowired
    private SysMenuService sysMenuService;

}

3. 功能实现   38

3.1 新增菜单  38

service-oa模块

SysMenuController

//新增菜单   38
    @ApiOperation(value = "新增菜单")
    @PostMapping("save")
    public Result save(@RequestBody SysMenu sysMenu) {
        sysMenuService.save(sysMenu);
        return Result.ok();
    }

3.2  修改菜单   38

service-oa模块

SysMenuController

//修改菜单  38
    @ApiOperation(value = "修改菜单")
    @PutMapping("update")
    public Result updateById(@RequestBody SysMenu sysMenu) {
        sysMenuService.updateById(sysMenu);
        return Result.ok();
    }

3.3  删除菜单  38

service-oa模块

SysMenuController

//删除菜单  38
    @ApiOperation(value = "删除菜单")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysMenuService.removeById(id);
        return Result.ok();
    }

3.4 菜单列表   38

菜单管理_Layout_08

菜单管理_ico_09

菜单管理_User_10

重点用于获取菜单信息  ,因为前端是需要类似于树型的数据,我们后端查询到返回的数据也要是树形的,不能简单的查询 返回

service-oa模块

菜单管理控制层SysMenuController

//菜单列表接口,用于获取菜单信息   38
    @ApiOperation("菜单列表")
    @GetMapping("findNodes")
    public Result findNodes() {

        List<SysMenu> list = sysMenuService.findNodes();
        return Result.ok(list);
    }

菜单管理业务层接口SysMenuService

package com.atguigu.auth.service;

import com.atguigu.model.system.SysMenu;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
 * <p>
 * 菜单表 服务类
 * </p>
 *
 * @author atguigu
 * @since 2023-06-27
 */
public interface SysMenuService extends IService<SysMenu> {
    
    //菜单列表接口,用于获取菜单信息   38
    List<SysMenu> findNodes();
}

菜单管理业务层接口实现类SysMenuServiceImpl

package com.atguigu.auth.service.impl;

import com.atguigu.auth.mapper.SysMenuMapper;
import com.atguigu.auth.service.SysMenuService;
import com.atguigu.auth.utils.MenuHelper;
import com.atguigu.model.system.SysMenu;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 菜单表 服务实现类
 * </p>
 *
 * @author atguigu
 * @since 2023-06-27
 */
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {

    //菜单列表接口,用于获取菜单信息   38
    @Override
    public List<SysMenu> findNodes() {
        //1 查询所有菜单数据    38
        //经过下面的处理将数处理成需要的树形
        List<SysMenu> sysMenuList = baseMapper.selectList(null);

        //2 构建树形结构
//        {
//            第一层
//            children:[
//            {
//                第二层
//                        ....
//            }
//            ]
//        }
        //将形成树形结构的代码分装成工具类MenuHelper
        List<SysMenu> resultList = MenuHelper.buildTree(sysMenuList);
        return resultList;
    }
}

service-oa模块

工具类MenuHelper

菜单管理_User_11

MenuHelper

package com.atguigu.auth.utils;

import com.atguigu.model.system.SysMenu;

import java.util.ArrayList;
import java.util.List;
//工具类   38
public class MenuHelper {
    //菜单列表接口,用于获取菜单信息   38
    //使用递归方法建菜单
    public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
        //创建list集合,用于返回最终数据
        List<SysMenu> trees = new ArrayList<>();
        //把所有菜单数据进行遍历
        for(SysMenu sysMenu:sysMenuList) {
            //递归入口进入
            //parentId=0是入口(父亲id=0即程序递归入口)
            if(sysMenu.getParentId().longValue()==0) {
                //找到父亲,把它放入树中
                trees.add(getChildren(sysMenu,sysMenuList));
            }
        }
        return trees;
    }

    public static SysMenu getChildren(SysMenu sysMenu,
                                      List<SysMenu> sysMenuList) {

        //父亲节点初始化 children 属性为空的 ArrayList 对象是为了确保递归过程中能
        // 够正常操作和添加子对象,确保即使在没有子对象的情况下,该属性也不会为 null,
        // 而是一个空的集合。避免可能的空指针异常
        sysMenu.setChildren(new ArrayList<SysMenu>());

        //遍历所有菜单数据,判断 id 和 parentId对应关系
        for(SysMenu it: sysMenuList) {
            //如果本对象的id和遍历到的对象的id一直,就证明遍历到的这个对象是本对象的孩子,可进入继续递归
            if(sysMenu.getId().longValue() == it.getParentId().longValue()) {
                //每个孩子节点初始化 children 属性为空的 ArrayList 对象是为了确保递归过程中能
                // 够正常操作和添加子对象,确保即使在没有子对象的情况下,该属性也不会为 null,
                // 而是一个空的集合。避免可能的空指针异常
                if (sysMenu.getChildren() == null) {
                    sysMenu.setChildren(new ArrayList<>());
                }
                //找到孩子就放到sysMenu的一个叫做children的属性中(这个children是个list集合)
                sysMenu.getChildren().add(getChildren(it,sysMenuList));
            }
        }
        //等递归结束,孩子也都封装好了,我们将对象(这个对象就是传来的父亲)返回
        //父亲孩子一层套一层,父亲有孩子,父亲的孩子有孩子,孩子有孩子,都被封装好了,最后将封装好的父亲返回
        return sysMenu;
    }
}

测试

浏览器输入http://localhost:8800/doc.html

菜单管理_ico_12

菜单管理_Layout_13

3.5 完善删除  39

当删除一个节点时,应该判断器下面有没有子节点,有的话就不能删

service-oa模块

菜单管理控制层SysMenuController

//完善删除菜单  39
    @ApiOperation(value = "删除菜单")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysMenuService.removeMenuById(id);
        return Result.ok();
    }

菜单管理业务层接口SysMenuService

//完善删除菜单   39
    void removeMenuById(Long id);

菜单管理业务层接口实现类SysMenuServiceImpl

//完善删除菜单   39
    @Override
    public void removeMenuById(Long id) {
        //判断当前菜单是否有下一层菜单
        LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
        //解释SysMenu::getParentId起到了限定查询范围的作用,将id值作为比较条件,再该范围内比较查询
        wrapper.eq(SysMenu::getParentId,id);
        Integer count = baseMapper.selectCount(wrapper);
        if(count > 0) {
            throw new GuiguException(201,"菜单不能删除");
        }
        baseMapper.deleteById(id);
    }

4. 前端   39

4.1  添加路由    39

菜单管理_ico_14

修改 src/router/index.js 文件

index.js

//菜单管理路由
      {
        name: 'sysMenu',
        path: 'sysMenu',
        component: () => import('@/views/system/sysMenu/list'),
        meta: {
          title: '菜单管理',
          icon: 'el-icon-s-unfold'
        },
      }

创建页面

菜单管理_User_15

4.2 定义基础api    39

创建文件 src/api/system/sysMenu.js

菜单管理_ico_16

sysMenu.js

import request from '@/utils/request'

/*
菜单管理相关的API请求函数
*/
const api_name = '/admin/system/sysMenu'

export default {

  /*
  获取权限(菜单/功能)列表
  */
  findNodes() {
    return request({
      url: `${api_name}/findNodes`,
      method: 'get'
    })
  },

  /*
  删除一个权限项
  */
  removeById(id) {
    return request({
      url: `${api_name}/remove/${id}`,
      method: "delete"
    })
  },

  /*
  保存一个权限项
  */
  save(sysMenu) {
    return request({
      url: `${api_name}/save`,
      method: "post",
      data: sysMenu
    })
  },

  /*
  更新一个权限项
  */
  updateById(sysMenu) {
    return request({
      url: `${api_name}/update`,
      method: "put",
      data: sysMenu
    })
  }
}

4.3 实现功能页面

src/views/system/sysMenu/list.vue

list.vue

<template>
  <div class="app-container">

    <!-- 工具条 -->
    <div class="tools-div">
      <el-button type="success" icon="el-icon-plus" size="mini" @click="add()">添 加</el-button>
    </div>
    <el-table
      :data="sysMenuList"
      style="width: 100%;margin-bottom: 20px;margin-top: 10px;"
      row-key="id"
      border
      :default-expand-all="false"
      :tree-props="{children: 'children'}">

      <el-table-column prop="name" label="菜单名称" width="160"/>
      <el-table-column label="图标">
        <template slot-scope="scope">
          <i :class="scope.row.icon"></i>
        </template>
      </el-table-column>
      <el-table-column prop="perms" label="权限标识" width="160"/>
      <el-table-column prop="path" label="路由地址" width="120"/>
      <el-table-column prop="component" label="组件路径" width="160"/>
      <el-table-column prop="sortValue" label="排序" width="60"/>
      <el-table-column label="状态" width="80">
        <template slot-scope="scope">
          <el-switch
            v-model="scope.row.status === 1" disabled="true">
          </el-switch>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间" width="160"/>
      <el-table-column label="操作" width="180" align="center" fixed="right">
        <template slot-scope="scope">
          <el-button type="success" v-if="scope.row.type !== 2" icon="el-icon-plus" size="mini" @click="add(scope.row)" title="添加下级节点"/>
          <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row)" title="修改"/>
          <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除" :disabled="scope.row.children.length > 0"/>
        </template>
      </el-table-column>
    </el-table>

    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%" >
      <el-form ref="dataForm" :model="sysMenu" label-width="150px" size="small" style="padding-right: 40px;">
        <el-form-item label="上级部门" v-if="sysMenu.id === ''">
          <el-input v-model="sysMenu.parentName" disabled="true"/>
        </el-form-item>
        <el-form-item label="菜单类型" prop="type">
          <el-radio-group v-model="sysMenu.type" :disabled="typeDisabled">
            <el-radio :label="0" :disabled="type0Disabled">目录</el-radio>
            <el-radio :label="1" :disabled="type1Disabled">菜单</el-radio>
            <el-radio :label="2" :disabled="type2Disabled">按钮</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="菜单名称" prop="name">
          <el-input v-model="sysMenu.name"/>
        </el-form-item>
        <el-form-item label="图标" prop="icon" v-if="sysMenu.type !== 2">
          <el-select v-model="sysMenu.icon" clearable>
            <el-option v-for="item in iconList" :key="item.class" :label="item.class" :value="item.class">
            <span style="float: left;">
             <i :class="item.class"></i>  <!-- 如果动态显示图标,这里添加判断 -->
            </span>
              <span style="padding-left: 6px;">{{ item.class }}</span>
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="排序">
          <el-input-number v-model="sysMenu.sortValue" controls-position="right" :min="0" />
        </el-form-item>
        <el-form-item prop="path">
              <span slot="label">
                <el-tooltip content="访问的路由地址,如:`sysUser`" placement="top">
                <i class="el-icon-question"></i>
                </el-tooltip>
                路由地址
              </span>
          <el-input v-model="sysMenu.path" placeholder="请输入路由地址" />
        </el-form-item>
        <el-form-item prop="component" v-if="sysMenu.type !== 0">
              <span slot="label">
                <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
                <i class="el-icon-question"></i>
                </el-tooltip>
                组件路径
              </span>
          <el-input v-model="sysMenu.component" placeholder="请输入组件路径" />
        </el-form-item>
        <el-form-item v-if="sysMenu.type === 2">
          <el-input v-model="sysMenu.perms" placeholder="请输入权限标识" maxlength="100" />
          <span slot="label">
                <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(hasAuthority('bnt.sysRole.list'))" placement="top">
                <i class="el-icon-question"></i>
                </el-tooltip>
                权限字符
              </span>
        </el-form-item>
        <el-form-item label="状态" prop="type">
          <el-radio-group v-model="sysMenu.status">
            <el-radio :label="1">正常</el-radio>
            <el-radio :label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
        <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>


<script>
import api from '@/api/system/sysMenu'
const defaultForm = {
  id: '',
  parentId: '',
  name: '',
  type: 0,
  path: '',
  component: '',
  perms: '',
  icon: '',
  sortValue: 1,
  status: 1
}
export default {
  // 定义数据
  data() {
    return {
      sysMenuList: [],
      expandKeys: [], // 需要自动展开的项

      typeDisabled: false,
      type0Disabled: false,
      type1Disabled: false,
      type2Disabled: false,
      dialogTitle: '',

      dialogVisible: false,
      sysMenu: defaultForm,
      saveBtnDisabled: false,

      iconList: [
        {
          class: "el-icon-s-tools",
        },
        {
          class: "el-icon-s-custom",
        },
        {
          class: "el-icon-setting",
        },
        {
          class: "el-icon-user-solid",
        },
        {
          class: "el-icon-s-help",
        },
        {
          class: "el-icon-phone",
        },
        {
          class: "el-icon-s-unfold",
        },
        {
          class: "el-icon-s-operation",
        },
        {
          class: "el-icon-more-outline",
        },
        {
          class: "el-icon-s-check",
        },
        {
          class: "el-icon-tickets",
        },
        {
          class: "el-icon-s-goods",
        },
        {
          class: "el-icon-document-remove",
        },
        {
          class: "el-icon-warning",
        },
        {
          class: "el-icon-warning-outline",
        },
        {
          class: "el-icon-question",
        },
        {
          class: "el-icon-info",
        }
      ]
    }
  },

  // 当页面加载时获取数据
  created() {
    this.fetchData()
  },

  methods: {
    // 调用api层获取数据库中的数据
    fetchData() {
      console.log('加载列表')
      api.findNodes().then(response => {
        this.sysMenuList = response.data
        console.log(this.sysMenuList)
      })
    },

    // 根据id删除数据
    removeDataById(id) {
      // debugger
      this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => { // promise
        // 点击确定,远程调用ajax
        return api.removeById(id)
      }).then((response) => {
        this.fetchData()
        this.$message({
          type: 'success',
          message: '删除成功!'
        })
      }).catch(() => {
         this.$message.info('取消删除')
      })
    },

    // -------------
    add(row){
      debugger
      this.typeDisabled = false
      this.dialogTitle = '添加下级节点'
      this.dialogVisible = true

      this.sysMenu = Object.assign({}, defaultForm)
      this.sysMenu.id = ''
      if(row) {
        this.sysMenu.parentId = row.id
        this.sysMenu.parentName = row.name
        //this.sysMenu.component = 'ParentView'
        if(row.type === 0) {
          this.sysMenu.type = 1
          this.typeDisabled = false
          this.type0Disabled = false
          this.type1Disabled = false
          this.type2Disabled = true
        } else if(row.type === 1) {
          this.sysMenu.type = 2
          this.typeDisabled = true
        }
      } else {
        this.dialogTitle = '添加目录节点'
        this.sysMenu.type = 0
        this.sysMenu.component = 'Layout'
        this.sysMenu.parentId = 0
        this.typeDisabled = true
      }
    },

    edit(row) {
      debugger
      this.dialogTitle = '修改节点'
      this.dialogVisible = true

      this.sysMenu = Object.assign({}, row)
      this.typeDisabled = true
    },

    saveOrUpdate() {
      if(this.sysMenu.type === 0 && this.sysMenu.parentId !== 0) {
        this.sysMenu.component = 'ParentView'
      }
      this.$refs.dataForm.validate(valid => {
        if (valid) {
          this.saveBtnDisabled = true // 防止表单重复提交
          if (!this.sysMenu.id) {
            this.saveData()
          } else {
            this.updateData()
          }
        }
      })
    },

    // 新增
    saveData() {
      api.save(this.sysMenu).then(response => {
        this.$message.success(response.message || '操作成功')
        this.dialogVisible = false
        this.fetchData(this.page)
      })
    },

    // 根据id更新记录
    updateData() {
      api.updateById(this.sysMenu).then(response => {
        this.$message.success(response.message || '操作成功')
        this.dialogVisible = false
        this.fetchData()
      })
    }
  }
}
</script>

4.4 测试  39

浏览器输入http://localhost:9528/

菜单管理_Layout_17

添加一个菜单

菜单管理_ico_18

菜单管理_User_19

菜单管理_User_20

修改

菜单管理_User_21

删除

菜单管理_Layout_22

5. 给角色分配权限  40

菜单管理_ico_23

5.1 接口功能实现   40

5.1.1 查询所有菜单和角色分配的菜单    40

用于数据回显

service-oa模块

SysMenuController

//查询所有菜单和角色分配的菜单    40
    @ApiOperation("查询所有菜单和角色分配的菜单")
    @GetMapping("toAssign/{roleId}")
    public Result toAssign(@PathVariable Long roleId) {
        List<SysMenu> list = sysMenuService.findMenuByRoleId(roleId);
        return Result.ok(list);
    }

业务层接口SysMenuService

//查询所有菜单和角色分配的菜单    40
    List<SysMenu> findMenuByRoleId(Long roleId);

业务层接口实现类SysMenuServiceImpl      41

菜单管理_User_24

//查询所有菜单和角色分配的菜单    41
    @Override
    public List<SysMenu> findMenuByRoleId(Long roleId) {
        //1 查询所有菜单- 添加条件 status=1
        LambdaQueryWrapper<SysMenu> wrapperSysMenu = new LambdaQueryWrapper<>();
        wrapperSysMenu.eq(SysMenu::getStatus,1);
        List<SysMenu> allSysMenuList = baseMapper.selectList(wrapperSysMenu);

        //2 根据角色id roleId查询 角色菜单关系表里面 角色id对应所有的菜单id
        LambdaQueryWrapper<SysRoleMenu> wrapperSysRoleMenu = new LambdaQueryWrapper<>();
        wrapperSysRoleMenu.eq(SysRoleMenu::getRoleId,roleId);
        //得到所有的菜单角色关系对象集合
        List<SysRoleMenu> sysRoleMenuList = sysRoleMenuService.list(wrapperSysRoleMenu);


        //3 遍历菜单角色关系对象集合,得到所有相应的菜单id集合,用于获取对应菜单对象
        List<Long> menuIdList = sysRoleMenuList.stream().map(c -> 
                c.getMenuId()).collect(Collectors.toList());

        //3.1 拿着菜单id 和所有菜单集合里面id进行比较,如果相同封装
        //注意这个setSelect属性很巧妙,通过他的true和false可以知道菜单是否被选中(即该角色是否有此菜单)
        allSysMenuList.stream().forEach(item -> {
            if(menuIdList.contains(item.getId())) {
                item.setSelect(true);//表示该角色有此菜单
            } else {
                item.setSelect(false);
            }
        });

        //4 返回规定树形显示格式菜单列表
        List<SysMenu> sysMenuList = MenuHelper.buildTree(allSysMenuList);
        return sysMenuList;
    }

5.1.2 给角色分配菜单  40

数据传输对象AssginMenuVo

package com.atguigu.vo.system;


import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@ApiModel(description = "分配菜单")
@Data
public class AssginMenuVo {

    @ApiModelProperty(value = "角色id")
    private Long roleId;

    @ApiModelProperty(value = "菜单id列表")
    private List<Long> menuIdList;

}

service-oa模块

SysMenuController

//给角色分配菜单  40
    @ApiOperation("角色分配菜单")
    @PostMapping("/doAssign")
    public Result doAssign(@RequestBody AssginMenuVo assginMenuVo) {
        sysMenuService.doAssign(assginMenuVo);
        return Result.ok();
    }

业务层接口SysMenuService

//给角色分配菜单  40
    void doAssign(AssginMenuVo assginMenuVo);

业务层接口实现类SysMenuServiceImpl    41

//给角色分配菜单  41
    @Override
    public void doAssign(AssginMenuVo assginMenuVo) {
        //1 根据角色id 删除菜单角色表 方便分配数据
        LambdaQueryWrapper<SysRoleMenu> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysRoleMenu::getRoleId,assginMenuVo.getRoleId());
        sysRoleMenuService.remove(wrapper);

        //2 从参数里面获取角色新分配菜单id列表,
        // 进行遍历,把每个id数据添加菜单角色表
        List<Long> menuIdList = assginMenuVo.getMenuIdList();
        for(Long menuId:menuIdList) {
            if(StringUtils.isEmpty(menuId)) {
                continue;
            }
            SysRoleMenu sysRoleMenu = new SysRoleMenu();
            sysRoleMenu.setMenuId(menuId);
            sysRoleMenu.setRoleId(assginMenuVo.getRoleId());
            sysRoleMenuService.save(sysRoleMenu);
        }
    }

5.2 前端   41

5.2.1 添加路由

修改 src/router/index.js 文件

菜单管理_Layout_25

index.js

hidden: true代表是个隐藏路由不进行显示

{
  path: 'assignAuth',
  component: () => import('@/views/system/sysRole/assignAuth'),
  meta: {
    activeMenu: '/system/sysRole',
    title: '角色授权'
  },
  hidden: true,
}

5.2.2 角色列表添加按钮及方法   42

src/views/system/sysRole/list.vue

list.vue

菜单管理_ico_26

<el-button type="warning" icon="el-icon-baseball" size="mini" @click="showAssignAuth(scope.row)" title="分配权限"/>

菜单管理_ico_27

//给角色分配菜单,跳转到分配菜单的页面   41
        showAssignAuth(row) {
            this.$router.push('/system/assignAuth?id='+row.id+'&roleName='+row.roleName);
        }

5.2.3 添加api   42

 src/api/system/sysMenu.js

sysMenu.js

/*
查看某个角色的权限列表
*/
toAssign(roleId) {
  return request({
    url: `${api_name}/toAssign/${roleId}`,
    method: 'get'
  })
},

/*
给某个角色授权
*/
doAssign(assginMenuVo) {
  return request({
    url: `${api_name}/doAssign`,
    method: "post",
    data: assginMenuVo
  })
}

5.2.4 实现页面功能   42

菜单管理_Layout_28

创建src/views/system/sysRole/assignAuth.vue

assignAuth.vue

<template>
  <div class="app-container">
    <div style="padding: 20px 20px 0 20px;">
      授权角色:{{ $route.query.roleName }}
    </div>
    <el-tree
      style="margin: 20px 0"
      ref="tree"
      :data="sysMenuList"
      node-key="id"
      show-checkbox
      default-expand-all
      :props="defaultProps"
    />
    <div style="padding: 20px 20px;">
      <el-button :loading="loading" type="primary" icon="el-icon-check" size="mini" @click="save">保存</el-button>
      <el-button @click="$router.push('/system/sysRole')" size="mini" icon="el-icon-refresh-right">返回</el-button>
    </div>
  </div>
</template>
<script>
  import api from '@/api/system/sysMenu'
  export default {
    name: 'roleAuth',

    data() {
      return {
        loading: false, // 用来标识是否正在保存请求中的标识, 防止重复提交
        sysMenuList: [], // 所有
        defaultProps: {
          children: 'children',
          label: 'name'
        },
      };
    },

    created() {
      this.fetchData()
    },

    methods: {
      /*
      初始化
      */
      fetchData() {
        const roleId = this.$route.query.id
        api.toAssign(roleId).then(result => {
          const sysMenuList = result.data
          this.sysMenuList = sysMenuList
          const checkedIds = this.getCheckedIds(sysMenuList)
          console.log('getPermissions() checkedIds', checkedIds)
          this.$refs.tree.setCheckedKeys(checkedIds)
        })
      },

      /*
      得到所有选中的id列表
      */
      getCheckedIds (auths, initArr = []) {
        return auths.reduce((pre, item) => {
          if (item.select && item.children.length === 0) {
            pre.push(item.id)
          } else if (item.children) {
            this.getCheckedIds(item.children, initArr)
          }
          return pre
        }, initArr)
      },

      /*
      保存权限列表
      */
      save() {
        debugger
        //获取到当前子节点
        //const checkedNodes = this.$refs.tree.getCheckedNodes()
        //获取到当前子节点及父节点
        const allCheckedNodes = this.$refs.tree.getCheckedNodes(false, true);
        let idList = allCheckedNodes.map(node => node.id);
        console.log(idList)
        let assginMenuVo = {
          roleId: this.$route.query.id,
          menuIdList: idList
        }
        this.loading = true
        api.doAssign(assginMenuVo).then(result => {
          this.loading = false
          this.$message.success(result.$message || '分配权限成功')
          this.$router.push('/system/sysRole');
        })
      }
    }
  };
</script>

把项目中的警告去掉,这一步做不做都可以运行没影响

菜单管理_ico_29

5.3 测试   42

浏览器输入 http://localhost:9528/

菜单管理_User_30

为管理员分配菜单

菜单管理_ico_31

菜单管理_ico_32

菜单管理_ico_33

再次分配

菜单管理_ico_34

菜单管理_ico_35