高仿頭條-廣告系統中的級聯面板組件

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

頭條的效果

需求分析

根據效果圖,首先須要把省市區的數據按列展現在左側區域,點擊父級節點聯動展現子級數據,每次點擊展開節點的下一級所在的列。
勾選父級節點,子級節點全選,反之全選子級節點,父節點變爲勾選狀態。每次進行勾選以後,右側面板展現勾選結果。
這裏有一個細節,就是右側面板展現的選擇結果不是簡單的展現每個被勾選中的節點。而是依據當某一個節點下的子節點所有被選中,則只展現父節點的原則,進行展現。我把它簡單稱爲級聯數據的壓縮原則。
通過上面的分析能夠發現這個組件本質上是一個扁平化了的checkbox-tree組件。以前關於相似省市區這種帶有級聯關係的數據選擇,傳統的交互設計每每就是採用的checkbox-tree。node

開發這個組件以前,已經有太重構老項目中的checkbox-tree的經驗,參考的是element-ui的tree組件,學習了不少關於依賴於樹形結構的組件構建技巧。組件借鑑了element-ui和iview的tree組件,在此由衷感受這些開源項目。 有了上述的分析,咱們來正式開擼代碼。由於是使用vue做爲技術棧。第一步,也是最關鍵的一步,就是定義好組件的props和data。git

props和data

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 類型的對象來包裝用戶傳入的數據,用來保存目前節點的狀態。關於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

Node類型的數據添加了checked(是否勾選),disabled(是否禁用),id(組件內部自增的惟一標識),level(節點深度),selected(是否選中),text(節點的文本)。提早定義好這些屬性纔可以讓咱們的組件變成響應式的(這些屬性用戶能夠在初始化的時候選擇傳入也能夠不傳)。
同時具備parent和childNodes來進行任意方向上的查找。

有了根節點以後,經過查找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);
      }
    }
複製代碼

擴展方向

  1. 組件須要支持懶加載,知足數據量大的狀況。
  2. 組件沒有提供插槽,例如右側面板數據展現的定製化,左側列的標題等。

結語

組件相對還只是提供了基礎的功能,有待完善。若有錯漏,歡迎指正!但願能讓你們有點收穫。下面是項目的github地址 github.com/juenanfeng/…

相關文章
相關標籤/搜索