话不多说,先上效果图:
https://player.bilibili.com/player.html?aid=292282209
实现思路
源数据结构
首先我们的数据结构大概是:
[
{
"id": "0",
"name": "动物分类",
"pid": "-1",
"children": [
{
"id": "1",
"name": "哺乳动物",
"pid": "0",
"children": [
{
"id": "3",
"name": "狗",
"pid": "1",
"children": []
},
{
"id": "4",
"name": "猫",
"pid": "1",
"children": []
},
{
"id": "5",
"name": "大象",
"pid": "1",
"children": []
}
]
},
{
"id": "2",
"name": "鸟类",
"pid": "0",
"children": [
{
"id": "6",
"name": "喜鹊",
"pid": "2",
"children": []
},
{
"id": "7",
"name": "麻雀",
"pid": "2",
"children": []
},
{
"id": "8",
"name": "乌鸦",
"pid": "2",
"children": []
}
]
}
]
}
]
其中id和pid方便进行父子节点的关系确定,不管服务器提供给我们的数据结构是怎样的,这两项必不可少。
同时,由于不同开发者的数据的内容会存在差异,所以我们需要定义一个通用的Node对象,将用户数据改为统一的标准对象方便操作。如下:
/**
* 创建Node对象
*/
createNode(id, pid, lable) {
let node = new Object();
//节点id
node.id = id;
//父节点id
node.pid = pid;
//文字
node.lable = lable;
//上一级Node(实际是记录父节点的索引的值)
node.parentNode = null;
//下一级子node的数据数组(实际是记录子节点的索引的值)
node.childrenNode = [];
//是否展开
node.isExpand = false;
//icon图标(+,-)
node.icon = -1;
//当前的级别(层级)
node.level = 0
//checkbox是否选中
node.checkbox = false;
//自己的索引值
node.index = -1;
return node;
},
==注意==:至于为什么parentNode和childrenNode 的属性值为什么是记录索引而不是对象,是因为小程序的setData方法在将js数据发送给wxml页面时,是需要调用JSON.stringify()转为json字符串,而我们如果parentNode或childrenNode的值为对象时,会存在对象关系的引用,==会报循环引用的错误,进而调用栈溢出异常==.暂时没有想到好的解决版本,只能曲线救国了,通过索引来找到具体的Node对象.有知道更好的解决办法的小伙伴可以讨论一下。
将源数据转为通用的Node的数组并排序确立父子关系
转化为通用的Node数组并排序
转化为通用的Node数组并排序并不难,我们将服务器给我们的数据通过递归循环调用,放入到Node的Array中。
部分代码如下:
getAllNodes(convertedNodesArray, array) {
array.forEach(function (ele) {
//转化为node对象
let nodedata = _this.createNode(ele.id, ele.pid, ele.name);
//存入数组中
convertedNodesArray.push(nodedata)
//如果有子节点继续递归调用
if (ele.children.length > 0) {
_this.getAllNodes(convertedNodesArray, ele.children);
}
})
},
通过这个递归调用,我们集合中的数据项,应该是 ==[动物分类,哺乳动物,狗,猫,大象,鸟类,麻雀,喜鹊,乌鸦];已经有了顺序了==
确立父子关系
将数据放到Node数据并排序比较容易,但如何确立父子关系(也就是给node的childNode和parenNode赋值)呢?解决方法是:循环比较当前节点和当前节点往后的所有节点一一进行比对:
for (let i = 0; i < convertedNodes.length; i++) {
//当前节点
let node = convertedNodes[i];
node.index = i;
for (let j = i + 1; j < convertedNodes.length; j++) {
//下一个节点
let nextNode = convertedNodes[j];
if (nextNode.pid == node.id) {
//将子节点的索引添加到自己的childrenNode数组中
node.childrenNode.push(j)
//给子节点添加父节点的索引
nextNode.parentNode = i;
} else if (nextNode.id == node.pid) {
nextNode.childrenNode.push(i);
node.parentNode = j;
}
}
}
通过上面的操作,我们现在获取的数据都是排序且已经有了父子关系的了(数据平级展示)。
过滤出可见的Node数组
我们的数据在排序后,并不是所有的数据都要展示,所以需要过滤出可以见的Node数组,真正展示到页面的数据(默认展示第一级数据),还记得我们的Node中有一个==isExpand==属性,这个属性值决定是否展示当前Node节点,默认为false,只有父节点的==isExpand==状态为true,子节点的==isExpand==才为true.还有一个属性是==parentNode==,默认为null,只有根节点的parentNode才为null,我们根据这两个属性值进行判断,相关代码如下:
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
//根节点或父节点展开的子节点(相对)都属于可见node
if (_this.isRoot(node) || _this.isParentExpand(nodes, node)) {
//设置左侧的图标为可展开
_this.setParentNodeIcon(node)
result.push(node);
}
}
//判断父节点是否展开
isParentExpand(nodes, node) {
//如果是根节点
if (node.parentNode == null) {
return false;
}
//获取父节点的索引,判断父节点是否打开
return nodes[node.parentNode].isExpand;
},
// 是否为根节点
isRoot(node) {
//根据是否有父节点判断是否是根节点
return node.parentNode == null
},
关于复选框
关于复选框的逻辑是:如果用户点击的复选框是父节点,那么对应的子节点都要选中,同样的,如果子节点全部选中,那么父节点要自动勾选.实现思路是:递归调用判断,改变Node的checkbox值,然后过滤出可见node数组,再setdata更新列表.
关于展开关闭列表
同复选框的实现思路和逻辑一致.
总结
我已经将TreeView作为一个Component组件使用了,点击确定按钮后,会返回所有的数据及状态,使用者只需要根据自己的需求过滤出想要的数据展示即可.详情请查看代码。
TreeView多选框