使用Vanilla JavaScript合并表格相同值行的技术问询
实现带合并单元格的HTML表格生成函数
需求背景
我正尝试通过原生JavaScript函数生成HTML表格,该函数接收INPUT数据集与config配置项(用于选择字段、执行转换(可为键或函数)并定义是否合并相同值)。目前已实现基础表格生成,但期望实现重复信息合并的效果(合并规则由config指定)。现有代码、INPUT数据及config配置如下,恳请提供实现预期效果的建议或思路。
当前进度代码(script.js)
import "./styles.css"; import { INPUT, config } from "../data"; import groupBy from "../utils"; function genReport(data, config = []) { const tableContainer = document.getElementById("table-container"); // 创建表格元素 const table = document.createElement("table"); // 创建表头行 const headerRow = table.insertRow(); // 循环生成表头 for (let { HeaderName } of config) { const th = document.createElement("th"); th.textContent = HeaderName; headerRow.appendChild(th); } // 创建表格主体 const tbody = document.createElement("tbody"); let previousValues = null; let currentValues = {}; // 循环生成表格行 for (let i = 0; i < INPUT.length; i++) { const row = tbody.insertRow(); // 循环生成单元格 for (let j = 0; j < config.length; j++) { let field = config[j]["Column"]; let value = INPUT[i][field]; const cell = row.insertCell(); cell.textContent = value || field(INPUT[i]); // 待实现合并逻辑 } console.log(previousValues); console.log("END OF LOOP", i); } // 将主体添加到表格 table.appendChild(tbody); // 将表格添加到容器 tableContainer.appendChild(table); } genReport(INPUT, config);
INPUT数据与config配置
import moment from "moment"; const INPUT = [ { Organization: "Google", UserId: "akumar", UserName: "Ashok Kumar", Department: "Sales", Designation: "Sales", CheckInTime: 1548909000000, CheckOutTime: 1548945000000 }, { Organization: "Google", UserId: "akumar", UserName: "Ashok Kumar", Department: "Sales", Designation: "Sales", CheckInTime: 1549081800000, CheckOutTime: 1549110600000 }, { Organization: "FB", UserId: "phanis", UserName: "Phani Sai", Department: "Sales", Designation: "Sales", CheckInTime: 1548909000000, CheckOutTime: 1548945000000 }, { Organization: "FB", UserId: "phanis", UserName: "Phani Sai", Department: "Sales", Designation: "Sales", CheckInTime: 1549081800000, CheckOutTime: 1549110600000 }, { Organization: "FB", UserId: "lakshmig", UserName: "Laskhmi Gayathri", Department: "Quality", Designation: "QA Engineer", CheckInTime: 1549081800000, CheckOutTime: 1549110600000 }, { Organization: "FB", UserId: "lakshmig", UserName: "Laskhmi Gayathri", Department: "Quality", Designation: "QA Engineer", CheckInTime: 1549081800000, CheckOutTime: 1549110600000 } ]; const config = [ { HeaderName: "Organization", Column: "Organization", Merge: true }, { HeaderName: "Department", Column: "Department", Merge: true }, { HeaderName: "UserName", Column: "UserName", Merge: true }, { HeaderName: "Date", Column: ({ CheckInTime }) => { return moment(CheckInTime).format("DD/MM/YYYY"); }, Merge: false }, { HeaderName: "Time", Column: ({ CheckInTime, CheckOutTime }) => { // Column可以是字符串或回调函数,传入行数据计算值 const secs = (CheckOutTime - CheckInTime) / 1000; // TODO: 转换为(x Hrs y Mins)格式 return secs / 60 + " Mins"; // 暂时返回分钟数 }, Merge: false } ]; module.exports = { INPUT, config };
实现思路与建议
1. 预处理:按合并字段分组并排序
合并单元格的前提是相同值的行连续排列,所以首先要按config中Merge: true的字段顺序对数据排序,再进行层级分组:
// 1. 先对数据排序,确保合并字段相同的行连续 const mergeFields = config.filter(item => item.Merge).map(item => item.Column); INPUT.sort((a, b) => { for (const field of mergeFields) { const valA = typeof field === 'function' ? field(a) : a[field]; const valB = typeof field === 'function' ? field(b) : b[field]; if (valA !== valB) return valA.localeCompare(valB); } return 0; }); // 2. 按合并字段层级分组 function groupData(data, fields) { if (fields.length === 0) return data; const currentField = fields[0]; const groups = {}; data.forEach(item => { const key = typeof currentField === 'function' ? currentField(item) : item[currentField]; if (!groups[key]) groups[key] = []; groups[key].push(item); }); return Object.entries(groups).map(([key, items]) => ({ key, children: fields.length > 1 ? groupData(items, fields.slice(1)) : items })); } const groupedData = groupData(INPUT, mergeFields);
2. 递归生成表格与合并单元格
通过递归遍历分组数据,为每个分组的首行单元格设置rowspan属性,后续行跳过对应单元格:
// 替换原有的tbody生成逻辑 const tbody = document.createElement("tbody"); // 递归渲染分组 function renderGroup(group, depth = 0) { // 计算当前分组的总行数 const getRowCount = (g) => { if (Array.isArray(g)) return g.length; return g.children.reduce((sum, child) => sum + getRowCount(child), 0); }; const totalRows = getRowCount(group); let isFirstRow = true; if (Array.isArray(group)) { // 叶子节点,直接渲染每行 group.forEach(item => { const row = tbody.insertRow(); config.forEach((col, idx) => { // 合并字段且非首行,隐藏单元格 if (col.Merge && !isFirstRow) { const cell = row.insertCell(); cell.style.display = 'none'; return; } const cell = row.insertCell(); const value = typeof col.Column === 'function' ? col.Column(item) : item[col.Column]; cell.textContent = value; // 合并字段且首行,设置rowspan if (col.Merge && isFirstRow) { cell.rowSpan = totalRows; } }); isFirstRow = false; }); } else { // 非叶子节点,先渲染首行的当前层级合并字段,再递归渲染子分组 const row = tbody.insertRow(); config.forEach((col, idx) => { const currentMergeField = mergeFields[depth]; if (col.Column === currentMergeField) { // 当前层级的合并字段 const cell = row.insertCell(); cell.textContent = group.key; cell.rowSpan = totalRows; } else if (col.Merge) { // 更深层级的合并字段,留空由子分组处理 const cell = row.insertCell(); cell.style.display = 'none'; } else { // 非合并字段,留空由子分组处理 const cell = row.insertCell(); cell.style.display = 'none'; } }); group.children.forEach(child => renderGroup(child, depth + 1)); } } // 渲染顶层分组 groupedData.forEach(group => renderGroup(group));
3. 关键注意点
- 排序优先级:必须严格按照config中
Merge: true字段的顺序排序,否则合并会出错 - 函数型字段处理:分组和排序时,要调用Column函数获取实际显示值,确保相同计算值被合并
- 样式适配:建议添加CSS确保合并单元格垂直居中:
td { vertical-align: middle; }
内容的提问来源于stack exchange,提问作者Dwaipayan Chakroborty
相关产品推荐
相关产品推荐

