基于element-ui的table實現的樹級表格操作及單元格合并

如題,公司業務需求,數據結構比較復雜,需要在一張表內實現多級樹狀數據展示及同屬性的單元格合并,并在表格內實現增刪改操作。

網上翻閱了很多實例,沒有能解決所有需求的案例,于是自己實現了一套。

時間匆忙,邏輯有優化的地方還請無償指出!

最終效果如下

image

圖上,編碼有父子層級,每個編碼可包含多個交付階段,每個交付階段可包含多個文件,每個文件可添加不同文檔項次

實現總結如下

一. 結構調整

首先跟后臺確認了數據結構,根據右側最詳細內容為基準,以單層數組返回(以編碼樹級返回更好)。獲取到數據后封裝為樹級數據。保證最詳細處表格每一行都對應一條數據。如圖示,忽略為展開子級數據,則圖上一共對應七條數據。

其中,每個數據對象帶有三個屬性:code_cnt(每條編碼下對應的第三部分行數)、stage_cnt(每個編碼下的交付階段對應的第三部分行數)、file_cnt(每個文件對應的第三部分行數)。后面用于表格合并。

  1. 封裝完數據或直接獲取到父子層級后,因存在多條數據同一編碼,每條數據下都有相同children數據存在,所以需刪除多余children,保留一條。又因展開時需展示在相同編碼下方,所以需保存相同編碼最后一條數據的children字段。如圖上所示,X-R1.1.4編碼有三條數據,應只保留項次編碼為-D3.2.2的children數據,以保證點擊展開子級時子層級展示在三條數據下方。
// 當同一編碼多條數據且有children時,保留最后一級children
    deleteChildren(data) {
      for (let i = 0; i < data.length; i++) {
        if (data[i].children && data[i].children.length) {
          data[i].hasChild = true;  // 后續解釋
          if ( data.some( (item, index) => index > i && item.code_id === data[i].code_id ) ) {
            delete data[i].children;
          } else {
            data[i].children = this.deleteChildren(data[i].children);
          }
        }
      }
      return data;
    }
  1. 因相同編碼、相同階段、相同文件需合并,所以需要遞歸標識出每個相同編碼、階段、文件的首條數據,以滿足后續單元格合并需求。
// 單元格需合并時,標記首條數據
    dealDataBefore(data) {
      let id = "",  stage = "",  file = ""; 
      for (let i = 0; i < data.length; i++) {
        if (!id || id !== data[i].interface_item_code) {
          // 第一條
          id = data[i].interface_item_code;
          data[i].isFirstLine = true;  // 標識編碼首條數據
          stage = data[i].stage_keyid;
          data[i].isFirstStage = true;  // 標識階段首條數據
          file = data[i].deliver_file_template_id;
          data[i].isFirstFile = true;  // 標識文件首條數據
        } else {
          if (!stage || stage !== data[i].stage_keyid) {
            stage = data[i].stage_keyid;
            data[i].isFirstStage = true;
            file = data[i].deliver_file_template_id;
            data[i].isFirstFile = true;
          } else {
            if (!file || file !== data[i].deliver_file_template_id) {
              file = data[i].deliver_file_template_id;
              data[i].isFirstFile = true;
            }
          }
        }
        if (data[i].children) {
          data[i].children = this.dealDataBefore(data[i].children);
        }
      }
      return data;
    },

二. 父子層級展開合并

第一步數據處理結束后,會發現交給element-ui渲染,無法展開關閉父子層級。

因為我們第一步對數據的處理,最左側編碼展示的數據已經沒有children數據了,而有children數據的單元格將被上方合并無法點擊。


1.jpg

如上圖所示,4、5兩條數據實則第3條數據的children,而顯示的X-R1.1.4為第1條數據的單元格。
因此,我們需自己做子級的展開合并操作。

  1. 首先重寫編碼列的渲染模板
<el-table-column
    label="編碼"
    key="code"
    prop="code"
    show-overflow-tooltip
