news 2026/10/31 8:08:01

Element UI 表格分页多选跨页勾选记忆与批量操作实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Element UI 表格分页多选跨页勾选记忆与批量操作实践

1. 先把需求拆开看:分页选中记忆到底难在哪

接手一个后台列表页面时,产品需求往往写得很短:表格分页展示,每行前面加个勾选框,运营翻到第三页勾了几条,再翻回第一页,之前勾的必须还在,最后点“批量导出”要能把几页勾过的数据全都导出去。看着像个一行代码的活儿,但真动手写,十有八九会卡在 Element UI 的el-table上——第一版代码往往长这样:在el-table标签上加了:reserve-selection="true",翻页回来发现该丢还是丢,于是开始怀疑是框架的锅。

问题基本都出在属性挂错了位置。row-key是el-table的属性,而reserve-selection是el-table-column的属性,只管type="selection"那一列。这两个东西一个管“认人”,一个管“记性”,缺一个都白搭。这篇就把这套东西从头捋一遍:为什么选中会丢、row-key到底怎么要求、翻页回填要怎么写、搜索和删除之后要怎么收场,再附一份我自己的项目里反复用过的封装代码。适合正在写后台列表、被这个需求折磨过一次的中级前端,也适合刚上手 Element UI、想知道“为什么这么写”的同学。

1.1 后台列表里藏着的三条隐性要求

表面上看需求只有一句“勾选要记住”,但实际交付时,验收标准通常会膨胀成三条。第一条是跨页可见:从第 1 页翻到第 5 页再翻回来,之前勾过的行必须还是勾着的,表头全选框的半选状态也要跟着对。第二条是跨页计数:页面上方通常要显示“已选 12 条”,这个数字是全局的,不是当前页的。第三条是跨页提交:点导出或者批量操作的时候,提交的是全部勾选行的主键,而不是当前页的。

这三条要求对应的技术手段完全不同。第一条靠el-table自己就能做一半,靠的是reserve-selection加row-key;第二条得自己维护一份选中集合,因为el-table内部的selection数组虽然会保留跨页数据,但里面存的是对象引用,不是干净的 id 列表;第三条最容易被忽略,很多人直接在selection-change里把val赋值给一个变量就去做提交,结果发现提交上去的数据字段是旧的、少了几个新增字段、甚至有几条是因为数据被后台改过导致状态不一致。三条要求层层递进,只满足第一条就以为做完了,上线之后必然被返工。

1.2 el-table 为什么会把选中状态弄丢

要理解这个,得先知道el-table是怎么判断“某一行被选中了”的。它内部维护一个selection数组,每次渲染,对每一行都会问一句:这一行在selection里吗?如果在,就画上勾。判断“在不在”的方式,取决于你有没有配row-key。

没配row-key的情况,判断依据是对象的引用地址,也就是selection.indexOf(row) > -1。这就出问题了:分页切换时,父组件把list整个换成了新的一页数据,新数据是接口返回的新对象,地址跟selection里存的老对象完全对不上,于是所有勾都掉了。更糟的是,就算你翻回原来那一页,接口重新请求了一次,返回的又是一批新对象,还是对不上。这就是为什么很多人觉得“我没改代码啊,怎么翻页就没了”。

配了row-key的情况,判断依据变成row[rowKey] === selection[i][rowKey],也就是按业务主键比对。只要两次拿到的行 id 一样,就能认出是同一条数据,勾选状态自然能延续。但这里还有个前提:el-table默认在data变化的时候,会把selection里那些“当前data里找不到”的行清掉。reserve-selection的作用就是关掉这个清理动作,让不在当前页的选中项继续躺在selection数组里。所以这两个属性是配合关系,不是二选一。

1.3 两条路线的取舍:组件内保留还是自己维护集合

真要做这个功能,其实有两条路。第一条是借力路线,靠row-key+reserve-selection,让el-table内部帮我们维护跨页选中;第二条是自管路线,自己维护一个Set或者数组存 id,在selection-change里同步,在翻页、搜索、增删之后用toggleRowSelection手动回填。

