资讯动态

UniApp复选交互全攻略:全选、多选、反选与状态管理

发布时间:2026/10/15 16:19:47 来源:尧图企业网站定制
做跨端项目时“复选框支持全选、多选、反选”几乎是每次都会点到的菜。最近做管理后台的批量操作又把这个需求完完整整走了一遍列表里每条记录要能单独勾选顶部一个全选底部一个反选勾选数量实时联动批删按钮。购物车结算、后台批量管理、权限配置里给角色勾菜单骨架全是同一套逻辑但很多项目就在这上面反复返工。我见过不少翻车现场只勾了一个商品底部“全选”还亮着第一页勾了几条翻页再回来状态全丢点了反选之后计数和按钮状态对不上列表里有禁用项“全选”把它们也一起勾上了。这些问题几乎都源于同一个根因——把勾选状态当成零散的事件去处理没有收敛出一套清晰的状态模型。这篇文章就围绕UniApp把整块逻辑拆开讲从数据结构设计、三个核心交互的实现、边界场景处理到原生组件和自定义视图的取舍、性能优化与逻辑复用都会给出可以直接搬走的方案。1. 复选需求的三种典型页面以及Bug高频出现的根源1.1 购物车结算、批量管理、权限勾选同一个核心三种附加逻辑这类复选出现最多的就是下面三种页面核心交互完全一样但附加复杂度不同这也是为什么“看起来简单”的需求总是做不完。典型页面核心交互附加复杂度购物车结算单选联动金额、全选后一键结算禁用项失效商品、金额汇总、数量汇总批量管理后台多选记录后批量删除/导出/下线分页翻页、跨页保留选中、批量接口参数组装权限配置角色与菜单树勾选父子节点联动、半选态、禁用项购物车的重点是金额联动勾选状态一变化底部结算栏的合计金额和商品数就要跟着变这里需要响应式的实时计算。批量管理后台的重点是接口分页用户往往要跨页选择几十条记录再统一操作选中状态必须撑得住翻页。权限配置最麻烦的是树形结构父节点勾了子节点要不要跟着勾子节点勾了一半父节点怎么显示属于在复选框基础上的递归联动本文先不展开但数据模型部分对它有参考价值。1.2 手动同步勾选状态才是万恶之源很多写法的通病是把 allChecked、selectedCount 当成普通 data 字段在事件回调里手动维护。单选方法里写 count取消时写 count--全选方法里写 allChecked true再在别的路径里同步按钮高亮。小需求里这么写能跑一旦叠加禁用项、分页、反选、批量删除状态就开始互相打架。我现在的做法是让“全选按钮是否点亮”和“已选数量”都变成派生状态由列表数据或选中集合计算出来而不是手工维护。Vue 的 computed 完全承担得了这件事数据层只负责记录每个条目有没有被选中全选状态、半选状态、选中数量都是算出来的。这样一来任何一条路径修改了勾选状态UI 都会自动跟着收敛不会出现“数据变了、按钮没变”的脱节。2. 先定数据模型checked内嵌数组还是用Map单独维护选中项动手写交互之前我建议先把数据模型想清楚这决定了后续坑的多少。常见有两种方案。2.1 内嵌checked方案直观但有两个前置条件最直接的做法是给列表数据加一个 checked 字段模板里直接绑定。适合列表一次性加载、不需要跨页保留选中的场景比如一个确认下单页或者单页展示的简单名单。data() { return { list: [ { id: 1, name: 无线蓝牙耳机, checked: false }, { id: 2, name: 机械键盘, checked: false }, { id: 3, name: 显示器支架, checked: false } ] } }这个方案有两个前置条件必须处理。第一初始化时必须给每条数据显式定义 checked 字段。Vue2 的响应式只对初始存在的属性生效如果接口返回的数据里没有 checked后续执行 item.checked true 时视图不会更新必须改用 $set。第二每次从接口重新拉列表后checked 会跟着新数据重置。如果刷新是用户手动触发的问题不大可如果是分页自动刷新用户的选中记录就莫名其妙消失了。所以接口返回的数据我会在进入页面数据层时先做一次规范化确保每个条目都有 checked 和 disabled 字段避免响应式埋雷methods: { loadList() { requestList().then(data { this.list data.map(item ({ ...item, checked: !!item.checked, disabled: !!item.disabled })) }) } }2.2 独立Map记录选中id分页和异步刷新更稳如果列表是分页加载的或者刷新频率高我强烈建议把“选中了谁”从列表数据里拆出来用一个对象 map 单独维护。模板里依然可以通过 selectedMap[item.id] 判断勾选状态但 list 本身不用动翻页、刷新都不会影响选中记录。data() { return { list: [], selectedMap: {} } }, methods: { toggleItem(item) { if (item.disabled) return if (this.selectedMap[item.id]) { this.$delete(this.selectedMap, item.id) } else { this.$set(this.selectedMap, item.id, true) } } }这里有两个响应式细节容易忽略。用 $set 增加键用 $delete 删除键直接赋值 this.selectedMap[id] true 不会触发依赖更新这是 Vue2 项目里最常见的坑。另外id 的类型必须全局统一。接口返回数字 id页面里一旦用了 String(item.id) 做键后面所有地方都得统一转字符串否则会出现同名不同型的两个键计数和全选判断全部错乱。我习惯在数据进入页面时固定一种类型存进去之前和拿出来之后都保持一致。3. 全选、单选、反选的实现从手动同步到computed收敛数据模型定好之后核心交互就可以写了。我先把 map 方案下的派生状态定义出来再逐个实现操作。3.1 单选与全选联动先避开 every 的空数组陷阱用 computed 定义全选状态、半选状态和选中数量这是整套逻辑里最重要的一段computed: { selectableList() { return this.list.filter(item !item.disabled) }, allChecked() { const list this.selectableList return list.length 0 list.every(item this.selectedMap[item.id]) }, partialChecked() { const list this.selectableList if (list.length 0) return false const count list.filter(item this.selectedMap[item.id]).length return count 0 count list.length }, selectedCount() { return Object.keys(this.selectedMap).length } }三个关键细节必须说明。第一JavaScript 里空数组执行 every 会返回 true[].every(item item.checked)的结果是 true所以 allChecked 必须先判断 length 0否则列表为空时全选按钮会莫名高亮这个坑我见过至少三个人踩。第二所有派生状态都以 selectableList 为基准禁用项根本不参与全选和半选的计算逻辑天然正确。第三selectedCount 直接数 map 的键选中集合的键就是当前选中项不需要额外维护一个 count 变量。如果选择内嵌 checked 的数据模型computed 写法同理只是把this.selectedMap[item.id]换成item.checked即可。重点在于不要用 data 里的字段手动维护这些值交给 computed 之后单选、全选、反选三条路径都会自动回到一致的显示。3.2 全选按钮的触发与“取消全选”全选按钮点击后目标值就是当前 allChecked 取反然后批量写入选中集合methods: { handleSelectAll() { const target !this.allChecked this.selectableList.forEach(item { if (target) { this.$set(this.selectedMap, item.id, true) } else { this.$delete(this.selectedMap, item.id) } }) } }很多项目里会手动维护一个 selectAllFlag勾掉一个条目之后忘了同步就出现“明明没全选按钮还亮着”的Bug。我这边因为 allChecked 是 computed全选按钮样式直接绑定它所有可选项都被勾上时按钮自然高亮再点一次就是取消全选不需要任何额外状态。这个设计把最大的隐患从根上掐掉了。3.3 反选的两个决策点反转范围与全选状态反选的实现本身很薄就是把每个可选条目的勾选状态取反methods: { handleInverse() { this.selectableList.forEach(item { if (this.selectedMap[item.id]) { this.$delete(this.selectedMap, item.id) } else { this.$set(this.selectedMap, item.id, true) } }) } }真正要决策的是两件事。第一反转范围是当前页可选条目还是全部条目要不要排除禁用项。我默认只反转可选列表禁用项保持不可勾选这符合大多数后台批量管理的预期。第二反选完成后全选按钮如何显示不要手动把它设成 false也不要设成 true让 computed 重新计算。之前一条没选反选后全选按钮会自动点亮之前部分选中反选后大概率是部分选中按钮自动回到半选态。写死状态恰恰是反选之后界面错乱的主要原因。另外提一句反选操作一般出现在后台批量管理、权限配置这类场景购物车反选反而很少见。接需求时如果没提反选别默认加先问清楚运营场景再决定。3.4 半选态全选框的第三种显示当列表只有部分被选中时全选按钮应该呈现半选态而不是简单的一个未选中空框。原生 checkbox 可以通过 indeterminate 属性设置半选自定义视图里我会画一条短横线。view classcb :class{ checked: allChecked, partial: partialChecked } text v-ifallChecked classmark✓/text view v-else-ifpartialChecked classminus/view /viewCSS 里给 minus 一个宽约 10rpx、高约 4rpx 的灰色横条就能出效果。半选态不是必须的但做了之后用户对“部分选中”的理解成本会明显降低尤其在权限配置、批量选择这种跨页场景里半选能给用户很强的反馈。4. 边界场景清单分页、删除、空列表与禁用项交互主流程写完只是第一步真正决定这套功能能不能扛住测试的是边界场景。我把自己踩过的坑整理成一张清单。4.1 翻页后的选中记录保留策略遇到分页列表第一件事是和产品确认语义是只操作当前页还是允许跨页批量选择。两种语义实现完全不同。当前页全选全选只作用于本页 list翻页后重新全选新页实现最简单对应批量操作也按页执行。跨页选择用户在第一页选3条翻到第二页再选5条底部栏显示已选8条。这种场景下选中状态必须独立于当前页数据存在map 方案几乎是必选。跨页选择还有一个配套细节。如果接口按查询条件分页而“全选”想选的是“符合这个条件的所有记录”那就不应该在前端逐页选中而是用当前查询参数调一个后端的“全选标记”接口再把标记传给批量操作接口。前端只负责展示选中提示和计数不要试图把几千条数据全拉到本地勾选。4.2 删除已选条目后的数据修复删除记录之后selectedMap 里可能残留已经不存在的数据selectedCount 会虚高。批量删除后我习惯直接清空 map单条删除或刷新列表后做一个兜底过滤把不在最新列表里的选中 id 清掉methods: { normalizeSelection(newList) { const validIds new Set(newList.map(item String(item.id))) Object.keys(this.selectedMap).forEach(key { if (!validIds.has(key)) { this.$delete(this.selectedMap, key) } }) } }这里有个类型坑Object.keys 拿到的 key 一定是字符串所以构造 validIds 集合时也要统一 String(item.id)否则数字和字符串对不上过滤完发现一个都没删掉。这个函数在接口刷新、删除返回后都能调用属于必留的兜底工具。4.3 空列表、加载态、禁用项的按钮规则空列表时全选按钮应该置灰不可点已选数量显示0。加载中时底部操作栏可以先禁用避免用户对着空列表狂点。禁用项在购物车里很常见比如失效商品checkbox 要置灰且点击无响应。我的实现是行内加 disabled 标记点击事件里直接 return同时样式上降低整行透明度让用户一眼看出不可以勾。这几点看着琐碎但没处理的话测试一上来就会提“列表为空时全选按钮还能点亮”“失效商品还能勾选”的Bug。我在需求阶段就会把这些写进验收标准省得后面来回改。5. 原生checkbox-group与自定义复选视图的取舍实现复选框外观有两条路线我分别说下适用场景和坑。5.1 原生checkbox-group事件模型与受控同步的麻烦UniApp 自带 checkbox 和 checkbox-groupH5、小程序、App 端都有对应实现。最简用法checkbox-group changeonGroupChange label v-foritem in selectableList :keyitem.id classrow checkbox :valueString(item.id) :checked!!selectedMap[item.id] / text{{ item.name }}/text /label /checkbox-groupmethods: { onGroupChange(e) { const values e.detail.value Object.keys(this.selectedMap).forEach(key this.$delete(this.selectedMap, key)) values.forEach(v this.$set(this.selectedMap, v, true)) } }使用原生组件有几个麻烦点。change 事件返回的 detail.value 是字符串数组业务 id 往往是数字类型对齐工作躲不掉。更重要的是原生 checkbox 在部分平台上样式不完全可控小程序原生渲染和 H5 渲染出来的尺寸、圆角有差异想要各端视觉统一就得靠 CSS 硬调容易调出既不一致又难看的中间态。不过对于内部管理系统这类对视觉要求不高的场景原生方案胜在代码少能用。5.2 自定义复选视图样式自由与命中区域控制我自己的偏好是自定义 checkbox用一个 view 画方块选中时加 class 换背景、打勾整行监听 tap 切换状态view v-foritem in list :keyitem.id classrow :class{ disabled: item.disabled } taptoggleItem(item) view classcb :class{ checked: selectedMap[item.id] } text v-ifselectedMap[item.id] classmark✓/text /view text classname{{ item.name }}/text /view好处是视觉完全可控各个端看起来完全一样而且能控制整个点击命中区域用户不用费劲去点那个小方块。两个细节需要注意整行都能点击时就别再给 checkbox 单独绑 tap否则事件冒泡会触发两次状态切换真要单独响应就用 tap.stop 阻断冒泡自定义 checkbox 的点击区域建议做到 44rpx 以上太窄在手机端很难点准。原生和自定义怎么选我的建议是内部工具型页面用原生省事面向真实用户的购物车、结算、权限配置页面用自定义视图体验更可控。两者配合前面这套数据模型都很流畅差别只在外层视图。6. 把选中逻辑抽成公共模块并留意大数据量性能同一个项目里好几个页面都要用选全选、多选、反选我不会在每个页面把这段逻辑重写一遍。抽成公共模块是顺手的事性能方面也有一些注意点。6.1 抽成纯函数或mixin页面里只留事件绑定Vue2 的 UniApp 工程里我习惯把整套选中逻辑抽成 mixin页面里只保留 list 数据源和事件绑定// mixins/selectable.js export default { data() { return { selectedMap: {} } }, computed: { selectableList() { return this.list.filter(item !item.disabled) }, allChecked() { const list this.selectableList return list.length 0 list.every(item this.selectedMap[item.id]) }, partialChecked() { const list this.selectableList if (list.length 0) return false const count list.filter(item this.selectedMap[item.id]).length return count 0 count list.length }, selectedCount() { return Object.keys(this.selectedMap).length } }, methods: { toggleItem(item) { /* ... */ }, handleSelectAll() { /* ... */ }, handleInverse() { /* ... */ }, clearSelection() { Object.keys(this.selectedMap).forEach(key this.$delete(this.selectedMap, key)) }, getSelectedIds() { return Object.keys(this.selectedMap).map(Number) } } }页面里引入 mixin提供 list 数据把三个方法绑到视图元素上批量操作时调用 getSelectedIds 取 id 数组传给接口即可。如果某个页面需要树形父子联动这种特殊逻辑在页面里单独扩展公共部分不用重写。Vue3 的 UniApp 工程则可以把同样的逻辑封装成组合式函数返回响应式状态和方法思路完全一致。6.2 大数据量下的筛选与渲染优化选中集合用 map 之后全选、反选、计数都是 O(n) 级别的遍历几千条数据完全没问题真正的瓶颈在列表渲染本身。几个优化点供参考。computed 要少做重复二次遍历比如 selectableList 每次依赖变化都会重新 filter数据量到几千条且渲染频繁时可以在数据加载阶段就把可选和禁用分组模板只遍历可选组。selectedCount 用 computed 缓存不要在模板里写 Object.keys(selectedMap).length那会每次渲染都创建新数组。v-for 务必用业务 id 做 key 而不是索引列表删除或插入中间项时用索引做 key 会导致节点复用错误勾选状态串到其他行。再往上万条的列表就不是这套逻辑能扛的了应该上虚拟列表方案而不是在勾选逻辑里硬撑。最后分享一个我自己的习惯每写完一套复选功能我都会拿四组用例过一遍——全选后单选取消一项全选按钮要回到半选态反选后全选按钮重新计算而不是写死状态跨页选择后翻回第一页勾选状态和计数一致删除最后一页所有已选项后计数归零且选中集合没有残留。这四组用例正好覆盖了 empty 数组的 every 陷阱、跨页 map 清理、computed 派生状态这几个最容易翻车的点。自己先跑通再交付比测试提一屏Bug回来再改要省心得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