news 2026/11/2 3:45:06

bootstrapTable跨页勾选与批量操作状态管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
bootstrapTable跨页勾选与批量操作状态管理

上周三下午,运营那边丢过来一句话:“我这一页勾了八个,翻到下一页又勾了两个,点批量导出只出来两个,剩下八个哪去了?”这大概是 bootstrapTable 复选框跨页勾选这个问题最典型的现场。它不属于那种一眼能看穿的 bug——代码没错,控制台干净,复选框也确实能在页面上点,可只要页码一动,之前所有辛苦勾起来的行就像从没存在过。

我把这类问题归到“组件语义与业务预期错位”里。bootstrapTable 的复选框从设计上只对当前渲染出来的 tbody 负责,而业务人员脑子里想的是“我勾的是数据,不是眼前这十行”。这两套心智模型一撞,跨页勾选就成了一个必须自己动手补的活。下面我按自己实际踩过的顺序,把原因、复现方式、可落地的方案、以及那些藏在版本差异里的细节,完整捋一遍。前端用 jQuery 系表格做后台系统的人、正在给批量操作加跨页选中的开发者,以及被运营追问过“为什么只导出了两条”的人,应该都能对上号。

1. 翻页后勾选丢失的真实原因:一次 tbody 全量重建

1.1 复选框的勾选状态其实只活在一行数据对象上

很多人以为点复选框就是给那个<input type="checkbox">打个勾,状态存在 DOM 里。在原生表单里这个理解没错,但在 bootstrapTable 里完全不成立。它会在渲染每一行时,读取该行数据对象上对应 stateField 字段的值,来反推这个 checkbox 该不该是选中的。默认复选框列的 field 一般写成state,那么第 N 行的数据对象里state: true,渲染出来的复选框就是勾上的;state是false或根本没有这个字段,渲染出来就是空的。

这个设计带来的直接后果是:当分页切换、表格重新拉数据、重新initBody的时候,旧的 tbody 整块被替换掉,旧的行数据对象也随之被丢弃。新的十行数据对象里state字段统统是假值,于是复选框全部复位。用户看到的“勾选被清空”,本质是“承载勾选状态的那批数据对象被换掉了”,DOM 只是忠实地反映了这个事实。想通这一点,后面所有方案都是围绕“怎么把被换掉的这层状态捞回来”展开的。

1.2 三种分页模式下,浏览器内存里到底躺着哪些行

这一步必须分清楚,因为它直接决定你的方案能不能成立。我见过不止一个人把客户端分页的代码直接搬到服务端分页的项目里,然后抱怨“上线就失效”,根子上就是没区分这两种模式下getData()返回的到底是什么。

分页模式配置方式getData()拿到什么全量数据在哪
客户端分页sidePagination: 'client'当前页的数据options.data,全量都在浏览器
服务端分页sidePagination: 'server'当前页的数据后端,前端只有当前页
无分页pagination: false全部数据全部在浏览器

客户端分页下,全量数据一直躺在options.data里,理论上只要在翻页前把 stateField 写回对应行,翻回来自动就是勾上的,实现上省事很多。但服务端分页下,前端压根不知道第 5 页有哪些数据,翻过去只能重新请求。所以服务端分页的跨页勾选,本质上是一个“前端自己维护一份 ID 清单”的问题,跟表格组件关系不大。这一点想明白,方案的骨架就定了:不要指望表格帮你记住,自己拿一张表记账,每次渲染完按账本回填。

1.3 maintainSelected 能救一半,但救不了全选与批量操作

bootstrapTable 官方其实给了一个开关:maintainSelected: true,配合idField一起用,官方文档的说法是翻页和搜索时保持选中行。我最初也是直接开这个选项,简单两行配置就以为完事,结果第二天就收到反馈:单行勾选确实能跨页保留,但表头那个全选按钮一按,翻页后新页的行一个都没勾上,之前选中的行反而在getSelections()里找不到了。

翻源码和试验之后大致能总结出它的边界:这个选项依赖idField指定的主键来识别“同一行”,行数据重新加载后如果主键命中就恢复勾选,单点的勾选场景基本够用。但表头全选只作用于当前渲染出来的行,它不会替你把后续页面的数据也标记上,因为那些行对象此时根本不存在。再加上getSelections()在不同小版本里返回的范围不一致,有的版本返回当前页选中,有的版本返回跨页累积,直接拿它去做批量删除很容易少删或者多删。所以我的结论是:小项目、只做单行勾选、不需要表头全选,开maintainSelected省事;只要涉及批量操作和全选语义,就别偷这个懒,自己维护一份选中账本更可控。

