SpringBoot整合SpringSecurity实现权限控制(六):菜单管理

x33g5p2x  于2021-10-25 转载在 Spring  
字(23.6k)|赞(0)|评价(0)|浏览(550)

一、前言

  • 后台管理系统可以通过菜单管理来实现系统的功能模块管理。通过清晰的树形菜单结构展现各种系统功能,无疑会大大提升系统的使用效率。

二、需求分析

  1. 系统功能模块需要按各个分类,形成菜单结构。比如说系统管理分类目录下,存在用户管理、角色管理、菜单管理等功能;系统设置分类目录下,存在商品设置、仓库设置、储位设置等功能。

  1. 每个菜单都需要包含以下信息:菜单id,菜单名称,父级菜单id,路由地址(vue-router),组件页面,图标,排序顺序等

3、菜单管理需要实现基本的增删改查

三、后端实现

3.1 创建菜单实体表

  • 根据菜单的基本信息,创建菜单实体类。
/** * 菜单表 * * @author zhuhuix * @date 2021-10-06 */
@ApiModel(value = "菜单表")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_menu")
public class SysMenu {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String name;

    private String path;

    private String component;

    private String type;

    @TableField(value = "p_id", updateStrategy = FieldStrategy.IGNORED,jdbcType = JdbcType.BIGINT)
    private Long pid;

    private String icon;

    private Integer sort;

    private Boolean hidden;

    private Boolean cache;

    private String redirect;

    private String url;

    private Integer level;

    @JsonIgnore
    @Builder.Default
    @TableLogic
    private Boolean enabled = true;

    private Timestamp createTime;

    @Builder.Default
    private Timestamp updateTime = Timestamp.valueOf(LocalDateTime.now());

    public String getLabel() {
        return name;
    }
}

3.2 添加操作菜单表的Mapper接口

  • 通过继承mybatis-plus的BaseMapper接口创建操作菜单表的DAO接口,该BaseMapper接口已经包含了基本的增删改查操作。
/** * 菜单DAO接口 * * @author zhuhuix * @date 2021-10-06 */
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenu> {

    /** * 根据父级菜单id查出下级菜单 * @param pid 父级菜单id * @return 下级菜单列表 */
    @Select("select * from sys_menu where p_id=#{pid} ")
    List<SysMenu> selectChilds(Long pid);
}

3.3 实现菜单的增删改查服务

  • 服务接口定义:
/** * 菜单资源服务接口 * * @author zhuhuix * @date 2021-10-06 */
public interface SysMenuService {

    /** * 创建菜单 * * @param menu 待新增的菜单 * @return 新增成功的菜单 */
    SysMenu create (SysMenu menu);

    /** * 删除菜单 * * @param ids 菜单id列表 * @return 是否删除成功 */
    Boolean delete (Set<Long> ids);

    /** * 更新菜单 * * @param menu 待更新的菜单 * @return 更新成功的菜单 */
    SysMenu update (SysMenu menu);

    /** * 根据id查找菜单 * * @param id 菜单id * @return 查找到的菜单 */
    SysMenu findById(Long id);

    /** * 根据菜单名称查找菜单 * * @param name 菜单名称 * @return 查找到的菜单 */
    SysMenu findByName(String name);

    /** * 根据菜单完整路由获取菜单信息 * * @param path 路由 * @param pId 父菜单 * @return 完整路由 */
    SysMenu findByMenuPath(String path,Long pId);

    /** * 根据查询条件查找菜单信息 * * @param sysMenuQueryDto 查询条件 * @return 菜单列表 */
    List<SysMenu> list(SysMenuQueryDto sysMenuQueryDto);
}
  • 服务实现类:
