个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview
树形菜单
在很多系统管理/菜单管理中经常会出现下面这样的树形菜单,它是通过前端的Tree
组件来渲染的。
后端返回的树形结构数据如下图所示。
在已有的Element
、ant-vue
等前端框架中这种组件都是有的,用起来也是非常简单。
问题
现在需求是这样的,如上图菜单搜索有两个条件,状态和菜单名称,在查询菜单时需要通过模糊匹配菜单名称和精确匹配菜单状态来查询。这个本身很简单,问题是在树形菜单搜索时需要带上父菜单和子菜单,而不是只展示匹配到的菜单。
如:搜索“用户”时应该有下面的结果,在查询到相关节点后同时带上父节点和子节点返回。
Ant Design Vue在这里前端组件是有这样的实现的,如下。
前端是有这样的实现,而且很简单,那么有没有后端的实现方案呢?
如何实现
最直接的思路就是查询满足条件的菜单,然后去查这些菜单的父菜单和子菜单,讲起来很容易,但是这里要很注意重复节点和死循环。
重复节点问题:同级菜单具有相同的父节点,在查完第一个子节点的父节点,将父节点收集后,查第二就没有必要的,可以省去。
死循环:查子节点时不要再去查其父节点的子节点和父节点,有可能会死循环。
下面贴上了所有代码仅供参考。
查询菜单列表
public List<MenuTreeRespVO> getMenuTreeList(MenuListVO reqVO) {
// 1、查询所有菜单
List<MenuPO> all = menuMapper.selectList();
// 2、查询满足条件的菜单
List<MenuPO> menus = menuMapper.selectList(reqVO);
Set<Long> menuIds = menus.stream().map(MenuPO::getId).collect(Collectors.toSet());
Set<MenuPO> menuSet = findMenusWithParentsOrChildrenByIds(all, menuIds, true, true);
// 3、形成树形结合
return buildMenuTree(new ArrayList<>(menuSet), false);
}
查询菜单的父/子菜单
/**
* 查找菜单的父/子菜单集合
*
* @param all 所有菜单
* @param menuIds 需要的菜单集合
* @param withParent 是否包含父菜单
* @param withChildren 是否包含子菜单
* @return 结果
*/
private Set<MenuPO> findMenusWithParentsOrChildrenByIds(List<MenuPO> all, Set<Long> menuIds, boolean withParent, boolean withChildren) {
Map<Long, MenuPO> menuMap = new HashMap<>();
for (MenuPO menu : all) {
menuMap.put(menu.getId(), menu);
}
// 使用LinkedHashSet保持插入顺序
Set<MenuPO> result = new LinkedHashSet<>();
// 存储已处理过的菜单ID
Set<Long> processedIds = new HashSet<>();
for (Long menuId : menuIds) {
if (withParent) {
collectMenuParents(result, menuMap, menuId, processedIds);
}
if (withChildren) {
collectMenuChildren(result, menuMap, menuId);
}
}
return result;
}
递归查找当前菜单的所有父菜单
/**
* 递归查找当前菜单的所有父菜单
*
* @param resultSet 结果
* @param menuMap menuMap
* @param menuId 需要的菜单id
* @param processedIds 存储已处理过的菜单id
*/
private void collectMenuParents(Set<MenuPO> resultSet, Map<Long, MenuPO> menuMap, Long menuId, Set<Long> processedIds) {
if (processedIds.contains(menuId)) {
return; // 如果已经处理过此菜单,则不再处理
}
processedIds.add(menuId);
MenuPO menu = menuMap.get(menuId);
if (menu != null) {
resultSet.add(menu);
// 如果当前菜单不是根节点(即parentId不为0),继续查找其父菜单
if (!Objects.equals(menu.getParentId(), ID_ROOT) && !processedIds.contains(menu.getParentId())) {
collectMenuParents(resultSet, menuMap, menu.getParentId(), processedIds);
}
}
}
递归查找当前菜单的所有子菜单
/**
* 递归查找当前菜单的所有子菜单
*
* @param resultSet 结果
* @param menuMap menuMap
* @param menuId 需要的菜单id
*/
private void collectMenuChildren(Set<MenuPO> resultSet, Map<Long, MenuPO> menuMap, Long menuId) {
MenuPO menu = menuMap.get(menuId);
if (menu != null) {
resultSet.add(menu);
// 添加当前菜单的所有子菜单
for (MenuPO child : menuMap.values()) {
if (child.getParentId().equals(menu.getId())) {
collectMenuChildren(resultSet, menuMap, child.getId());
}
}
}
}
构建菜单树
public List<MenuTreeRespVO> buildMenuTree(List<MenuPO> menuList, boolean removeButton) {
if (removeButton) {
// 移除按钮
menuList.removeIf(menu -> menu.getType().equals(MenuType.BUTTON.getType()));
}
List<MenuTreeRespVO> convert = MenuConvert.INSTANCE.convert2TreeRespList(menuList);
Map<Long, MenuTreeRespVO> menuTreeMap = new HashMap<>();
for (MenuTreeRespVO menu : convert) {
menuTreeMap.put(menu.getId(), menu);
}
menuTreeMap.values().stream().filter(menu -> !ID_ROOT.equals(menu.getParentId())).forEach(childMenu -> {
MenuTreeRespVO parentMenu = menuTreeMap.get(childMenu.getParentId());
if (parentMenu == null) {
log.info("id:{} 找不到父菜单 parentId:{}", childMenu.getId(), childMenu.getParentId());
return;
}
// 将自己添加到父节点中
if (parentMenu.getChildren() == null) {
parentMenu.setChildren(new ArrayList<>());
}
parentMenu.getChildren().add(childMenu);
}
);
return menuTreeMap.values().stream().filter(menu -> ID_ROOT.equals(menu.getParentId())).collect(Collectors.toList());
}
总结
基于以上后端查询树形菜单的代码就可以实现“筛选树形菜单时关联其父节点和子节点”了,效果还是可以的。
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。