☰
Jspreadsheet 嵌套表头(nestedHeaders)实战指南:多层级列头配置与源码解析
2026/10/5 2:12:39 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】ce

Jspreadsheet is a lightweight JavaScript data grid component for creating interactive data grids with advanced spreadsheet controls.

项目地址:https://gitcode.com/gh_mirrors/ce/ce
点击查看免费下载

Jspreadsheet(jspreadsheet-ce)是一款轻量级的 JavaScript 数据网格组件,其nestedHeaders配置项允许你在单一工作表内构建多层级的分组列头(如"大区 → 国家 / 商品 / 库存"),无需任何第三方插件。本文以仓库中 v4 嵌套表头示例 和 v5 当前文档 为核心脉络,完整覆盖配置语法、四框架示例(原生 JS / React / Vue / Angular),并结合 src/utils/internal.js、src/utils/columns.js 等源码讲解其底层渲染与动态调整机制。读完本文,你将掌握如何声明嵌套表头、如何让嵌套表头在增删列、撤销重做时自动保持 colspan 正确,以及如何将嵌套表头一并导出到 CSV。

nestedHeaders 是什么:一个配置项撑起的分层级表头

在 Jspreadsheet 中,普通列头通过columns[].title设置,只会生成一行<tr>。而nestedHeaders允许你声明多行表头结构,每行由若干"表头单元格"组成,每个单元格通过colspan横向合并若干列,从而形成类似 Excel 的分组表头层级。

从 v5 文档 可以确认其类型定义:

属性说明
nestedHeaders: { id?: string; colspan?: number; title?: string; align?: string; }[][]工作表(Worksheet)的嵌套表头定义

其中外层数组的每一项对应一行表头(一个<tr>),内层数组的每一项对应一个合并后的表头单元格。每个单元格支持四个属性:

  • title:单元格显示的文本;
  • colspan:该单元格横向跨越的列数,缺省时按 1 处理;
  • align:单元格内文本对齐方式,缺省为center;
  • id:为单元格设置 HTMLid属性,便于后续定位和样式控制。

完整示例:从 v4 原生写法到 v5 多框架写法

v4 原生 HTML 示例(仓库原样继承)

仓库中的 v4 示例文档 给出了最精简的嵌套表头用法。示例定义了三列(国家/食品/库存),并在其上方叠加两层嵌套表头:第一层用colspan: 3将整张表合并为"Supermarket information",第二层拆分为"Location"(跨 1 列)与"Other Information"(跨 2 列):

<html> <!-- 原文档示例直接引用在线 CDN 资源,这里以本地 npm 安装路径示意,效果等价: 实际项目中可按构建产物或 @jspreadsheet-ce 包方式引入 --> <script src="node_modules/jspreadsheet-ce/dist/jspreadsheet.js"></script> <script src="node_modules/jsuites/dist/jsuites.js"></script> <link rel="stylesheet" href="node_modules/jspreadsheet-ce/dist/jspreadsheet.css" type="text/css" /> <link rel="stylesheet" href="node_modules/jsuites/dist/jsuites.css" type="text/css" /> <div id="spreadsheet"></div> <script> var data = [ ['BR', 'Cheese', 1], ['CA', 'Apples', 0], ['US', 'Carrots', 1], ['GB', 'Oranges', 0], ]; let table = jspreadsheet(document.getElementById('spreadsheet'), { data:data, columns: [ { type: 'autocomplete', title: 'Country', width: '300', url: '/jspreadsheet/countries' }, { type: 'dropdown', title: 'Food', width: '150', source: ['Apples','Bananas','Carrots','Oranges','Cheese'] }, { type: 'checkbox', title: 'Stock', width:'100' }, ], nestedHeaders:[ [ { title: 'Supermarket information', colspan: '3', }, ], [ { title: 'Location', colspan: '1', }, { title: ' Other Information', colspan: '2' } ], ] }); </script> </html>

注意两个细节:

  1. v4 示例中nestedHeaders直接挂在jspreadsheet()顶层配置上(与data、columns平级),而 v5 起配置被组织进worksheets数组,嵌套表头随之挂到每个工作表内部;
  2. 第二层表头的单元格colspan之和(1 + 2)正好等于第一层的colspan(3),也正好等于数据列数(3 列)。虽然组件本身不会强制校验这个等式的成立,但只有按此规则设计,层级关系才能在视觉上正确对齐。

