美文网首页
基于element-ui的table实现的树级表格操作及单元格合

基于element-ui的table实现的树级表格操作及单元格合

作者: Lepetitange | 来源:发表于2022-10-29 17:59 被阅读0次

    如题,公司业务需求,数据结构比较复杂,需要在一张表内实现多级树状数据展示及同属性的单元格合并,并在表格内实现增删改操作。

    网上翻阅了很多实例,没有能解决所有需求的案例,于是自己实现了一套。

    时间匆忙,逻辑有优化的地方还请无偿指出!

    最终效果如下

    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. 删除
      删除可直接调用后端接口,后合并数据,无需多余处理

    至此,该表格的完整功能实现完成!!!

    相关文章

      网友评论

          本文标题:基于element-ui的table实现的树级表格操作及单元格合

          本文链接:https://www.haomeiwen.com/subject/feuhtdtx.html