1、概述
关于树形结构的新增同级节点,新增子级节点,修改节点名称,删除节点等四种操作,各种参数配置完全继承el-tree,本篇使用vue2 + element-ui
2、效果图展示
3、调用方式
<template>
<Tree
:data="treeData"
:props="defaultProps"
:default-expanded-keys="expandedKeys"
node-key="id"
highlight-current
:highligh-color="highlighColor"
:show-btn-group="showBtnGroup"
:draggable="true"
@node-click="nodeClick"
@editNodeSubmit="addNode"
@deleteNodeSubmit="deleteNode"
@node-drag-end="handleDragEnd"
/>
</template>
<script>
export default {
name: 'Tree',
data() {
return {
defaultProps: {
children: 'children',
label: 'label'
},
expandedKeys: [], // 展开节点
highlighColor: {
color: '#FFAE0D',
bgColor: 'rgba(255, 174, 13, .1)'
},
showBtnGroup: true,
treeData: []
}
},
methods: {
/**
* @param data 当前node节点数据
* @param node node节点
* @param component 当前组件本身
*/
nodeClick(data, node, component) {
},
/**
* @param node node节点
* @param data 当前node节点数据
* @param currentLabel 输入框的值
* @param callback 回调函数
*/
addNode(node, data, currentLabel, callback) {
// 你调用后端接口,成功则callback传true,失败则传false
callback(true)
},
/**
* @param node node节点
* @param data 当前node节点数据
* @param callback 回调函数
*/
deleteNode(node, data, callback) {
// 你调用后端接口,成功则callback传true,失败则传false
callback(true)
},
/**
* @param beDrag 被拖拽节点对应的 Node
* @param toDrag 结束拖拽时最后进入的节点
* @param position 被拖拽节点的放置位置(before、after、inner)
* @param event event事件
*/
handleDragEnd(beDrag, toDrag, position, event) {
}
}
}
</script>
4、源码解析
<template>
<el-tree
ref="tree"
:class="[
'menu-el-tree',
iconStyle.src && 'menu-expand-icon',
highlighColor && 'menu-node-highligh'
]"
:style="{
'--bgUrl': 'url(' + iconStyle.src + ')',
'--iconWidth': iconStyle.width,
'--iconHeight': iconStyle.height,
'--iconTransform': iconStyle.transform,
'--hoverColor': hoverStyle.color,
'--hoverBgColor': hoverStyle.bgColor,
'--highlighColor': highlighColor.color,
'--highlighBgColor': highlighColor.bgColor
}"
node-key="id"
:draggable="draggable"
:allow-drag="allowDrag"
:expand-on-click-node="expandOnClickNode"
v-bind="$attrs"
v-on="$listeners"
>
<template v-slot="{ node, data }">
<div v-if="!data.isEdit" class="custom-tree-node-root" @mouseenter="nodeMouseEnter(node)" @mouseleave="nodeMouseLeve(node)">
<span class="custom-tree-node">{
{ node.label }}</span>
<section v-show="node.showBtn" class="tree-btn-group">
<el-tooltip
v-for="item in btnGroup"
:key="item"
effect="light"
placement="top"
popper-class="common-tooltip-primary"
:content="{
'addChild': '新增子级',
'delete': '删除',
'edit': '修改',
'addSibling': '新增同级'
}[item]"
>
<i
:class="{
'addChild': 'el-icon-circle-plus-outline',
'delete': 'el-icon-circle-close',
'edit': 'el-icon-edit',
'addSibling': 'el-icon-plus'
}[item]"
@click.stop="() => handleOperaion(item, node, data)"
/>
</el-tooltip>
</section>
</div>
<div v-else class="custom-tree-node-root-edit">
<el-input v-model="inputValue" :size="editInputStyle.size" :style="{ width: editInputStyle.width }" />
<section :id="data.id">
<el-tooltip
content="确定"
popper-class="common-tooltip-primary"
effect="light"
placement="top"
>
<i class="el-icon-success" @click.stop="() => editMenuSubmit(node, data)" />
</el-tooltip>
<el-tooltip
content="取消"
popper-class="common-tooltip-primary"
effect="light"
placement="top"
>
<i class="el-icon-error" @click.stop="() => addSiblingCancel(node, data)" />
</el-tooltip>
</section>
</div>
</template>
</el-tree>
</template>
<script>
export default {
name: 'MenuTree',
inheritAttrs: true,
props: {
hoverStyle: {
type: Object,
default: () => ({
color: '#606266',
bgColor: '#F5F7FA'
})
},
/**
* @description 当前行选中高亮颜色配置
*/
highlighColor: {
type: Object,
default: () => ({
color: '#606266',
bgColor: '#fff9ec'
})
},
/**
* @description 动态左侧图标配置(图片格式)
*/
iconStyle: {
type: Object,
default: () => ({
src: '',
width: '20px',
height: '17px',
transform: 'rotate(270deg)'
})
},
/**
* @description 操作按钮组
* addChild => 新增子级,delete => 删除, edit => 修改, addSibling => 新增同级
*/
btnGroup: {
type: Array,
default: () => ['edit', 'addSibling', 'addChild', 'delete']
},
/**
* @description 是否展示操作按钮
*/
showBtnGroup: {
type: Boolean,
default: false
},
/**
* @description 编辑模式输入框样式
*/
editInputStyle: {
type: Object,
default: () => ({
size: 'mini',
width: '200px'
})
},
/**
* 是否可拖拽
*/
draggable: {
type: Boolean,
default: false
},
/**
* 是否在点击节点的时候展开或者收缩节点
* 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
*/
expandOnClickNode: {
type: Boolean,
default: true
}
},
data() {
return {
inputValue: ''
}
},
methods: {
/**
* @description 鼠标移入目录层级
*/
nodeMouseEnter(data) {
if (!this.showBtnGroup) return;
this.$set(data, 'showBtn', true)
},
/**
* @description 鼠标移除目录层级
*/
nodeMouseLeve(data) {
if (!this.showBtnGroup) return;
this.$set(data, 'showBtn', false)
},
/**
* 当前目录操作分发
* @param {string} eventKey 事件名称
* @param {object} currentNode 当前Node节点内容
* @param {object} data 当前节点数据
*/
handleOperaion(eventKey, currentNode, data) {
console.log(currentNode, data, 'menuRoot');
switch (eventKey) {
case 'addSibling':
this.addSibling(currentNode, data);
break;
case 'edit':
this.editTreeItem(data);
break;
case 'addChild':
this.addChild(currentNode, data);
break;
case 'delete':
this.deleteTreeItem(currentNode, data);
break;
}
},
/**
* @description 编辑当前节点
*/
editTreeItem(data) {
this.$set(data, 'isEdit', true);
this.inputValue = data.label; // 当前正在编辑内容赋值
this.$nextTick(() => {
document.getElementById(data.id).previousElementSibling.firstElementChild.focus();
})
},
/**
* @description 添加同级节点
*/
addSibling(currentNode, data) {
const treeDOM = this.$refs.tree;
const id = Math.ceil(Math.random() * 100)
const newData = { id: id, pId: data.pId, label: '', isEdit: true, isNew: true, children: [] };
treeDOM.insertAfter(newData, currentNode);
// 聚焦当前新增目录
this.$nextTick(() => {
document.getElementById(newData.id).previousElementSibling.firstElementChild.focus();
})
},
/**
* @description 添加子级节点
*/
addChild(currentNode, data) {
const treeDOM = this.$refs.tree;
const id = Math.ceil(Math.random() * 100)
const newData = { id: id, pId: data.id, label: '', isEdit: true, isNew: true, children: [] };
treeDOM.append(newData, currentNode);
// 展开子节点后才能获取DOM聚焦
treeDOM.store.nodesMap[data.id].expanded = true;
setTimeout(() => {
document.getElementById(newData.id).previousElementSibling.firstElementChild.focus();
}, 500)
},
/**
* @description 编辑模式修改确认
*/
editMenuSubmit(node, data) {
this.$emit('editNodeSubmit', {
node,
data,
currentLabel: this.inputValue,
// 新增同级/子级节点接口调用成功的话,即新增同级/子级节点
callback: (status) => {
if (status) {
this.resetNode()
}
}
});
},
/**
* @description 删除当前节点
*/
deleteTreeItem(node, data) {
this.$emit('deleteNodeSubmit', {
node,
data,
callback: (status) => {
// 删除接口调用成功的话,即删除节点
if (status) {
const treeDOM = this.$refs.tree;
treeDOM.remove(node);
}
}
});
},
/**
* @description 取消同级节点添加
*/
addSiblingCancel(node, data) {
// 如果是新增的节点,取消即是删除
if (data.isNew) {
const treeDOM = this.$refs.tree;
treeDOM.remove(node);
} else {
// 重置修改内容
this.inputValue = '';
data.isEdit = false;
}
},
/**
* 寻找第一个叶子节点及叶子节点的父节点
* @param {*} tree 平铺数组
*/
findFirstChildAndParent(tree) {
let firstChild = null;
let parentOfFirstChild = null;
const dfs = (node, parent) => {
if (firstChild !== null) {
return; // 如果已经找到了第一个子节点,则不再继续搜索
}
if (node.children && node.children.length > 0) {
// eslint-disable-next-line
for (const child of node.children) {
dfs(child, node);
}
} else {
firstChild = node;
parentOfFirstChild = parent;
}
}
// eslint-disable-next-line
for (const node of tree) {
dfs(node, null);
}
return {
firstChild,
parentOfFirstChild
};
},
/**
* 获取自身树结构实例
*/
getTree() {
return this.$refs.tree;
},
/**
* 重置节点数据
*/
resetNode() {
this.inputValue = '';
},
/**
* 寻找节点对应的父级节点
* @param {*} tree
* @param {*} nodeId
*/
findParentByChildId(tree, nodeId) {
let parentOfFirstChild = null;
const dfs = (node, parent) => {
if (parentOfFirstChild !== null) {
return;
}
if (node.children && node.children.length > 0) {
// eslint-disable-next-line
for (const child of node.children) {
dfs(child, node);
}
} else {
// 找到对应节点后,返回其父节点
if (node.id === nodeId) {
parentOfFirstChild = parent;
}
}
}
// eslint-disable-next-line
for (const node of tree) {
dfs(node, null);
}
return parentOfFirstChild
},
/**
* 允许拖动的节点
* @param {*} draggingNode 拖动节点
*/
allowDrag(draggingNode) {
// 不处于编辑状态
return !draggingNode.data.isEdit
}
}
}
</script>
<style scoped lang="scss">
// 动态配置右侧图标
.menu-expand-icon {
::v-deep .el-tree-node__expand-icon:not(.is-leaf) {
&::before {
background: var(--bgUrl);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
content: '';
width: var(--iconWidth);
height: var(--iconHeight);
display: inline-block;
transform: var(--iconTransform);
}
}
}
// 动态配置hover样式
.menu-el-tree {
::v-deep .el-tree-node__content {
&:hover {
background: var(--hoverBgColor);
color: var(--hoverColor);
}
}
.custom-tree-node-root {
display: flex;
align-items: center;
flex: 1;
.tree-btn-group {
margin-left: 15px;
display: flex;
align-items: center;
column-gap: 8px;
i {
font-size: 15px;
&:hover {
color: $primary
}
}
}
}
}
.menu-node-highligh.el-tree--highlight-current {
::v-deep .el-tree-node.is-current>.el-tree-node__content {
background: var(--highlighBgColor);
color: var(--highlighColor);
}
}
.custom-tree-node-root-edit {
display: flex;
align-items: center;
.el-input, ::v-deep .el-input .el-input__inner {
height: 26px;
}
section {
i {
margin-left: 10px;
font-size: 15px;
color: $primary;
}
}
}
</style>
5、疑难解答
1、拖拽节点时,如何监听和区分拖拽事件?
使用的是el-tree内置的node-drag-end事件,当拖拽完成时触发,抛出四个参数
* @param {*} draggingNode 被拖拽的节点
* @param {*} dropNode 最后进入的节点
* @param {*} dropType 相对比的位置
* @param {*} ev 事件
2、新增和删除节点,如何再触发后端接口后再执行节点的新增和删除?
节点新增和删除执行下列两个事件
@editNodeSubmit="addNode"
@deleteNodeSubmit="deleteNode"
他们回调的最后一个参数都是callback,当callback(true)传入为true时,则代表接口成功,则进行后续的树节点操作。
------有不懂的或建议欢迎直接评论噢~------
文章评论