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

如題,公司業(yè)務需求庸论,數(shù)據(jù)結構比較復雜职辅,需要在一張表內(nèi)實現(xiàn)多級樹狀數(shù)據(jù)展示及同屬性的單元格合并,并在表格內(nèi)實現(xiàn)增刪改操作聂示。

網(wǎng)上翻閱了很多實例域携,沒有能解決所有需求的案例,于是自己實現(xiàn)了一套鱼喉。

時間匆忙秀鞭,邏輯有優(yōu)化的地方還請無償指出!

最終效果如下

image

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

實現(xiàn)總結如下

一. 結構調(diào)整

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

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

  1. 封裝完數(shù)據(jù)或直接獲取到父子層級后,因存在多條數(shù)據(jù)同一編碼瓶颠,每條數(shù)據(jù)下都有相同children數(shù)據(jù)存在拟赊,所以需刪除多余children,保留一條粹淋。又因展開時需展示在相同編碼下方吸祟,所以需保存相同編碼最后一條數(shù)據(jù)的children字段。如圖上所示桃移,X-R1.1.4編碼有三條數(shù)據(jù)屋匕,應只保留項次編碼為-D3.2.2的children數(shù)據(jù),以保證點擊展開子級時子層級展示在三條數(shù)據(jù)下方借杰。
// 當同一編碼多條數(shù)據(jù)且有children時过吻,保留最后一級children
    deleteChildren(data) {
      for (let i = 0; i < data.length; i++) {
        if (data[i].children && data[i].children.length) {
          data[i].hasChild = true;  // 后續(xù)解釋
          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. 因相同編碼、相同階段、相同文件需合并纤虽,所以需要遞歸標識出每個相同編碼乳绕、階段、文件的首條數(shù)據(jù)逼纸,以滿足后續(xù)單元格合并需求洋措。
// 單元格需合并時,標記首條數(shù)據(jù)
    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;  // 標識編碼首條數(shù)據(jù)
          stage = data[i].stage_keyid;
          data[i].isFirstStage = true;  // 標識階段首條數(shù)據(jù)
          file = data[i].deliver_file_template_id;
          data[i].isFirstFile = true;  // 標識文件首條數(shù)據(jù)
        } 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;
    },

二. 父子層級展開合并

第一步數(shù)據(jù)處理結束后杰刽,會發(fā)現(xiàn)交給element-ui渲染菠发,無法展開關閉父子層級。

因為我們第一步對數(shù)據(jù)的處理专缠,最左側編碼展示的數(shù)據(jù)已經(jīng)沒有children數(shù)據(jù)了雷酪,而有children數(shù)據(jù)的單元格將被上方合并無法點擊。


1.jpg

如上圖所示涝婉,4哥力、5兩條數(shù)據(jù)實則第3條數(shù)據(jù)的children,而顯示的X-R1.1.4為第1條數(shù)據(jù)的單元格墩弯。
因此吩跋,我們需自己做子級的展開合并操作。

  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標識意義就出來了渔工,當有多條數(shù)據(jù)時锌钮,末條保留children,首條標記hasChild引矩。

  1. 遞歸獲取到點擊條目的同層級下所有相同編碼的數(shù)據(jù)梁丘,后將最后一條數(shù)據(jù)子級做展開/關閉操作。即點擊上圖中X-R1.1.4的按鈕時旺韭,需獲取到相同編碼的1氛谜、2、3數(shù)據(jù)区端,后將3設為展開/關閉狀態(tài)值漫。
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);
},
// 獲取點擊層級同編碼所有數(shù)據(jù)數(shù)組
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;
},

三. 單元格合并

第一步已經(jīng)封裝好了數(shù)據(jù),直接綁定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,
        };
      }
    },

*四. 表格增刪改操作