v5 多框架写法:React / Vue / Angular

v5 文档 在原生写法之上,进一步给出四个框架的等价实现。其配置统一放进worksheets[0],并用minDimensions指定最小网格尺寸。以原生 JS 为例:

<div id="spreadsheet"></div> <script> // Create the spreadsheet let table = jspreadsheet(document.getElementById('spreadsheet'), { worksheets: [{ data: [ ['BR', 'Cheese', 1], ['CA', 'Apples', 0], ['US', 'Carrots', 1], ['GB', 'Oranges', 0], ], columns: [ { type: 'autocomplete', title: 'Country', width: '200px' }, { type: 'dropdown', title: 'Food', width: '100px', source: ['Apples','Bananas','Carrots','Oranges','Cheese'] }, { type: 'checkbox', title: 'Stock', width: '100px' }, { type: 'number', title: 'Price', width: '100px' }, ], minDimensions: [6,4], nestedHeaders:[ [ { title: 'Supermarket information', colspan: '6' }, ], [ { title: 'Location', colspan: '1' }, { title: ' Other Information', colspan: '2' }, { title: ' Costs', colspan: '3' }, ], ] }] }); </script>

这个 v5 示例演示了更典型的三段式分组:第一层整行合并(colspan: 6,与minDimensions的 6 列对齐),第二层按 1 / 2 / 3 拆分,分别对应 Location、Other Information、Costs 三个分组。

React写法(来自 @jspreadsheet-ce/react 包 对应的 React 封装)通过<Worksheet>子组件传入nestedHeaders与minDimensions:

import React, { useRef } from "react"; import { Spreadsheet, Worksheet } from "@jspreadsheet-ce/react"; import "jsuites/dist/jsuites.css"; import "jspreadsheet-ce/dist/jspreadsheet.css"; export default function App() { const spreadsheet = useRef(); const data = [ ['BR', 'Cheese', 1], ['CA', 'Apples', 0], ['US', 'Carrots', 1], ['GB', 'Oranges', 0], ]; const columns = [ { type: 'autocomplete', title: 'Country', width: '200px' }, { type: 'dropdown', title: 'Food', width: '100px', source: ['Apples','Bananas','Carrots','Oranges','Cheese'] }, { type: 'checkbox', title: 'Stock', width: '100px' }, { type: 'number', title: 'Price', width: '100px' }, ]; const nestedHeaders = [ [ { title: 'Supermarket information', colspan: '8' } ], [ { title: 'Location', colspan: '1' }, { title: ' Other Information', colspan: '2' }, { title: ' Costs', colspan: '5' }, ], ]; return ( <Spreadsheet ref={spreadsheet}> <Worksheet data={data} columns={columns} nestedHeaders={nestedHeaders} minDimensions={[8,4]} /> </Spreadsheet> ); }

Vue写法(@jspreadsheet-ce/vue)与 React 结构一一对应,通过:nestedHeaders绑定响应式数据:

<template> <Spreadsheet ref="spreadsheet"> <Worksheet :data="data" :columns="columns" :nestedHeaders="nestedHeaders" :minDimensions="[8,4]" /> </Spreadsheet> </template> <script setup> import { ref } from 'vue' import { Spreadsheet, Worksheet } from "@jspreadsheet-ce/vue"; import "jsuites/dist/jsuites.css"; import "jspreadsheet-ce/dist/spreadsheet.css"; const data = ref([ ['BR', 'Cheese', 1], ['CA', 'Apples', 0], ['US', 'Carrots', 1], ['GB', 'Oranges', 0], ]); const columns = ref([ { type: 'autocomplete', title: 'Country', width: '200px' }, { type: 'dropdown', title: 'Food', width: '100px', source: ['Apples','Bananas','Carrots','Oranges','Cheese'] }, { type: 'checkbox', title: 'Stock', width: '100px' }, { type: 'number', title: 'Price', width: '100px' }, ]); const nestedHeaders = ref([ [ { title: 'Supermarket information', colspan: '8' } ], [ { title: 'Location', colspan: '1' }, { title: ' Other Information', colspan: '2' }, { title: ' Costs', colspan: '5' }, ], ]); const spreadsheet = ref(null); </script>

Angular写法(Standalone 组件)在ngAfterViewInit中通过jspreadsheet工厂创建实例,配置对象与原生 JS 完全一致:

import { Component, ViewChild, ElementRef } from "@angular/core"; import jspreadsheet from "jspreadsheet-ce"; import "jspreadsheet-ce/dist/jspreadsheet.css" import "jsuites/dist/jsuites.css" @Component({ standalone: true, selector: "app-root", template: `<div #spreadsheet></div>`, }) export class AppComponent { @ViewChild("spreadsheet") spreadsheet: ElementRef; worksheets: jspreadsheet.worksheetInstance[]; ngAfterViewInit() { this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, { worksheets: [{ data: [ ['BR', 'Cheese', 1], ['CA', 'Apples', 0], ['US', 'Carrots', 1], ['GB', 'Oranges', 0], ], columns: [ { type: 'autocomplete', title: 'Country', width: '200px' }, { type: 'dropdown', title: 'Food', width: '100px', source: ['Apples','Bananas','Carrots','Oranges','Cheese'] }, { type: 'checkbox', title: 'Stock', width: '100px' }, { type: 'number', title: 'Price', width: '100px' }, ], minDimensions: [8,4], nestedHeaders:[ [ { title: 'Supermarket information', colspan: '8' } ], [ { title: 'Location', colspan: '1' }, { title: ' Other Information', colspan: '2' }, { title: ' Costs', colspan: '5' }, ], ] }] }); } }

四个框架示例在 React / Vue 中还将colspan扩展到了 8(与minDimensions: [8,4]对齐),可见colspan与minDimensions的第一维保持一致的惯用做法。

源码解析:嵌套表头如何被渲染出来

初始化阶段:把配置变成多行<tr>

工作表创建时,src/utils/worksheets.js 会在构建thead的过程中检测options.nestedHeaders,只要第一行第一个单元格存在有效配置,就为每一行嵌套表头调用createNestedHeader并追加到thead:

// src/utils/worksheets.js if (obj.options.nestedHeaders && obj.options.nestedHeaders.length > 0 && obj.options.nestedHeaders[0] && obj.options.nestedHeaders[0][0]) { for (let j = 0; j < obj.options.nestedHeaders.length; j++) { obj.thead.appendChild(createNestedHeader.call(obj, obj.options.nestedHeaders[j])); } }

也就是说,嵌套表头会渲染在普通列头行之前,形成"分组行 + 列头行"的堆叠结构。

createNestedHeader:每个单元格的完整语义

src/utils/internal.js 中的createNestedHeader是嵌套表头的核心渲染函数,其处理逻辑揭示了配置项的完整语义:

  • 默认值兜底:colspan缺省为 1,title缺省为空字符串,id缺省为空字符串——因此文档类型定义中所有字段均为可选;
  • 隐藏列兼容:遍历单元格覆盖的列数时,若某列类型为hidden,会自动把有效列数 +1,避免隐藏列导致合并错位;
  • 单元格属性:生成的<td>会写入data-column(记录该单元格覆盖的列索引,以逗号分隔)、colspan、align(缺省center)与id,文本内容即title;
  • 行标记:整行<tr>添加jss_nested类,首列还保留一个jss_selectall单元格,用于承载行选择功能,这正是嵌套表头行从"列 0"开始渲染的原因。
// src/utils/internal.js(节选) tr.classList.add('jss_nested'); const td = document.createElement('td'); td.classList.add('jss_selectall'); ... td.setAttribute('data-column', column.join(',')); td.setAttribute('colspan', nestedInformation[i].colspan); td.setAttribute('align', nestedInformation[i].align || 'center'); td.setAttribute('id', nestedInformation[i].id); td.textContent = nestedInformation[i].title;

同时,渲染函数会把该行的 DOM 元素回写到配置对象(nestedInformation.element = tr),为后续动态调整(如修改 colspan)预留了直接的操作句柄。

动态行为:增删列与撤销重做时的自动修正

嵌套表头不是"静态贴图"。仓库源码在以下三处对nestedHeaders做了联动修正:

  1. 插入列:src/utils/columns.js 在插入列后,遍历每一行嵌套表头,将该行最后一个单元格的colspan加上新增列数,并同步更新 DOM 上的colspan属性——新增列会被自动并入最后一组;
  2. 删除列:src/utils/columns.js 执行相反的减法操作,同样作用于每行最后一个单元格;
  3. 撤销 / 重做:src/utils/history.js 在恢复插入列(type 为删除方向时)或撤销删除(type 为插入方向时)的历史记录时,按historyRecord.numOfColumns对最后一个单元格的colspan做增减。

