js中table多级表头渲染,表体行合并

736 阅读4分钟

原生jsTable动态表头合并

在日常开发中,你是否有遇到表格多级表头需求,如果是数据死的,相信大家还可以手动去设置 colspan/rowspan 属性来达到合并的效果,如果是动态的数据呢?就要各种计算当前单元格格的 colspan / rowspan,下面我就分享一下我遇到的需求吧!

前言: 动态表头的关键字无非就是在单元格上面设置 colspan/rowspan,不清楚的同学可以去MDN上看看 developer.mozilla.org/zh-CN/docs/…

文章中只是展示了核心代码,代码大家请去:gitee源码仓库看看

表头数据源

const header = [
    { key: "idx", label: "序号",type:'input' },
    { label: "用户信息",children:[ 
        { key: "name", label: "姓名",type:'input'},
        { label: "居住地址",children:[
            { key: "province", label: "省份",type:'input'},
            { key: "city", label: "市区", type: 'input' },
            { key: "county", label: "区县",type:'input'},
            { key: "address", label: "详细<br/>地址",type:'input'},
        ]},
    ]},
    { key: "remark", label: "备注信息" },
    { key: "create_time", label: "创建时间" },
]

产品的要求是,通过这个数据生成动态表头 大概的效果图就是这种,如图:

表头合并图

image.png 表体单元格合并图

image.png

image.png

废话不多说,直接上代码,代码中也是计算好了单元格的 colspanrowspan

表头核心代码

/**
 * 核心表格合并模块
 * @description 有子集设置colspan、没有子集设置rowspan
 */
(function (G, Utils) {
  const { getColumnCount } = Utils;

  /**
   * 格式化表土数据
   * @param {array} headerTree 
   * @returns {array}
   */
  function parseHeaderData(dataTee) {
    const _d = JSON.parse(JSON.stringify(dataTee));
    let resultGroup = {};
    const _walk = (data, __level = 1) => {
      const groupKey = `level_${__level}`;
      for (let i = 0, len = data.length; i < len; i++) {
        const column = data[i];
        // 是否存在子集
        if (column.children && column.children.length) {
          column.rowspan = 1;
          column.colspan = getColumnCount(column);
          _walk(column.children, __level + 1); // 继续往下走
        } else {
          column.rowspan = data.length || 1;
          column.colspan = 1;
        }
        if (!resultGroup[groupKey]) {
          resultGroup[groupKey] = []
        }
        resultGroup[groupKey].push(column)
      }
      return resultGroup;
    }
    return Object.keys(_walk(_d))
      .sort()
      .map(key => resultGroup[key]);
  }
  
  // export
  G.parseHeaderData = G.parseHeaderData || parseHeaderData
})(window, window.utils);

此方法大概转出来的数据就是一个二维数组

[
    [
        {
            "dataIndex": "idx",
            "id": "1",
            "label": "序号",
            "type": "input",
            "rowspan": 4,
            "colspan": 1
        },
        {
            "label": "用户信息",
            "dataIndex": "userInfo",
            "id": "2",
            "rowspan": 1,
            "colspan": 5
        },
        {
            "id": "3",
            "dataIndex": "remark",
            "label": "备注信息",
            "rowspan": 1,
            "colspan": 3
        },
        {
            "dataIndex": "create_time",
            "id": "4",
            "label": "创建时间",
            "rowspan": 4,
            "colspan": 1
        }
    ],
    [
        {
            "dataIndex": "name",
            "id": "2-1",
            "pid": "2",
            "label": "姓名",
            "type": "input",
            "rowspan": 2,
            "colspan": 1
        },
        {
            "label": "居住地址",
            "id": "2-2",
            "pid": "2",
            "dataIndex": "homeAddress",
            "rowspan": 1,
            "colspan": 4
        },
        {
            "dataIndex": "remark_name",
            "id": "3-1",
            "pid": "3",
            "label": "备注人",
            "rowspan": 3,
            "colspan": 1
        },
        {
            "dataIndex": "remark_content",
            "id": "3-1-1",
            "pid": "3-1",
            "label": "内容",
            "rowspan": 3,
            "colspan": 1
        },
        {
            "dataIndex": "remark_date",
            "id": "3-1-2",
            "pid": "3-1",
            "label": "日期",
            "rowspan": 3,
            "colspan": 1
        }
    ],
    [
        {
            "dataIndex": "province",
            "id": "2-2-1",
            "pid": "2-2",
            "label": "省份",
            "type": "input",
            "rowspan": 4,
            "colspan": 1
        },
        {
            "dataIndex": "city",
            "id": "2-2-2",
            "pid": "2-2",
            "label": "市区",
            "type": "input",
            "rowspan": 4,
            "colspan": 1
        },
        {
            "dataIndex": "county",
            "id": "2-2-3",
            "pid": "2-2",
            "label": "区县",
            "type": "input",
            "rowspan": 4,
            "colspan": 1
        },
        {
            "dataIndex": "address",
            "id": "2-2-4",
            "pid": "2-2",
            "label": "详细<br/>地址",
            "type": "input",
            "rowspan": 4,
            "colspan": 1
        }
    ]
]