2. 先把问题稳定复现:一份可跑的最小 Demo

2.1 模拟接口与初始化参数

调试这类问题最忌讳直接在业务项目里改,因为业务项目里混着权限、格式化、默认排序,你根本判断不出是哪一环把选中状态吃掉了。我习惯先搭一个只有表格和假数据的最小页面,把问题稳定复现出来,再往业务代码里套。下面这份 Demo 用的是服务端分页模式,接口用ajax选项接管,不依赖任何后端就能跑起来。

// mock 数据:总共 87 条,每次按 offset/limit 切片返回 function mockList(offset, limit) { var total = 87; var rows = []; for (var i = 0; i < limit; i++) { var idx = offset + i; if (idx >= total) break; rows.push({ orderId: 'NO' + (1000 + idx), // 注意:这里是字符串 customer: '客户' + (idx + 1), amount: (100 + idx * 3.7).toFixed(2), status: idx % 3 === 0 ? '待发货' : '已发货' }); } return { total: total, rows: rows }; }

接口数据造好之后,初始化参数里有四个必须显式写清楚的配置:pagination: true打开分页、sidePagination: 'server'走服务端分页、idField: 'orderId'指定唯一主键、复选框列显式写field而不是留空。主键这一条尤其关键,后面所有的比对、去重、回填都依赖它,如果表格数据里存在重复主键,或者主键字段名拼错,你会在很后面才发现勾选行为“飘忽不定”,排查成本极高。

2.2 复现三步:勾选、翻页、回看

页面跑起来之后,按下面三步操作,问题百分百复现:

  1. 在第一页勾选第 2 行和第 5 行,此时打开控制台执行$('#demoTable').bootstrapTable('getSelections'),能看到两条记录。
  2. 点击下一页,再执行同一条命令,返回空数组。
  3. 点回第一页,刚才勾的两个复选框全是空的。

这个三步法我建议每个人都亲手做一遍,因为很多人是在业务代码里“听说”这个问题的,没有亲眼看到getSelections()从有到空的过程,就很难理解为什么后面要自己维护一份 Map。

2.3 从控制台观察到的三个关键事实

复现过程中有三个现象值得盯住,它们直接决定了方案设计。

第一个,翻页时网络面板里确实发了一次请求,返回的是全新的十行数据,跟第一页的行对象完全是两个不同的引用。第二个,翻页瞬间 tbody 的 DOM 节点数量会经历一次“清零—重建”,说明表格是整块替换而不是就地更新属性。第三个,如果你在onCheck事件里打日志,会发现翻页时这个事件压根不触发——复选框是被渲染成未选中,而不是被“取消勾选”了。这三点合在一起说明:跨页勾选不是“状态被抹掉”,而是“状态从来没有被保存下来”。想清楚这个区别,就不会去尝试监听什么onUncheck之类的偏门事件,而是老老实实做一份外部账本。

3. 自己记账:用一张 Map 承载跨页选中集合

3.1 为什么用 Map 存整行而不是只存 id

最省事的做法是维护一个 id 数组,勾选就 push,取消就 splice。但我在实际项目里更推荐用Map存“id 到整行对象”的映射,原因有三个,都是踩过之后才体会到的。

第一,工具条上通常要显示“已选 N 条”,如果还想展示已选行的摘要信息(比如“已选 3 单,合计金额 1,280.00 元”),手里只有 id 是算不出来的,必须能拿到整行。第二,批量操作如果后端接口接受整行数据而不是 id 列表,你直接Array.from(map.values())就能提交,省一次回查。第三,去重和判断“是否已选”用map.has(id)是 O(1),而数组indexOf在几百条选中时会明显拖慢渲染。当然,存整行也有代价:内存占用略高,而且行对象可能是旧数据,提交前如果涉及金额之类的关键字段,最好还是以服务端为准。

// 全局只留这一份账本,页面生命周期内不重建 var selectedMap = new Map(); // key: orderId, value: row function selectedIds() { return Array.from(selectedMap.keys()); }

