多級菜單 多級樹形結構 多級樹排序 多級樹節點移動


此文將介紹一種簡單可行的多級樹結構算法,並支持節點的上下移動。

首先,本文的算法是啟蒙於一個.net項目中的多級樹結構算法。該項目中,所有節點的排序值,通通按照顯示順序排列(如圖)。

這種方式的缺點是:當“插入”,“移動”,“修改(修改所屬父節點)”和“刪除”節點,需要對子節點和父節點的排序值都要重新設置(需要有序下移當前節點之后的所有節點。如果只是一個簡單的二叉樹,那還好,但如果是多級樹,則使得邏輯異常復雜)。

那么,如何在此基礎之上,進行修改。得到一個簡單的算法,在插入或移動節點時,不用使用那么復雜的算法呢?答案如下:

1.(表)結構

表結構不變(Id,ParentId,Position,Name ... ),排序值的設置邏輯變成:局部排序,即,只做(same level)同級別的節點的排序。(如下圖)

2.節移動,節點移動規則,只做同級節點之間上下移動(采用新結構之后,也就屏蔽掉了移動一個節點時,還得同時移動當前節點之后所有節點的排序位的煩惱,現在就只要關系自己的排序位就行了。),非同級移動可以通過修改掛靠父節點來做到。

3.新增,只用查詢同級節點中Max 排序位,然后+1作為自己的排序位即可。

4.修改,保持當前排序位不變。如果是修改掛靠父節點,也只是單純修改ParentId即可,當前節點排序修改為將來父節點中子節點中Max排序值,當前節點的子節點的排序值不用變。

5.刪除,將當前節點之后的同級節點的排序值+1,其他不變。


最后,JAVA CODE直接上代碼!

由於管理系統中,有很多地方都用到樹形結構,所以抽象繼承是必須的。

第一大類就是TreeRsVo,所有樹形結構的model類都繼承它,TreeRsVo使用泛型來指定“子節點列表”屬性(因為每個樹形類可能有自己特有的屬性,而“子節點列表”的元素類型就是自身,所以通過泛型進行傳遞,復用TreeRsVo基類里面的排序方法)。 


/**
* 樹-基類
* Created by TonyZeng on 2017/4/21.
*/
public class TreeRsVo<T extends TreeRsVo> {
private Long id;//ID
private Long parentId;//父節點ID
private String name;//
private Boolean available;//是否可用
private Integer position;//位置(用於排序)
private List<T> children;//子列表

//此處省略部分get&set方法

/**
* 孩子節點排序
*/
public void sortChildren() {
this.children.sort((m1, m2) -> {
int orderBy1 = m1.getPosition();
int orderBy2 = m2.getPosition();
return orderBy1 < orderBy2 ? -1 : (orderBy1 == orderBy2 ? 0 : 1);
});
// 對每個節點的下一層節點進行排序
for (T n : children) {
if (n != null && n.getChildren() != null) {
n.sortChildren();
}
}
}
}

/**
* 系統菜單-響應Vo(樹)(此類Mapping from DB Table Model Class,即相當於數據庫model類)
* Created by TonyZeng on 2017/3/13.
*/
public class MenuResponseVo extends TreeRsVo<MenuResponseVo> {
private String link; //鏈接
private String icon;//圖標
private List<ButtonResponseVo> buttons;//按鈕列表
//此處省略部分get&set方法
}

/**
* 創建有序樹
* Created by TonyZeng on 2016/5/13.
*/
public class TreeMapper {
/**
* 創建有序菜單樹
* @param list
* @return
*/
public static MenuResponseVo MenuTree(List<MenuResponseVo> list) {
//組裝Map數據,將所有菜單全部放到Map中,並用菜單ID作為Key來標記,方便后面的各節點尋找父節點。
Map<Long, MenuResponseVo> dataMap = new HashMap<>();//<{菜單ID},{菜單對象}>
for (MenuResponseVo menu : list) {
dataMap.put(menu.getId(), menu);
}
//創建根節點
MenuResponseVo root = new MenuResponseVo();
//組裝樹形結構,將子節點全部掛靠到自己的父節點
for (Map.Entry<Long, MenuResponseVo> entry : dataMap.entrySet()) {
MenuResponseVo menu = entry.getValue();
if (menu.getParentId().equals(0L)) {
root.getChildren().add(menu);
} else {
dataMap.get(menu.getParentId()).getChildren().add(menu);
}
}
//對多級樹形結構進行“二叉樹排序”
root.sortChildren();
return root;
}
}

