使用el-table实现单元格合并

需要把相同内容的相邻单元格合并。起初,我知道el-table有span-method这样一个属性,让我们告诉它怎么合并,但是由于我并没有花太多的时间在这上面,所以就认为这个东西不能实现我的需求,毕竟文档给的例子太过简单,我的举一反三能力有差,就这样完美的错过了。

我就发挥了自己的想象力。打算在表格渲染完成后,用原生js来直接操作el-table生成的table。并且我也实现了这个思路,代码如下:

function mergeCells(tbody) {
	const trs = tbody.querySelectorAll('tr');
// 记录原始总行
const trTotal = trs.length;

// 逐行合并同行里的内容相同的单元格
for (let i = 0; i < trTotal; i++) {
	let lastTd;
	let lastTdText;
	let colSpan;
	const tds = trs[i].querySelectorAll('td');
	for (let j = 0; j < tds.length; j++) {
		const currentTd = tds[j];
		const currentTdText = currentTd.innerText;
		// 如果相同,colSpan 加1
		if (lastTd && lastTdText === currentTdText) {
			colSpan++;
			if (j === tds.length - 1) { // 最后一列了
				mergeColumns(lastTd, colSpan);
			}
		} else {
			if (lastTd && colSpan > 1) {
				mergeColumns(lastTd, colSpan);
			}
			lastTd = currentTd;
			lastTdText = currentTdText;
			colSpan = 1;
		}
	}
}

// 逐列合并相同内容的单元格
for (let i = 0; i < trTotal; i++) {
	const tds = trs[i].querySelectorAll('td');
	let lastTd;
	for (let j = 0; j < tds.length; j++) {
		tds[j].start = lastTd && (lastTd.start + lastTd.colSpan) || 0;
		lastTd = tds[j];
	}
}

for (let i = 0; i < trTotal; i++) {
	let rowSpan;
	const tds  = trs[i].querySelectorAll('td');
	for (let j = 0; j < tds.length; j++) {
		const currentTd = tds[j];
		const currentTdText = tds[j].innerText;
		rowSpan = 1;
		let nextTd = currentTd;
		while ((nextTd  = nextRowTdOfCell(nextTd)) && nextTd.innerText === currentTdText) {
			rowSpan++;
		}
		if (rowSpan > 1) {
			mergeRows(currentTd, rowSpan);
		}
	}
}
}

/**
寻找td的下一行中相同位置的td
何为相同位置?start和colSpan相同即是
**/
function nextRowTdOfCell(td) {
	const nextTr = td.parentNode.nextElementSibling;
	if (!nextTr) {
		return;
	}
	const start = td.start;
	const colSpan = td.colSpan;
	const tds = nextTr.querySelectorAll('td');
	let item;
	for (let i = 0; i < tds.length; i++) {
		if (tds[i].start === start && tds[i].colSpan === colSpan) {
			return tds[i];
		}
	}
}

/**
合并不同行上的列
@param {Element} startId 开始合并的td
@param {Number} rowSpan 要合并的行数
**/
function mergeRows(startTd, rowSpan) {
	let nextTd = nextRowTdOfCell(startId);
	for (let i = 0; i < rowSpan; i++) {
		const tempTd = nextRowTdOfCell(nextTd);
		nextTd.remove();
		nextTd = tempTd;
	}

	startTd.rowSpan  = rowSpan;
}

/**
合并同行上的列
**/
function mergeColumns(startId, colSpan) {
	startTd.colSpan = colSpan;
	let nextTd = startTd.nextEelementSibling;
	for (let i = 0; i < colSpan; i++) {
		const tempTd = nextTd.nextElementSibling;
		nextTd.remove();
		nextTd = tempTd;
	}
}

以上就是我的第一个方案,很快我就发现它跟el-table不能很好的协作。如果我点击了排序或是分页,结构就会乱掉,具体原因是什么我没兴趣去研究了。我只能另找方案了。这次我觉得我应该看一下span-method。终于,我搞懂了span-method的用法,它接收一个对象,里面包含row column rowIndex columnIndex。对于每一个单元格都会调用这个函数。以它返回的值来确定合并的策略。它可以返回一个有两个元素的数组,抑或是一个对象,反正都是用来表示当前单元格的行跨度和列跨度,即rowSpan和colSpan.

