公司最近的設計中用的不少的一個組件,大致是參考的頭條-廣告系統中的級聯面板。在此簡單記錄一下組件的設計和開發心得。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/…