截止前三步沥邻,表格的展示及交互已全部完成危虱。
本業(yè)務流程中,文件為彈框選擇唐全,所以不做介紹槽地。因產(chǎn)品要求,需在表格內(nèi)直接完成文件后文檔項次等增刪改及操作,所以實現(xiàn)了后續(xù)功能(無需求可止步)捌蚊。
isEdit標識當前行的編輯狀態(tài),據(jù)其修改表格列渲染模板近弟。

  1. 新增
    因表格中文件缅糟、項次并非一定存在,所以會如第一張圖第二條數(shù)據(jù)所示祷愉,直接出現(xiàn)文件后面為空的情況窗宦。此種情況可直接將該行置為編輯狀態(tài)。
    若是后面幾行二鳄,則需處理數(shù)據(jù)赴涵。
    矛盾點在于,因交付文件也是合并過的單元格订讼,所以點擊的時候也是同類數(shù)據(jù)首條髓窜,而我們添加的習慣是添加到其最后面。即當我們點擊X-R1.1.4中 測試2 交付文件的+時欺殿,我們需要在其兩條后加一條數(shù)據(jù)寄纵,并把前面單元格合并。
async handleAddFileItem(row) {
    // 該文件下無項次脖苏,則直接修改該項
    if (!row.file_item_code) {
        this.editMap[row.id] = { ...row };   // 該map用于存儲當前在編輯項的原始狀態(tài)程拭,用于取消操作
        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; // 標記加入數(shù)據(jù)下標
      let firstLineIndex = "";
      for (let i = 0; i < list.length; i++) {
        // 已循環(huán)至該添加項次,退出循環(huán)并返回修改后數(shù)據(jù)
        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,  // 用于后續(xù)刪除時標識刪除條目為新增還是編輯條目
              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遷移!!!
            // 因當前條變?yōu)樽詈笠粭l棍潘,需將前面條目children遷移至本條恃鞋,并同步開閉狀態(tài)
            list[i + list[i].file_cnt - 2].children = [];
            // 在所有相同文件數(shù)據(jù)最后一條后添加
            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 {
          // 未找到編碼則繼續(xù)尋找
          if (list[i].children && list[i].children.length) {
            list[i].children = this.addCnt(row, list[i].children);
          }
        }
      }
      return list;
    },
  1. 編輯
    編輯操作較為簡單,將isEdit置為true亦歉,并在editMap中保存初始狀態(tài)即可
    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++) {
        // 已刪除并循環(huán)至其他項次恤浪,退出循環(huán)
        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 {
            // 未找到編碼則繼續(xù)尋找
            if (list[i].children && list[i].children.length) {
                list[i].children = this.delCnt(row, list[i].children);
            }
        }
    }
    return list;
},
  1. 刪除
    刪除可直接調(diào)用后端接口,后合并數(shù)據(jù)鳍徽,無需多余處理

至此资锰,該表格的完整功能實現(xiàn)完成!=准馈绷杜!

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市濒募,隨后出現(xiàn)的幾起案子鞭盟,更是在濱河造成了極大的恐慌,老刑警劉巖瑰剃,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齿诉,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機粤剧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門歇竟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抵恋,你說我怎么就攤上這事焕议。” “怎么了弧关?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵盅安,是天一觀的道長。 經(jīng)常有香客問我世囊,道長别瞭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任株憾,我火速辦了婚禮蝙寨,結果婚禮上,老公的妹妹穿的比我還像新娘号胚。我一直安慰自己籽慢,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布猫胁。 她就那樣靜靜地躺著箱亿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪弃秆。 梳的紋絲不亂的頭發(fā)上届惋,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音菠赚,去河邊找鬼脑豹。 笑死,一個胖子當著我的面吹牛衡查,可吹牛的內(nèi)容都是我干的瘩欺。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼拌牲,長吁一口氣:“原來是場噩夢啊……” “哼俱饿!你這毒婦竟也來了?” 一聲冷哼從身側響起塌忽,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤拍埠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后土居,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枣购,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嬉探,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棉圈。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涩堤。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖分瘾,靈堂內(nèi)的尸體忽然破棺而出定躏,到底是詐尸還是另有隱情,我是刑警寧澤芹敌,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站垮抗,受9級特大地震影響氏捞,放射性物質發(fā)生泄漏。R本人自食惡果不足惜冒版,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一液茎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辞嗡,春花似錦捆等、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挺狰,卻和暖如春明郭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丰泊。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工薯定, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞳购。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓话侄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親学赛。 傳聞我的和親對象是個殘疾皇子年堆,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容