这里有个细节值得单独说:不要用普通对象{}代替Map。因为对象做键时会被强制转成字符串,如果某天后端返回的 id 是数字、前端某处又用了字符串,1和'1'在对象里是同一个键,在 Map 里是两个键。听起来 Map 更“危险”,其实正好相反——对象会把类型差异悄悄吞掉,让你在别的地方出更诡异的问题;Map 保持类型严格,一旦不一致当场就能暴露,配合后面要讲的类型统一处理,反而更稳。

3.2 四个勾选事件的真实触发语义

bootstrapTable 提供了一组复选框相关事件,用之前必须搞清楚每个事件的参数到底代表什么,否则很容易在onCheckAll里把不该加的行加进账本。

事件名触发时机回调参数参数含义
onCheck单行由未选中变选中(row, $element)被选中的那一行
onUncheck单行由选中变未选中(row, $element)被取消的那一行
onCheckAll表头全选被点击且当前页非全选(rows)本次变为选中的行
onUncheckAll表头全选被点击且当前页已全选(rows)本次变为未选中的行

需要注意的是onCheckAll的参数在不同版本里语义有微妙差异:有的版本只把“本次从 false 变 true 的行”传进来,有的版本传当前页全部数据。如果你的逻辑写成“把 rows 参数全部塞进账本”,在部分版本上没问题,在另一些版本上会因为参数范围不同导致结果不稳定。我后来的写法是干脆不信参数,在回调里统一用$table.bootstrapTable('getData')取当前页数据做同步,行为在所有版本上都一致。

