Vue-Giant-Tree
项目中需要一个 支持移动端,支持懒加载,支持vue,支持单选多选,支持大批量数据的树插件。
最终选择了开源项目 巨树 :基于ztree封装的Vue树形组件,轻松实现海量数据的高性能渲染。
用起来十分简单,按照教程安装后当做一个普通组件来用就可以。
我们需要给他提供setting配置和nodes节点数据。我们这里采用懒加载的形式(初始默认加载根节点,点击哪里加载哪里)
<template>
<div class="selectTree">
<tree
:setting="setting"
:nodes="options"
@onExpand="onExpand"
@onClick="onClick"
@onCheck="onCheck"
@onCreated="onCreated"
/>
</div>
</template>
<script>
import tree from "vue-giant-tree";
export default {
name: "selectTree",
components: {
tree
},
data () {
return {
setting: {
treeId: 'myTree',
check: {
enable: true,
chkStyle: this.multiple ? "checkbox" : "radio",
chkboxType: { "Y": "", "N": "" },
radioType: 'all'
},
async: {
enable: true
},
view: {
showIcon: false,
showLine: true
}
},
options: []
}
},
created() {
this.getSelectList()
}
}
</script>
api和zTree一样,所以在使用时,直接去到了zTree的官网。
zTree
需求一: 点击输入框,跳入选择树界面,单选或多选数据后,点击确认,界面收缩,将所选数据的id和text赋给输入框。
需求二: 在树的上方放一个选中池,每次选中或取消树节点,都要和选中池联动。当然在选中池的标签叉掉时,树中的节点也跟着取消。
需求三: 数据的末级节点不需要显示下拉按钮(因为是懒加载的树,只要不请求,谁也不知道这个节点还有没有孩子,所以需要特殊处理)
需求四: 根据数据类型(单位和个人)来控制是否可以选中(也就是说,在树节点加载的时候,就要判断他是否展示单选或多选按钮)
需求五: 点击节点label可以展开节点
需求六: 展开节点请求数据时,图标加载样式(关于懒加载树添加加载图标,找一个gif图片,作为class名的背景图。然后在expand操作加载时,通过id找到相应的options加入iconSkin属性值(class名),加载完毕的回调里去掉该属性值)
问题来了
我的懒加载是这么实现的
- 组件初始化时先请求一次数据得到根节点,把数据中有用的name,id等属性map给绑定的树绑定的options数组。
这里的nocheck就是控制我们的节点是否展示选择按钮的属性,在此时我们就把他配置到options中。树节点在渲染时就会根据他进行显示。
同时,我们如果删除该节点数据中的children属性,就等于告知组件,此节点没有子节点,那么也就不会展示下拉箭头。(测了很多声明属性,还是这个好使) - 每次在点击展开按钮触发执行onExpand方法,我们再次根据节点id进行请求数据,并对属性一通处理,然后根据id获取到当前节点在options的位置,将数据塞入相应的children~
// 获取人员级联下拉菜单
getSelectList () {
listPersonnel('0').then(res => {
this.options = res.data.map(item => {
if (item.type === this.treeType) {
delete item.children
}
return {
name: item.label,
nocheck: item.type !== this.treeType,
// checked: this.checkedData.some(key => key.id == item.id), // 这种boolean方式赋值造成无法响应式回显
...item
}
})
})
},
// 加载子节点
mapOptions (arr, id, list) {
for(let i in arr) {
if (arr[i].id === id) {
if(list.length) {
arr[i].open = true
const curNode = arr[i].children
curNode.splice(0)
curNode.push(...list)
} else {
arr[i].isParent = false
}
} else {
this.mapOptions(arr[i].children, id, list)
}
}
},
// 懒加载
onExpand (evt, treeId, treeNode) {
if (treeNode.children && !treeNode.children.length){ // 防止重复请求同个节点数据
listPersonnel(treeNode.id).then(res => {
if (res.code === 200) {
const arr = res.data.map(child => {
if (child.type === this.treeType) {
delete child.children
}
return {
name: child.label,
nocheck: child.type !== this.treeType,
// checked: this.checkedData.some(key => key.id == child.id),
...child
}
})
this.mapOptions(this.options, treeNode.id, arr) // ========问题出在这,每次展开都会强制按初始化的options重新渲染所有节点哦,但不会他并不会更新绑定到data里的那个options数据。
this.checkedData.forEach(item => { // 后来加上了这段,好使!!!checkedData是选择节点的集合,可用于初始化回显选中
this.initChecked(this.options, item.id, true)
})
}
})
}
},
问题一: 我们选中的节点,在每次expand加载子节点之后就会消失???
解决: 每次展开都会强制按初始化的options重新渲染所有节点哦,但不会他并不会更新绑定到data里的那个options数据。
加入此方法(根据id找到options的数据自己更新一下):
initChecked (options, curId, check) {
options.forEach(item => {
if (!this.multiple) {
item.checked = false
}
if (item.id === curId) {
item.checked = check
} else {
if(item.children) {
this.initChecked(item.children, curId, check)
}
}
})
},
问题二: 点击文字展开
解决:
// 如果不是末级节点,而且当前还没有进行过请求数据(children为空),就请求数据进行懒加载
onClick(evt, treeId, treeNode) {
if (treeNode.children && !treeNode.children.length) {
this.onExpand(evt, treeId, treeNode)
}
this.ztreeObj.expandNode (treeNode)
},
问题三: 我们是在onCreated (ztreeObj)里获取树的实例对象,这个oncreated会在每次懒加载点开时都会重新刷新,造成获取的内容有问题,所以我们只保存最初的一次就可以了
解决:
// 获取dom对象
onCreated (ztreeObj) {
if (!this.ztreeObj) {
this.ztreeObj = ztreeObj
}
},
问题四: 点击节点选中后,我们可以通过 this.ztreeObj.getCheckedNodes(true)
来获取当前树节点被选中的节点对象集合,但是我发现,他每次获取的都是当前父节点分组的,而不是以全部树为基础。
解决: setting里有个属性 radioType: 'all'
,配置一下
setting: {
treeId: 'myTree',
check: {
enable: true,
chkStyle: this.multiple ? "checkbox" : "radio",
chkboxType: { "Y": "", "N": "" },
radioType: 'all'
},
async: {
enable: true
},
view: {
showIcon: false,
showLine: true
}
},
问题五: 现在就差通过tags标签取消选中,意味着我们需要通过id来找到树中相应的dom节点,然后对其的checked状态进行切换。切换状态需要用到this.ztreeObj.checkNode(node, false)
两个参数,第一个是节点元素,第二个是想要更新到的状态。
- 我试着通过
this.ztreeObj.getNodeByTId(id)
获取节点对象,但是不行,人家这个api要求的id是节点自己生成的treeId,不是咱们数据的id。 - 然后又试了
this.ztreeObj.getChangeCheckedNodes()
,这个能用哎。但是当选中一个节点后,再懒加载操作,树被强制刷新后,此方法便不会找到目标节点元素。所以我们换个思路,在每次操作节点后,都将得到的改变数据存起来,放入数据池。用的时候再根据id拿?但是遍历之后通过id拿treeNode发现还是不行哎
zTreeObj.getChangeCheckedNodes
返回全部勾选状态被改变的节点集合 Array
如果需要获取每次操作后全部被改变勾选状态的节点数据,请在每次勾选操作后,遍历所有被改变勾选状态的节点数据,让其 checkedOld = checked 就可以了。
解决:
onClose (id) {
this.checkedData = this.checkedData.filter(item => item.id !== id) // 去掉选中池tags
this.initChecked(this.options, id, false) // 去掉树options中的初始化checked属性,方便下次dom树刷新回显
const node = this.oldCheck.filter(item => item.id === id) // 遍历到相应的treeNode树了还是不能实时的刷新之后叉掉的item,那么只能证明是ztreeObj的问题
if (node.length) {
this.ztreeObj.checkNode(node[0], false) // 更新节点元素的选中状态
}
}
问题六: 节点选择框样式调整一下
解决:
::v-deep .ztree {
.button{
// 多选和单选框
&.chk{
height: 20px;
width: 20px;
margin-right: 8px;
// 多选选中
&:after{
height: 10px;
width: 6px;
top: 3px!important;
left: 6px!important;
}
// 单选框圆角
&.radio_false_full,
&.radio_true_full,
&.radio_true_full_focus,
&.radio_false_full_focus{
border-radius: 50%;
// 单选选中
&:after{
height: 10px;
width: 10px;
top: 4px!important;
left: 4px!important;
border-radius: 50%;
}
}
}
&:before{
/*display: none;*/
/*top: 1px !important;*/
/*border: 9px solid !important;*/
/*border-color: transparent transparent transparent #666 !important;*/
}
}
.node_name {
font-size: 18px;
}
}
所以目前基本能实现取消,只要不在操作tags之前进行懒加载操作。
唯一的问题就是在刷新树之后(懒加载),无法再通过checkNode改变节点元素的选中状态。