本帖最后由 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. 初始化与自定义列头按钮
首先,我们需要在列头添加一个“下拉箭头”按钮。这是用户交互的入口。
- // 为指定列添加自定义筛选按钮
- function addCustomFilterButton(sheet, colIndex) {
- var style = new GC.Spread.Sheets.Style();
- style.cellButtons = [
- {
- imageType: GC.Spread.Sheets.ButtonImageType.dropdown, // 使用内置下拉图标
- command: (sheet, row, col, option) => {
- // 点击时触发弹窗逻辑
- openFilterDialog(sheet, col);
- },
- useButtonStyle: true,
- }
- ];
- // 将样式应用到列头区域
- sheet.setStyle(-1, colIndex, style, GC.Spread.Sheets.SheetArea.colHeader);
- }
复制代码
2. 构建自定义 DOM 弹窗
这是本方案中最灵活的部分。我们创建一个 div 容器,模拟 Excel 的筛选菜单。主要包含三个部分:
- 排序区:升序/降序按钮。
- 搜索区:一个 Input 输入框,用于快速检索选项。
- 列表区:包含“全选”和各个数据项的 Checkbox。
关键代码逻辑在于计算弹窗的坐标,使其准确出现在列头的下方:
- function openFilterDialog(sheet, colIndex) {
- // ... 前置清理逻辑 ...
- // 1. 获取单元格位置信息
- const cellRect = sheet.getCellRect(-1, colIndex, -1, -1, GC.Spread.Sheets.SheetArea.colHeader);
- const offset = document.getElementById("ss").getBoundingClientRect();
-
- // 2. 计算绝对坐标
- const top = offset.top + cellRect.y + cellRect.height;
- const left = offset.left + cellRect.x;
- // 3. 创建 DOM 并挂载
- const popup = createPopupDOM(distinctValues, { ...callbacks... });
- popup.style.top = top + "px";
- popup.style.left = left + "px";
- document.body.appendChild(popup);
- }
复制代码
3. 实现复杂的交互逻辑(全选与搜索)
为了提供良好的用户体验,我们需要手动处理 Checkbox 的联动逻辑:
- 全选联动:取消任意子项,全选框应取消;所有子项选中,全选框应自动勾选。
- 搜索联动:输入关键字后,列表仅展示匹配项。此时点击“全选”,应只选中当前可见的匹配项。
- // 搜索框输入事件
- searchInput.oninput = (e) => {
- const val = e.target.value.toLowerCase();
- itemCheckboxes.forEach(cb => {
- const isVisible = cb.value.toString().toLowerCase().includes(val);
- cb.parentElement.style.display = isVisible ? 'flex' : 'none';
- });
- // 搜索后重新计算全选状态...
- };
复制代码
4. 核心数据处理:排序与筛选
当用户点击“确定”或“排序”按钮时,我们不再操作 SpreadJS 的 UI,而是直接操作内存中的数据 `currentDisplayData`。
排序逻辑:
- function handleSort(sheet, fieldName, isAsc) {
- // 对当前展示的数据进行排序
- currentDisplayData.sort((a, b) => {
- const valA = a[fieldName];
- const valB = b[fieldName];
- if (valA < valB) return isAsc ? -1 : 1;
- if (valA > valB) return isAsc ? 1 : -1;
- return 0;
- });
- // 重新绑定数据源,视图自动更新
- sheet.setDataSource(currentDisplayData);
- }
复制代码
筛选逻辑:
- function handleFilter(sheet, fieldName, selectedValues, isSelectAll) {
- if (isSelectAll) {
- // 恢复原始数据
- currentDisplayData = JSON.parse(JSON.stringify(RAW_DATA));
- } else {
- // 过滤原始数据
- currentDisplayData = RAW_DATA.filter(row => selectedValues.includes(row[fieldName]));
- }
- // 重新绑定
- sheet.setDataSource(currentDisplayData);
- }
复制代码
5. 进阶技巧:高性能数据填充
在示例代码中,还包含了一个关于 `DragFillBlock`(拖拽填充)的性能优化彩蛋。当处理大量公式或数据填充时,频繁的重绘会严重拖慢性能。
我们可以通过拦截事件,利用 `suspendPaint`(挂起绘制)和 `suspendCalcService`(挂起计算服务)来通过“批处理”的方式极大提升性能。
- sheet.bind(GC.Spread.Sheets.Events.DragFillBlock, function (e, info) {
- info.cancel = true; // 阻止默认行为
- // 核心优化三部曲:挂起绘制、挂起计算、挂起事件
- spread.suspendPaint();
- spread.suspendCalcService();
- spread.suspendEvent();
-
- try {
- // 执行填充命令...
- spread.commandManager().execute({ cmd: "clipboardPaste", ... });
- } finally {
- // 恢复服务,触发一次性重绘和计算
- spread.resumeEvent();
- spread.resumeCalcService();
- spread.resumePaint();
- }
- });
复制代码
四、 总结
通过上述方案,我们实现了一个完全自主可控的筛选排序功能。相比于原生功能,本方案具有以下优势:
1. 逻辑解耦:筛选逻辑与 UI 渲染分离,方便后续接入后端 API 实现服务端筛选。
2. 样式自由:完全基于 DOM 的弹窗,可以轻松应用 CSS 样式,适配各种设计稿。
3. 性能可控:通过直接操作数据源(DataSource)而非操作单元格样式,在大数据量下的渲染效率更高。
这种“外挂式”的开发模式,展示了 SpreadJS 作为一个底层表格引擎的极高扩展性,非常适合对交互体验有极致要求的业务场景。
代码示例:
|