两条路的差别挺明显的,我列个表对比一下:

维度借力路线(reserve-selection)自管路线(自己维护 id 集合)
代码量两个属性搞定,约 5 行需要写回填、同步、清理逻辑,约 60 行起
数据新鲜度selection里是历史快照,字段可能过期只存 id,提交前用最新数据合并
搜索过滤过滤掉的选中项仍留在 selection 里,容易计错数想清就清,想留就留,完全可控
跨路由/刷新记忆做不到,组件销毁即丢存 sessionStorage 就能带过去
树形表格父子联动跨页时状态容易错乱自己算,逻辑清楚
适用场景简单列表、只有翻页没有搜索复杂筛选、批量操作、多 Tab 复用

我自己的选择标准是这样:如果这个表格只有分页、没有搜索框、没有行删除、选完立刻提交,就直接用借力路线,五分钟完事。但凡上面有一条不满足,就直接上自管路线,别贪那几行代码,后面补丁会补到你怀疑人生。真实项目里我基本都走自管路线,因为“只有分页没有筛选”的表格几乎不存在,产品经理迟早会给你加个搜索框。

2. 原理拆解:row-key 和 reserve-selection 各管什么

2.1 选中状态到底存在哪儿

很多人写这个功能时脑子里没有状态归属的概念,导致到处找“为什么”。实际上el-table的选中状态有两处:一处是组件内部 store 里的selection数组,另一处是父组件通过selection-change事件拿到的那份数据。这两份数据是引用关系,父组件拿到的数组就是内部selection的映射,你改它等于改内部状态,虽然不推荐这么干。

翻页的时候,data变化会触发内部的一轮“对账”:遍历selection,看看每一项还能不能在当前data里找到。找到就留着,找不到就看reserve-selection。为false(默认)时直接剔除,为true时保留。所以从行为上描述,reserve-selection做的事情非常单纯:在数据更新时,不要把不在当前页的选中项删掉。

理解这一点之后,很多“怪现象”就有解释了。比如你在第二页勾了三条,翻到第三页,selection-change不会触发,因为selection数组本身没变。比如你翻回第二页,那三条会重新显示勾选,因为内部按row-key比对上了。比如你在第一页点全选,只会选中第一页的十条,selection变成十条,不会变成“全部数据”。这些行为都是同一套逻辑的推论,不是 bug。

2.2 row-key 的三个硬性要求

row-key看起来就是个属性,随便填填也能跑,但出问题的时候九成出在它身上。我总结下来有三条硬要求。

第一,必须唯一。后端返回的id是首选。但如果数据里混了id为null或者undefined的行,el-table会把它们当成同一行——多个null相互匹配,勾一个全勾。这种情况在临时新增行、草稿行里很常见,处理办法是给没有 id 的行分配一个本地临时 key,比如temp_${Date.now()}_${index}。第二,必须稳定。同一行数据在两次请求里row-key的值必须一样,不能这次是数字1001,下次变成字符串"1001"。el-table内部是用全等比较的,数字和字符串永远不等,表现就是“明明 id 一样,怎么不勾选”。这个问题在对接某些后端时特别容易踩——分页接口返回数字,详情接口返回字符串,或者JSON.parse大整数时精度丢失导致 id 变了。第三,尽量用字符串。对象 key 会被隐式转成字符串,用数字做 key 时虽然大部分场景正常,但遇到精度问题就会翻车,我一般会在模板里写:row-key="row => String(row.id)"一劳永逸。

还有一个细节值得提:row-key写函数的时候,每次渲染都会调用,行数上千的话会有性能影响。我实测过 800 行数据、每行row-key是函数调用的表格,滚动时会有一点卡,改成字符串属性名row-key="id"就顺畅了。数据量大的话别用函数。

2.3 selection 里存的是对象引用,不是你以为的 id

