Demo:CheckSafe|页面:CheckPlanPage|任务:CS-3109|规则修订:rev=4。本文所有界面、HiLog 和日志均为构造的演示数据,不代表真实 DevEco Studio 编译、真机运行或现场验收。
一、一键勾选后,锁定点位为什么也进了清单
一个巡检班组的操作其实很简单:打开待检点位列表,先全选今天能去的机柜,再取消临时不去的点位,最后生成执行清单。麻烦出在“能去”不是固定属性。后台排程会把某些点位锁住,例如消防阀正在检修,现场人员仍能看到它,却不该把它放进本轮执行清单。若界面只根据复选框是否显示来判断可选范围,一次全选就可能把锁定项也带进结果。
我把这个问题缩小成 CheckSafe 的一张页面。任务CS-3109含五个点位:P-01 机房门禁、P-02 供电柜、P-03 消防阀、P-04 空调主机、P-05 应急灯。修订号rev=4下,只有P-03被锁定,其余四项可选。演示的最终选择是P-01、P-02、P-04,因此屏幕应明确显示“可选4、锁定1、已选3”,并生成SNAPSHOT_READY。这些数字不是测量值,而是文章、代码和配图共同使用的一组确定性输入。
这里有一个容易被忽略的差别:界面勾选状态是编辑中的意图,提交快照才是经过规则过滤的结果。锁定项不应该通过“看起来灰了”来保证安全,也不该依靠按钮点击前的一次清空来掩盖状态污染。真正的边界要放在数据层,让批量选择、单项切换和最终提交都执行同一套资格判断。
二、先固定数据身份,再谈全选交互
复选框是 ArkUI 组件,但点位是否可用是业务属性。华为的 Checkbox 参考文档明确给出Checkbox({ name })、.select(boolean)和.onChange((value: boolean) => …)的用法,也可以使用通用的.enabled限制交互。它没有替应用提供“后台锁定仍不可提交”的业务保证;这部分必须由我们实现。本文仅使用上述稳定组件能力,避免把“批量筛选”包装成不存在的系统接口。
先写一层最小的数据模型,解决“全选究竟选哪些 ID”的问题。全选返回新数组,不直接改写点位数组;ID 是稳定身份,而不是列表下标。这样后续即使排序或刷新,勾选也不会莫名串到另一行。
interfaceCheckPoint{id:string;title:string;locked:boolean;}constPOINTS:CheckPoint[]=[{id:'P-01',title:'机房门禁',locked:false},{id:'P-02',title:'供电柜',locked:false},{id:'P-03',title:'消防阀',locked:true},{id:'P-04',title:'空调主机',locked:false},{id:'P-05',title:'应急灯',locked:false}];functionselectEligible(points:CheckPoint[]):string[]{returnpoints.filter((p:CheckPoint)=>!p.locked).map((p:CheckPoint)=>p.id);}selectEligible在这组数据上会返回四个 ID,其中绝不包含P-03。用户随后取消P-05,页面中才剩下三个选择。方法是纯计算,没有文件句柄、监听器或网络连接可释放;生命周期取决于调用它的页面。真实后台更新时必须传入当时最新的规则列表,不能把首次加载的POINTS当成永远有效。后台列表为空时结果就是空数组,不能自行恢复旧勾选。
三、灰态只是视觉反馈,提交必须重新过滤
我最在意的不是“点下去会不会变色”,而是提交瞬间是否仍可能把不可用 ID 混进结果。比如页面刚打开时P-03可选,用户勾了它;随后规则修订变更,点位锁定。仅将 Checkbox 置灰,不会自动消除之前保存在数组里的 ID。更糟的是,批量全选和逐项取消可能来自不同入口,状态残留肉眼很难发现。
因此提交操作不直接复制selectedIds,而是先获取当前可用 ID 集合,再按页面可见顺序重新筛一遍。这里还要比对规则修订号:提交时若不是期望的rev=4,宁可返回拒绝,也不拿旧规则假装成功。下面代码负责生成前端演示快照,不包含真实服务端写入或权限确认。
interfaceCommitSnapshot{taskId:string;ruleRev:number;ids:string[];status:string;}functionmakeSnapshot(points:CheckPoint[],chosen:string[],actualRev:number,expectedRev:number):CommitSnapshot|null{if(actualRev!==expectedRev)returnnull;constallowed:Set<string>=newSet<string>(points.filter((p:CheckPoint)=>!p.locked).map((p:CheckPoint)=>p.id));constids:string[]=points.filter((p:CheckPoint)=>allowed.has(p.id)&&chosen.includes(p.id)).map((p:CheckPoint)=>p.id);if(ids.length===0)returnnull;return{taskId:'CS-3109',ruleRev:actualRev,ids:ids.slice(),status:'SNAPSHOT_READY'};}这个函数解决的是提交一致性,不是将状态写入磁盘。ids.slice()是为了避免外部继续修改同一数组,Set只是本次运算的临时索引。条件失败返回null,调用方需要显示“规则已变化,请重新核验”或“没有可提交点位”,不能把两种异常都解释成服务器异常。实际产品还应在服务端再次校验点位权限、锁定版本及操作者身份,前端快照并不构成越权访问的凭据。
四、把编辑态留给页面,把不可选项留给规则
进入 ArkUI 页面后,我们只维护selectedIds和提交状态,不在组件里复制另一套lockedMap。将一套资格来源拆成两个可修改副本,正是热更新时产生不一致的根源。Checkbox的name使用点位 ID,.select由数组计算,锁定项通过.enabled(false)禁止人工点击;即使 UI 层回调异常触发,togglePoint也再次检查锁定条件。
下面保留页面最关键的事件路径,其他配色和间距可以按产品规范扩展。演示启动时已经走过“全选可用项,再取消 P-05”的流程,因此最终三项选中;点击生成快照只更新页面状态,不声称完成网络提交。
@Entry@Componentstruct CheckPlanPage{privatepoints:CheckPoint[]=POINTS;@StateselectedIds:string[]=['P-01','P-02','P-04'];@StatecommitStatus:string='SNAPSHOT_READY';privatetogglePoint(id:string,checked:boolean):void{constpoint:CheckPoint|undefined=this.points.find((p:CheckPoint)=>p.id===id);if(!point||point.locked)return;this.selectedIds=checked?Array.from(newSet<string>([...this.selectedIds,id])):this.selectedIds.filter((item:string)=>item!==id);}build(){Column({space:12}){Text('CS-3109 · rev=4 · 可选4 / 锁定1 / 已选3')ForEach(this.points,(p:CheckPoint)=>{Row({space:12}){Checkbox({name:p.id}).select(this.selectedIds.includes(p.id)).enabled(!p.locked).onChange((checked:boolean)=>this.togglePoint(p.id,checked))Text(`${p.id}${p.title}${p.locked?' · 锁定':''}`)}},(p:CheckPoint)=>p.id)Button('生成快照').onClick(()=>{constsnapshot=makeSnapshot(this.points,this.selectedIds,4,4);this.commitStatus=snapshot?snapshot.status:'REJECTED';})Text(`状态:${this.commitStatus}`)}}}代码里标题行的“已选3”只对应这次固定演示。若将它作为真正可交互工程页面,应换成this.selectedIds.length的动态显示,并在后台规则变化后重新计算;否则用户继续点击时数字会与实际勾选不一致。演示图采用冻结的最终状态,目的是让读者直接核对快照边界,而不是展示一个完整生产级选择流程。@State数组采用整体赋值,是为了让框架看到状态变化;如果原地修改普通数组却没有触发刷新,UI 就可能停留在旧值。
五、校验时别只看“都打了勾”
我建议验收时先做三个反向动作:第一,按全选路径运行,确认P-03永远不进入结果;第二,把已选的P-02设成锁定后再提交,确保快照会过滤它,而不是依赖界面是否及时刷新;第三,将规则修订从4改到5,确认提交返回拒绝。它们测试的分别是批量选择边界、提交边界和版本边界。三者通过后,再去调整样式才有意义。
本轮固定演示中,五个点位里可选四个、锁定一个、选中三个,快照中的 ID 顺序是P-01,P-02,P-04,状态SNAPSHOT_READY。日志示例统一写成CS-3109 rev=4 selectable=4 locked=1 selected=3 status=SNAPSHOT_READY。这是为了方便文章与两张运行示意图逐项核对,不是从 HiLog 采集的真机证据。
六、页面消失以后,哪些状态该保留
这份 Demo 没有注册定时器、长期网络请求或蓝牙监听,因此离开页面时没有额外句柄需要释放。但“没有句柄”不代表“没有生命周期问题”:selectedIds是页面编辑草稿,页面重新创建后可能恢复为初值。真实应用应明确选中态是一次性操作还是需要跨页保存,不能默认把快照当成已经持久化的任务。若从服务端重新打开,首先加载规则版本和点位列表,再根据稳定 ID 恢复尚有效的草稿。
对于锁定项,显示为灰态是有价值的:它告诉巡检人员这个点位存在,但当前不能安排。把它从列表直接删除反而可能导致误以为任务丢失。另一方面,锁定原因、权限和服务端变更应显示可解释文案,不能把所有禁用都写成“网络异常”。页面级enabled是交互限制,业务级makeSnapshot是数据限制,两层必须同时存在。
还有一个容易被团队忽略的验收细节:服务端可能同时把新点位插入列表。旧客户端如果只保存“第几行被勾选”,新列表一到就会产生位置漂移。因此恢复草稿时应按 ID 做交集,遇到已经删除的点位直接标为待确认;如果任务已经正式提交,则展示提交时的规则版本与快照内容,而不是拿今天的最新列表去覆盖历史记录。这样的记录才能支持事后追溯,也能让产品明确区分“当时允许执行”和“现在仍可执行”。
这次复盘最终落在一个非常具体的结论:批量选中只处理用户意图,提交时必须按最新规则重新建立有效集。当数据变动、页面重建或点击回调顺序不可控时,只有把稳定 ID、规则修订和不可变快照分开,才能避免“明明灰掉了,却仍被带进工单”的隐蔽问题。本文示例没有覆盖真实协同冲突、离线同步及后台竞态,那些必须交给端到端测试继续验证。
参考资料
- 华为官方 Checkbox API:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/api/ts-basic-components-checkbox
- 华为官方 ArkTS 简介:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-overview