公司最近的设计中用的不少的一个组件,大致是参考的头条-广告系统中的级联面板。在此简单记录一下组件的设计和开发心得。vue
根据效果图,首先须要把省市区的数据按列展现在左侧区域,点击父级节点联动展现子级数据,每次点击展开节点的下一级所在的列。
勾选父级节点,子级节点全选,反之全选子级节点,父节点变为勾选状态。每次进行勾选以后,右侧面板展现勾选结果。
这里有一个细节,就是右侧面板展现的选择结果不是简单的展现每个被勾选中的节点。而是依据当某一个节点下的子节点所有被选中,则只展现父节点的原则,进行展现。我把它简单称为级联数据的压缩原则。
通过上面的分析能够发现这个组件本质上是一个扁平化了的checkbox-tree组件。以前关于相似省市区这种带有级联关系的数据选择,传统的交互设计每每就是采用的checkbox-tree。node
开发这个组件以前,已经有太重构老项目中的checkbox-tree的经验,参考的是element-ui的tree组件,学习了不少关于依赖于树形结构的组件构建技巧。组件借鉴了element-ui和iview的tree组件,在此由衷感受这些开源项目。 有了上述的分析,咱们来正式开撸代码。由于是使用vue做为技术栈。第一步,也是最关键的一步,就是定义好组件的props和data。git
props: {
data: {//展现数据
type: Array,
required: true
},
props: {//数据中的key和label字段别名,
由于外部数据(例如后端返回的树形结构)中标志label和key的字段每每不是固定的
type: Object,
default() {
return {
key: "id",
label: "label"
};
}
},
settings: {
/*配置,容许自定义每一级,eg:[
{
level: 1,//列的级别,由于组件内部有一个虚拟的根节点,因此level从1开始
title: "一级分类",//列的标题
hasAllCheck: true,//是否展现全选checkbox
showCheckBox: true//是否带有checkbox
},
{
level: 2,
title: "二级分类",
hasAllCheck: true,
showCheckBox: true
}
]*/
type: Array,
required: true
},
checkedLevel: {//数据展现的级别
type: Number,
required: true
},
zipLevel: {//数据压缩的级别
type: Number,
required: true
},
isSingle: {//是不是单选模式,若是为true,则降级为一个级联选择器
type: Boolean,
default: false
}
}
data() {
return {
rootNode: null,//组件内部使用的树形数据结构。采用Node类型对data进行包装获得的树的根节点
flattenData: [],//扁平化后的数据,方便查找任意节点
curShowList: [],//控制当前面板的展开收起状态
checkedData: []//勾选中的数据
};
}
复制代码
组件内部使用了Node 类型的对象来包装用户传入的数据,用来保存目前节点的状态。关于Node类型的具体包装过程这里就再也不赘述,须要的话能够看源码或者搜索相关数据结构的介绍。这里仅对比一下用户传入的data和组建内部的Node。
用户传入的:github
[
{
id: 1,
label: "一级 1",
children: [
{
id: 4,
label: "二级 1-1",
children: [
{
id: 9,
label: "三级 1-1-1"
},
{
id: 10,
label: "三级 1-1-2"
}
]
}
]
},
{
id: 2,
label: "一级 2",
children: [
{
id: 5,
label: "二级 2-1"
},
{
id: 6,
label: "二级 2-2"
}
]
},
{
id: 3,
label: "一级 3",
children: [
{
id: 7,
label: "二级 3-1"
},
{
id: 8,
label: "二级 3-2",
children: [
{
id: 11,
label: "三级 3-2-1"
},
{
id: 12,
label: "三级 3-2-2"
},
{
id: 13,
label: "三级 3-2-3"
}
]
}
]
}
]
复制代码
组件内部包装过的rootNodeelement-ui
有了根节点以后,经过查找childNodes而且递归就可以构建出级联面板的template。后端
handleCheck(isCheck, id, immediate = true/*是否当即进行数据压缩 */) {
const checkedLevel = this.checkedLevel;
//勾选当前级别及子级
const selectNode = this.flattenData.find(item => item.id === id);
if (!selectNode) {
return;
}
//递归
//由父到子
function setCheck(node) {
node.checked = isCheck;
if (!node.childNodes.length && node.level < checkedLevel) {
node.noChildChecked = isCheck;
}
if (!Array.isArray(node.childNodes) || !node.childNodes.length) {
return;
}
node.childNodes.forEach(node => {
setCheck(node);
});
}
//由子到父
function setParentCheck(parent) {
if (!parent || !parent.parent) {
return;
}
parent.checked = parent.childNodes.every(child => {
return child.checked === true;
});
setParentCheck(parent.parent);
}
setCheck(selectNode);
setParentCheck(selectNode.parent);
if (immediate) {
this.getCheckedData();
}
}
复制代码
节点的联动首先经过扁平化数据查找到当前节点。再对该节点进行由父到子和由子到父两个方向的checked设置。数组
这里由于涉及到全选或者批量设置节点的勾选状态,因此有一个参数标志是否当即调用数据压缩的方法。bash
列的展开经过节点的select来触发,包括勾选和点击事件。数据结构
handleSelect(id) {
//单选
const selectNode = this.flattenData.find(item => item.id === id);
selectNode.parent.childNodes.forEach(node => (node.selected = false));
selectNode.selected = true;
//下一级展现出来,更深的层级不渲染
if (selectNode.level < this.maxLevel) {
this.curShowList[selectNode.level] = !!selectNode.childNodes.length;
for (let i = selectNode.level + 1; i < this.maxLevel; i++) {
this.curShowList[i] = false;
}
}
//单选模式,逻辑变为相似级联选择器,选择非最深层次的节点直接清空当前节点下全部的checked结果,视为从新选择
if (this.isSingle && selectNode.level !== this.maxLevel) {
this.flattenData.forEach(p => (p.checked = false));
this.getCheckedData();
}
}
复制代码
列的展开经过控制curShowList数组,数组的每一项的true or false对应每一列的展开或者收起。这里额外提供一个isSingle的props能够把组件降级为级联选择器,知足只能单选的状况。iview
getCheckedData() {
const result = [];
const toZipData = this.flattenData.filter(p => p.level === this.zipLevel);
function step(nodes) {
if (!nodes || !nodes.length) {
return;
}
const curSelectData = nodes.filter(p => p.checked || p.noChildChecked);
const noSelectData = nodes.filter(
p => !(p.checked || p.noChildChecked)
);
result.push(...curSelectData);
noSelectData.forEach(p => step(p.childNodes));
}
step(toZipData);
this.checkedData = result;
}
复制代码
首先经过扁平化的数组过滤出目标压缩级别的数据,直接把其中选中的数据push到结果中,只把没有勾选的数据看成下一次递归过程的目标数据,递归出口是节点不存在或者没有子节点。
setCheckedNodes(keys) {//设置节点的选中,可用于搜索
/* public API */
keys.forEach(key => {
this.setCheckedNode(key, false);
});
this.getCheckedData();
},
getCheckedNodes(isZip = true) {//获取选中的数据
/* public API */
if (isZip) {
return this.checkedData.map(item => {
return {
id: item.id,
text: item.text,
data: item.data,
level: item.level
};
});
} else {
return this.flattenData.filter(p => p.checked || p.noChildChecked);
}
}
复制代码
组件相对还只是提供了基础的功能,有待完善。若有错漏,欢迎指正!但愿能让你们有点收获。下面是项目的github地址 github.com/juenanfeng/…