el-tree 从叶子节点入手解决选中问题
0. 缘起
第一次用el-tree,是一个很阴间的任务,不过可以抄小伙伴的,快乐抄了过来结果发现我这边出了个BUG,省市区,市级选中其下所有的区也被选中。但我一看请求返回值,它的是否选中状态是正确的。所以我需要抓抓代码BUG。
1. 封装el-tree
整体封装组件代码见附录
<self-tree
:tree-data="treeData"
:is-check="true"
:use-plus="true"
:defaultProps="defaultProps"
:is-search="false"
:defaultCheckedKeys="defaultCheckedKeys"
@check="handleNodeCheck"
:show-indicator="true"
/>
props传进去一些当前的value名
defaultProps: {
children: "list",
label: "name",
isLeaf: "leaf",
},
这一步非常关键,要记住el-tree判断是否选中要从叶子结点看!如果单纯以是否选中(tick为true),就会出现非叶节点选中,其下级全部选中的BUG。
resolveData(data, newArr, level, regionStr)这个递归函数深度搜索,如果到达该分支叶节点,就进行下一分支的查找。
data :递归数组 ---newArr: 记 id ---level:深度 --- regionStr:
// 数据处理
resolveData(data, newArr, level, regionStr) {
level++;
data.forEach((item) => {
if (level <= 3) {
if (level === 1) {
regionStr = "";
item.region = item.id + "";
} else {
item.region = regionStr + "," + item.id;
}
item.type = "region";
}
// important step
if (item.tick && level === 3) {
newArr.push("" + item.id);
this.checkedIdList.push(item.region);
}
if (item.list) {
this.resolveData(item.list, newArr, level, item.region);
}
});
},
获取生成树的数据,同时也是递归开始
async getAreaConnectList() {
let res = await getAreaConnectList(this.chosen);
if (res.code === 200) {
this.treeDataAll = res.data || [];
} else {
this.$message.error("获取区域关联数据失败", res.code);
}
let arr = [];
let level = 0;
let regionStr = "";
this.resolveData(this.treeDataAll, arr, level, regionStr);
this.defaultCheckedKeys = arr || [];
},
handleNodeCheck(val) {
this.checkedIdList = val.checkedNodes.map((item) => {
return item.region;
});
},
2. 心得
这几天搞什么级联选择器 树结构,和数组打交道很多。要注意数据格式和回显方式,想清楚怎么来 怎么发送。
el-tree判断是否选中要从叶子结点看!如果单纯以是否选中(tick为true),就会出现非叶节点选中,其下级全部选中的BUG
附录
<!--基于el-tree的树形组件-->
<template>
<div class="self-tree">
<el-input v-if="isSearch" ref="treeInputRef" v-model="search" size="small" placeholder="搜索" @change="inputChange"
clearable>
<em slot="suffix" class="el-input__icon el-icon-search"></em>
</el-input>
<el-tree :class="['self-tree-common',usePlus ?'self-plus-icon-tree':'',showIndicator?'self-indicator-tree':'']"
ref="treeRef"
style="margin-top: 10px"
:data="treeData"
:props="defaultProps"
:filter-node-method="filterNode"
:show-checkbox="isCheck"
:node-key="nodeKey"
:indent="showIndicator ? 0 : 16"
:icon-class="usePlus ? 'el-icon-circle-plus-outline':''"
:default-checked-keys="defaultCheckedKeys"
:expand-on-click-node="expandOnClickNode"
:default-expand-all="defaultExpandAll"
:lazy="lazy"
:load="lazyLoad"
:draggable="draggable"
@node-click="handleNodeClick"
@check="handleNodeCheck"
@node-contextmenu="handleContextMenu"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleNodeDrop"
:allow-drop="allowDrop"
:allow-drag="allowDrag"
highlight-current>
<span slot-scope="{node,data}">
<!--添加图标-->
<span><vab-icon :icon="['fas',data.icon]"></vab-icon>{{ node.label }} <i class="el-icon-circle-close"
style="margin-left: 10px"
@click="removeSelectNode(node,data)"
v-if="node.level !== unDragLevel && showNodeClose"></i></span>
</span>
</el-tree>
</div>
</template>
<script>
export default {
name: "selfTree",
props: {
// 节点数据
treeData: {
type: Array,
default: () => []
},
// 是否显示搜索框
isSearch: {
type: Boolean,
default: () => true
},
// 默认搜索名
defaultSearch: {
type: String,
default: () => "浙江省"
},
// 是否开启复选框
isCheck: {
type: Boolean,
default: () => false
},
// 节点key值
nodeKey: {
type: String,
default: () => "id"
},
// 是否在点击节点的时候展开或者收缩节点
expandOnClickNode: {
type: Boolean,
default: () => true
},
// 默认选中的节点
defaultCheckedKeys: {
type: Array,
default: () => []
},
// 使用+/- icon图标
usePlus: {
type: Boolean,
default: () => false
},
// 显示指示器
showIndicator: {
type: Boolean,
default: () => false
},
// 使用懒加载方式
lazy: {
type: Boolean,
default: () => false
},
// 懒加载函数
lazyLoad: {
type: Function,
default: () => 1
},
defaultProps: {
type: Object,
default: () => {
return {
children: "children",
label: "name",
isLeaf: "leaf"
};
}
},
// 是否允许拖拽
draggable: {
type: Boolean,
default: () => false
},
// 不可拖拽的层级
unDragLevel: {
type: Number,
default: () => 1
},
allowDrag: {
type: Function,
default: () => true
},
allowDrop: {
type: Function,
default: () => true
},
// 是否默认全部展开
defaultExpandAll: {
type: Boolean,
default: () => false
},
// 是否显示节点删除按钮
showNodeClose: {
type: Boolean,
default: () => false
}
},
data() {
return {
search: ""
};
},
watch: {
search(val) {
this.$refs?.treeRef?.filter(val);
}
},
methods: {
// 输入框值变化
inputChange() {
this.$emit("inputChange", this.search);
},
filterNode(value, data) {
if (!value) return true;
return data.label ? data.label.indexOf(value) !== -1 : data.name.indexOf(value) !== -1;
},
// 处理节点点击事件
handleNodeClick(data, node, $el) {
// 复选框模式下,点击事件不触发
if (this.isCheck) return;
this.$emit("node-click", data, node);
},
// 处理节点复选事件
handleNodeCheck(data, list) {
this.$emit("check", list);
},
// 处理右键点击事件
handleContextMenu(event, data, node, $el) {
this.$emit("node-contextmenu", {event, data, node, el: $el});
},
// 新增节点
appendNode(data, key) {
let parentNode = this.$refs.treeRef.getNode(key);
parentNode.isLeaf = false;
if (parentNode.expanded) {
if (!parentNode.children) {
parentNode.children = [];
}
this.$refs.treeRef.append(data, parentNode);
} else {
parentNode.expand();
}
},
// 删除节点
removeNode(key) {
let node = this.$refs.treeRef.getNode(key);
let parentNode = node.parent;
this.$refs.treeRef.remove(node);
parentNode.isLeaf = !(parentNode.childNodes && parentNode.childNodes.length);
parentNode.expand();
},
// 删除选中节点
removeSelectNode(node, data) {
this.$emit("on-remove-node", node, data);
},
// 更新节点名称
refreshNode(name, key) {
let node = this.$refs.treeRef.getNode(key);
node.data.label = name;
node.data.name = name;
},
handleDragStart(node, ev) {
this.$emit("node-drag-start", node, ev)
},
handleDragEnter(draggingNode, dropNode, ev) {
this.$emit("node-drag-enter", draggingNode, dropNode, ev)
},
handleDragLeave(draggingNode, dropNode, ev) {
this.$emit("node-drag-leave", draggingNode, dropNode, ev)
},
handleDragOver(draggingNode, dropNode, ev) {
this.$emit("node-drag-over", draggingNode, dropNode, ev)
},
handleDragEnd(draggingNode, dropNode, dropType, ev) {
this.$emit("node-drag-end", draggingNode, dropNode, dropType, ev)
},
handleNodeDrop(draggingNode, dropNode, dropType, ev) {
this.$emit("node-drop", draggingNode, dropNode, dropType, ev)
}
},
mounted() {
if (this.isSearch) {
setTimeout(() => {
this.search = this.defaultSearch;
}, 500);
}
}
};
</script>
<style lang="scss">
.self-indicator-tree {
.el-tree-node {
position: relative;
padding-left: 16px; // 缩进量
}
.el-tree-node__children {
padding-left: 16px; // 缩进量
}
// 竖线
.el-tree-node::before {
content: "";
height: 100%;
width: 1px;
position: absolute;
left: -3px;
top: -26px;
border-width: 1px;
border-left: 1px dashed #52627C;
}
// 当前层最后一个节点的竖线高度固定
.el-tree-node:last-child::before {
height: 38px; // 可以自己调节到合适数值
}
// 横线
.el-tree-node::after {
content: "";
width: 24px;
height: 20px;
position: absolute;
left: -3px;
top: 12px;
border-width: 1px;
border-top: 1px dashed #52627C;
}
// 去掉最顶层的虚线,放最下面样式才不会被上面的覆盖了
& > .el-tree-node::after {
border-top: none;
}
& > .el-tree-node::before {
border-left: none;
}
// 展开关闭的icon
.el-tree-node__expand-icon {
font-size: 16px;
&.is-leaf {
color: transparent;
}
}
}
.self-plus-icon-tree {
.el-tree-node__expand-icon.expanded {
transform: rotateX(0deg);
}
.el-tree-node__expand-icon.expanded:before {
content: '\e722';
}
}
</style>
人生到处知何似,应似飞鸿踏雪泥。