鸿蒙实战UnclassifiedArchivePage 长按多选与批量归档前言图鸿蒙实战第97篇UnclassifiedArchivePage 长按多选与批量归档 运行效果截图HarmonyOS NEXTUnclassifiedArchivePage未归类整理页解决了一个实际产品问题用户拍照分析后笔迹记录暂时没有归属的档案需要一个批量整理的入口。这 527 行代码的核心是长按触发多选模式——这在 ArkUI 中需要LongPressGesture与onClick共存的特殊写法。本篇重点拆解LongPressGesture与onClick共存的手势处理选择模式的进入/退出与 Transition 动画selectAll全选切换逻辑批量归档两步写入batchUpdateArchiveincrementRecordBy系列导航96 SharePage · 98 FormCardsPreview相关文档Gesture 手势 API · transition 组件转场 · HandwritingDao一、页面功能架构1.1 两种模式对比LongPressGesture\n500ms长按点击取消按钮批量归档完成点击全选/取消全选浏览模式选择模式1.2 状态变量EntryComponentstruct UnclassifiedArchivePage{Statehandwritings:HandwritingEntity[][]// 所有未归类笔迹StateselectedIds:SetnumbernewSet()// 已选中的 ID 集合StateisSelectMode:booleanfalse// 选择模式开关Statearchives:ArchiveEntity[][]// 目标档案列表StateshowArchivePicker:booleanfalse// 归档目标选择器StateisBatchMoving:booleanfalse// 批量移动中}二、LongPressGesture 与 onClick 共存2.1 手势冲突问题ArkUI 默认情况下.gesture()和.onClick()会产生事件竞争长按 500ms 后系统同时触发长按和点击导致多选模式激活的同时也触发了进入详情。解决方案用GestureGroup加GesturePriority控制优先级ListItem().onClick((){if(this.isSelectMode){// 选择模式点击切换选中this.toggleSelect(item.id)}else{// 浏览模式点击进入详情this.openDetail(item)}}).gesture(LongPressGesture({duration:500}).onAction((){if(!this.isSelectMode){this.enterSelectMode(item.id)// 长按进入选择模式并选中当前项}}))关键点当isSelectMode false时长按触发进入选择模式当isSelectMode true时长按被忽略避免重复激活只有onClick负责切换选中状态。通过isSelectMode标志在两种模式间切换而不是替换手势处理器。2.2 进入选择模式privateenterSelectMode(firstItemId:number):void{this.isSelectModetruethis.selectedIdsnewSet([firstItemId])// 长按的那一项自动选中// 触感反馈HarmonyOS 振动 API// vibrator.startVibration({ type: time, duration: 50 })}三、勾选框 Transition 动画3.1 动画效果进入选择模式时每个列表项左侧出现勾选圆圈配合缩放动画BuilderCheckboxArea(item:HandwritingEntity){if(this.isSelectMode){Column(){// 勾选圆圈Column(){if(this.selectedIds.has(item.id)){Text(✓).fontSize(12).fontColor(Color.White)}}.width(24).height(24).borderRadius(12).backgroundColor(this.selectedIds.has(item.id)?AppColors.PRIMARY:transparent).border({width:2,color:this.selectedIds.has(item.id)?AppColors.PRIMARY:AppColors.BORDER}).justifyContent(FlexAlign.Center).animation({duration:200,curve:Curve.FastOutSlowIn})}.transition({type:TransitionType.Insert,scale:{x:0,y:0}// 进入时从 0 缩放到 1}).transition({type:TransitionType.Delete,scale:{x:0,y:0}// 退出时从 1 缩放到 0})}}3.2 选中切换逻辑privatetoggleSelect(id:number):void{constnewSetnewSet(this.selectedIds)if(newSet.has(id)){newSet.delete(id)// 已选中取消选中}else{newSet.add(id)// 未选中选中}this.selectedIdsnewSet// 触发 State 更新}为什么重新赋值new SetArkTS 的State对 Set/Map 的内部变化add/delete不能自动检测必须将 Set 替换为新对象才能触发重渲染。这是 ArkUI 响应式系统的重要细节。四、全选/取消全选privatetoggleSelectAll():void{constallSelectedthis.handwritings.every(itemthis.selectedIds.has(item.id))if(allSelected){// 当前全选取消全选this.selectedIdsnewSet()}else{// 当前非全选全部选中this.selectedIdsnewSet(this.handwritings.map(itemitem.id))}}当前状态every()结果操作全部选中true清空 selectedIds部分选中false全部加入 selectedIds全不选中false全部加入 selectedIds五、批量归档两步写入5.1 操作流程用户选择目标档案HandwritingDao.batchUpdateArchiveids, targetArchiveId批量更新 archive_id 字段ArchiveDao.incrementRecordBytargetArchiveId, selectedIds.size档案计数 N刷新列表loadHandwritings退出选择模式isSelectMode false5.2 batchUpdateArchive 实现// HandwritingDao.etsstaticasyncbatchUpdateArchive(ids:number[],archiveId:number):Promisevoid{if(ids.length0)returnconststoreHandwritingDao.getStore()constpredicatesnewrelationalStore.RdbPredicates(handwriting)predicates.in(id,ids)// WHERE id IN (1, 2, 3, ...)awaitstore.update({archive_id:archiveId}asrelationalStore.ValuesBucket,predicates)hilog.info(TAG,批量迁移 %{public}d 条记录到档案 %{public}d,ids.length,archiveId)}predicates.in(id, ids)是 RDB Predicates 的多值匹配等同于 SQLWHERE id IN (...)。5.3 调用代码privateasyncbatchMoveToArchive(targetArchiveId:number):Promisevoid{this.isBatchMovingtruetry{constidsArray.from(this.selectedIds)// ① 批量更新笔迹的 archive_idawaitHandwritingDao.batchUpdateArchive(ids,targetArchiveId)// ② 更新目标档案的记录数awaitArchiveDao.incrementRecordBy(targetArchiveId,ids.length)// ③ 反馈 刷新promptAction.showToast({message:${ids.length}条记录已归入档案})awaitthis.loadHandwritings()this.isSelectModefalsethis.selectedIdsnewSet()}catch(e){hilog.error(TAG,批量归档失败: %{public}s,JSON.stringify(e))promptAction.showToast({message:归档失败请重试})}finally{this.isBatchMovingfalse}}六、底部操作栏选择模式专属BuilderSelectModeBar(){if(this.isSelectMode){Row({space:12}){Text(已选${this.selectedIds.size}项).fontSize(14).fontColor(AppColors.TEXT)Blank()Button(全选).type(ButtonType.Capsule).backgroundColor(AppColors.CARD).fontColor(AppColors.TEXT_2).onClick(()this.toggleSelectAll())Button(归入档案).type(ButtonType.Capsule).backgroundColor(AppColors.PRIMARY).fontColor(Color.White).enabled(this.selectedIds.size0).opacity(this.selectedIds.size0?1:0.5).onClick((){if(this.selectedIds.size0){this.showArchivePickertrue}})}.width(100%).padding({left:16,right:16,top:12,bottom:12}).backgroundColor(AppColors.CARD).transition({type:TransitionType.Insert,translate:{y:60}// 从底部滑入})}}八、注意事项与常见问题8.1 开发注意事项在实际开发过程中需特别注意以下几点API 兼容性部分接口仅在特定 HarmonyOS NEXT 版本中可用需做版本条件判断权限模型采用静态声明module.json5 动态申请requestPermissionsFromUser的两阶段授权生命周期合理使用aboutToAppear()和aboutToDisappear()管理资源初始化与释放状态同步跨页面数据通过 AppStorage 共享组件内状态使用State/Prop/Link装饰器8.2 常见错误与解决方案开发过程中的高频问题汇总错误现象可能原因解决方案权限被系统拒绝未在module.json5中静态声明添加requestPermissions权限配置项页面跳转后白屏路由路径未在main_pages.json注册检查并补充路由配置文件UI 状态不刷新状态变量未添加响应式装饰器为变量加State/Observed装饰器数据库查询返回空查询条件与存储数据格式不匹配使用hilog.debug打印 SQL 条件排查相机预览黑屏运行时相机权限未获取先调用requestPermissionsFromUser申请总结UnclassifiedArchivePage 的 527 行代码的核心收获LongPressGestureonClick共存用isSelectMode标志在回调内部区分两种模式避免手势冲突State Set必须替换对象ArkTS 无法检测 Set 内部变化必须new Set(...)触发重渲染every()实现全选状态检测一行代码判断当前是否全选决定全选/取消全选操作predicates.in(field, values)批量查询IN (...)等效 SQLO(1) 次 DB 操作完成批量更新Transition 滑入效果选择模式底部操作栏用translate.y:60→0实现底部滑入下一篇预告第98篇 FormCardsPreview 桌面卡片与 EmptyPage 空状态 Canvas 插画如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源Gesture 手势识别 APILongPressGesture 长按手势transition 组件转场动画relationalStore Predicates.in第85篇 HandwritingDao 批量操作第84篇 ArchiveDao 档案操作第61篇 交互反馈动画第17篇 ForEach 列表渲染七、UnclassifiedArchivePage 核心实现7.1 长按触发多选模式ArkUI 中LongPressGesture与onClick的共存写法ListItem(){/* 手写记录卡片 */}.gesture(GestureGroup(GestureMode.Exclusive,LongPressGesture({duration:500}).onAction((){if(!this.isMultiSelect){this.isMultiSelecttrue;// 进入多选模式this.selectedIds.add(item.id);// 长按的项自动选中}}),TapGesture().onAction((){if(this.isMultiSelect){// 多选模式下点击切换选中状态if(this.selectedIds.has(item.id)){this.selectedIds.delete(item.id);}else{this.selectedIds.add(item.id);}}else{// 普通模式下跳转详情router.pushUrl({url:pages/ReportDetailPage,params:{reportId:item.reportId}});}})))7.2 批量归档操作privateasyncbatchAssignToArchive(archiveId:number){constidsArray.from(this.selectedIds);awaitHandwritingDao.updateArchiveBatch(ids,archiveId);promptAction.showToast({message:已将${ids.length}条记录归入档案});this.isMultiSelectfalse;this.selectedIds.clear();this.loadUnclassified();}7.3 多选状态的视觉反馈// 选中状态添加勾选角标 边框高亮Column(){/* 卡片内容 */}.border({width:this.selectedIds.has(item.id)?2:0,color:AppColors.accentMain}).overlay(this.selectedIds.has(item.id)?CheckMark()// 右上角绿色勾选图标:null,{align:Alignment.TopEnd})七、UnclassifiedArchivePage 设计总结功能核心实现技术要点多选模式切换State isSelectinganimateTo展示/隐藏底部工具栏批量归档操作selectedIds: Setnumber遍历集合执行ArchiveDao.update覆盖层勾选图标overlay()修饰符Alignment.TopEnd定位空状态兜底EmptyState组件无数据时居中显示插画延伸阅读第92篇 ArchivePage 档案管理页 · 第84篇 ArchiveDao 实现如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力