系列文章目錄
《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 選單渲染
- 五、效果演示
- 六、原始碼
一、前言
- 后臺管理系統可以通過選單管理來實作系統的功能模塊管理,通過清晰的樹形選單結構展現各種系統功能,無疑會大大提升系統的使用效率,

二、需求分析
- 系統功能模塊需要按各個分類,形成選單結構,比如說系統管理分類目錄下,存在用戶管理、角色管理、選單管理等功能;系統設定分類目錄下,存在商品設定、倉庫設定、儲位設定等功能,

- 每個選單都需要包含以下資訊:選單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 撰寫前端頁面
- 構成查詢條件,增刪改查按鈕與選單樹形表的布局

- 樹形選單可以展開或折疊

- 點擊增加或編輯選單按鈕,填寫相應選單資訊后,進行保存,

- 專門撰寫了一個圖示選擇組件,該組件可以選擇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實作