这是最容易埋雷的一点。开了reserve-selection之后,selection数组里保留的跨页项,是第一次被选中时那个 row 对象的引用,不是新数据。也就是说,你在第二页勾了一行,然后翻到第五页,后台把这行的状态从“待审核”改成了“已通过”,你再回到第二页,界面上显示的是“已通过”(因为重新请求了数据),但selection数组里躺着的那个对象,status还是“待审核”。

带来的直接后果是:如果你在提交时直接用this.multipleSelection.map(row => row.status),拿到的会是过期数据。真实项目里出过更隐蔽的问题——批量操作时需要提交“操作前的版本号”做乐观锁校验,结果提交上去的是上一次选择时的旧版本号,后端直接判定冲突,用户看到的是“部分数据操作失败”,排查了半天才定位到是选中项快照的问题。

解法很简单:永远不要直接拿selection里的对象去做业务提交,只从中提取主键。提交前用主键去和最新的完整数据做合并,或者干脆只提交 id 数组,让后端自己查。这既是数据正确性的要求,也是减少请求体积的实际考虑。我在封装的时候会强制把selection转换成selectedIds数组再往上传,父组件根本拿不到那份对象引用,从源头上把这个坑堵死。

3. 动手实现:一份能直接抄的多选表格

3.1 最小改动版本:注意属性写在哪一层

先给一个最小可用的版本,适合“只有分页、没有搜索”的简单场景。模板部分的关键是三处:el-table上写row-key,el-table-column上写reserve-selection,然后监听selection-change。

<template> <div> <el-table ref="table" :data="list" row-key="id" @selection-change="handleSelectionChange" > <el-table-column type="selection" width="48" :reserve-selection="true" /> <el-table-column prop="name" label="名称" /> <el-table-column prop="status" label="状态" /> </el-table> <el-pagination :current-page="page" :page-size="size" :total="total" layout="prev, pager, next, total" @current-change="handlePageChange" /> <div>已选 {{ selectedIds.length }} 条</div> </div> </template>

这里最值得强调的是reserve-selection的位置。我见过至少四五次有人把它写在el-table上,然后跑来问为什么没效果。Element UI 的文档里,这个属性确实归属于el-table-column,它的语义是“这一列(选择列)在数据更新后保留之前选中的记录”,写在表上语义上说不通,代码自然也不认。同样容易搞混的还有:selectable,那是控制某一行能不能被勾选的,也挂在el-table-column上。

还有一个隐含条件:选择列一般要放第一列。虽然技术上放后面也能跑,但reserve-selection在固定列和动态列的场景下偶发状态不同步,我习惯性放第一列,避开了这类边缘情况。另外选择列不要写成v-if动态渲染,切换显示时selection会被重置,跨页记忆直接失效。

3.2 服务端分页下怎么把勾选状态画回界面上

如果是前端一次性拿全量数据自己分页(el-table只渲染当前页),reserve-selection基本够用。但绝大多数后台是服务端分页,每次翻页都是一个新请求,返回一页新数据。这时候reserve-selection依然有效,因为它比的是row-key而不是数据引用,翻回来时主键能对上,勾选状态就能画出来。

但有一个前提必须满足:新数据里必须包含完整的row-key字段。我遇到过后端为了省带宽,翻页接口返回的列表数据里把id字段去掉了(详情接口才给),前端就只能靠index做 key,这时候跨页记忆彻底失效。这种问题在前端是解不掉的,得回去让后端把主键带上,别自己在前端编造假 key,一旦做排序、筛选就全乱。

还有一种情况是数据要经过前端二次处理,比如用map给每行加一个计算字段。如果处理过程中不小心把id删了或者改名了(比如解构时漏了id),也会导致比对失败。我的习惯是在请求回来的第一层就做一次完整性检查:

async fetchList() { const res = await api.getList({ page: this.page, size: this.size }); this.list = (res.data.list || []).map(item => { if (item.id === undefined || item.id === null) { console.warn('[table] 缺少主键,跨页记忆将失效', item); } return item; }); this.total = res.data.total; }

这条console.warn在开发环境里救过我两次,都是在联调早期就发现了字段缺失,比上线之后才暴露强得多。

3.3 全选和半选:只能覆盖当前页的现实

el-table的全选框有个明确的行为边界:它只对当前data里可见的行生效。你在第一页点全选,选中的是第一页那十条,selection变成十条;翻到第二页,全选框是未勾选状态,因为第二页的十条不在selection里。这个行为不是缺陷,而是设计如此,因为组件只能看到当前的数据。

但产品经理的预期往往是“全选所有页”。这个需求需要自己实现,思路是维护一个“是否全选全部”的标记,配合手动toggleRowSelection。我在项目里的做法是:

handleSelectAll(selection) { // selection 是当前页被选中的行 const currentPageIds = this.list.map(i => i.id); if (selection.length === 0) { // 当前页被取消全选,从集合里移除当前页所有 id currentPageIds.forEach(id => this.selectedMap.delete(id)); } else { currentPageIds.forEach(id => this.selectedMap.set(id, true)); } this.syncSelectedIds(); }

关键是不要试图让el-table自己的全选框表达“全选了所有页”的语义,它做不到,强行改内部状态只会让半选计算乱掉。正确做法是在表格外面单独放一个“全选全部结果”的复选框,跟表头的全选框区分开,文案写清楚是“选中本页”还是“选中全部匹配结果”。用户其实能理解这个区分,含糊其辞才会引发困惑。

半选状态也是同理,el-table计算的半选是基于当前页数据的,所以当你在第一页勾了 3 条、翻到第二页时,第二页的表头是全不选状态,而不是半选。这不是错,如果产品非要“全局半选”,同样得在外面自己做一个指示器。

3.4 交给后端的应该是一串 id,而不是 selection

这是整个方案里最容易被忽略的一步。selection-change给你的val是对象数组,直接丢给接口有三个问题:数据可能是过期的、请求体积大、字段里可能含有敏感信息被带到日志里。正确姿势是事件里只做 id 收集,业务提交时用 id。

handleSelectionChange(selection) { // 不要 this.multipleSelection = selection // 只提取主键,并且保留一份用于展示的轻量信息 this.selectedIds = selection.map(row => row.id); this.selectedBrief = selection.map(row => ({ id: row.id, name: row.name })); this.$emit('selection-change', { ids: [...this.selectedIds], count: this.selectedIds.length }); }

如果业务确实需要提交时带上完整数据(比如导出时需要行内的所有字段),那也别用selection里的老对象,而是用 id 去当前最新数据里找,找不到就单独请求一次详情。看起来多了一次请求,但换来的是数据一致性,批量操作出错时排查成本会低很多。金额、状态、版本号这类字段一旦带了旧值,问题往往不是立刻暴露,而是几天后从用户反馈里冒出来,那时候查起来极其痛苦。

3.5 一份可直接复用的完整代码

下面这份是我自己在多个后台列表里复用过的版本,去掉了业务字段,保留了核心逻辑。功能包括:跨页记忆、跨页计数、选中项被删除后自动清理、搜索后可选是否保留选中。

<template> <div class="page"> <div class="toolbar"> <el-input v-model="keyword" placeholder="搜索名称" clearable @clear="handleSearch" /> <el-button type="primary" @click="handleSearch">查询</el-button> <span class="tip">已选 {{ selectedIds.length }} 条</span> <el-button :disabled="!selectedIds.length" @click="handleBatch">批量操作</el-button> </div> <el-table ref="table" :data="list" row-key="id" @selection-change="handleSelectionChange" > <el-table-column type="selection" width="48" :reserve-selection="true" /> <el-table-column prop="name" label="名称" /> <el-table-column prop="status" label="状态" /> </el-table> <el-pagination :current-page="page" :page-size="size" :total="total" layout="prev, pager, next, total" @current-change="handlePageChange" /> </div> </template> <script> export default { name: 'SelectionMemoryTable', data() { return { list: [], total: 0, page: 1, size: 10, keyword: '', selectedIds: [], selectedBrief: [] }; }, methods: { async fetchList() { const res = await api.getList({ page: this.page, size: this.size, keyword: this.keyword }); this.list = res.data.list || []; this.total = res.data.total || 0; this.$nextTick(this.restoreSelection); }, // 关键:新数据渲染完之后,按 row-key 把勾选状态画回去 restoreSelection() { if (!this.selectedIds.length || !this.$refs.table) return; const idSet = new Set(this.selectedIds.map(String)); this.list.forEach(row => { const shouldSelect = idSet.has(String(row.id)); const isSelected = this.$refs.table.selection.some( item => String(item.id) === String(row.id) ); if (shouldSelect !== isSelected) { this.$refs.table.toggleRowSelection(row, shouldSelect); } }); }, handleSelectionChange(selection) { this.selectedIds = selection.map(row => row.id); this.selectedBrief = selection.map(row => ({ id: row.id, name: row.name })); }, handlePageChange(page) { this.page = page; this.fetchList(); }, handleSearch() { this.page = 1; this.fetchList(); }, handleBatch() { this.$emit('batch', { ids: [...this.selectedIds] }); }, // 供父组件调用:某条数据被删除后,把它从选中集合里摘掉 removeSelected(id) { const idx = this.selectedIds.indexOf(id); if (idx > -1) this.selectedIds.splice(idx, 1); const row = this.list.find(item => item.id === id); if (row) this.$refs.table.toggleRowSelection(row, false); }, clearAll() { this.selectedIds = []; this.selectedBrief = []; this.$refs.table.clearSelection(); } } }; </script>

restoreSelection里那个“先比对再切换”的判断是有意加的。如果无脑调用toggleRowSelection(row, true),在已经选中的行上再调一次,el-table不会做幂等处理,selection-change会重复触发,进而引发无限循环的风险(尤其在selection-change里又调用了回填逻辑的写法下)。加这个判断成本极低,能避免很多诡异的重复触发。

4. 边界场景:搜索、删除、封装与持久化

4.1 搜索之后,之前勾的还算不算数

这是所有问题里争议最大的一个,也没有标准答案,得看业务语义。我的判断原则是:如果搜索是“缩小范围”,选中项应该保留;如果搜索是“切换业务集合”,选中项应该清空。

举个例子。用户在一个订单列表里勾了 5 单准备批量发货,然后输入关键词筛出某个客户,这时候他多半是想“在这个客户里再补勾两单”,之前的 5 单应该保留。但如果用户切换的是“订单类型”这种一级筛选(从“销售订单”切到“采购订单”),那之前的选中就毫无意义,应该清空。

实现上,如果选了保留,要处理一个视觉上的问题:被筛掉的选中项在界面上看不见,但“已选 5 条”的数字还挂着,用户会疑惑。我在项目里的做法是在数字旁边加一个可展开的标签区,把已选中的名称列出来,每一项带个删除按钮,让用户能看到“看不见的那部分”是什么,随时可以摘掉。这个小交互看起来不起眼,但能显著降低“我明明没勾这么多啊”这类工单。

如果选了清空,记得一定要调clearSelection(),不能只清selectedIds,否则内部selection还留着,翻页回来会发现莫名又勾上了。

handleFilterTypeChange() { this.page = 1; this.clearAll(); // 内部同时清 selectedIds 和 clearSelection this.fetchList(); }

4.2 选中行被删掉之后的悬空 key

批量操作之后经常要删掉几行,删除成功后重新请求列表,这时候selectedIds里可能还留着已经被删掉的 id。带上去提交,后端要么报“数据不存在”,要么默默忽略了,前端显示“已选 3 条”但实际只处理了 2 条,这种静默失败最难查。

处理方式有两种。乐观一点的做法是在删除成功的回调里手动把 id 摘掉:

async handleDelete(row) { await api.delete(row.id); this.removeSelected(row.id); // 组件内部方法,见上一节 this.fetchList(); }

稳妥一点的做法是每次列表刷新之后做一次校验,把不在当前全量集合里的 id 清掉。但服务端分页拿不到全量集合,所以只能靠“显式删除时同步清理 + 提交前由后端校验”双保险。我在提交批量操作时会要求后端返回“成功列表和失败列表”,前端根据失败列表反查哪些 id 有问题,把它们从选中集合里摘掉并提示用户,这样即使用户挂着悬空 id 也能自愈。

4.3 抽成 mixin 复用

后台系统里这种表格往往有七八个,逐个复制粘贴维护成本太高。我一般抽成一个 mixin(Vue 2 项目里)或者组合式函数(Vue 3 / Element Plus 项目里)。抽的时候有个原则:mixin 只负责选中集合的维护和回填,不负责请求。请求逻辑因页面而异,塞进 mixin 里会导致它越来越臃肿,最后变成一个什么都管、什么都管不好的巨型混入。

// mixins/selectionMemory.js export default { data() { return { selectedIds: [], selectedBrief: [] }; }, methods: { syncSelection(selection) { this.selectedIds = selection.map(row => row.id); this.selectedBrief = selection.map(row => ({ id: row.id, name: row.name })); }, restoreSelection(list, tableRef) { if (!this.selectedIds.length || !tableRef) return; const idSet = new Set(this.selectedIds.map(String)); list.forEach(row => { const shouldSelect = idSet.has(String(row.id)); const isSelected = tableRef.selection.some( item => String(item.id) === String(row.id) ); if (shouldSelect !== isSelected) { tableRef.toggleRowSelection(row, shouldSelect); } }); }, removeSelectedId(id) { const idx = this.selectedIds.indexOf(id); if (idx > -1) this.selectedIds.splice(idx, 1); const idx2 = this.selectedBrief.findIndex(item => item.id === id); if (idx2 > -1) this.selectedBrief.splice(idx2, 1); }, resetSelection(tableRef) { this.selectedIds = []; this.selectedBrief = []; tableRef && tableRef.clearSelection(); } } };

页面里用的时候,只需要在fetchList之后调用this.restoreSelection(this.list, this.$refs.table),在selection-change里调用this.syncSelection(val),剩下的都是业务自己的事。这样一套下来,新页面接入这个功能大概五分钟,比每次重新想一遍逻辑快得多。

4.4 刷新页面、切换路由之后还要记住

el-table的记忆只活在组件实例的生命周期里,一旦路由切走再回来(没开keep-alive)或者用户按了 F5,选中状态全没。这个需求在“用户需要跨页面选数据”的场景下确实存在,比如从列表页勾选若干条去配置页做关联。

实现方式是把selectedIds序列化存到sessionStorage,key 里带上页面标识和用户标识,避免多个 Tab 之间互相污染。存的时候只存 id 数组,别存整行数据,一是体积大,二是数据会过期。回填的时候在fetchList之后走同一套restoreSelection逻辑,逻辑复用,没有额外成本。

const STORAGE_KEY = 'order_list_selected_ids'; saveToStorage() { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(this.selectedIds)); }, loadFromStorage() { try { const raw = sessionStorage.getItem(STORAGE_KEY); this.selectedIds = raw ? JSON.parse(raw) : []; } catch (e) { this.selectedIds = []; } }

这里有个小提醒:用sessionStorage而不是localStorage。localStorage是永久的,用户三天后回来打开这个页面,发现莫名其妙勾着几条数据,体验很怪。sessionStorage跟着标签页走,关掉就清,符合直觉。另外记得在用户点“清空选择”或者提交成功之后,把 storage 里的也一并删掉,否则会残留。

5. 排错实录:那些文档里不会写的坑

5.1 高频问题速查表

下面这张表是我这两年陆陆续续攒下来的,基本覆盖了这类需求 90% 的故障现象。遇到问题先按现象对一遍,能省掉大量瞎猜的时间。

现象最可能的原因处理办法
翻页回来勾选全丢reserve-selection写在了el-table上挪到el-table-column,同时配row-key
配了row-key还是丢row-key值类型不一致(数字 vs 字符串)统一String(row.id),检查后端字段类型
勾一行结果勾了好几行存在重复的row-key(常见于null、0)给无 id 的行分配本地唯一 key
已选数量对了但提交数据是旧的selection里是历史快照只提 id,或提交前用最新数据合并
表头全选后翻页,第二页没勾全选只作用于当前页,这是正常行为需要全局全选就在表格外单独做控件
搜索后数量虚高被筛掉的选中项仍留在selection里搜索时决定清空或提供已选清单可视化
删除数据后提交报错选中集合里有已删除的悬空 id删除成功回调里同步摘掉,或后端返回失败列表反查
调用toggleRowSelection没反应传入的 row 不在当前list里确保行对象来自当前list,或用row-key重新查一次
切换 Tab 后状态错乱多个表格共用同一个 ref 或 data每个表格独立实例、独立选中集合
selection-change疯狂触发回填逻辑里没有做幂等判断toggleRowSelection前先比对当前是否已选

5.2 我踩过的几个坑,说出来让大家少走弯路

第一个坑:树形表格配reserve-selection。带父子层级的表格,勾父节点会自动勾选所有子节点,这在单页里没问题,但跨页之后就乱了。因为子节点可能分散在不同页,父节点在第一页被勾上,第三页的子节点被toggleRowSelection回填时,父节点的半选状态计算会出错,表现出来就是“父节点显示勾选,展开后发现子节点没勾全”。后来这个场景我彻底放弃reserve-selection,改成全部自己维护 key 集合,父子联动也自己算,代码多了几十行,但状态永远是对的。

第二个坑:动态列导致的 selection 列重建。有个页面允许用户自定义显示哪些列,我用v-for渲染所有el-table-column,包括那个type="selection"的列。用户一改列配置,v-for的 key 变了,选择列被销毁重建,selection直接重置。解决办法是把选择列从v-for里单独拎出来,固定写在最前面,不参与动态渲染。这个坑当时排查了很久,因为现象是“偶尔丢”,其实不是偶尔,是用户改了列配置才丢。

第三个坑:在selection-change里做深拷贝。早期为了保险,我在事件回调里写了JSON.parse(JSON.stringify(selection)),想着快照一份不会互相影响。结果数据量上来之后(用户勾了几百条),每次勾选都卡一下,因为深拷贝是同步阻塞的。后来改成只提取需要的几个字段(map一下就完事),流畅度立刻恢复。选中事件里只做轻量操作,这是性能上的一条铁律。

第四个坑:clearSelection之后selection-change不一定按预期触发。有些版本里清空选中后事件触发的时机和调用顺序有关系,如果在clearSelection之后立刻读this.selectedIds,可能拿到的是旧值。稳妥写法是把清空选中集合的动作放在事件回调里统一做,而不是依赖调用的同步性。

5.3 上线前值得自己过一遍的自测清单

代码写完别急着提交,下面这几条我会在每次做完这类功能后手动过一遍,成本十分钟,但能挡掉大部分线上问题。第一,勾选第一页三条,翻到最后一页,再翻回来,三条是否还在,表头是否显示半选。第二,勾选后点搜索,确认选中数量和列表内容是否符合预期(保留还是清空,得跟产品对齐)。第三,勾选几条之后删掉其中一条,看“已选”数字是否自动减一,提交时是否还带着已删除的 id。第四,把浏览器窗口缩小到出现横向滚动,确认固定列下的勾选框显示正常。第五,连续快速翻页五次,看是否有重复请求导致的选中状态闪回。第六,清空选择后立刻翻页,确认不会莫名其妙又勾上。这六条覆盖了绝大多数回归问题,尤其是第五条的快速翻页,很容易暴露出请求竞态导致的回填错乱——老请求后返回,把新页面的勾选状态覆盖了。真遇到这种情况,就在请求里带上一个自增的requestId,回来时比对一下,不是最新的一次就直接丢弃。

这套东西说到底没什么高深的技术,难的是把每个边界都想清楚。我个人的体会是,遇到“选中记忆”这种看起来简单的需求,先别急着写代码,花五分钟把“谁会被选中、选中之后会做什么、什么情况下选中会失效”这三件事在纸上列一遍,后面的返工会少很多。最后再分享一个实用的小习惯:在组件里加一个开发环境专用的调试按钮,一键打印当前selectedIds、selection里的对象和当前页数据,联调时比翻控制台快得多,联调完记得用环境变量关掉就行。

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

Python分页读取大PDF并调用AI处理:解决上下文窗口限制的工程实践

1. 大文件PDF处理的真实困境与破局思路 1.1 为什么300页PDF成了AI的"消化不良" 做过文档处理的人都有一个共同体会&#xff1a;几十页的PDF丢给大模型&#xff0c;回答质量还不错&#xff1b;一旦页数上到两三百页&#xff0c;情况就完全变了。要么是接口直接报超出…

作者头像 李华
网站建设 2026/10/31 8:07:00

STM32移植FX3U兼容PLC:软元件、指令解释器与编程口通信

简介&#xff1a;这份 ZIP 包面向学习三菱 FX3U 系列 PLC 编程与仿真的开发者、嵌入式学习者及自动化方向学生&#xff0c;核心提供 FX3U V8.8 相关源码升级资料&#xff0c;并涉及 STM32 与仿三菱方案的结合&#xff0c;可用于理解指令逻辑、固件升级思路和软硬件协同调试。包…

作者头像 李华
网站建设 2026/10/31 8:06:27

深入剖析双向链表dlist:从设计到实战的工程实现

数据结构里最容易被低估的&#xff0c;就是双向链表。单链表写起来顺手&#xff0c;但一到"删除当前节点"或者"从后往前遍历"这种场景&#xff0c;就得从头再扫一遍&#xff0c;时间复杂度直接退化。双向链表多存一个前驱指针&#xff0c;换来的是 O(1) 的…

作者头像 李华
网站建设 2026/10/31 8:05:05

SX1262/SX1268 LoRa驱动源码:SPI初始化与收发避坑

简介&#xff1a;面向嵌入式与物联网开发者的SX126x系列LoRa芯片驱动源码包&#xff0c;覆盖SX1261、SX1262、SX1268等型号&#xff0c;可用于远距离、低功耗无线通信项目的底层移植、收发控制与信道活动检测&#xff08;CAD&#xff09;功能验证&#xff0c;也适合作为从SX127…

作者头像 李华
网站建设 2026/10/31 8:04:25

WinForm医疗管理系统:离线可用、SQLite加密、状态机驱动的桌面端实践

简介&#xff1a;这是一套基于WinForm开发的医疗设备管理系统&#xff0c;面向医院信息科人员、医疗IT实施工程师及.NET初学者&#xff0c;解决中小型医疗机构在设备台账、合同履约、配件库存、维修工单及数据统计等环节的手动管理痛点。资源包共155个文件&#xff0c;含30个核…

作者头像 李华
网站建设 2026/10/31 8:03:25

上下文模式(Context-Mode)实战:多环境多角色状态管理与切换设计

1. 从"上下文模式"说起&#xff1a;一个被低估的工程概念第一次看到"context-mode"这个词&#xff0c;很多人会下意识地把它归类成某个框架里的配置项&#xff0c;或者某个编辑器插件的开关。但如果你真正在工程一线待过几年&#xff0c;就会发现这个词背后…

作者头像 李华