/** * 菜单资源服务实现类 * * @author zhuhuix * @date 2021-10-06 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class SysMenuServiceImpl implements SysMenuService {

    private final SysMenuMapper sysMenuMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysMenu create(SysMenu menu) {
        if (findByName(menu.getName()) != null) {
            throw new RuntimeException("该菜单名称已存在,不得重复添加!!");
        }
        if (findByMenuPath(menu.getPath(), menu.getPid()) != null) {
            throw new RuntimeException("该菜单路由已存在,不得重复添加!!");
        }
        menu.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
        if (sysMenuMapper.insert(menu) > 0) {
            return menu;
        }

        throw new RuntimeException("增加菜单失败!!");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(Set<Long> ids) {
        if (sysMenuMapper.deleteBatchIds(ids) > 0) {
            return true;
        }
        throw new RuntimeException("删除菜单失败!!");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysMenu update(SysMenu menu) {
        SysMenu sysMenu = findByName(menu.getName());
        if (sysMenu != null && !sysMenu.getId().equals(menu.getId())) {
            throw new RuntimeException("该菜单名称已存在,不得重复添加!!");
        }
        sysMenu = findByMenuPath(menu.getPath(), menu.getId());
        if (sysMenu != null && !sysMenu.getId().equals(menu.getId())) {
            throw new RuntimeException("该菜单路由已存在,不得重复添加!!");
        }

        // 判断修改菜单的上级菜单不能是该修改菜单原有的子菜单
        if (menu.getPid() != null) {
            List<SysMenu> childMenus = new ArrayList<>();
            childLoop(menu.getId(), childMenus);
            if (childMenus.stream().filter(m -> m.getId().equals(menu.getPid())).count() > 0) {
                throw new RuntimeException("上级菜单不能设置为下级子菜单,防止引起嵌套循环错误!!");
            }
        }

        if (sysMenuMapper.updateById(menu) > 0) {
            return menu;
        }
        throw new RuntimeException("更新菜单失败!!");

    }

    /** * 返回菜单下所有的子菜单 * * @param id 菜单id */
    private void childLoop(Long id, List<SysMenu> childMenus) {
        List<SysMenu> sysMenus = sysMenuMapper.selectChilds(id);
        if (sysMenus == null || sysMenus.size() ==0) {
            return;
        }
        for (SysMenu m : sysMenus) {
            childMenus.add(m);
            childLoop(m.getId(), childMenus);
        }

    }

    @Override
    public SysMenu findById(Long id) {
        return sysMenuMapper.selectById(id);
    }

    @Override
    public SysMenu findByName(String name) {
        return sysMenuMapper.selectOne(new QueryWrapper<SysMenu>().lambda().eq(SysMenu::getName, name));
    }

    @Override
    public SysMenu findByMenuPath(String path, Long pId) {
        return sysMenuMapper.selectOne(new QueryWrapper<SysMenu>().lambda().eq(SysMenu::getPath, path)
                .and(wrapper -> wrapper.eq(SysMenu::getPid, pId)));
    }

    @Override
    public List<SysMenu> list(SysMenuQueryDto sysMenuQueryDto) {
        QueryWrapper<SysMenu> queryWrapper = new QueryWrapper<>();
        if (!StringUtils.isEmpty(sysMenuQueryDto.getName())) {
            queryWrapper.lambda().like(SysMenu::getName, sysMenuQueryDto.getName());
        }

        if (!StringUtils.isEmpty(sysMenuQueryDto.getCreateTimeStart())
                && !StringUtils.isEmpty(sysMenuQueryDto.getCreateTimeEnd())) {
            queryWrapper.lambda().between(SysMenu::getCreateTime,
                    new Timestamp(sysMenuQueryDto.getCreateTimeStart()),
                    new Timestamp(sysMenuQueryDto.getCreateTimeEnd()));
        }

        return sysMenuMapper.selectList(queryWrapper);
    }
}

3.4 编写Controller层

  • 形成以下API访问接口

/** * api菜单资源 * * @author zhuhuix * @date 2021-10-16 */
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/api/menu")
@Api(tags = "菜单资源接口")
public class SysMenuController {