>
    <template v-slot="{ row }">
        <span v-if="row.hasChild" class="arrow-icon" @click="toggleRowExpansion(row)">
            <i :class="row.isExpand ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
        </span>
        <span>{{ row.code }}</span>
    </template>
</el-table-column>

第一步的hasChild標識意義就出來了,當有多條數據時,末條保留children,首條標記hasChild。

  1. 遞歸獲取到點擊條目的同層級下所有相同編碼的數據,后將最后一條數據子級做展開/關閉操作。即點擊上圖中X-R1.1.4的按鈕時,需獲取到相同編碼的1、2、3數據,后將3設為展開/關閉狀態。
toggleRowExpansion(row) {
    row.isExpand = !row.isExpand;
    let rowList = this.getRowList(row, this.tableList);
    const expansionRow = rowList[rowList.length - 1];
    this.$refs.detailTable &&
        this.$refs.detailTable.toggleRowExpansion(expansionRow, row.isExpand);
},
// 獲取點擊層級同編碼所有數據數組
getRowList(row, list) {
    for (let i = 0; i < list.length; i++) {
        if (list[i].id === row.id)
            return list.filter((item) => item.code === row.code );
        if (list[i].children && list[i].children.length) {
            let res = this.getRowList(row, list[i].children);
            if (res) return res;
        }
    }
    return false;
},

三. 單元格合并

第一步已經封裝好了數據,直接綁定table組件的span-method方法如下

//合并單元格
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      if (row.code_cnt > 1 && columnIndex < 3) {
        // 同編碼,前三行合并
        return {
          rowspan: row.code_cnt,
          colspan: row.isFirstLine ? 1 : 0,
        };
      }
      if (row.stage_cnt > 1 && columnIndex === 3) {
        // 同交付階段多文件,階段合并
        return {
          rowspan: row.stage_cnt,
          colspan: row.isFirstStage ? 1 : 0,
        };
      }
      if (row.file_cnt > 1 && columnIndex === 4) {
        // 同文件多項次,文件合并
        return {
          rowspan: row.file_cnt,
          colspan: row.isFirstFile ? 1 : 0,
        };
      }
    },

*四. 表格增刪改操作

截止前三步,表格的展示及交互已全部完成。
本業務流程中,文件為彈框選擇,所以不做介紹。因產品要求,需在表格內直接完成文件后文檔項次等增刪改及操作,所以實現了后續功能(無需求可止步)。
isEdit標識當前行的編輯狀態,據其修改表格列渲染模板。

  1. 新增
    因表格中文件、項次并非一定存在,所以會如第一張圖第二條數據所示,直接出現文件后面為空的情況。此種情況可直接將該行置為編輯狀態。
    若是后面幾行,則需處理數據。
    矛盾點在于,因交付文件也是合并過的單元格,所以點擊的時候也是同類數據首條,而我們添加的習慣是添加到其最后面。即當我們點擊X-R1.1.4中 測試2 交付文件的+時,我們需要在其兩條后加一條數據,并把前面單元格合并。