为什么处理的数据要二维数组,大家可以在MDN上看看,多级表头的HTML如何定义;

image.png

表体如何生成

  1. 表体服务端给的数据肯定不是表头上所有的字段,所有这个时候,要通过表头的数据,取出真正的渲染表体的数据,说简单一点就是没有children的字段,也就是colspan === 1 的字段,如果 > 1 表明是分组字段,而不是真正的渲染字段;
  2. 如何做到表体的字段跟表头的字段位置对齐,这个时候就需要给表头字段递归加一个sort排序值,然后再进行遍历渲染字段,再做对应的渲染;

表体核心代码

; (function (G, Utils) {
  const { treeToList, setTreeSort } = Utils;
  const B = {
    // 获取渲染字段列表
    getRenderColumns: function (columns) {
      const fields = treeToList(JSON.parse(JSON.stringify(columns)));
      const tableFields = fields.filter((field) => !field.hasChildren).sort((a, b) => a.sort - b.sort);
      return tableFields;
    },

    /**
     * 获取单元格合并信息
     * @param {array} columns 字段列表 
     * @param {array} data 数据
     * @returns 
     */
    getCellMerge: function (columns, data) {
      const cellMap = {};
      // 创建单元格数据
      const createCellMergeItem = function (start, end, rowspan, value) {
        return { startIndex: start, endIndex: end, rowspan, value: value };
      };

      // 对比当前列的父亲、祖宗及当前上一条记录的祖宗
      const diffParent = function (rowIndex, prevRowIndex, columnIndex) {
        let rowRes = [];
        let prevRowRes = [];
        // 依次从查询字段对比到第一个字段,是否一样;
        while (columnIndex >= 0) {
          const col = columns[columnIndex];
          rowRes.unshift(data[rowIndex][col.dataIndex]);
          prevRowRes.unshift(data[prevRowIndex][col.dataIndex]);
          columnIndex--;
        }
        return rowRes.join("") === prevRowRes.join("");
      };

      // 设置单元格合并信息
      const setMergeInfo = function (dataIndex, prev, rowspan) {
        // 找当跟上一个一个组的成员,设置偏移数量
        cellMap[dataIndex] = cellMap[dataIndex].map((row, idx) => {
          if (row.startIndex === prev.startIndex && row.value === prev.value) {
            row.rowspanIndex = idx - row.startIndex;
            row.endIndex = prev.startIndex + rowspan - 1;
            row.rowspan = rowspan;
          }
          return row;
        });
      };
      for (let j = 0; j < columns.length; j++) {
        const { dataIndex } = columns[j];
        let rowspan = 1;
        let startIndex = 0;
        for (let i = 0; i < data.length; i++) {
          const rowValue = data[i][dataIndex];
          if (!cellMap[dataIndex]) {
            cellMap[dataIndex] = [];
          }
          const diffCur = i > 0 && rowValue !== undefined && rowValue === data[i - 1][dataIndex];
          const diffParents = i > 0 && (j === 0 ? true : diffParent(i, i - 1, j));
          /**
           * 1、对比当前字段与上一条记录的同一字段是否一样;
           * 2、对比当前的上n个字段与上一条的记录的上n个字段是否一样(简单的说就是判断是否在一个分组下)
           * 两个条件都满足情况下合并;
           */
          if (diffCur && diffParents) {
            rowspan++;
            // 是否为最后一条数据
          } else {
            // 如果与上一个值不一样, 则给当前的上一条数据设置合并信息
            setMergeInfo(dataIndex, cellMap[dataIndex][i - 1], rowspan);
            startIndex = i;
            rowspan = 1;
          }
          cellMap[dataIndex].push(createCellMergeItem(startIndex, i, rowspan, rowValue));
          // 是否为最后一条数据
          if (i === data.length - 1) {
            setMergeInfo(dataIndex, cellMap[dataIndex][i], rowspan);
          }
        }
      }
      return cellMap;
    },
  };

  /**
   * 创建表体
   * @param {array} columnTree
   * @param {array} dataList
   * @return {object}
   */
  function create(columns, dataList) {
    columns = setTreeSort(columns)
    const renderColumns = B.getRenderColumns(columns); // 获取渲染字段
    const cellMerge = B.getCellMerge(renderColumns, dataList); // 合并表体单元格
    return { renderColumns, cellMerge };
  }

  // export
  G.BodyCore = G.BodyCore || { create: create };
})(this, window.utils);