HarmonyOS时光胶囊——详情页的胶囊查看与状态管理
胶囊详情页是用户和未来的自己对话的终点——从时间线列表点击一张卡片跳转到这里看到封印时写下的内容。这个页面的特殊之处在于它不是静态的胶囊可能还在等待中可能刚好到期也可能已经被开启过。三种状态决定三种完全不同的 UI 和交互——等待中显示倒计时刚到期引导开启已开启显示完整内容。完整效果一、页面的路由入口// 首页 handleCardClickprivatehandleCardClick(capsule:Capsule):void{setCurrentCapsuleId(capsule.id)router.pushUrl({url:pages/CapsuleDetail,params:{capsuleId:capsule.id}})}两个操作的分工操作作用setCurrentCapsuleId(capsule.id)在全局记录当前胶囊 IDrouter.pushUrl 传 capsuleId通过路由参数告诉详情页要显示哪个胶囊为什么要 setCurrentCapsuleId路由参数只能传基本类型string/number/boolean。如果详情页还需要其他上下文比如胶囊的完整数据用全局变量比传多个路由参数更方便。CapsuleDetail 可以用getCurrentCapsuleId()从全局获取 ID再从数据库查完整数据。路由参数的设计params:{capsuleId:capsule.id}asRecordstring,Object只传了 capsuleId——一个 string 类型的 ID。详情页根据这个 ID 从数据库查询完整数据。传完整胶囊对象的问题是如果数据库里的数据被修改了比如用户在其他地方修改了标签详情页显示的还是旧数据。传 ID 实时查询保证数据最新。二、CapsuleDetail 需要处理的三种状态状态判断逻辑privategetStatus():string{if(this.capsule.isOpened)returnopenedif(this.capsule.unlockAtDate.now())returnreadyreturnsealed}三种状态对应的 UI状态条件页面标题主要内容按钮SEALED未开启且未到期胶囊封印中倒计时 胶囊外观无READY未开启但已到期胶囊已到期开启引导 开启胶囊OPENED已开启胶囊内容完整内容 心情 标签返回三、封印中状态的 UI// 推断的封印中状态 UIColumn(){Text().fontSize(80).margin({bottom:Spacing.lg})Text(胶囊封印中).fontSize(FontSize.xl).fontColor(this.getColors().text).fontWeight(FontWeight.Bold).margin({bottom:Spacing.sm})Text(距离开启还有).fontSize(FontSize.sm).fontColor(this.getColors().textSecondary).margin({bottom:Spacing.sm})// 倒计时显示Text(this.getTimeLeft()).fontSize(FontSize.hero).fontColor(this.getColors().primary).fontWeight(FontWeight.Bold)// 内容预览模糊遮罩Stack(){Text(this.capsule.content).fontSize(FontSize.md).fontColor(this.getColors().textSecondary).lineHeight(24)// 高斯模糊遮罩}}倒计时的显示privategetTimeLeft():string{constdiffthis.capsule.unlockAt-Date.now()if(diff0)return已到期constdaysMath.floor(diff/86400000)consthoursMath.floor((diff%86400000)/3600000)constminutesMath.floor((diff%3600000)/60000)if(days0)return${days}天${hours}小时if(hours0)return${hours}小时${minutes}分钟return${minutes}分钟}详情页的倒计时比 CapsuleCard 更精确——显示到分钟级别。因为用户专门点进来看详情说明他们在乎这个胶囊需要更精确的等待信息。时间线卡片只显示天小时——因为那是概览不需要太精确。内容预览的模糊处理封印中的胶囊内容不应该完全显示——否则封印就失去意义了。用高斯模糊遮罩让用户能看到这里有内容但看不清具体内容。这增加了神秘感和期待感。四、可开启状态的 UIColumn(){Text(✨).fontSize(80).margin({bottom:Spacing.lg})Text(胶囊已到期).fontSize(FontSize.xl).fontColor(this.getColors().text).fontWeight(FontWeight.Bold).margin({bottom:Spacing.sm})Text(你的时间胶囊已经准备好了).fontSize(FontSize.sm).fontColor(this.getColors().textSecondary).margin({bottom:Spacing.lg})// 开启按钮Row(){Text( 开启胶囊).fontSize(FontSize.lg).fontColor(#FFFFFF).fontWeight(FontWeight.Bold)}.width(100%).height(52).justifyContent(FlexAlign.Center).backgroundColor(this.getColors().primary).borderRadius(BorderRadius.lg).onClick(()this.openCapsule())}开启胶囊的逻辑privateasyncopenCapsule():Promisevoid{this.capsule.isOpenedtrueawaitthis.database.updateCapsule(this.capsule)// 重新加载数据this.capsulethis.database.getCapsuleById(this.capsuleId)}开启操作只改一个字段——isOpened true。不删除胶囊不改变内容只是标记为已读。这保证了胶囊数据的完整性——用户可以在统计 Tab 里看到所有历史胶囊包括已开启的。开启后的 UI 变化开启前大号 ✨ 胶囊已到期 开启按钮 开启后完整内容 心情 标签 创建时间 返回按钮一次性展示——没有渐变动画。因为胶囊的核心体验是内容本身而不是开启动画。简洁直接比花哨动画更符合写给未来的自己这个产品调性。五、已开启状态的 UIColumn(){// 顶部状态栏Row(){Text(this.capsule.mood).fontSize(24)Blank()Text(this.formatDate(this.capsule.createdAt)).fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)}.width(100%).margin({bottom:Spacing.md})// 胶囊内容Text(this.capsule.content).fontSize(FontSize.md).fontColor(this.getColors().text).lineHeight(28).width(100%).margin({bottom:Spacing.lg})// 标签if(this.capsule.tags.length0){Flex({wrap:FlexWrap.Wrap}){ForEach(this.capsule.tags,(tag:string){Text(#tag).fontSize(FontSize.sm).fontColor(this.getColors().primary).padding({left:8,right:8,top:4,bottom:4}).backgroundColor(this.getColors().primary15).borderRadius(BorderRadius.full).margin({right:Spacing.xs,bottom:Spacing.xs})})}}}信息展示的优先级优先级内容字号颜色1胶囊内容mdtext主色2心情24pxemoji 原色3创建时间xstextSecondary4标签smprimary内容最重要——字号最大位于页面中央。心情和时间是辅助信息——放在顶部作为上下文。标签是分类信息——放在底部作为补充。时间格式化privateformatDate(timestamp:number):string{constdatenewDate(timestamp)constyeardate.getFullYear()constmonthdate.getMonth()1constdaydate.getDate()consthourdate.getHours().toString().padStart(2,0)constminutedate.getMinutes().toString().padStart(2,0)return${year}年${month}月${day}日${hour}:${minute}}显示2024年7月16日 17:58格式——比时间戳更可读。用户不需要知道 Unix 时间戳他们需要知道这是哪天写的。六、数据库的查询方法从首页调用推断的数据库方法// 首页this.database.getAllCapsules()// 获取所有胶囊// 详情页需要this.database.getCapsuleById(id)// 根据 ID 获取单个胶囊this.database.updateCapsule(capsule)// 更新胶囊标记已开启getCapsuleById 的实现推断// 方案 A遍历查找getCapsuleById(id:string):Capsule|null{for(leti0;ithis.capsules.length;i){if(this.capsules[i].idid)returnthis.capsules[i]}returnnull}// 方案 BMap 查找getCapsuleById(id:string):Capsule|null{returnthis.capsuleMap.get(id)||null}方案 A 更可能——因为首页已经把所有胶囊加载到内存了。不需要额外的 Map 数据结构遍历数组在数据量小的情况下足够快。CapsuleDetail 的调用频率很低用户不会频繁切换详情页性能不是瓶颈。updateCapsule 的实现推断asyncupdateCapsule(capsule:Capsule):Promisevoid{// 找到对应位置替换数据for(leti0;ithis.capsules.length;i){if(this.capsules[i].idcapsule.id){this.capsules[i]capsulebreak}}// 持久化到文件awaitthis.saveToFile()}内存更新 文件持久化两步走。先改内存中的数组再写入文件。这保证了数据一致性——内存和文件里的数据始终同步。七、setCurrentCapsuleId 的作用// 首页setCurrentCapsuleId(capsule.id)router.pushUrl({url:pages/CapsuleDetail,params:{capsuleId:capsule.id}})全局 ID 的使用场景场景用途CapsuleDetail 读取获取当前胶囊 ID查询数据库MemoryShuffle 读取记住当前洗牌展示的胶囊返回后恢复用户从详情页返回时间线能定位到刚才的位置全局 ID 是一个轻量级的状态共享机制。比传多个路由参数更简洁。在单用户 App 里全局变量不会有并发冲突——任何时候只有一个当前胶囊。八、详情页的嵌套结构Column 页面容器 ├→ Row (顶部栏) │ ├→ Text(mood) 心情 emoji │ └→ Text(createdAt) 创建时间 ├→ Text(content) 胶囊内容 └→ Flex (标签区) └→ ForEach(tags) 标签列表嵌套深度 3 层——比首页更浅。因为详情页不需要 Stack 层叠没有底部导航栏也不需要 Scroll内容通常不会太长。和首页的嵌套对比页面嵌套深度是否有 Scroll是否有 StackIndex3 层是是底部导航栏CapsuleDetail3 层可能否详情页可能不需要 Scroll——胶囊内容通常不会超过一屏。但如果用户写了很长的内容Scroll 还是需要的。这取决于 CapsuleDetail 的实际实现。九、页面生命周期aboutToAppear():void{// 从路由参数获取胶囊 ID// 从数据库查询胶囊数据// 判断状态并渲染对应 UI}onPageShow():void{// 刷新胶囊状态// 可能从可开启变为已开启}aboutToAppear 做初始化onPageShow 做刷新。和首页的生命周期模式一致。onPageShow 在页面每次显示时触发——用户从其他页面返回时详情页会重新检查胶囊状态。状态可能在页面间变化用户打开 CapsuleDetail状态ready → 按返回键回到首页 → 时间过去胶囊状态没变化 → 用户再次点击同一胶囊 → CapsuleDetail 重新渲染状态ready如果用户不点击开启胶囊状态不会变化。但如果用户在另一个设备上开启了同一胶囊云同步场景onPageShow 会刷新为 opened 状态。这是 onPageShow 存在的意义——处理跨页面的状态变化。十、详情页的三种状态与用户心理状态用户心理设计策略SEALED期待、好奇倒计时增加仪式感模糊遮罩保持神秘READY兴奋、迫不及待大按钮引导开启减少干扰OPENED回味、感慨完整内容展示时间信息唤起记忆三种状态对应三种情绪——UI 设计要配合情绪。封印中是等待的仪式可开启是拆礼物的兴奋已开启是翻看老照片的温暖。核心设计原则封印中尽量少展示信息保持神秘已开启尽量多展示信息满足好奇可开启用最醒目的按钮引导操作减少决策成本。