    private final SysMenuService sysMenuService;

    @ApiOperation("根据件查询菜单资源")
    @PostMapping("/list")
    public ResponseEntity<Object> getMenuList(@RequestBody SysMenuQueryDto sysMenuQueryDto) {
        return ResponseEntity.ok(sysMenuService.list(sysMenuQueryDto));
    }

    @ApiOperation("根据id获取单个菜单资源")
    @GetMapping("{id}")
    public ResponseEntity<Object> getMenuById(@PathVariable Long id) {
        return ResponseEntity.ok(sysMenuService.findById(id));
    }

    @ApiOperation("保存菜单资源")
    @PostMapping
    public ResponseEntity<Object> saveMenu(@RequestBody SysMenu sysMenu) {
        if (sysMenu.getId() != null) {
            return ResponseEntity.ok(sysMenuService.update(sysMenu));
        } else {
            return ResponseEntity.ok(sysMenuService.create(sysMenu));
        }
    }

    @ApiOperation("删除菜单资源")
    @DeleteMapping
    public ResponseEntity<Object> deleteMenu(@RequestBody Set<Long> ids) {
        return ResponseEntity.ok(sysMenuService.delete(ids));
    }
}

四、前端实现

4.1 添加菜单api访问接口

  • 根据后端的API在前端添加相应的访问接口
// menu.js 
import request from '@/utils/request'
// 根据条件查询
export function getMenuList(params) {
  return request({
    url: '/api/menu/list',
    method: 'post',
    data: JSON.stringify(params)
  })
}
// 根据菜单id获取菜单信息
export function getMenuById(id) {
  return request({
    url: '/api/menu/' + id,
    method: 'get'
  })
}
// 保存菜单信息
export function saveMenu(data) {
  return request({
    url: '/api/menu',
    method: 'post',
    data
  })
}
// 删除菜单
export function deleteMenu(ids) {
  return request({
    url: '/api/menu',
    method: 'delete',
    data: ids
  })
}

4.2 编写前端页面

  1. 构成查询条件,增删改查按钮与菜单树形表的布局

  1. 树形菜单可以展开或折叠

  1. 点击增加或编辑菜单按钮,填写相应菜单信息后,进行保存。

  • 专门编写了一个图标选择组件,该组件可以选择element-ui自带的图标

