请选择 进入手机版 | 继续访问电脑版
Richard.Huang SpreadJS 开发认证
超级版主   /  发表于:2026-1-7 12:07  /   查看:33  /  回复:0
本帖最后由 Richard.Huang 于 2026-1-7 12:19 编辑

在企业级 Web 应用开发中,表格(Grid/Spreadsheet)是数据展示的核心组件。虽然 SpreadJS 原生提供了强大的 Excel 样式的筛选和排序功能,但在某些复杂的业务场景下,我们往往需要更深度的定制能力:比如需要完全自定义的 UI 风格、需要对接后端的复杂筛选接口,或者是为了极致性能而采用“数据驱动视图”的更新策略。

本文将基于一个实际的代码示例,详细拆解如何利用 SpreadJS 的 API 配合原生 DOM,实现一套轻量、可控且高性能的自定义筛选与排序方案。

一、 背景与需求
在默认情况下,SpreadJS 的筛选器是基于行隐藏(Row Hidden)机制实现的。但在处理大数据量或需要与后端深度联动时,我们可能面临以下需求:
1. UI 高度定制:默认的筛选菜单样式可能与系统整体 UI(如 Ant Design 或 Element UI)不一致,我们需要完全掌控弹窗的渲染。
2. 数据驱动(Data Driven):不希望仅仅隐藏行,而是希望通过改变底层数据源(DataSource)来驱动视图更新。这样更符合现代前端框架(React/Vue)的设计理念。
3. 性能优化:在进行大量数据填充或更新时,需要利用挂起绘制等手段保证流畅度。

二、 实现原理
本方案的核心思想是 “UI 分离,数据驱动”。
1. 入口定制:利用 SpreadJS 的 `CellButtons` 功能,在列头绘制自定义的下拉按钮,接管点击事件。
2. DOM 弹窗:不使用 Canvas 绘制菜单,而是使用原生 HTML/CSS 创建绝对定位的 DOM 弹窗。这样做的好处是开发成本低,且容易处理复杂的交互(如搜索、全选联动)。
3. 逻辑处理
   - 排序:通过 JavaScript 的 `Array.prototype.sort` 对数据源进行重排。
   - 筛选:通过 `Array.prototype.filter` 过滤数据源。
4. 视图更新:处理完数据后,调用 `sheet.setDataSource()` 重新绑定数据,从而刷新视图。

三、 实现步骤
1. 初始化与自定义列头按钮

首先,我们需要在列头添加一个“下拉箭头”按钮。这是用户交互的入口。
  1. // 为指定列添加自定义筛选按钮
  2. function addCustomFilterButton(sheet, colIndex) {
  3.     var style = new GC.Spread.Sheets.Style();
  4.     style.cellButtons = [
  5.         {
  6.             imageType: GC.Spread.Sheets.ButtonImageType.dropdown, // 使用内置下拉图标
  7.             command: (sheet, row, col, option) => {
  8.                 // 点击时触发弹窗逻辑
  9.                 openFilterDialog(sheet, col);
  10.             },
  11.             useButtonStyle: true,
  12.         }
  13.     ];
  14.     // 将样式应用到列头区域
  15.     sheet.setStyle(-1, colIndex, style, GC.Spread.Sheets.SheetArea.colHeader);
  16. }
复制代码

2. 构建自定义 DOM 弹窗
这是本方案中最灵活的部分。我们创建一个 div 容器,模拟 Excel 的筛选菜单。主要包含三个部分:
- 排序区:升序/降序按钮。
- 搜索区:一个 Input 输入框,用于快速检索选项。
- 列表区:包含“全选”和各个数据项的 Checkbox。

关键代码逻辑在于计算弹窗的坐标,使其准确出现在列头的下方:
  1. function openFilterDialog(sheet, colIndex) {
  2.     // ... 前置清理逻辑 ...

  3.     // 1. 获取单元格位置信息
  4.     const cellRect = sheet.getCellRect(-1, colIndex, -1, -1, GC.Spread.Sheets.SheetArea.colHeader);
  5.     const offset = document.getElementById("ss").getBoundingClientRect();
  6.    
  7.     // 2. 计算绝对坐标
  8.     const top = offset.top + cellRect.y + cellRect.height;
  9.     const left = offset.left + cellRect.x;

  10.     // 3. 创建 DOM 并挂载
  11.     const popup = createPopupDOM(distinctValues, { ...callbacks... });
  12.     popup.style.top = top + "px";
  13.     popup.style.left = left + "px";
  14.     document.body.appendChild(popup);
  15. }