/**
* 系統設置-系統菜單設置服務
* Created by TonyZeng on 2017/2/6.
*/
@Service("sysMenuBIService")
public class SysMenuBIServiceImpl implements SysMenuBIService {
@Autowired
SysMenuService sysMenuService;

/**
* 獲取菜單樹形結構接口
*
* @return 完整的菜單多級樹形結構
*/
@Override
public BaseDto getMenuList() {
List<MenuResponseVo> list = new ArrayList<>();
for (SysMenu x : sysMenuService.findAll(new Sort(Sort.Direction.ASC, "Position"))) {
list.add(VoMapper.mapping(x));
}
return new BaseDto(TreeMapper.MenuTree(list).getChildren());
}

/**
* 添加菜單
*
* @param requestVo
* @return
*/
@Override
public BaseDto addMenu(AddMenuRqVo requestVo) {
//驗證參數
String validateResult = new ValidateUtil<AddMenuRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}
try {
//新增時,檢查是否存在相同的菜單編碼
List sameCodeMenus = sysMenuService.findByMenuCode(requestVo.getCode());
if (sameCodeMenus != null && sameCodeMenus.size() > 0) {
return new BaseDto(-1, "菜單編碼已存在,請重新填寫");
}
//vo to po
SysMenu model = PoMapper.mapping(requestVo);
//查詢兄弟節點中,最大的排序值,並加一作為自己的排序值
model.setPosition(sysMenuService.findMaxPositionOfBrother(requestVo.getParentId()) + 1);
model = sysMenuService.save(model);
if (model.getId() != null) {
return new BaseDto(model.getId());
} else {
return new BaseDto(-1, "系統設置-添加菜單 失敗");
}
} catch (Exception ex) {
return new BaseDto(-1, "系統設置-添加菜單 失敗");
}
}

/**
* 更新菜單
*
* @param requestVo
* @return
*/
@Override
public BaseDto updateMenu(UpdateMenuRqVo requestVo) {
//驗證參數
String validateResult = new ValidateUtil<AddMenuRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}
try {
//新增時,檢查是否存在相同的菜單編碼
SysMenu po = sysMenuService.find(requestVo.getId());
if (po == null) {
return new BaseDto(-1, "菜單不存在");
}

SysMenu model = PoMapper.mapping(requestVo);
//排序值不變
model.setPosition(po.getPosition());
model = sysMenuService.save(model);
if (model.getId() != null) {
return new BaseDto(model.getId());
} else {
return new BaseDto(-1, "系統設置-更新菜單 失敗");
}
} catch (Exception ex) {
return new BaseDto(-1, "系統設置-更新菜單 失敗");
}
}

/**
* 刪除菜單
*
* @param id 菜單id (修改菜單必填)
* @return
*/
@Override
public BaseDto deleteMenu(Long id) {
//驗證參數
if (id == null) {
return new BaseDto(-1, "請提供id");
}
try {
SysMenu po = sysMenuService.find(id);
if (po == null) {
return new BaseDto(-1, "菜單不存在");
}
//刪除
sysMenuService.delete(id);
//重置菜單的排序值
resetMenu(po.getParentId());
return new BaseDto(id);
} catch (Exception ex) {
return new BaseDto(-1, "系統設置-刪除菜單 失敗");
}
}

/**
* 移動菜單排序
* 算法:
* 1.找到(previous or next)菜單
* 2.與其交換位置
*
* @param requestVo
* @return
*/
@Override
public BaseDto moveMenu(MoveRqVo requestVo) {
//驗證參數
String validateResult = new ValidateUtil<MoveRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}

