一、介绍:el-tree在element文档中有查询全树的代码,本文主要是在此基础上添加了增加、删除、修改的界面样式与功能。
二、具体来说:
1、鼠标移动到树上显示删除和修改:
点击删除,当前节点删除;
点击修改,树的选中节点变成input可以重新输入名称并且右边出现取消或确认的icon。
2、点击底部添加按钮,如果未选中树节点,则在最外层新增input进行名称输入,右边同样有取消或确认的icon。若选中树节点,则在选中节点下添加子节点,同样以input形式输入名称。
三、实现效果(这个操作速度慢得我怀疑自己开了0.5倍速😂😂):
文章会先用截图对部分代码进行解释(element文档中有的代码,比如查询就不做解释了),随后文章底部会给出相应的demo代码。
四、实现代码+解释+坑:
1、dom代码:
2、data里的代码:
3、节点的数据格式
4、methods里的代码:
项目要求,最多两层组:
到这里这个整个代码就介绍完了,这里还有个坑。
问题:当点击新增节点,再点击取消的icon,这时候观察fetchData方法中两处this.curNode的打印,会发现打印结果是不一样的。这个问题其实并不影响树功能的实现,因为在fetchData中获取数据以后,进行了this.curNode=undefined。只是出于好奇,为什么会出现这个现象?
分析:观察cancelUpdate执行后是不是走了其他方法?没有,也没有触发watch。会不会是引用数据类型的地址引用问题?发现没有。最后发现,是因为点击取消的时候冒泡了!!触发了handleNodeClick方法。
解决:
五、demo代码(以下代码根据小伙伴需要已经改成不限深度的模板组插入,并把原来深度为2的模板组插入改成了注释):
<template>
<div>
<el-input placeholder="请搜索" v-model="filterText"></el-input>
<el-tree
:highlight-current="true"
class="filter-tree"
:data="templateTree"
:props="defaultProps"
default-expand-all
:filter-node-method="filterNode"
ref="tree"
@node-click="handleNodeClick"
:expand-on-click-node="false"
:render-content="isUpdateGroup ? updateRenderContent :renderContent"
></el-tree>
<el-button type="primary" @click="handleAddGroup">添加组</el-button>
</div>
</template>
<script>
export default {
name:'tree',
data() {
return {
templateTree: [
//树的数据
{
id: "1",
text: "模板组1",
nodeId: "11",
depth: 1,
typeName: "模板组",
childrenNum: 2,
nodes: [
{
id: "12",
text: "模板组2",
nodeId: "122",
depth: 2,
typeName: "模板组",
childrenNum: 0,
nodes: [
{
id: "21",
text: "模板组3",
nodeId: "144",
depth: 3,
typeName: "模板",
childrenNum: 0,
nodes:[]
}
]
},
{
id: "13",
text: "模板1",
nodeId: "133",
depth: 2,
typeName: "模板",
childrenNum: 0
}
]
}
],
defaultProps: {
children: "nodes",
label: "text"
},
isact: "", //当前hover的节点
isactTitle: "", //记录修改节点名称
curNode: undefined, //当前选中节点
isUpdateGroup: false, //是否在修改模板组
filterText: "",
indexRecord:[],//记录节点轨迹
isBreak:false,//是否结束循环
};
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
}
},
methods: {
filterNode(value, data) {
if (!value) return true;
return data.text.indexOf(value) !== -1;
},
renderContent(h, { node, data, store }) {
return (
<span
style="flex: 1; display: flex; align-items: center; justify-content: space-between; padding-right: 8px;"
on-mouseenter={() => this.mouseenteract(data)}
on-mouseleave={() => this.mouseleaveact(data)}
>
<span>
<span>{node.label}</span>
</span>
{this.isact == data ? (
<span>
{this.isact.typeName == "模板组" ? (
<el-button
class="m-r-10"
type="text"
icon="el-icon-edit"
on-click={() => this.handleUpdateGroup(node, data)}
></el-button>
) : (
<span></span>
)}
<el-button
type="text"
icon="el-icon-delete"
on-click={(e) => this.handleDelete(node, data, e)}
></el-button>
</span>
) : (
<span></span>
)}
</span>
);
},
updateRenderContent(h, { node, data, store }) {
return (
<span style="flex: 1; display: flex; align-items: center; justify-content: space-between; padding-right: 8px;">
<span>
{this.isact == data ? (
<input
type="text"
onChange={this.handleChangeTitle.bind(this)}
value={node.label}
/>
) : (
<span>{node.label}</span>
)}
</span>
{this.isact == data ? (
<span>
{this.isact.typeName == "模板组" ? (
<span>
<el-button
class="m-r-10"
type="text"
icon="el-icon-check"
on-click={() => this.updateGroup(node, data)}
></el-button>
<el-button
class="m-r-10"
type="text"
icon="el-icon-close"
on-click={(e) => this.cancelUpdate(node, data, e)}
></el-button>
</span>
) : (
<span></span>
)}
</span>
) : (
<span></span>
)}
</span>
);
},
//获取鼠标进入节点的数据
mouseenteract(da) {
this.isact = da;
},
mouseleaveact(da) {
this.isact = "";
},
handleNodeClick(pdata) {
this.curNode = pdata;
document
.getElementsByClassName("el-tree-node__content")[0]
.setAttribute("class", "el-tree-node__content");
},
handleDelete(node, data, e) {
e.stopPropagation();
//存在则添加到子级
const parent = node.parent;
const children = parent.data.nodes || parent.data;
//若parent.data是对象,操作的是子级;如果是数组,操作的是最外层
if(Array.isArray(parent.data)){
const parentIndex = parent.data.findIndex(d => d.id === data.id);
parent.data.splice(parentIndex, 1);
}else{
const childIndex = children.findIndex(d => d.id === data.id) ;
children.splice(childIndex, 1);
}
this.curNode = undefined;
},
//新增组
handleAddGroup() {
/**
* 如果模版深度最多两层,取消该部分注释
* //最多只有两层组 不可能添加在dept2的组上 curNode存在 并且 深度超过1
if (this.curNode != undefined && this.curNode.depth != 1) {
this.$message.warning("不能添加超过两层");
return;
}
*/
//如果isUpdateGroup 已经是true了 说明重复点击了
if(this.isUpdateGroup){
return;
}
let id = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
let newChild = {
parentId: "", //如果有这个id 是插入第二层 否则是第一层 可有可无
text: "", //必须有 this.templateContent.tempName
nodes: [],
id: id,
typeName: "模板组",
temporaryData: "1" //用来区分临时数据
};
/* 如果模版深度最多两层,以下条件改成该部分注释
this.curNode && this.curNode.depth == 1
*/
this.indexRecord=[]
if (this.curNode) {
if(!this.curNode.nodes){
this.$message.warning('模板不可添加')
return
}
newChild.parentId = this.curNode.id;
/* 如果模版深度最多两层,以下条件改成该部分注释
const index = this.templateTree.findIndex(
item => item.id == this.curNode.id
);
this.templateTree[index].nodes.push(newChild);
*/
//找到tree中的index轨迹
this.getTemplateTreeNode(this.curNode.id,this.templateTree, 0)
//按照index轨迹插入节点
this.insertNode(newChild, this.templateTree, this.indexRecord, this.indexRecord.length )
this.isBreak = false
} else if (this.curNode == undefined) {
//没有选中的时候 添加到最外层
newChild.depth = 1
this.templateTree.push(newChild);
}
//调用出updateRender的input
this.isact = newChild;
this.isUpdateGroup = true;
},
//递归遍历获得选中node
getTemplateTreeNode(target, list, dept){
//空数组直接返回
if(list.length == 0) return;
let dataLen = list.length;
for(let i = 0; i < dataLen; i++){
//如果不匹配
if(target != list[i].id){
//存在nodes 遍历nodes里的节点
if(list[i].nodes){
this.indexRecord[dept] = i
let recordDept = dept+1
this.getTemplateTreeNode(target,list[i].nodes, recordDept)
}else{
//不存在nodes 继续遍历
continue;
}
}else{
//匹配,则修改下标数组
this.indexRecord[dept] = i
this.isBreak = true
break
}
//删除不匹配的轨迹 如果已经break了说明已经找到正确的节点,就不用再删了
if(!this.isBreak){
this.indexRecord.pop()
}
}
},
//插入节点
insertNode(insertChild, tree, indexArr, len){
let index = indexArr.length - len
if(len == 0) {
tree.push(insertChild)
} else{
this.insertNode(insertChild, tree[indexArr[index]].nodes, indexArr, len-1)
}
},
//修改组
handleUpdateGroup() {
this.isUpdateGroup = true;
},
//修改组名时获取title
handleChangeTitle(e) {
let value = e.target.value;
this.isactTitle = value;
},
updateGroup(node, data) {
//先handleChangeTitle获取title 再调用
setTimeout(() => {
if (this.isactTitle.trim() == "") {
this.$message.warning("名称不能为空");
return;
}
//修改数据组
this.isUpdateGroup = false;
const parent = node.parent;
const children = parent.data.nodes || parent.data;
const index = children.findIndex(d => d.id === data.id);
let temp = data;
temp.text = this.isactTitle;
children.splice(index, 1, temp);
}, 500);
},
cancelUpdate(node, data, e) {
this.$message.info("已取消");
this.isUpdateGroup = false;
//如果是插入操作 需要移除数据
if (this.isact.temporaryData) {
this.handleDelete(node, data, e);
}
}
}
};
</script>
<style lang="scss" scoped>
.el-tree-node__content {
.el-button {
display: none;
}
}
.el-tree-node__content:hover {
.el-button {
display: inline;
}
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: #eaebed;
color: #4796ec;
font-weight: bold;
}
.el-tree {
height: 350px;
overflow-y: auto !important;
.el-tree-node__content span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
</style>