问题明朗了,我只要能判断出来哪个单元格需要合并单元格,并且把合并的策略返回就好了。还要说明一下返回值的具体意思:
[1, 1] 表示不合并,如果没有返回任何值,默认值就是这个。
[0, 0] 当前单元格不会显示。

为了能判断出来某个列的合并策略,需要对原始数据进行处理。我的原始数据是常见的格式,一个对象的数组,如下
[{
name: ‘李白’,
age: ‘10’,

}]

简单说一下我的思路,这里借鉴了我上面操作tbody的思路。

  1. 先把对象数组转换成一个对象二维数组,如下
    [
    	[{value: '李白'}, {value: '10'}...]
    	...
    ]
    
    这里的顺序一定要跟el-table-column的prop顺序一致。
  2. 然后先合并行,再合并列
function generateMergeCellInfo(data) {
	// 1. 处理数据的代码我就省略了

	// 收集每行中的相同数据的信息
	for (let i = 0; i < data.length; i++) {
		const row = data[i];
		let colSpan = 1;
		let lastText;
		let lastItem;
		for (let j = 0; j < row.length; j++) {
			const currentText = row[j].value;
			if (currentText === lastText) {
				colSpan++;
				row[j] = null;
			} else {
				if (colSpan > 1) {
					lastItem.colSpan = colSpan;
				}
				lastItem = row[j];
				lastText = currentText;
			}
		}
	}

	// 给每个数据计算colStart
	for (let i = 0; i < data.length; i++) { // 先去除为空的对象
		data[i] = data[i].filter(Boolean);
	}
	for (let i = 0; i < data.length; i++) {
		const columns = data[i];
		let lastItem;
		for (let j = 0; j < columns.length; j++) {
			columns[j].colorStart = lastItem && (lastItem.colStart + (lastItem.colSpan || 1)) || 0;
			columns[j].colSpan = columns[j].colSpan || 1;
			lastItem = columns[j];
		}
	}
	// 获取下一行中跟当前单元格相同的单元格信息
	function nextRowTdOfCell(column, nextRow) {
		return nextRow && nextRow.find(c => c && c.colStart === column.colStart && c.colSpan === column.colSpan);
	}
	// 跨行计算
	for (let i = 0; i < data.length; i++) {
		let rowSpan;
		const columns = data[i];
		for (let j = 0; j < columns.length; j++) {
			const currentColumn = columns[j];
			if (!currentColumn) {
				continue;
			}
			
			const currentText = currentColumn.value;
			rowSpan = 1;
			let nextColumn;
			let nextRowIndex = i + 1;
			while ((nextColumn = nextRowTdOfCell(currentColumn, data[nextRowIndex])) && nextColumn.value === currentText) {
				const index = data[nextRowIndex].indexOf(nextColumn);
				data[nextRowIndex][index] = null;
				nextRowIndex++;
				rowSpan++;
			}
			if (rowSpan > 1) {
				currentColumn.rowSpan = rowSpan;
			} else {
				currentColumn.rowSpan = 1;
			}
			currentColumn.rowStart = i;
		}
	}

	for (let i = 0; i < data.length; i++) {
		data[i] = data[i].filter(Boolean);
	}

	const ret = [];
	for (let i = 0; i < data.length; i++) {
		if (data[i] && data[i].length) {
			ret.push(...data[i]);
		}
	}

	return ret;
}


// 接下来就是span-method
spanMethod({row, column, rowIndex, columnIndex}) {
	// 这里假设已经获取了mergeCellInfo
	if (!mergeCellInfo) return;

	const info = mergeCellInfo.find(item => {
		return rowIndex >= item.rowStart && rowIndex < (item.rowStart + item.rowSpan) && columnIndex >= item.colSpan && columnIndex < (item.colStart + item.colSpan);
	});

	if (info) {
		if (info.colStart === columnIndex && info.rowStart === rowIndex) {
			return [info.rowSpan, info.colSpan];
		} else {
			return [0, 0];
		}
	} else {
		return [1,1];
	}
}

到此代码就敲完了,好累呀,基本可用,边界条件应该也考虑了。

如果对你有帮助,请帮我点赞呀,嘻嘻:)

你可能感兴趣的:(前端之路)