复制代码

3. 实现复杂的交互逻辑(全选与搜索)
为了提供良好的用户体验,我们需要手动处理 Checkbox 的联动逻辑:
- 全选联动:取消任意子项,全选框应取消;所有子项选中,全选框应自动勾选。
- 搜索联动:输入关键字后,列表仅展示匹配项。此时点击“全选”,应只选中当前可见的匹配项。

  1. // 搜索框输入事件
  2. searchInput.oninput = (e) => {
  3.     const val = e.target.value.toLowerCase();
  4.     itemCheckboxes.forEach(cb => {
  5.         const isVisible = cb.value.toString().toLowerCase().includes(val);
  6.         cb.parentElement.style.display = isVisible ? 'flex' : 'none';
  7.     });
  8.     // 搜索后重新计算全选状态...
  9. };
复制代码

4. 核心数据处理:排序与筛选
当用户点击“确定”或“排序”按钮时,我们不再操作 SpreadJS 的 UI,而是直接操作内存中的数据 `currentDisplayData`。
排序逻辑:
  1. function handleSort(sheet, fieldName, isAsc) {
  2.     // 对当前展示的数据进行排序
  3.     currentDisplayData.sort((a, b) => {
  4.         const valA = a[fieldName];
  5.         const valB = b[fieldName];
  6.         if (valA < valB) return isAsc ? -1 : 1;
  7.         if (valA > valB) return isAsc ? 1 : -1;
  8.         return 0;
  9.     });
  10.     // 重新绑定数据源,视图自动更新
  11.     sheet.setDataSource(currentDisplayData);
  12. }
复制代码

筛选逻辑
  1. function handleFilter(sheet, fieldName, selectedValues, isSelectAll) {
  2.     if (isSelectAll) {
  3.         // 恢复原始数据
  4.         currentDisplayData = JSON.parse(JSON.stringify(RAW_DATA));
  5.     } else {
  6.         // 过滤原始数据
  7.         currentDisplayData = RAW_DATA.filter(row => selectedValues.includes(row[fieldName]));
  8.     }
  9.     // 重新绑定
  10.     sheet.setDataSource(currentDisplayData);
  11. }
复制代码

5. 进阶技巧:高性能数据填充
在示例代码中,还包含了一个关于 `DragFillBlock`(拖拽填充)的性能优化彩蛋。当处理大量公式或数据填充时,频繁的重绘会严重拖慢性能。
我们可以通过拦截事件,利用 `suspendPaint`(挂起绘制)和 `suspendCalcService`(挂起计算服务)来通过“批处理”的方式极大提升性能。
  1. sheet.bind(GC.Spread.Sheets.Events.DragFillBlock, function (e, info) {
  2.     info.cancel = true; // 阻止默认行为

  3.     // 核心优化三部曲:挂起绘制、挂起计算、挂起事件
  4.     spread.suspendPaint();
  5.     spread.suspendCalcService();
  6.     spread.suspendEvent();
  7.    
  8.     try {
  9.         // 执行填充命令...
  10.         spread.commandManager().execute({ cmd: "clipboardPaste", ... });
  11.     } finally {
  12.         // 恢复服务,触发一次性重绘和计算
  13.         spread.resumeEvent();
  14.         spread.resumeCalcService();
  15.         spread.resumePaint();
  16.     }
  17. });
复制代码

四、 总结
通过上述方案,我们实现了一个完全自主可控的筛选排序功能。相比于原生功能,本方案具有以下优势:
1. 逻辑解耦:筛选逻辑与 UI 渲染分离,方便后续接入后端 API 实现服务端筛选。
2. 样式自由:完全基于 DOM 的弹窗,可以轻松应用 CSS 样式,适配各种设计稿。
3. 性能可控:通过直接操作数据源(DataSource)而非操作单元格样式,在大数据量下的渲染效率更高。

这种“外挂式”的开发模式,展示了 SpreadJS 作为一个底层表格引擎的极高扩展性,非常适合对交互体验有极致要求的业务场景。


代码示例:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

0 个回复

您需要登录后才可以回帖 登录 | 立即注册
返回顶部