第91篇 | HarmonyOS 空态与加载态相册、视频、保险箱都不能空白一个成熟页面不能只设计“有数据”的样子。相册第一次打开、系统相册导入前、视频还没生成、保险箱还没有私密记录这些状态如果只是白屏用户会以为应用坏了。双镜记忆相机把加载态、空态和行动按钮都写进页面帮助用户知道下一步该做什么。这一篇从galleryLoading开始串起相册、视频管理和保险箱三个页面。重点不在“写一句没有数据”而在空态是否有下一步入口加载态是否防止重复操作失败态是否回到可理解文案。本篇目标区分 loading、empty、error 三类状态避免都用空白页代替。看懂loadGalleryRecords如何保证加载开始和结束都能更新状态。检查相册、视频、保险箱三处空态是否有行动入口。把空态作为发布前必测项而不是最后补文案。对应源码位置superImage/entry/src/main/ets/pages/Index.etssuperImage/entry/src/main/ets/services/GalleryRecordService.ets加载态必须能结束loadGalleryRecords里先判断是否正在加载避免重复进入随后把galleryLoading置为 true读取完成或失败后都进入 finally 分支改回 false。这个结构比单纯在成功分支关 loading 更稳。如果 finally 漏掉用户遇到解析失败或存储异常时页面可能一直显示“正在整理照片”。这类问题在真机上很难靠肉眼复现所以需要从源码结构检查。加载态和空态一起设计页面才不会在无数据时变成白屏private async loadGalleryRecords(): Promisevoid { if (this.galleryLoading) { this.galleryLoadQueued true; return; } this.galleryLoading true; try { do { this.galleryLoadQueued false; const records await GalleryRecordService.loadRecords(this.getAbilityContext()); await this.applyGalleryRecords(records); } while (this.galleryLoadQueued); } catch (error) { const err error as BusinessError; this.galleryNoticeText 读取相册失败 ${err.code ?? -1}; } finally { this.galleryLoading false; }相册空态要给出拍摄入口相册页如果没有记录页面不应该结束在“暂无照片”。项目里的空态会说明拍照完成后照片会进入这里并提供“去相机拍摄”的入口。这样用户第一次安装后也能顺着路径走下去。空态文案要和产品能力一致这里不是系统相册浏览器而是双镜记忆记录所以空态要引导用户回到相机页生成第一条记忆。相册页在 loading、分组列表、普通列表和空态之间明确分支void this.importSystemAlbumPhotos(gallery); }) } Scroll() { Column({ space: 14 }) { if (this.galleryLoading) { Text(正在加载...) .fontSize(13) .lineHeight(20) .fontColor($r(app.color.album_on_surface)) } if (this.getFeaturedGalleryRecord()) { this.buildGalleryMovieEntryCard() if (this.getGalleryGroups().length 0) { Column({ space: 10 }) { Row() { Text(按时间地点) .fontSize(13) .fontColor($r(app.color.album_accent)) Blank() Text(${this.getGalleryGroups().length}组) .fontSize(11) .fontColor($r(app.color.album_on_surface_variant)) } .width(100%) ForEach(this.getGalleryGroups(), (group: GalleryDatePlaceGroup) { this.buildGalleryAlbumGroupSection(group) }, (group: GalleryDatePlaceGroup) group.key) } .width(100%) } else if (this.getGalleryListRecords().length 0) { Column({ space: 10 }) { Text(全部照片) .fontSize(13) .fontColor($r(app.color.album_accent)) ForEach(this.getGalleryListRecords(), (record: GalleryMoment) { this.buildGalleryRecordCard(record) }, (record: GalleryMoment) record.id) } .width(100%) } } else {视频页空态要回到选照片视频管理页的空态不能引导拍照就结束因为用户可能已经有照片只是还没有生成视频。这里更合适的下一步是“去选照片”让用户从已有记录进入成片流程。这就是空态设计的细节同样是没有数据相册页和视频页的下一步不同不能复用一句统一文案。视频页空态把下一步指向选照片而不是简单提示为空private buildGalleryVideoManagerPage() { Column({ space: 16 }) { Column({ space: 6 }) { Text(短片) .fontSize(30) .fontWeight(FontWeight.Bold) .fontColor($r(app.color.album_on_surface)) .textAlign(TextAlign.Center) Text(this.getVideoManagerRecordsForRender().length 0 ? ${this.getVideoManagerRecordsForRender().length}\u6761 : ) .fontSize(13) .lineHeight(20) .fontColor($r(app.color.album_on_surface_variant)) } .width(100%) .alignItems(HorizontalAlign.Center) this.buildGalleryMediaSwitch() this.buildGalleryCloudSyncCard() Scroll() { Column({ space: 14 }) { this.buildGalleryMovieEntryCard() if (this.getVideoManagerRecordsForRender().length 0) { Column({ space: 12 }) { Text(\u8fd8\u6ca1\u6709\u89c6\u9891) .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor($r(app.color.album_on_surface)) Text(\u5148\u53bb\u7167\u7247\u91cc\u9009\u56fe) .fontSize(13) .lineHeight(20) .fontColor($r(app.color.album_on_surface_variant)) Button(\u53bb\u9009\u62e9) .height(42) .width(100%) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor($r(app.color.album_on_primary)) .backgroundColor($r(app.color.album_primary_container)) .borderRadius(18) .onClick(() { this.switchGalleryMediaTab(photo); }) } .width(100%) .padding(18) .backgroundColor($r(app.color.album_panel)) .borderRadius(24) .alignItems(HorizontalAlign.Start) } else { ForEach(this.getVideoManagerRecordsForRender(), (record: GalleryVideoRecord) { this.buildVideoManagerRecordCardV2(record) }, (record: GalleryVideoRecord) record.id) } } .width(100%) } .layoutWeight(1) .scrollBar(BarState.Off) this.buildBottomNavigation()保险箱空态要尊重解锁状态保险箱页还多一层隐私状态没有私密记录、未解锁、有记录且已解锁这三种状态不能混成一种。项目会先显示云同步卡片再根据vaultUnlocked和记录数量决定导入、解锁或展示记录。验收时不要只看默认未解锁状态。至少要测没有私密记录、导入一张私密照片后未解锁、解锁后查看详情、重新上锁后状态恢复。保险箱空态必须同时考虑隐私状态和记录数量空态的标准不是“没有报错”而是用户能理解当前为什么没有内容并能找到下一步动作。工程验收表检查项通过标准加载态读取成功、失败、空数据都会关闭 loading。相册空态首次进入能看到去相机拍摄的明确入口。视频空态没有成片时能进入选照片流程。保险箱空态未解锁、无私密记录、有私密记录三种状态不混淆。真机复测口令先清空相册记录再关闭网络随后分别进入相册页、视频管理页和保险箱页。预期结果是页面有明确空态或失败文案按钮仍然给出下一步入口loading 不会一直停留在屏幕上。再做一次反向测试导入一条记录后立即离开页面再返回观察galleryLoading是否能正确结束列表是否重新刷新。空态文章最怕只写“没有数据”真正要验的是“没有数据时用户还能做什么”。今日练习手动制造一次相册读取失败确认 finally 分支会关闭 loading。清空视频任务列表检查视频管理页是否有下一步提示。在保险箱未解锁时进入页面确认私密内容不会提前展示。