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

    技术2025-08-27  7

    需要把相同内容的相邻单元格合并。起初,我知道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的思路。

    先把对象数组转换成一个对象二维数组,如下[ [{value: '李白'}, {value: '10'}...] ... ] 这里的顺序一定要跟el-table-column的prop顺序一致。然后先合并行,再合并列 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]; } }

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

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

    Processed: 0.017, SQL: 9