原生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: "创建时间" },
]
产品的要求是,通过这个数据生成动态表头 大概的效果图就是这种,如图:
表头合并图
表体单元格合并图
废话不多说,直接上代码,代码中也是计算好了单元格的 colspan
、rowspan
表头核心代码
/**
* 核心表格合并模块
* @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如何定义;
表体如何生成
- 表体服务端给的数据肯定不是表头上所有的字段,所有这个时候,要通过表头的数据,取出真正的渲染表体的数据,说简单一点就是没有children的字段,也就是
colspan === 1
的字段,如果 > 1 表明是分组字段,而不是真正的渲染字段; - 如何做到表体的字段跟表头的字段位置对齐,这个时候就需要给表头字段递归加一个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);