主頁 > 後端開發 > SpringBoot整合SpringSecurity實作權限控制(六):選單管理

SpringBoot整合SpringSecurity實作權限控制(六):選單管理

2021-10-24 09:30:45 後端開發

系列文章目錄
《SpringBoot整合SpringSecurity實作權限控制(一):實作原理》
《SpringBoot整合SpringSecurity實作權限控制(二):權限資料基本模型設計》
《SpringBoot整合SpringSecurity實作權限控制(三):前端動態裝載路由與選單》
《SpringBoot整合SpringSecurity實作權限控制(四):角色管理》
《SpringBoot整合SpringSecurity實作權限控制(五):用戶管理》


本文目錄

  • 一、前言
  • 二、需求分析
  • 三、后端實作
    • 3.1 創建選單物體表
    • 3.2 添加操作選單表的Mapper介面
    • 3.3 實作選單的增刪改查服務
    • 3.4 撰寫Controller層
  • 四、前端實作
    • 4.1 添加選單api訪問介面
    • 4.2 撰寫前端頁面
    • 4.3 選單渲染
  • 五、效果演示
  • 六、原始碼

一、前言

  • 后臺管理系統可以通過選單管理來實作系統的功能模塊管理,通過清晰的樹形選單結構展現各種系統功能,無疑會大大提升系統的使用效率,

在這里插入圖片描述

二、需求分析

  1. 系統功能模塊需要按各個分類,形成選單結構,比如說系統管理分類目錄下,存在用戶管理、角色管理、選單管理等功能;系統設定分類目錄下,存在商品設定、倉庫設定、儲位設定等功能,
    在這里插入圖片描述
  2. 每個選單都需要包含以下資訊:選單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. 構成查詢條件增刪改查按鈕選單樹形表的布局
    在這里插入圖片描述
  2. 樹形選單可以展開或折疊
    在這里插入圖片描述
  3. 點擊增加或編輯選單按鈕,填寫相應選單資訊后,進行保存,
    在這里插入圖片描述
  • 專門撰寫了一個圖示選擇組件,該組件可以選擇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可以實作選單的前端渲染,本期請大家先了解一下該組件的基本情況,選單動態渲染將在下期文章中詳細說明,
    在這里插入圖片描述

  • 五、效果演示

在這里插入圖片描述

六、原始碼

  • 前端
    https://gitee.com/zhuhuix/startup-frontend
    https://github.com/zhuhuix/startup-frontend
  • 后端
    https://gitee.com/zhuhuix/startup-backend
    https://github.com/zhuhuix/startup-backend

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/333781.html

標籤:java

上一篇:青蛙跳臺階-普通版-Java實作

下一篇:UseOfMethods - 方法的使用 - Java

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more