async handleAddFileItem(row) {
    // 該文件下無項次,則直接修改該項
    if (!row.file_item_code) {
        this.editMap[row.id] = { ...row };   // 該map用于存儲當前在編輯項的原始狀態,用于取消操作
        row.isEdit = true;
    } else {
        this.tableList = this.addCnt(row, this.tableList);
    }
},
addCnt(row, list) {
      // code_cnt 相同編碼加一
      // stage_cnt 該編碼下相同stage加一
      // file_cnt 該文件加一
      let hasAdd = false,
        addIndex = 0; // 標記加入數據下標
      let firstLineIndex = "";
      for (let i = 0; i < list.length; i++) {
        // 已循環至該添加項次,退出循環并返回修改后數據
        if (hasAdd && addIndex === i) return list;

        if (list[i].id === row.id) {
          firstLineIndex === "" && (firstLineIndex = i);
          // 同編碼所有項次cnt加一
          list[i].code_cnt++;
          if (list[i].stage_keyid === row.stage_keyid) {
            // 同交付階段cnt加一
            list[i].stage_cnt++;
            if (list[i].file_code === row.file_code) {
              list[i].file_cnt++;
            }
          }
          // 當前點擊條目
          if (list[i].union_id === row.union_id) {
            let children =
              list[i + list[i].deliver_file_cnt - 2].children || [];
            let newLine = {
              code_id: list[i].code_id,
              code_cnt: list[i].code_cnt,
              file_cnt: list[i].file_cnt,
              file_code: list[i].file_code,
              deliver_file_template_id: list[i].deliver_file_template_id,
              isEdit: true,
              isAdd: true,  // 用于后續刪除時標識刪除條目為新增還是編輯條目
              id: new Date().getTime(),  // row-key必須字段
              parent_id: list[i].parent_id,
              stage: list[i].stage,
              stage_cnt: list[i].stage_cnt,
              stage_keyid: list[i].stage_keyid,
              children: children,
              isExpand: list[firstLineIndex].isExpand,
            };
            // children遷移!!!
            // 因當前條變為最后一條,需將前面條目children遷移至本條,并同步開閉狀態
            list[i + list[i].file_cnt - 2].children = [];
            // 在所有相同文件數據最后一條后添加
            addIndex = i + list[i].file_cnt - 1;
            list.splice(addIndex, 0, newLine);
            hasAdd = true;
            if (children.length) {
              this.$nextTick(() => {
                this.$refs.detailTable.toggleRowExpansion(
                  newLine,
                  list[firstLineIndex].isExpand
                );
              });
            }
          }
        } else {
          // 未找到編碼則繼續尋找
          if (list[i].children && list[i].children.length) {
            list[i].children = this.addCnt(row, list[i].children);
          }
        }
      }
      return list;
    },
  1. 編輯
    編輯操作較為簡單,將isEdit置為true,并在editMap中保存初始狀態即可
    this.editMap[row.union_id] = { ...row };
    row.isEdit = true;
  2. 新增/編輯條目刪除/取消修改操作
async cancelFileItemDeal(row) {
    if (row.isAdd) {
        // 新增條目
        this.tableList= this.delCnt(row, this.tableList);
    } else {
        // 編輯項復原
        for (let key in this.editMap[row.id]) {
            row[key] = this.editMap[row.id][key];
        }
        delete this.editMap[row.id];
    }
},
delCnt(row, list) {
    // code_cnt 相同編碼減一
    // stage_cnt 該編碼下相同stage減一
    // file_cnt 該文件減一
    let hasDelete = false;
    let firstLineIndex = "";
    for (let i = 0; i < list.length; i++) {
        // 已刪除并循環至其他項次,退出循環
        if (hasDelete && list[i].id !== row.id) return list;

        if (list[i].id === row.id) {
            firstLineIndex === "" && (firstLineIndex = i);
            // 同編碼所有項次cnt加一
            list[i].code_cnt--;
            if (list[i].stage_keyid === row.stage_keyid) {
                // 同交付階段cnt加一
                list[i].stage_cnt--;
                if (list[i].file_code === row.file_code) {
                  list[i].file_cnt--;
                }
            }
            // 當前點擊條目
            if (list[i].id === row.id) {
                let children = list[i].children;
                if (children && children.length) {
                    list[i - 1].children = children;
                    this.$nextTick(() => {
                        this.$refs.detailTable.toggleRowExpansion(
                            list[i - 1],
                            list[firstLineIndex].isExpand
                        );
                    });
                }
                // 直接刪除
                list.splice(i, 1);
                hasDelete = true;
            }
        } else {
            // 未找到編碼則繼續尋找
            if (list[i].children && list[i].children.length) {
                list[i].children = this.delCnt(row, list[i].children);
            }
        }
    }
    return list;
},
  1. 刪除
    刪除可直接調用后端接口,后合并數據,無需多余處理

至此,該表格的完整功能實現完成!!!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容