$('#demoTable').bootstrapTable({ // ... 省略其他配置 onCheck: function (row) { selectedMap.set(row.orderId, row); refreshToolbar(); }, onUncheck: function (row) { selectedMap.delete(row.orderId); refreshToolbar(); }, onCheckAll: function () { // 不依赖 rows 参数,直接取当前页数据全量同步 $('#demoTable').bootstrapTable('getData').forEach(function (row) { selectedMap.set(row.orderId, row); }); refreshToolbar(); }, onUncheckAll: function () { $('#demoTable').bootstrapTable('getData').forEach(function (row) { selectedMap.delete(row.orderId); }); refreshToolbar(); } });

还有一个坑要提前说:onCheckAll里千万不要再去调checkAll(),会自己触发自己,控制台刷屏最后栈溢出。同理,onCheck里也不要调check()。

3.3 checkBy 回填的时机为什么只能放在 onPostBody

账本有了,接下来是把账本上的勾选状态“画”回页面上。bootstrapTable 提供了checkBy方法,可以按字段和值批量勾选:

$('#demoTable').bootstrapTable('checkBy', { field: 'orderId', values: selectedIds() });

这个方法的内部实现是遍历当前渲染的tbody里的每一行,从行上取出对应的数据,比对 field 的值是否在 values 数组里,命中就勾上。这里有两个关键限制你必须知道。

第一,它只对当前渲染出来的行生效。跨页的 id 传进去也没用,因为那些行压根不在 DOM 里。这一点其实正好符合需求:每次翻到新页,用全量 id 去checkBy,命中的自然就是当前页里被选过的行,没命中的会被忽略。第二,它必须在 tbody 渲染完成之后调用。如果在onLoadSuccess里调,数据虽然返回了,但 tbody 还没建出来,checkBy找不到任何行,等于什么都没做,而且不报错,你会以为方案失效了。正确的位置是onPostBody,这个事件在 tbody 渲染完毕之后触发,此时 DOM 已经就绪。

onPostBody: function () { if (selectedMap.size === 0) return; $('#demoTable').bootstrapTable('checkBy', { field: 'orderId', values: selectedIds() }); }

onPostBody里加一个空判断是习惯性的防御。如果没有选中项还去调checkBy,虽然结果一样,但在数据量大的时候多一轮全表遍历,翻页会有一点点可感知的卡顿,加上这一行代码成本极低。

3.4 完整代码与逐段说明

把上面几块拼起来,就是一份可以直接抄的完整实现。我按功能分块写,方便你对照自己的项目裁剪。

var $table = $('#demoTable'); var selectedMap = new Map(); // 工具条计数与按钮状态 function refreshToolbar() { var count = selectedMap.size; $('#selectedCount').text(count); $('#btnBatchDelete').prop('disabled', count === 0); $('#btnExport').prop('disabled', count === 0); } // 回填当前页的勾选状态 function restoreChecked() { if (selectedMap.size === 0) return; $table.bootstrapTable('checkBy', { field: 'orderId', values: Array.from(selectedMap.keys()) }); } $table.bootstrapTable({ url: '/api/order/list', method: 'get', pagination: true, sidePagination: 'server', pageSize: 10, pageList: [10, 20, 50], idField: 'orderId', // 不用官方 maintainSelected,避免与自维护账本互相干扰 maintainSelected: false, columns: [ { field: 'checked', checkbox: true }, { field: 'orderId', title: '订单号', sortable: false }, { field: 'customer', title: '客户' }, { field: 'amount', title: '金额', align: 'right' }, { field: 'status', title: '状态' } ], queryParams: function (params) { return { offset: params.offset, limit: params.limit }; }, responseHandler: function (res) { return { total: res.total, rows: res.rows }; }, onCheck: function (row) { selectedMap.set(row.orderId, row); refreshToolbar(); }, onUncheck: function (row) { selectedMap.delete(row.orderId); refreshToolbar(); }, onCheckAll: function () { $table.bootstrapTable('getData').forEach(function (row) { selectedMap.set(row.orderId, row); }); refreshToolbar(); }, onUncheckAll: function () { $table.bootstrapTable('getData').forEach(function (row) { selectedMap.delete(row.orderId); }); refreshToolbar(); }, onPostBody: function () { restoreChecked(); } });

有两个地方值得单独解释。第一,maintainSelected我显式设成false。不是它不好,而是它会和自维护账本抢同一份状态:它自己内部也记了一份选中集合,翻页时按主键恢复,你再checkBy一次,某些版本上会出现重复触发或者状态互相覆盖,排查起来非常费劲。两个方案只用一个,我用自维护这套,可控性高。第二,复选框列的field我写成了checked而不是常见的state,这只是命名习惯,重点是必须显式写,不能留空。留空的话表格拿不到 stateField,行数据的勾选标记会写到一个不存在的键上,表现就是“点上去看着勾了,一刷新又没了”。

4. 表头全选的语义选择:只覆盖当前页,还是整张表

4.1 默认全选只作用于渲染出来的行

表头那个复选框的行为很朴素:当前页只要不是全部选中,点它就把当前页所有行勾上;当前页已经全选,点它就取消当前页所有行。它不会去管第二页、第三页有什么,因为那些数据此时不在浏览器里。

这个行为在多数场景下是对的,但在“批量删除”“批量导出”这类业务里就会引发误解。用户点了表头全选,看到复选框全变绿,以为“全选了”,结果只勾了十行,导出只出十条,然后来找你。所以在动手写代码之前,先跟产品把语义对齐:表头全选到底是“选中当前页”还是“选中整个结果集”。这两种选择实现成本差得很远,方案完全是两套。

4.2 虚拟全选:用排除法反向记账

如果业务要的是“全选整个结果集”,服务端分页下最靠谱的做法是排除法。用户点全选时,不要真去把几十万条 id 拉下来,而是记一个标记位selectAllMode = true,同时清空已有的排除集合。之后用户手动取消勾选某一行,就把那行的 id 放进excludedIds集合。提交给后端的时候传的是三个参数:查询条件、selectAll: true、excludedIds。后端按条件查出来之后排除掉这些 id,语义就是“符合条件的全部,除了这几个”。

var selectAllMode = false; // 是否处于全选模式 var excludedIds = new Set(); // 全选模式下被手动取消的行 function onHeaderCheckAll(checked) { if (checked) { selectAllMode = true; excludedIds.clear(); selectedMap.clear(); } else { selectAllMode = false; excludedIds.clear(); selectedMap.clear(); } refreshToolbar(); } // 全选模式下的取消勾选,走排除逻辑 function excludeOne(row) { if (selectAllMode) { excludedIds.add(row.orderId); selectedMap.delete(row.orderId); } } // 提交时组装参数 function buildBatchParams(query) { if (selectAllMode) { return { query: query, selectAll: true, excludedIds: Array.from(excludedIds) }; } return { query: query, selectAll: false, ids: Array.from(selectedMap.keys()) }; }

这套方案的好处是不管结果集多大,前端传参都很小,后端一条 SQL 就能搞定。代价是它必须后端配合,前端单方面做不到。而且排除集合要和账本严格区分开,别混在一个 Map 里,否则“全选后又单独取消”和“逐个勾选”两种路径会互相污染。我在一个订单模块里就因为两者混用,出现过“全选后取消两条,结果导出了全部”的事故,排查了半天才发现是判断顺序写反了。

4.3 半选状态与工具条计数的联动

表头复选框有一个很多人忽略的半选状态(indeterminate)。当前页部分选中时,表头那个框应该显示成一个小横杠而不是空格,这是浏览器原生支持的状态,但要手动设置。bootstrapTable 在某些版本会自动处理,某些版本不会,我一般自己在onPostBody和每次勾选事件之后统一刷新一次。

function syncHeaderCheckbox() { var pageData = $table.bootstrapTable('getData'); if (!pageData.length) return; var checkedCount = 0; pageData.forEach(function (row) { if (selectedMap.has(row.orderId)) checkedCount++; }); var $headerCb = $table .find('.bs-table thead input[type="checkbox"]') .first(); if (checkedCount === 0) { $headerCb.prop('checked', false).prop('indeterminate', false); } else if (checkedCount === pageData.length) { $headerCb.prop('checked', true).prop('indeterminate', false); } else { $headerCb.prop('checked', false).prop('indeterminate', true); } }

工具条上的计数逻辑也要跟着分两种情况。非全选模式下显示selectedMap.size;全选模式下显示的应该是“全部(已排除 N 条)”,因为总数你前端算不准。这个文案细节看着小,但用户就是靠它判断自己到底选了多少,写错会比功能 bug 更让人困惑。

5. 六个把我卡住的细节:类型、时机、重复渲染

5.1 id 类型不一致,勾选会随机丢失

这是最难查的一类问题,因为它时好时坏。后端 JSON 里 id 是数字,表格渲染后行对象里的orderId是1001。如果某处代码从 URL 参数里读出来、或者存进了localStorage再取出来,就变成了字符串'1001'。Map 用的是严格相等,map.has(1001)和map.has('1001')是两个结果。

表现就是:同一页里勾了一部分,刷新之后有的恢复有的没恢复,你盯着代码看怎么都想不通。我的处理原则是账本的 key 统一做一次字符串化,并且在读取时也统一转换,让类型差异在入口就被抹平。

function sid(id) { return String(id); } onCheck: function (row) { selectedMap.set(sid(row.orderId), row); } // 回填时 values: Array.from(selectedMap.keys()) // 已经是字符串

注意checkBy内部是拿values和行数据的字段做比较的,如果你的行数据字段是数字、values 是字符串,同样会匹配不上。所以更彻底的做法是在数据加载完成后,统一把主键字段转成字符串,或者在checkBy之前把values转回行数据的类型。我倾向于前者,一次性处理,后面所有逻辑都省心。

5.2 在 onLoadSuccess 里回填为什么永远是无效的

这个坑我踩过两次,每次都是隔了很久才想起来。onLoadSuccess的语义是“数据加载成功”,但此时表格还在按新数据构建 tbody,DOM 里可能还是上一页的残留节点,也可能已经清空。在这个时机调checkBy,遍历不到目标行,静默失败。

判断标准很简单:如果你在onLoadSuccess里回填,翻页后偶尔能看到勾选闪一下又消失,或者在慢网络下完全无效,那就是时机不对。换到onPostBody之后一切正常。onPostBody的字面意思就是“tbody 渲染完毕”,用它准没错。顺带提一句,如果用了可编辑列或者详情展开行,onPostBody也可能在你预期之外触发,回填逻辑里加上幂等判断(比如先比对当前 DOM 的勾选状态,一致就跳过)能减少不必要的遍历。

5.3 checkBy 到底会不会触发 onCheck

这关系到会不会死循环。答案是:checkBy内部设置勾选状态时不会触发onCheck事件,它走的是内部标记路径而不是模拟用户点击。所以onPostBody里调checkBy回填,不会反过来把行再塞一遍账本。

但有一个例外情况要注意:如果复选框列是你用formatter自己拼的<input>,那checkBy根本管不了它,因为它找的是表格内置渲染出来的复选框。凡是自定义 formatter 的复选框列,事件全得自己绑,跨页状态也得自己维护,工作量翻倍。能用内置checkbox: true就别自己拼,这是我用了几年下来最实在的一条建议。

5.4 固定列(fixedColumns)下 onPostBody 触发两次

项目里表格列多的时候都会开固定列,把前几列钉在左边。开了之后你会发现账本里的数量莫名其妙翻倍,或者勾选一次触发两次日志。原因是固定列在实现上会克隆一份 tbody 用于横向滚动同步,onPostBody在克隆渲染时也会被触发一次。

处理办法有两种:一是在回填函数里加一个节流标记,同一轮渲染只执行一次;二是判断事件来源的 DOM 是不是主 tbody。我一般用前者,简单粗暴。

var restoring = false; onPostBody: function () { if (restoring) return; restoring = true; restoreChecked(); setTimeout(function () { restoring = false; }, 0); }

用setTimeout解标记是为了避免异常情况下永久卡住。这种小技巧在别的组件里也通用,但要注意它只是防重入,不是线程安全,别指望它解决并发问题。

5.5 搜索、排序、pageSize 变化时的连带处理

搜索和翻页本质上是同一类操作:重新请求数据、重渲染 tbody。所以onPostBody里的回填逻辑对搜索同样生效。但有一个额外的判断要做:如果用户搜出来的结果里包含账本中的 id,回填后这些行是勾上的,用户可能会问“我搜索是为了找别的,怎么这些还是勾着”。

这属于产品决策,不是技术问题。我的做法是搜索、切换每页条数、切换排序这三种操作都不清空账本,因为用户很可能是在“先粗筛后精筛”地挑数据,擅自清空反而打断他。但如果页面有“重置筛选”按钮,那个按钮应该把账本一并清掉,因为它的语义是“从头开始”。

$('#btnReset').on('click', function () { selectedMap.clear(); excludedIds.clear(); selectAllMode = false; $table.bootstrapTable('refresh', { pageNumber: 1 }); refreshToolbar(); });

5.6 删除行与刷新后的状态清理

批量删除成功后,账本必须同步清理,否则下一次提交会带上已经不存在的 id。这一点很容易被忽略,因为页面上看不到任何异常,直到后端返回“部分数据不存在”才反应过来。

function afterBatchDelete(deletedIds) { deletedIds.forEach(function (id) { selectedMap.delete(sid(id)); }); // 注意:删除后当前页可能已空,需要往回退一页 var pageData = $table.bootstrapTable('getData'); if (pageData.length === 0 && $table.bootstrapTable('getOptions').pageNumber > 1) { $table.bootstrapTable('prevPage'); } else { $table.bootstrapTable('refresh'); } refreshToolbar(); }

回退一页这个处理值得加。用户把整页十条全删了,当前页就空了,如果不自动回退,他会看到一个空表格以为系统卡死,然后手动刷新。这种体验细节算不上技术难点,但确实能省掉不少反馈。

6. 把选中结果交给后端:批量操作的接口设计

6.1 传 id 数组还是整行数据

这个问题取决于后端接口的形态。如果是“批量删除”“批量改状态”这类只需要主键的操作,传 id 数组最干净,前端Array.from(selectedMap.keys())直接扔过去,请求体小,后端也简单。如果是“批量导出选中行的明细”,那就得看后端是接受 id 列表自己去查,还是要求前端把整行数据传上来。后者前端也能做,Array.from(selectedMap.values())就行,但要注意账本里存的可能是旧数据,导出前最好提醒用户重新刷新一次。

我个人的偏好是永远只传 id,让后端按 id 查最新数据。除了请求体小的好处,更重要的是避免了前端旧数据覆盖后端新数据的风险,尤其是在多个人同时操作同一批订单的场景下。

6.2 批量删除、批量导出的两种取舍

删除和导出这两类操作在跨页勾选上的实现差异挺大,值得分开说。批量删除是写操作,必须保证“用户看到的勾选”和“实际删除的行”严格一致,所以提交前最好再做一次确认弹窗,把数量报出来。如果是全选模式,弹窗里写“将删除符合条件的全部数据(已排除 N 条)”,让用户自己确认一遍。

批量导出是读操作,相对宽松,但要注意导出的数据量。如果用户全选了十万条,前端传个selectAll: true让后端流式生成文件是合理的;如果只是勾了二十条,那也可以直接把 id 传过去。代码里根据selectAllMode分支处理,是这类需求的通用解法。

6.3 后端返回部分失败时,前端怎么回收状态

真实系统里批量操作很难做到 100% 成功,可能有几条因为状态已变更而失败。这时候前端不能简单地把账本全清空,得把失败的 id 保留下来,同时把成功的从账本里删掉,然后提示用户“成功 N 条,失败 M 条,失败的行已保留勾选”。

function handleBatchResult(res) { // 假设后端返回 { successIds: [...], failIds: [...], message: '...' } res.successIds.forEach(function (id) { selectedMap.delete(sid(id)); }); // failIds 保留在账本里,用户可以重新操作 $table.bootstrapTable('refresh'); refreshToolbar(); if (res.failIds.length > 0) { alert('成功 ' + res.successIds.length + ' 条,失败 ' + res.failIds.length + ' 条,失败项已保留勾选'); } }

这个设计的关键是失败项保留勾选,让用户能直接重试,而不需要重新翻几页找回那几条。听起来是小事,但在几百条数据里重新定位失败项,用户的耐心会消耗得很快。

最后再分享一个小技巧。这套账本逻辑在项目里不止一个页面会用,我后来把它抽成了一个很小的封装,只暴露getIds()、clear()、getRows()三个方法,内部负责事件绑定和回填。抽出来之后,新页面的接入成本从半小时降到五分钟,而且不会因为某个页面漏绑了一个事件导致状态不一致。如果你手上超过两个表格需要跨页勾选,这个抽象值得做,早做早省事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 3:44:18

低成本DDoS防御方案:利用云服务免费额度构建防护体系

1. 项目背景与核心挑战2026年的互联网环境正面临前所未有的安全威胁升级。根据最新的网络安全报告显示&#xff0c;全球DDoS攻击规模正以每年47%的速度增长&#xff0c;100G级别的流量攻击已成为中小企业的日常威胁。与此同时&#xff0c;攻击工具的地下市场售价持续走低&#…

作者头像 李华
网站建设 2026/11/2 3:44:18

CES Asia 2026前瞻:具身智能与低空经济的融合创新

1. CES Asia 2026前瞻&#xff1a;两大技术趋势的碰撞与融合去年在拉斯维加斯CES展台前排队三小时才摸到概念产品的经历&#xff0c;让我对今年6月即将在上海举办的CES Asia 2026充满期待。从主办方释放的信息来看&#xff0c;具身智能&#xff08;Embodied AI&#xff09;与低…

作者头像 李华
网站建设 2026/11/2 3:44:10

Pt100热电阻测温电路:三线制、桥式与恒流源选型避坑

简介&#xff1a;这份PDF文档围绕铂电阻Pt100测温电路的设计与调试展开&#xff0c;面向电子设计、传感器应用及嵌入式采集方向的初学者与工程实践者&#xff0c;重点解决非线性校正、两线制/三线制接法选择、桥式与恒流源采样电路搭建等实际问题。压缩包共1个PDF文件&#xff…

作者头像 李华
网站建设 2026/11/2 3:44:10

从云栖2026看Agentic AI Infra:智能体落地的关键基础设施

云栖大会的展区这两年越逛越累&#xff0c;不是因为腿&#xff0c;是因为脑子。今年从入口开始就能感觉到风向变了&#xff1a;几乎每个展台都在讲智能体&#xff0c;讲Agentic AI&#xff0c;但真正被围得水泄不通的&#xff0c;反而是那些看起来没那么“性感”的AI Infra展区…

作者头像 李华
网站建设 2026/11/2 3:43:05

Java String不可变性的设计原理与实战优势

1. 为什么Java的String被设计为不可变在Java的世界里&#xff0c;String的不可变性&#xff08;immutability&#xff09;是一个基础但极其重要的设计决策。当你写下String s "hello"时&#xff0c;这个"hello"字符串在内存中的状态将永远不会改变。这种设…

作者头像 李华
网站建设 2026/11/2 3:42:49

基于51单片机与DS1302/ISD1420的语音报时数字时钟设计

简介&#xff1a;《基于51单片机、时钟芯片、语音芯片的数字时钟设计与实现》PPT面向电子类课程设计、单片机入门项目与小型电子竞赛参考人群&#xff0c;围绕STC89C52、DS1302与ISD1420构建可24小时制计时、按键校时、三组闹铃设定及语音报时的数字钟方案。内容按总体设计、硬…

作者头像 李华