try {
//新增時,檢查是否存在相同的菜單編碼
SysMenu self = sysMenuService.find(requestVo.getId());
if (self == null) {
return new BaseDto(-1, "菜單不存在");
}

List<SysMenu> brotherList = sysMenuService.findByParentId(self.getParentId());
//如果有兄弟節點
if (brotherList.size() > 0) {
//Get index of this menus in brother menus.
int indexOfThisMenus = 0;
for (int i = 0; i < brotherList.size(); i++) {
if (brotherList.get(i).getId().equals(self.getId())) {
indexOfThisMenus = i;
}
}
SysMenu brother;
if (requestVo.getDown().equals(true)) {
//判斷是否已經為最底部的菜單
if (indexOfThisMenus == (brotherList.size() - 1)) {
return new BaseDto(-1, "已經是(同級中)最底部的菜單了");
} else {
//獲取后臨的兄弟菜單
brother = brotherList.get(indexOfThisMenus + 1);
}
//交換位置
brother.setPosition(brother.getPosition() - 1);
self.setPosition(self.getPosition() + 1);
} else {
//判斷是否已經為最底部的菜單
if (indexOfThisMenus == 0) {
return new BaseDto(-1, "已經是(同級中)最頂部的菜單了");
} else {
//獲取前臨的兄弟菜單
brother = brotherList.get(indexOfThisMenus - 1);
}
//交換位置
brother.setPosition(brother.getPosition() + 1);
self.setPosition(self.getPosition() - 1);
}
sysMenuService.save(brother);
sysMenuService.save(self);
if (self.getId() != null && brother.getId() != null) {
return new BaseDto(self.getId());
} else {
return new BaseDto(-1, "系統設置-移菜單位置 失敗");
}
} else {
//如果沒有兄弟節點
return new BaseDto(self.getId());
}
} catch (Exception ex) {
return new BaseDto(-1, "系統設置-移菜單位置 失敗");
}
}

/**
* 重置菜單的排序值
*
* @param parentId 父節點ID
* @return
*/
@Override
public BaseDto resetMenu(Long parentId) {
return resetMenu(sysMenuService.findByParentId(parentId));
}

/**
* 重置菜單的排序值
*
* @param brothers 兄弟節點列表
* @return
*/
private BaseDto resetMenu(List<SysMenu> brothers) { //如果有兄弟節點
try {
if (brothers.size() > 0) {
for (int i = 0; i < brothers.size(); i++) {
brothers.get(i).setPosition(i + 1);
}
sysMenuService.save(brothers);
return new BaseDto(0, "系統設置-重置菜單的排序值 成功");
} else {
return new BaseDto(0, "系統設置-重置菜單的排序值 失敗");
}
} catch (Exception ex) {
return new BaseDto(-1, "系統設置-重置菜單的排序值 失敗");
}
}
}

3.JSON 結構,在TreeMapper生成了樹之后,to json 將得到如下json結構。
{
"data": [
{
"id": 1,
"parentId": 0,
"name": "會員檔案",
"available": true,
"position": 1,
"children": [
{
"id": 33,
"parentId": 1,
"name": "檔案管理",
"available": true,
"position": 1,
"children": [],
"link": "~/user/CustomerMgr.aspx",
"icon": "user"
},
{
"id": 29,
"parentId": 1,
"name": "密碼修改",
"available": true,
"position": 2,
"children": [
{
"id": 30,
"parentId": 29,
"name": "密碼修改Child44",
"available": true,
"position": 1,
"children": [],
"link": "",
"icon": ""
},
{
"id": 26,
"parentId": 29,
"name": "密碼修改Child1",
"available": true,
"position": 2,
"children": [],
"link": "",
"icon": ""
}
],
"link": "~/user/PersonalInfoMgr.aspx",
"icon": "userhome"
}
],
"link": "",
"icon": "groupgear"
}
]
}







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM