多级菜单 多级树形结构 多级树排序 多级树节点移动

此文将介绍一种简单可行的多级树结构算法,并支持节点的上下移动。

首先,本文的算法是启蒙于一个.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"
}
]
}






posted @ 2017-04-21 17:23  TNZYM  阅读(3769)  评论(0编辑  收藏  举报