鸿蒙实战:UnclassifiedArchivePage 长按多选与批量归档
鸿蒙实战:UnclassifiedArchivePage 长按多选与批量归档
前言

图:鸿蒙实战第97篇:UnclassifiedArchivePage 长按多选与批量归档 运行效果截图(HarmonyOS NEXT)
UnclassifiedArchivePage(未归类整理页)解决了一个实际产品问题:用户拍照分析后,笔迹记录暂时没有归属的档案,需要一个批量整理的入口。这 527 行代码的核心是长按触发多选模式——这在 ArkUI 中需要 LongPressGesture 与 onClick 共存的特殊写法。
本篇重点拆解:
LongPressGesture与onClick共存的手势处理- 选择模式的进入/退出与 Transition 动画
selectAll全选切换逻辑- 批量归档两步写入(
batchUpdateArchive+incrementRecordBy)
系列导航:96 SharePage · 98 FormCardsPreview
相关文档:Gesture 手势 API · transition 组件转场 · HandwritingDao
一、页面功能架构
1.1 两种模式对比
1.2 状态变量
@Entry
@Component
struct UnclassifiedArchivePage {
@State handwritings: HandwritingEntity[] = [] // 所有未归类笔迹
@State selectedIds: Set<number> = new Set() // 已选中的 ID 集合
@State isSelectMode: boolean = false // 选择模式开关
@State archives: ArchiveEntity[] = [] // 目标档案列表
@State showArchivePicker: boolean = false // 归档目标选择器
@State isBatchMoving: boolean = false // 批量移动中
}
二、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 进入选择模式
private enterSelectMode(firstItemId: number): void {
this.isSelectMode = true
this.selectedIds = new Set([firstItemId]) // 长按的那一项自动选中
// 触感反馈(HarmonyOS 振动 API)
// vibrator.startVibration({ type: 'time', duration: 50 })
}
三、勾选框 Transition 动画
3.1 动画效果
进入选择模式时,每个列表项左侧出现勾选圆圈,配合缩放动画:
@Builder
CheckboxArea(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 选中切换逻辑
private toggleSelect(id: number): void {
const newSet = new Set(this.selectedIds)
if (newSet.has(id)) {
newSet.delete(id) // 已选中:取消选中
} else {
newSet.add(id) // 未选中:选中
}
this.selectedIds = newSet // 触发 @State 更新
}
为什么重新赋值
new Set? ArkTS 的@State对 Set/Map 的内部变化(add/delete)不能自动检测,必须将 Set 替换为新对象才能触发重渲染。这是 ArkUI 响应式系统的重要细节。
四、全选/取消全选
private toggleSelectAll(): void {
const allSelected = this.handwritings.every(
item => this.selectedIds.has(item.id)
)
if (allSelected) {
// 当前全选:取消全选
this.selectedIds = new Set()
} else {
// 当前非全选:全部选中
this.selectedIds = new Set(this.handwritings.map(item => item.id))
}
}
| 当前状态 | every() 结果 |
操作 |
|---|---|---|
| 全部选中 | true |
清空 selectedIds |
| 部分选中 | false |
全部加入 selectedIds |
| 全不选中 | false |
全部加入 selectedIds |
五、批量归档两步写入
5.1 操作流程
5.2 batchUpdateArchive 实现
// HandwritingDao.ets
static async batchUpdateArchive(
ids: number[],
archiveId: number
): Promise<void> {
if (ids.length === 0) return
const store = HandwritingDao.getStore()
const predicates = new relationalStore.RdbPredicates('handwriting')
predicates.in('id', ids) // WHERE id IN (1, 2, 3, ...)
await store.update(
{ archive_id: archiveId } as relationalStore.ValuesBucket,
predicates
)
hilog.info(TAG, '批量迁移 %{public}d 条记录到档案 %{public}d', ids.length, archiveId)
}
predicates.in('id', ids) 是 RDB Predicates 的多值匹配,等同于 SQL WHERE id IN (...) 。
5.3 调用代码
private async batchMoveToArchive(targetArchiveId: number): Promise<void> {
this.isBatchMoving = true
try {
const ids = Array.from(this.selectedIds)
// ① 批量更新笔迹的 archive_id
await HandwritingDao.batchUpdateArchive(ids, targetArchiveId)
// ② 更新目标档案的记录数
await ArchiveDao.incrementRecordBy(targetArchiveId, ids.length)
// ③ 反馈 + 刷新
promptAction.showToast({
message: `${ids.length} 条记录已归入档案`
})
await this.loadHandwritings()
this.isSelectMode = false
this.selectedIds = new Set()
} catch (e) {
hilog.error(TAG, '批量归档失败: %{public}s', JSON.stringify(e))
promptAction.showToast({ message: '归档失败,请重试' })
} finally {
this.isBatchMoving = false
}
}
六、底部操作栏(选择模式专属)
@Builder
SelectModeBar() {
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.size > 0)
.opacity(this.selectedIds.size > 0 ? 1 : 0.5)
.onClick(() => {
if (this.selectedIds.size > 0) {
this.showArchivePicker = true
}
})
}
.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 行代码的核心收获:
LongPressGesture+onClick共存:用isSelectMode标志在回调内部区分两种模式,避免手势冲突@State Set必须替换对象:ArkTS 无法检测 Set 内部变化,必须new Set(...)触发重渲染every()实现全选状态检测:一行代码判断当前是否全选,决定"全选/取消全选"操作predicates.in(field, values)批量查询:IN (...)等效 SQL,O(1) 次 DB 操作完成批量更新- Transition 滑入效果:选择模式底部操作栏用
translate.y:60→0实现底部滑入
下一篇预告:第98篇 FormCardsPreview 桌面卡片与 EmptyPage 空状态 Canvas 插画
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- Gesture 手势识别 API
- LongPressGesture 长按手势
- 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.isMultiSelect = true; // 进入多选模式
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 批量归档操作
private async batchAssignToArchive(archiveId: number) {
const ids = Array.from(this.selectedIds);
await HandwritingDao.updateArchiveBatch(ids, archiveId);
promptAction.showToast({ message: `已将 ${ids.length} 条记录归入档案` });
this.isMultiSelect = false;
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 isSelecting |
animateTo 展示/隐藏底部工具栏 |
| 批量归档操作 | selectedIds: Set<number> |
遍历集合执行 ArchiveDao.update |
| 覆盖层勾选图标 | overlay() 修饰符 |
Alignment.TopEnd 定位 |
| 空状态兜底 | EmptyState 组件 |
无数据时居中显示插画 |
延伸阅读:第92篇 ArchivePage 档案管理页 · 第84篇 ArchiveDao 实现
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐




所有评论(0)