// SelectIcon.js
<template>
  <div class="ui-fas">
    <el-input v-model="name" class="inputIcon" suffix-icon="el-icon-search" placeholder="请输入图标名称" @input.native="filterIcons" />
    <ul class="fas-icon-list">
      <li v-for="(item, index) in icons" :key="index" @click="selectedIcon(item)">
        <i class="fas" :class="[item]" />
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  name: 'CompIcons',
  data() {
    return {
      name: '',
      icons: [],
      iconList: ['el-icon-platform-eleme', 'el-icon-delete-solid', 'el-icon-eleme', 'el-icon-c-scale-to-original', 'el-icon-sort-up', 'el-icon-sort-down', 'el-icon-upload', 'el-icon-goods', 'el-icon-video-pause', 'el-icon-video-play', 'el-icon-s-cooperation', 'el-icon-s-order', 'el-icon-s-platform', 'el-icon-s-unfold', 'el-icon-s-operation', 'el-icon-s-promotion', 'el-icon-s-home', 'el-icon-s-release', 'el-icon-s-ticket', 'el-icon-s-management', 'el-icon-s-open', 'el-icon-s-shop', 'el-icon-s-help', 'el-icon-s-goods', 'el-icon-s-marketing', 'el-icon-s-flag', 'el-icon-s-comment', 'el-icon-s-finance', 'el-icon-s-claim', 'el-icon-s-tools', 'el-icon-s-custom', 'el-icon-s-opportunity', 'el-icon-s-fold', 'el-icon-s-data', 'el-icon-s-check', 'el-icon-s-grid', 'el-icon-user-solid', 'el-icon-question', 'el-icon-warning', 'el-icon-remove', 'el-icon-info', 'el-icon-circle-plus', 'el-icon-picture', 'el-icon-location', 'el-icon-error', 'el-icon-success', 'el-icon-camera-solid', 'el-icon-d-caret', 'el-icon-message-solid', 'el-icon-menu', 'el-icon-star-on', 'el-icon-video-camera-solid', 'el-icon-phone', 'el-icon-more', 'el-icon-share', 'el-icon-caret-left', 'el-icon-caret-right', 'el-icon-caret-bottom', 'el-icon-caret-top', 'el-icon-date', 'el-icon-circle-close', 'el-icon-edit', 'el-icon-folder', 'el-icon-folder-opened', 'el-icon-folder-add', 'el-icon-folder-remove', 'el-icon-folder-delete', 'el-icon-folder-checked', 'el-icon-tickets', 'el-icon-document-remove', 'el-icon-document-delete', 'el-icon-document-copy', 'el-icon-document-checked', 'el-icon-document', 'el-icon-document-add', 'el-icon-printer', 'el-icon-paperclip', 'el-icon-download', 'el-icon-upload2', 'el-icon-takeaway-box', 'el-icon-camera', 'el-icon-search', 'el-icon-zoom-in', 'el-icon-zoom-out', 'el-icon-monitor', 'el-icon-attract', 'el-icon-mobile', 'el-icon-video-camera', 'el-icon-scissors', 'el-icon-umbrella', 'el-icon-headset', 'el-icon-brush', 'el-icon-data-line', 'el-icon-mouse', 'el-icon-coordinate', 'el-icon-magic-stick', 'el-icon-reading', 'el-icon-data-board', 'el-icon-pie-chart', 'el-icon-data-analysis', 'el-icon-collection-tag', 'el-icon-edit-outline', 'el-icon-film', 'el-icon-suitcase', 'el-icon-suitcase-1', 'el-icon-picture-outline-round', 'el-icon-picture-outline', 'el-icon-receiving', 'el-icon-collection', 'el-icon-files', 'el-icon-notebook-1', 'el-icon-notebook-2', 'el-icon-toilet-paper', 'el-icon-office-building', 'el-icon-school', 'el-icon-table-lamp', 'el-icon-house', 'el-icon-no-smoking', 'el-icon-smoking', 'el-icon-shopping-cart-full', 'el-icon-shopping-cart-1', 'el-icon-shopping-cart-2', 'el-icon-shopping-bag-1', 'el-icon-shopping-bag-2', 'el-icon-present', 'el-icon-box', 'el-icon-bank-card', 'el-icon-money', 'el-icon-coin', 'el-icon-wallet', 'el-icon-discount', 'el-icon-price-tag', 'el-icon-bicycle', 'el-icon-truck', 'el-icon-ship', 'el-icon-news', 'el-icon-help', 'el-icon-guide', 'el-icon-male', 'el-icon-female', 'el-icon-thumb', 'el-icon-cpu', 'el-icon-link', 'el-icon-connection', 'el-icon-open', 'el-icon-turn-off', 'el-icon-set-up', 'el-icon-chat-round', 'el-icon-chat-line-round', 'el-icon-chat-square', 'el-icon-chat-dot-round', 'el-icon-chat-dot-square', 'el-icon-chat-line-square', 'el-icon-message', 'el-icon-postcard', 'el-icon-position', 'el-icon-turn-off-microphone', 'el-icon-microphone', 'el-icon-close-notification', 'el-icon-bell', 'el-icon-bangzhu', 'el-icon-circle-plus-outline', 'el-icon-remove-outline', 'el-icon-circle-check', 'el-icon-time', 'el-icon-odometer', 'el-icon-crop', 'el-icon-aim', 'el-icon-switch-button', 'el-icon-full-screen', 'el-icon-copy-document', 'el-icon-star-off', 'el-icon-basketball', 'el-icon-football', 'el-icon-soccer', 'el-icon-baseball', 'el-icon-mic', 'el-icon-stopwatch', 'el-icon-medal-1', 'el-icon-medal', 'el-icon-trophy', 'el-icon-trophy-1', 'el-icon-first-aid-kit', 'el-icon-discover', 'el-icon-place', 'el-icon-location-outline', 'el-icon-location-information', 'el-icon-add-location', 'el-icon-delete-location', 'el-icon-map-location', 'el-icon-alarm-clock', 'el-icon-timer', 'el-icon-watch-1', 'el-icon-watch', 'el-icon-wind-power', 'el-icon-light-rain', 'el-icon-lightning', 'el-icon-heavy-rain', 'el-icon-sunrise', 'el-icon-sunrise-1', 'el-icon-sunset', 'el-icon-sunny', 'el-icon-cloudy', 'el-icon-partly-cloudy', 'el-icon-cloudy-and-sunny', 'el-icon-moon', 'el-icon-moon-night', 'el-icon-bottom-left', 'el-icon-bottom-right', 'el-icon-bottom', 'el-icon-back', 'el-icon-right', 'el-icon-top-left', 'el-icon-top-right', 'el-icon-top', 'el-icon-lock', 'el-icon-unlock', 'el-icon-user', 'el-icon-key', 'el-icon-arrow-up', 'el-icon-arrow-right', 'el-icon-arrow-down', 'el-icon-arrow-left', 'el-icon-d-arrow-left', 'el-icon-d-arrow-right', 'el-icon-close', 'el-icon-check', 'el-icon-plus', 'el-icon-minus', 'el-icon-delete', 'el-icon-sold-out', 'el-icon-sell', 'el-icon-service', 'el-icon-mobile-phone', 'el-icon-sort', 'el-icon-rank', 'el-icon-refresh', 'el-icon-loading', 'el-icon-view', 'el-icon-finished', 'el-icon-more-outline', 'el-icon-phone-outline', 'el-icon-setting', 'el-icon-warning-outline', 'el-icon-refresh-right', 'el-icon-refresh-left', 'el-icon-dish', 'el-icon-dish-1', 'el-icon-food', 'el-icon-chicken', 'el-icon-fork-spoon', 'el-icon-knife-fork', 'el-icon-burger', 'el-icon-tableware', 'el-icon-sugar', 'el-icon-dessert', 'el-icon-ice-cream', 'el-icon-hot-water', 'el-icon-water-cup', 'el-icon-coffee-cup', 'el-icon-cold-drink', 'el-icon-goblet', 'el-icon-goblet-full', 'el-icon-goblet-square', 'el-icon-goblet-square-full', 'el-icon-refrigerator', 'el-icon-grape', 'el-icon-watermelon', 'el-icon-cherry', 'el-icon-apple', 'el-icon-pear', 'el-icon-orange', 'el-icon-coffee', 'el-icon-ice-tea', 'el-icon-ice-drink', 'el-icon-milk-tea', 'el-icon-potato-strips', 'el-icon-lollipop', 'el-icon-ice-cream-square', 'el-icon-ice-cream-round']

    }
  },
  created() {
    this.icons = this.iconList
  },
  methods: {
    filterIcons() {
      if (this.name) {
        this.icons = this.iconList.filter(item => item.includes(this.name))
      } else {
        this.icons = this.iconList
      }
    },
    selectedIcon(name) {
      this.$emit('selected', name)
      document.body.click()
    },
    reset() {
      this.name = ''
      this.icons = this.iconList
    }
  }
}
</script>
<style>
.inputIcon{
    width: 100%;
    height: 30px;
    margin-bottom: 10px;
}
.ui-fas{
    height: 300px;
    overflow: hidden;
}
.fas-icon-list{
    height: 100%;
    overflow:scroll;
    list-style: none;
}
.fas-icon-list li {
    float: left;
    margin:10px 10px;
}
.fas{
    font-size: 20px;
    color:#1989fa;
    cursor: pointer;
}
</style>

上级菜单选择时,引用了TreeSelect组件,使用可参考https://www.vue-treeselect.cn/

前端完整代码
– /src/menu/index.vue

<template>
  <div class="app-container">
    <!--工具栏-->
    <div class="head-container">
      <!-- 搜索 -->
      <el-input
        v-model="name"
        size="small"
        clearable
        placeholder="输入菜单名称搜索"
        style="width: 200px"
        class="filter-item"
        @keyup.enter.native="doQuery"
      />
      <el-date-picker
        v-model="createTime"
        :default-time="['00:00:00', '23:59:59']"
        type="daterange"
        range-separator=":"
        size="small"
        class="date-item"
        value-format="yyyy-MM-dd HH:mm:ss"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
      />
      <el-button
        class="filter-item"
        size="mini"
        type="success"
        icon="el-icon-search"
        @click="doQuery"
      >搜索</el-button>
      <el-button
        class="filter-item"
        size="mini"
        type="primary"
        icon="el-icon-document-add"
        @click="doAdd"
      >增加</el-button>
      <el-button
        class="filter-item"
        size="mini"
        type="danger"
        icon="el-icon-circle-plus-outline"
        :disabled="selections.length === 0"
        @click="doDelete"
      >删除{{ selections.length }}</el-button>
    </div>

    <el-row>
      <!-- 表单渲染 -->
      <el-dialog
        append-to-body
        :close-on-click-modal="false"
        :visible.sync="showDialog"
        width="620px"
      >
        <el-form
          ref="form"
          :inline="true"
          :model="form"
          :rules="rules"
          size="small"
          label-width="80px"
        >
          <el-form-item label="菜单名称" prop="name">
            <el-input v-model="form.name" />
          </el-form-item>
          <el-form-item label="路由地址" prop="path">
            <el-input
              v-model="form.path"
              placeholder="根目录菜单需前置加斜杠/"
            />
          </el-form-item>
          <el-form-item label="组件路径" prop="component">
            <el-input
              v-model="form.component"
              placeholder="根目录菜单输入Layout"
            />
          </el-form-item>
          <el-form-item label="菜单排序" prop="sort">
            <el-input-number
              v-model.number="form.sort"
              :min="0"
              :max="999"
              controls-position="right"
              style="width: 185px"
            />
          </el-form-item>
          <el-form-item label="菜单图标">
            <el-popover
              placement="bottom-start"
              width="450"
              trigger="click"
              @show="$refs['iconSelect'].reset()"
            >
              <el-input
                slot="reference"
                v-model="form.icon"
                placeholder="请选择菜单图标"
                readonly
                style="cursor: pointer; width: 460px"
              >
                <template slot="prepend">
                  <i
                    v-if="form.icon && form.icon.includes('el-icon')"
                    :class="form.icon"
                  />
                  <svg-icon v-else :icon-class="form.icon ? form.icon : ''" />
                </template>
              </el-input>
              <select-icon ref="iconSelect" @selected="selected" />
            </el-popover>
          </el-form-item>
          <el-form-item label="上级菜单" prop="pid">
            <treeselect
              v-model="form.pid"
              :options="menuTree"
              :show-count="true"
              style="width: 460px"
              placeholder="选择上级菜单"
            />
          </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button type="text" @click="doCancel">取消</el-button>
          <el-button
            :loading="formLoading"
            type="primary"
            @click="doSubmit(form)"
          >确认</el-button>
        </div>
      </el-dialog>
      <el-tabs v-model="activeName" type="border-card">
        <el-tab-pane label="菜单列表" name="menuList">
          <el-table
            ref="table"
            v-loading="loading"
            :data="menuTree"
            row-key="id"
            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
            style="width: 100%; font-size: 12px"
            @selection-change="selectionChangeHandler"
            @select="selectChange"
            @select-all="selectAllChange"
          >
            <el-table-column type="selection" width="55" />
            <el-table-column
              :show-overflow-tooltip="true"
              width="150"
              prop="name"
              label="菜单名称"
            />
            <el-table-column
              :show-overflow-tooltip="true"
              width="150"
              prop="path"
              label="路由地址"
            />
            <el-table-column
              :show-overflow-tooltip="true"
              prop="component"
              width="150"
              label="组件路径"
            />
            <el-table-column
              prop="icon"
              label="菜单图标"
              align="center"
              width="80px"
            >
              <template slot-scope="scope">
                <i
                  v-if="scope.row.icon.includes('el-icon')"
                  :class="scope.row.icon ? scope.row.icon : ''"
                />
                <svg-icon
                  v-else
                  :icon-class="scope.row.icon ? scope.row.icon : ''"
                />
              </template>
            </el-table-column>
            <el-table-column prop="sort" align="center" label="菜单排序">
              <template slot-scope="scope">
                {{ scope.row.sort }}
              </template>
            </el-table-column>
            <el-table-column
              :show-overflow-tooltip="true"
              prop="createTime"
              width="155"
              label="创建日期"
            >
              <template slot-scope="scope">
                <span>{{ parseTime(scope.row.createTime) }}</span>
              </template>
            </el-table-column>
            <el-table-column
              label="操作"
              width="160"
              align="center"
              fixed="right"
            >
              <template slot-scope="scope">
                <el-button
                  size="mini"
                  type="text"
                  round
                  @click="doEdit(scope.row.id)"
                >编辑菜单</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-tab-pane>
      </el-tabs>
    </el-row>
  </div>