这种"只修正每行最后一个单元格"的设计意味着:如果你在删除列后想让某个中间分组收缩,可能需要结合setHeader/配置更新自行调整;但从默认行为看,动态增删列不会破坏嵌套表头的整体行数结构。

导出与复制:嵌套表头随 CSV 一起输出

在 src/utils/copyPaste.js 中,当复制或下载数据且开启了表头导出(obj.parent.config.includeHeadersOnDownload === true或调用时显式传入includeHeaders)时,组件会把nestedHeaders逐行转换为 CSV 文本:每个单元格输出title,其后按colspan - 1补充空字符串占位,最终行间以\r\n分隔:

// src/utils/copyPaste.js(节选) if (obj.options.nestedHeaders && obj.options.nestedHeaders.length > 0) { tmp = obj.options.nestedHeaders; for (let j = 0; j < tmp.length; j++) { const nested = []; for (let i = 0; i < tmp[j].length; i++) { const colspan = parseInt(tmp[j][i].colspan); nested.push(tmp[j][i].title); for (let c = 0; c < colspan - 1; c++) { nested.push(''); } } nestedHeaders += nested.join(delimiter) + '\r\n'; } }

因此,导出的 CSV 会在数据行之前包含完整的嵌套表头层级,供下游报表或表格工具直接消费。

与普通表头的协同:title、onchangeheader 与历史记录

嵌套表头解决的是"列分组展示"问题,而普通列头仍有自己的编程接口,两者互补。仓库 src/utils/headers.js 提供了三个方法(其行为被 test/headers.js 中的测试用例逐一验证):

方法说明
getHeader(column)获取指定列(从 0 起)的表头文本,直接读取obj.headers[column].textContent
getHeaders(asArray)获取全部表头;asArray为真时返回数组,否则返回以csvDelimiter拼接的字符串
setHeader(column, newValue)设置列头文本;传入空字符串或undefined时自动回退为默认列名(如 A、B、C),并触发onchangeheader事件

setHeader的实现细节值得注意(src/utils/headers.js):

  • 同步更新 DOM 文本、title属性,以及options.columns[column].title,保证配置与界面一致;
  • 将操作写入历史(setHistory),因此undo()/redo()可以回退/重放表头修改——test/headers.js 专门为此提供了测试;
  • 派发onchangeheader(instance, colIndex, newValue, oldValue)事件,供业务侧监听表头变化。

这些能力与nestedHeaders是正交的:嵌套表头负责分组标题,setHeader负责单个数据列标题;例如在"Other Information"分组下,你依然可以随时把列头从 "Food" 改成 "Food Category",而嵌套分组的colspan布局不受影响。

配置要点速查与常见误区

  1. colspan 必须与列数匹配:每一层各单元格colspan之和应与该层实际覆盖的列数一致;虽然组件不强制校验,但失配会导致合并错位。可参照文档示例让顶层colspan等于minDimensions的列维度。
  2. hidden 列会自动补偿:渲染时组件会把隐藏列计入单元格覆盖范围,无需手动调整colspan。
  3. 增删列只修正每行最后一个单元格:新增/删除列时,嵌套表头每行的最后一个分组会自动伸缩,中间分组保持不变。
  4. 导出嵌套表头需开启表头导出:通过includeHeadersOnDownload配置或显式includeHeaders参数,嵌套表头才会写入 CSV 的前几行。
  5. v4 与 v5 的配置位置不同:v4 中nestedHeaders与data/columns平级;v5 起放入worksheets[0]内。迁移旧代码时务必调整层级。

延伸阅读

  • 列头基础配置(title / tooltip / onchangeheader)
  • 页脚行与公式汇总(footers)
  • v4 嵌套表头示例原文
  • 列相关的动态操作(插入、删除、隐藏)测试用例
  • 前端
  • UI组件

【免费下载链接】ce

Jspreadsheet is a lightweight JavaScript data grid component for creating interactive data grids with advanced spreadsheet controls.

项目地址:https://gitcode.com/gh_mirrors/ce/ce
点击查看免费下载

相关推荐

上一篇:精通ProperTree:7个高效Plist编辑技巧与进阶实战指南
下一篇:视频文件突然损坏打不开?3分钟学会用Untrunc抢救你的珍贵回忆

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询