SpringBoot+Vue+ElementUI: Tree形数据增删改查及拖拽
数据查询可以查看《SpringBoot:查询Tree形数据》,这里不另外展开,主要讲拖拽的逻辑。数据库存储结构如下:project_id、create_time、update_time是业务逻辑字段(查询条件),可忽略。SpringBootMODELpublic class TestCaseNodeDO {/*** 节点id*/@TableId(value = "id", type = IdTyp
·
数据查询可以查看《SpringBoot:查询Tree形数据》,这里不另外展开,主要讲拖拽的逻辑。
数据库存储结构如下:
- project_id、create_time、update_time是业务逻辑字段(查询条件),可忽略。
SpringBoot
- MODEL
public class TestCaseNodeDO {
/**
* 节点id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 项目id
*/
private Integer projectId;
/**
* 节点名称
*/
private String name;
/**
* 父节点id
*/
private Integer parentId;
/**
* 节点层级
*/
private Integer level;
/**
* 创建时间
*/
@JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新时间
*/
@JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 排序
*/
private Integer pos;
}
public class TreeBO {
private Integer id;
private String name;
private Integer level;
private Integer pos;
private List<TreeBO> children;
}
- Controller
提供两个接口,一个是节点的拖拽/drag
,一个是拖拽后节点的排序/position
@PostMapping("/create")
public CreatedVO create(@RequestBody TestCaseNodeDO testCaseNodeDO) {
testCaseNodeService.addNode(testCaseNodeDO);
return new CreatedVO();
}
// 这里的ids代表要删除的节点id于其所有子节点id
@PostMapping("/delete")
public DeletedVO delete(@RequestBody List<Integer> ids) {
testCaseNodeService.deleteNodes(ids);
return new DeletedVO();
}
@PostMapping("/drag")
public UpdatedVO drag(@RequestBody TreeBO treeBO) {
testCaseNodeService.nodeDrag(treeBO);
return new UpdatedVO();
}
@PostMapping("/position")
public UpdatedVO position(@RequestBody List<Integer> ids) {
testCaseNodeService.nodePosition(ids);
return new UpdatedVO();
}
- service
拖拽节点,并遍历子节点(所有子节点只修改了level),所有数据库操作使用了mybatis-plus
,语法进自行百度
public void nodeDrag(TreeBO nodeTree) {
List<TestCaseNodeDO> updateNodes = new ArrayList<>();
buildUpdateTestCase(nodeTree, updateNodes, 0, 1);
updateNodes.forEach( node -> {
testCaseNodeMapper.updateById(node);
});
}
private void buildUpdateTestCase(TreeBO rootNode, List<TestCaseNodeDO> updateNodes, Integer parentId, int level) {
TestCaseNodeDO testCaseNode = new TestCaseNodeDO();
testCaseNode.setId(rootNode.getId());
testCaseNode.setLevel(level);
testCaseNode.setParentId(parentId);
testCaseNode.setPos(rootNode.getPos());
updateNodes.add(testCaseNode);
List<TreeBO> children = rootNode.getChildren();
if (children != null && children.size() > 0) {
for (TreeBO child : children) {
buildUpdateTestCase(child, updateNodes, rootNode.getId(), level + 1);
}
}
}
设置节点pos:获取相邻节点,然后将该节点pos设置为相邻两节点的中间值
public boolean nodePosition(List<Integer> ids) {
// 获取相邻节点 id
Integer before = ids.get(0);
Integer current = ids.get(1);
Integer after = ids.get(2);
TestCaseNodeDO beforeCase = null;
TestCaseNodeDO afterCase = null;
TestCaseNodeDO currentNode = getCaseNode(current);
// 获取相邻节点
if (before != 0) {
beforeCase = getCaseNode(before);
beforeCase = beforeCase.getLevel().equals(currentNode.getLevel()) ? beforeCase : null;
}
if (after != 0) {
afterCase = getCaseNode(after);
afterCase = afterCase.getLevel().equals(currentNode.getLevel()) ? afterCase : null;
}
int pos;
if (beforeCase == null) {
pos = afterCase != null ? afterCase.getPos() / 2 : 1024;
} else {
pos = afterCase != null ? (beforeCase.getPos() + afterCase.getPos()) / 2 : beforeCase.getPos() + 1024;
}
currentNode.setPos(pos);
return testCaseNodeMapper.updateById(currentNode) > 0;
}
// 通过节点id获取节点
private TestCaseNodeDO getCaseNode(Integer id) {
return testCaseNodeMapper.selectById(id);
}
随便补充一下添加节点的逻辑,方便理解pos
字段定义逻辑。在添加节点时,如果在同一父节点下已经存在同级节点,则获取所有节点中pos最大的值,再+1024,否则直接设为1024
public boolean addNode(TestCaseNodeDO node) {
node.setCreateTime(new Date());
node.setUpdateTime(new Date());
Integer pos = getNextLevelPos(node.getProjectId(), node.getLevel(), node.getParentId());
node.setPos(pos);
return testCaseNodeMapper.insert(node) > 0;
}
/**
* 获得同级模块下一个 pos 值
* @param projectId project id
* @param level node level
* @param parentId node parent id
* @return 同级模块下一个 pos 值
*/
private Integer getNextLevelPos(Integer projectId, Integer level, Integer parentId) {
List<TestCaseNodeDO> list = new LambdaQueryChainWrapper<>(testCaseNodeMapper)
.eq(TestCaseNodeDO::getProjectId, projectId)
.eq(TestCaseNodeDO::getLevel, level)
.eq(parentId != null, TestCaseNodeDO::getParentId, parentId)
.orderByDesc(TestCaseNodeDO::getPos)
.list();
if (!CollectionUtils.isEmpty(list)) {
return list.get(0).getPos() + 1024;
} else {
return 1024;
}
}
删除比较简单
public boolean deleteNodes(List<Integer> ids) {
return testCaseNodeMapper.deleteBatchIds(ids) > 0;
}
Vue
前端展示效果如图所示:每个节点鼠标停留时,会出现编辑、添加和删除的按钮
<el-tree
class="filter-tree node-tree"
:data="treeNodes"
:default-expanded-keys="expandedNode"
node-key="id"
@node-drag-end="handleDragEnd"
@node-expand="nodeExpand"
@node-collapse="nodeCollapse"
:filter-node-method="filterNode"
:expand-on-click-node="false"
highlight-current
:draggable="draggable"
:props="defaultProps"
ref="tree">
<template v-slot:default="{node,data}">
<span class="custom-tree-node father" @click="handleNodeSelect(node)">
<span class="node-icon">
<i class="el-icon-folder"></i>
</span>
<!--如果没修改过字段,这里对应后端应该使用node.name-->
<span class="node-title">{{ node.label }}</span>
<span v-if="type === 'edit' && !disabled" class="node-operate child">
<el-tooltip
class="item"
effect="dark"
:open-delay="200"
content="重命名"
placement="top">
<i @click.stop="openEditNodeDialog('edit', data)" class="el-icon-edit"></i>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:open-delay="200"
content="添加子模块"
placement="top">
<i @click.stop="openEditNodeDialog('add', data)" class="el-icon-circle-plus-outline"></i>
</el-tooltip>
<el-tooltip class="item" effect="dark"
:open-delay="200" content="删除" placement="top">
<i @click.stop="remove(node, data)" class="el-icon-delete"></i>
</el-tooltip>
</span>
</span>
</template>
</el-tree>
因为是封装过的tree,有一些不需要的元素或字段大家可自行删除,icon的@click事件请自行添加
async handleDragEnd(draggingNode, dropNode, dropType) {
if (dropType === 'none' || dropType === undefined) {
return
}
const param = this.buildParam(draggingNode, dropNode, dropType)
this.list = []
this.getNodeTree(this.treeNodes, draggingNode.data.id, this.list)
// 这里进行drag的接口请求,如果拖拽成功,再调用position接口
const res = await Node.dragNode(param)
if (res.code < window.MAX_SUCCESS_CODE) {
await Node.nodePosition(this.list)
...
} else {
...
}
},
buildParam(draggingNode, dropNode, dropType) {
let param
if (dropNode.level === 1 && dropType !== 'inner') {
param = draggingNode.data
} else {
this.treeNodes.some(node => {
param = this.findTreeByNodeId(node, dropNode.data.id)
return param
})
}
return param
},
findTreeByNodeId(rootNode, nodeId) {
if (rootNode.id === nodeId) {
return rootNode
}
if (rootNode.children) {
for (let i = 0; i < rootNode.children.length; i++) {
if (this.findTreeByNodeId(rootNode.children[i], nodeId)) {
return rootNode
}
}
}
},
getNodeTree(nodes, id, list) {
if (!nodes) {
return
}
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === id) {
list[0] = i - 1 >= 0 ? nodes[i - 1].id : 0
list[1] = nodes[i].id
list[2] = i + 1 < nodes.length ? nodes[i + 1].id : 0
return
}
if (nodes[i].children) {
this.getNodeTree(nodes[i].children, id, list)
}
}
},
// 这里点击删除按钮时会调用remove弹出弹出层,二次确认删除才会调用confirmRemove进行删除
remove(node) {
const nodeIds = []
this.getChildNodeId(node.data, nodeIds)
this.removeNodeIds = nodeIds
this.confirmDeleteDialog = true
},
async confirmRemove() {
const res = await Node.deleteNodes(this.removeNodeIds)
if (res.code < window.MAX_SUCCESS_CODE) {
...
} else {
...
}
},
getChildNodeId(rootNode, nodeIds) {
// 递归获取所有子节点ID
nodeIds.push(rootNode.id)
if (rootNode.children) {
for (let i = 0; i < rootNode.children.length; i++) {
this.getChildNodeId(rootNode.children[i], nodeIds)
}
}
},
补上css
.custom-tree-node {
flex: 11 auto;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
width: 100%;
}
.node-tree {
margin-top: 15px;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0 5px;
}
到这里大概功能就已经OK了,写文章时删除了部分我觉得不需要po出的代码,如有遗漏请在文章下方留言,我会再不断更新文章内容~
更多推荐
已为社区贡献12条内容
所有评论(0)