我们常常会遇到这样一个问题,就是前端要实现的样式是一个级联菜单或者是下拉树,如图
这样的数据接口是怎么实现的呢,是什么样子的呢?
我们可以看看 Elemui
中的假数据
<el-cascader :options="options" clearable></el-cascader>
<script>
export default {
data() {
return {
options: [{
value: 'zhinan',
label: '指南',
children: [{
value: 'shejiyuanze',
label: '设计原则',
children: [{
value: 'yizhi',
label: '一致'
}, {
value: 'fankui',
label: '反馈'
}, {
value: 'xiaolv',
label: '效率'
}, {
value: 'kekong',
label: '可控'
}]
}, {
value: 'daohang',
label: '导航',
children: [{
value: 'cexiangdaohang',
label: '侧向导航'
}, {
value: 'dingbudaohang',
label: '顶部导航'
}]
}]
}, {
value: 'zujian',
label: '组件',
children: [{
value: 'basic',
label: 'Basic',
children: [{
value: 'layout',
label: 'Layout 布局'
}, {
value: 'color',
label: 'Color 色彩'
}, {
value: 'typography',
label: 'Typography 字体'
}, {
value: 'icon',
label: 'Icon 图标'
}, {
value: 'button',
label: 'Button 按钮'
}]
}, {
value: 'form',
label: 'Form',
children: [{
value: 'radio',
label: 'Radio 单选框'
}, {
value: 'checkbox',
label: 'Checkbox 多选框'
}, {
value: 'input',
label: 'Input 输入框'
}, {
value: 'input-number',
label: 'InputNumber 计数器'
}, {
value: 'select',
label: 'Select 选择器'
}, {
value: 'cascader',
label: 'Cascader 级联选择器'
}, {
value: 'switch',
label: 'Switch 开关'
}, {
value: 'slider',
label: 'Slider 滑块'
}, {
value: 'time-picker',
label: 'TimePicker 时间选择器'
}, {
value: 'date-picker',
label: 'DatePicker 日期选择器'
}, {
value: 'datetime-picker',
label: 'DateTimePicker 日期时间选择器'
}, {
value: 'upload',
label: 'Upload 上传'
}, {
value: 'rate',
label: 'Rate 评分'
}, {
value: 'form',
label: 'Form 表单'
}]
}, {
value: 'data',
label: 'Data',
children: [{
value: 'table',
label: 'Table 表格'
}, {
value: 'tag',
label: 'Tag 标签'
}, {
value: 'progress',
label: 'Progress 进度条'
}, {
value: 'tree',
label: 'Tree 树形控件'
}, {
value: 'pagination',
label: 'Pagination 分页'
}, {
value: 'badge',
label: 'Badge 标记'
}]
}, {
value: 'notice',
label: 'Notice',
children: [{
value: 'alert',
label: 'Alert 警告'
}, {
value: 'loading',
label: 'Loading 加载'
}, {
value: 'message',
label: 'Message 消息提示'
}, {
value: 'message-box',
label: 'MessageBox 弹框'
}, {
value: 'notification',
label: 'Notification 通知'
}]
}, {
value: 'navigation',
label: 'Navigation',
children: [{
value: 'menu',
label: 'NavMenu 导航菜单'
}, {
value: 'tabs',
label: 'Tabs 标签页'
}, {
value: 'breadcrumb',
label: 'Breadcrumb 面包屑'
}, {
value: 'dropdown',
label: 'Dropdown 下拉菜单'
}, {
value: 'steps',
label: 'Steps 步骤条'
}]
}, {
value: 'others',
label: 'Others',
children: [{
value: 'dialog',
label: 'Dialog 对话框'
}, {
value: 'tooltip',
label: 'Tooltip 文字提示'
}, {
value: 'popover',
label: 'Popover 弹出框'
}, {
value: 'card',
label: 'Card 卡片'
}, {
value: 'carousel',
label: 'Carousel 走马灯'
}, {
value: 'collapse',
label: 'Collapse 折叠面板'
}]
}]
}, {
value: 'ziyuan',
label: '资源',
children: [{
value: 'axure',
label: 'Axure Components'
}, {
value: 'sketch',
label: 'Sketch Templates'
}, {
value: 'jiaohu',
label: '组件交互文档'
}]
}]
}
}
}
</script>
可以看见,这样的数据我们直接用SQL查出来是会损耗SQL性能的,我们这里展示如何在业务层做数据处理,实现树结构,这里就以若依的菜单数据为例,做一个基本演示!
查询数据我们分为两种,一种是获取指定的菜单,一种是获取全部的,获取指定的菜单我们需要写一个递归SQL,比如
我们如果获取全部那就是正常的查询所以,但是只要目录管理下面的菜单结构,不要其他的,那么这个SQL就是这样的:
如果想从sys_menu
表中查询并获取菜单树的数据,可以使用递归查询。以下是一个基于MySQL的示例查询,该查询假设每个记录都有唯一的menu_id
标识符和parent_id
表示父菜单ID:
WITH RECURSIVE MenuCTE AS (
SELECT
menu_id,
menu_name,
parent_id,
order_num,
path,
component,
query,
is_frame,
is_cache,
menu_type,
visible,
status,
perms,
icon,
create_by,
create_time,
update_by,
update_time,
remark
FROM sys_menu
WHERE parent_id = 0 -- 根节点的条件
UNION ALL
SELECT
m.menu_id,
m.menu_name,
m.parent_id,
m.order_num,
m.path,
m.component,
m.query,
m.is_frame,
m.is_cache,
m.menu_type,
m.visible,
m.status,
m.perms,
m.icon,
m.create_by,
m.create_time,
m.update_by,
m.update_time,
m.remark
FROM sys_menu m
JOIN MenuCTE cte ON m.parent_id = cte.menu_id
)
SELECT * FROM MenuCTE;
这个查询使用了MySQL的递归CTE(Common Table Expressions)功能,通过WITH RECURSIVE
来逐级查询父菜单与子菜单的关系。查询结果包含了所有菜单及其层次结构关系。
我们分析一下这个SQL
这是一个使用递归CTE(Common Table Expressions)的SQL查询,用于获取具有层次结构关系的菜单数据。以下是对查询各部分的解释:
WITH RECURSIVE MenuCTE AS
: 这是一个递归CTE的开始,MenuCTE
是一个临时表名。递归CTE用于递归地查询表中的数据。
SELECT ... FROM sys_menu WHERE parent_id = 0
: 这是递归CTE的初始查询部分,它选择根节点(parent_id = 0
)的菜单记录。
UNION ALL
: 这是联结两个查询结果集的关键字,它将上述初始查询结果与后续递归查询的结果联结在一起。
SELECT ... FROM sys_menu m JOIN MenuCTE cte ON m.parent_id = cte.menu_id
: 这是递归查询的部分,通过连接sys_menu
表自身,并使用递归关系 m.parent_id = cte.menu_id
来获取每个菜单的子菜单。
最后,整个递归CTE的最后部分是 SELECT * FROM MenuCTE
,它选择了所有递归CTE的结果,包括根节点和其下的所有子节点。
该查询的结果是包含所有菜单数据的表,每一行都表示一个菜单项,具有其父菜单的引用关系,形成了一个层次结构。这对于表示树形结构的数据非常有用,例如用于构建具有层次关系的菜单系统。
当然,毫无疑问,获取出来的数据就是普通列表,下面我们就进行处理:
public static List<SysMenu> buildMenuTree(List<SysMenu> menuList) {
Map<Long, SysMenu> menuMap = new HashMap<>();
// 创建一个菜单ID到菜单对象的映射
for (SysMenu menu : menuList) {
menuMap.put(menu.getMenuId(), menu);
}
// 构建菜单树
List<SysMenu> menuTree = new ArrayList<>();
for (SysMenu menu : menuList) {
Long parentId = menu.getParentId();
if (parentId != null && menuMap.containsKey(parentId)) {
SysMenu parentMenu = menuMap.get(parentId);
parentMenu.getChildren().add(menu);
} else {
menuTree.add(menu); // 没有父菜单或父菜单未找到,将其作为根节点添加到树中
}
}
return menuTree;
}
这段 Java 代码是一个用于构建菜单树的方法,它接受一个包含 SysMenu
对象的列表作为输入,然后返回一个构建好的菜单树的列表。
让我对这段代码进行详细解读:
创建映射表:
Map<Long, SysMenu> menuMap = new HashMap<>();
在这里,创建了一个 HashMap
对象 menuMap
,用于将菜单ID映射到相应的 SysMenu
对象。
建立映射关系:
for (SysMenu menu : menuList) {
menuMap.put(menu.getMenuId(), menu);
}
通过遍历输入的菜单列表 menuList
,将每个菜单的 menuId
与对应的 SysMenu
对象建立映射关系。
构建菜单树:
List<SysMenu> menuTree = new ArrayList<>();
for (SysMenu menu : menuList) {
Long parentId = menu.getParentId();
if (parentId != null && menuMap.containsKey(parentId)) {
SysMenu parentMenu = menuMap.get(parentId);
parentMenu.getChildren().add(menu);
} else {
menuTree.add(menu); // 没有父菜单或父菜单未找到,将其作为根节点添加到树中
}
}
遍历菜单列表 menuList
,对于每个菜单,检查其 parentId
是否存在并且在 menuMap
中有对应的父菜单。如果是,将当前菜单添加到其父菜单的子菜单列表 children
中;如果不是,说明当前菜单是根节点,将其添加到 menuTree
中。
返回菜单树:
return menuTree;
返回构建好的菜单树列表 menuTree
。
这样,该方法将输入的扁平的菜单列表转换为一个带有层次结构的菜单树,其中每个菜单节点都包含了其子菜单的引用。这种结构更适合在用户界面上展示树形菜单。
这是我们自己写的,然后看看人家若依自己的,用到了递归:
/**
* 构建前端所需要树结构
*
* @param menus 菜单列表
* @return 树结构列表
*/
public List<SysMenu> buildMenuTree(List<SysMenu> menus) {
List<SysMenu> returnList = new ArrayList<SysMenu>();
List<Long> tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());
for (Iterator<SysMenu> iterator = menus.iterator(); iterator.hasNext(); ) {
SysMenu menu = (SysMenu) iterator.next();
// 如果是顶级节点, 遍历该父节点的所有子节点
if (!tempList.contains(menu.getParentId())) {
recursionFn(menus, menu);
returnList.add(menu);
}
}
if (returnList.isEmpty()) {
returnList = menus;
}
return returnList;
}
/**
* 递归列表
*
* @param list 分类表
* @param t 子节点
*/
private void recursionFn(List<SysMenu> list, SysMenu t) {
// 得到子节点列表
List<SysMenu> childList = getChildList(list, t);
t.setChildren(childList);
for (SysMenu tChild : childList) {
if (hasChild(list, tChild)) {
recursionFn(list, tChild);
}
}
}
这段源码是一个用于构建菜单树的方法,输入是一个 List
,表示扁平结构的菜单列表,输出是一个构建好的菜单树。
让我对这段源码进行详细解读:
初始化:
List<SysMenu> returnList = new ArrayList<SysMenu>();
List<Long> tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());
returnList
是最终返回的菜单树列表。tempList
是将 menus
列表中的菜单ID提取出来的列表。遍历菜单列表:
for (Iterator<SysMenu> iterator = menus.iterator(); iterator.hasNext(); ) {
SysMenu menu = (SysMenu) iterator.next();
使用迭代器遍历 menus
列表中的每个菜单。
判断是否为顶级节点:
if (!tempList.contains(menu.getParentId())) {
如果当前菜单的 parentId
不在 tempList
中,说明它是顶级节点。
递归构建子节点:
recursionFn(menus, menu);
调用 recursionFn
方法递归构建当前顶级节点的子节点。
将当前节点添加到返回列表:
returnList.add(menu);
将当前菜单节点添加到最终返回的菜单树列表中。
处理空的返回列表:
if (returnList.isEmpty()) {
returnList = menus;
}
如果最终返回的菜单树列表为空,说明输入的菜单列表本身就是一个树,直接将其作为返回结果。
返回最终结果:
return returnList;
返回构建好的菜单树列表。
第二段代码是一个递归方法recursionFn
getChildList
方法:
List<SysMenu> childList = getChildList(list, t);
通过调用 getChildList
方法获取当前节点 t
的子节点列表。
设置子节点列表:
t.setChildren(childList);
将获取到的子节点列表设置到当前节点 t
的 children
属性中。
递归处理子节点:
for (SysMenu tChild : childList) {
if (hasChild(list, tChild)) {
recursionFn(list, tChild);
}
}
遍历当前节点的子节点列表,对每个子节点进行递归处理。如果子节点还有子节点(通过 hasChild
方法判断),则继续递归调用 recursionFn
方法。
整个递归方法的作用是从当前节点开始,递归地设置其子节点列表,并对每个子节点的子节点进行递归处理,以构建完整的树形结构。这种递归方式有助于处理树状结构的数据,例如在构建菜单树时,每个菜单节点都包含了其下级菜单的引用。