</template>

<script>
import SelectIcon from '@/components/SelectIcon'
import { mapGetters } from 'vuex'
import { parseTime } from '@/utils/index'
import { getMenuList, getMenuById, saveMenu, deleteMenu } from '@/api/menu'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
  name: 'Menu',
  components: { SelectIcon, Treeselect },
  data() {
    return {
      activeName: 'menuList',
      showDialog: false,
      loading: false,
      formLoading: false,
      form: {},
      menus: [],
      menuTree: [],
      selections: [],
      name: '',
      createTime: null,
      rules: {
        name: [
          { required: true, message: '请输入菜单名称', trigger: 'blur' }
        ],
        path: [
          { required: true, message: '请输入路由地址', trigger: 'blur' }
        ],
        component: [
          { required: true, message: '请输入组件路径', trigger: 'blur' }
        ]
      }
    }
  },
  computed: {
    ...mapGetters([
      'baseApi'
    ])
  },
  created() {
    var param = { name: '' }
    getMenuList(param).then(res => {
      if (res) {
        this.menuTree = this.ArrayToTreeData(res)
      }
    })
  },
  methods: {
    parseTime,
    doQuery() {
      this.menus = []
      var param = { name: this.name }
      if (this.createTime != null) {
        param.createTimeStart = Date.parse(this.createTime[0])
        param.createTimeEnd = Date.parse(this.createTime[1])
      }
      getMenuList(param).then(res => {
        if (res) {
          this.menus = res
          this.menuTree = this.ArrayToTreeData(res)
        }
      })
    },
    doAdd() {
      this.form = { icon: '' }
      this.showDialog = true
      this.formLoading = false
    },
    doSubmit(menu) {
      this.$refs.form.validate(valid => {
        if (valid) {
          // 判断菜单id与父菜单id是否一样
          if (menu.pid === undefined) {
            menu.pid = null
          }
          if (menu.id && menu.id === menu.pid) {
            this.$notify({
              title: '上级菜单不能是自己',
              type: 'error',
              duration: 2500
            })
            return
          }
          // console.log(menu)
          this.formLoading = true
          saveMenu(menu).then(res => {
            if (res) {
              this.showDialog = false
              this.$notify({
                title: '保存成功',
                type: 'success',
                duration: 2500
              })
              this.doQuery()
            }
          }).catch(() => {
            this.formLoading = false
          })
        }
      })
    },
    doDelete() {
      const ids = []
      this.selections.forEach((res) => {
        ids.push(res.id)
      })
      this.$confirm(`确认删除这些菜单吗?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() =>
        deleteMenu(ids).then(res => {
          if (res) {
            this.$notify({
              title: '删除成功',
              type: 'success',
              duration: 2500
            })
            this.doQuery()
          }
        })
      ).catch(() => {
      })
    },
    // 选择改变
    selectionChangeHandler(val) {
      this.selections = val
    },
    // 选择所有
    selectAllChange(selection) {
      // 如果选中的数目与请求到的数目相同就选中子节点,否则就清空选中
      if (selection && selection.length === this.menuTree.length) {
        selection.forEach(val => {
          this.selectChange(selection, val)
        })
      } else {
        this.$refs.table.clearSelection()
      }
    },
    // 单个选中
    selectChange(selection, row) {
      // 如果selection中存在row代表是选中,否则是取消选中
      if (selection.find(val => { return val.id === row.id })) {
        if (row.children) {
          row.children.forEach(val => {
            this.$refs.table.toggleRowSelection(val, true)

            // 过滤重复值
            let i = 0
            let exist = false
            for (i = 0; i < selection.length; i++) {
              if (selection[i].id === val.id) {
                exist = true
                break
              }
            }
            if (!exist) {
              selection.push(val)
            }

            if (val.children) {
              this.selectChange(selection, val)
            }
          })
        }
      } else {
        this.toggleRowSelection(selection, row)
      }
    },
    // 取消选中
    toggleRowSelection(selection, data) {
      if (data.children) {
        this.$nextTick(() => {
          data.children.forEach(val => {
            this.$refs.table.toggleRowSelection(val, false)
            if (val.children) {
              this.toggleRowSelection(selection, val)
            }
          })
        })
      }
    },

    doEdit(id) {
      this.showDialog = true
      this.formLoading = true
      this.form = {}
      getMenuById(id).then(res => {
        this.form = res
        this.formLoading = false
      })
    },
    doCancel() {
      this.showDialog = false
      this.formLoading = true
      this.form = {}
    },
    ArrayToTreeData(data) {
      const cloneData = JSON.parse(JSON.stringify(data)) // 对源数据深度克隆
      return cloneData.filter(father => {
        const branchArr = cloneData.filter(child => father.id === child.pid) // 返回每一项的子级数组
        branchArr.length > 0 ? father.children = branchArr : '' // 如果存在子级,则给父级添加一个children属性,并赋值
        const parentArr = cloneData.filter(parent => parent.id === father.pid) // 判断该菜单的父级菜单是否存在
        if (parentArr.length === 0) { return father } // 如果该菜单的父级菜单不存在,则直接返回该菜单
        return father.pid === null // 返回第一层
      })
    },
    // 选中图标
    selected(name) {
      this.form.icon = name
    }
  }
}

</script>

<style rel="stylesheet/scss" lang="scss">
.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}
</style>

<style rel="stylesheet/scss" lang="scss" scoped>
::v-deep .el-input-number .el-input__inner {
  text-align: left;
}

::v-deep .vue-treeselect__control,
::v-deep .vue-treeselect__placeholder,
::v-deep .vue-treeselect__single-value {
  height: 30px;
  line-height: 30px;
}
</style>

4.3 菜单渲染

通过element-ui的 NavMenu可以实现菜单的前端渲染,本期请大家先了解一下该组件的基本情况,菜单动态渲染将在下期文章中详细说明。

五、效果演示

六、源码

相关文章