You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 12:15:43