HarmonyOS时光胶囊——数据模型的设计与胶囊生命周期
2026/7/16 17:53:43 网站建设 项目流程

时光胶囊的核心是一个有"生命周期"的数据对象——胶囊被封印后进入等待状态,时间到了才能开启。这个生命周期决定了数据模型的设计:它需要存储内容、心情、解锁时间、标签,还需要一个状态标记来区分"等待中"“可开启”"已开启"三种状态。

完整效果

一、从首页代码推断的数据模型

首页 Index.ets 的 import 和使用方式揭示了 Capsule 接口的完整字段:

// 从 getFiltered() 推断capsule.isOpened// boolean — 是否已开启capsule.unlockAt// number — 解锁时间戳capsule.createdAt// number — 创建时间戳// 从 handleCardClick() 推断capsule.id// string — 唯一标识// 从 createCapsule() 推断// createCapsule(content, mood, unlockAt, tags) 返回 Capsule// 所以 Capsule 还有:content, mood, tags 字段

推断的 Capsule 接口

字段类型来源用途
idstringcreateCapsule 生成唯一标识,路由传参
contentstring用户输入胶囊内容(写给未来自己的话)
moodstringMoodPicker 选择心情 emoji
unlockAtnumber用户选择解锁时间戳(毫秒)
isOpenedboolean开启后置 true状态标记
tagsstring[]用户输入标签列表
createdAtnumbercreateCapsule 生成创建时间戳

二、胶囊的三种状态

// 从 getFiltered() 的筛选逻辑推断enumCapsuleState{SEALED='sealed',// 未开启 && 解锁时间未到READY='ready',// 未开启 && 解锁时间已到OPENED='opened'// 已开启}

状态转换图

创建胶囊 ↓ SEALED(等待中) ↓ 时间到达 READY(可开启) ↓ 用户点击开启 OPENED(已开启)

状态不是存储在 Capsule 对象上的——而是通过计算得出。首页的getFiltered()isOpenedunlockAt两个字段推断状态:

状态判断条件
SEALED!isOpened && unlockAt > now
READY!isOpened && unlockAt <= now
OPENEDisOpened === true

为什么不用状态字段?因为"时间是否到达"是动态的——同一胶囊在不同时间点状态不同。如果存储静态状态字段,需要在每次时间变化时更新。用unlockAt时间戳 + 当前时间计算,状态永远是最新的。

三、createCapsule 工厂函数

// 从 sealCapsule() 推断constcapsule=createCapsule(this.content.trim(),// string — 内容this.selectedMood,// string — 心情this.selectedUnlock,// number — 解锁时间戳this.tags// string[] — 标签)

工厂函数需要做的事

步骤操作说明
1生成唯一 id时间戳 + 随机数,保证不重复
2组装 Capsule 对象填充 7 个字段
3isOpened 默认 false新胶囊一定是未开启状态
4createdAt = Date.now()记录创建时间

id 生成策略的常见方案

方案格式优点缺点
时间戳+随机数1721088000000_abc简单,不重复可读性差
UUID550e8400-e29b-41d4-a716-446655440000标准,全局唯一太长
自增数字1, 2, 3...简洁,有序并发冲突

时光胶囊是单用户 App,时间戳+随机数已经足够——不需要 UUID 的全局唯一性,也不需要自增数字的严格有序。

四、解锁时间的设计

UnlockOption 接口

// 从 unlockOptions 和 opt.label / opt.getTime() 推断interfaceUnlockOption{label:string// 显示文本,如"1天后"getTime():number// 返回解锁时间戳}

常见的解锁选项

选项时间间隔毫秒数
1天后1 × 24h86400000
7天后7 × 24h604800000
30天后30 × 24h2592000000
1年后365 × 24h31536000000

默认选第二个选项(7天后)。首页初始化代码this.unlockOptions[1]证实了这一点。7天是一个"不长不短"的心理预期——足够期待,又不至于等太久。

Flex 布局的解锁选项

Flex({wrap:FlexWrap.Wrap}){ForEach(this.unlockOptions,(opt:UnlockOption)=>{Text(opt.label).fontColor(this.selectedUnlock===opt.getTime()?'#FFFFFF':this.getColors().text).backgroundColor(this.selectedUnlock===opt.getTime()?this.getColors().primary:this.getColors().surface).border({width:1,color:this.selectedUnlock===opt.getTime()?this.getColors().primary:this.getColors().border}).onClick(()=>this.selectedUnlock=opt.getTime())})}

选中状态用主色背景+白色文字,未选中用表面色背景+边框。和筛选标签用同样的选中/未选中视觉模式——整个 App 的交互语言统一。

五、心情系统

MoodOption 接口

// 从 MOOD_OPTIONS 和 opt.emoji / opt.label 推断interfaceMoodOption{emoji:string// 心情图标,如😊label:string// 心情名称,如"开心"}

常见的心情选项

emoji名称使用场景
😊开心高兴的时刻
😢难过伤感的回忆
🤔思考有感悟的时候
😤生气愤怒的记录
🥳庆祝里程碑事件

心情不只是装饰——它在统计 Tab 里生成"心情分布"图表。每个心情的使用次数被统计,通过进度条可视化。用户能看到"我最常用的心情是开心"——这是一种元认知(对自己情绪的认知)。

MoodPicker 组件

MoodPicker({selectedMood:this.selectedMood,colors:this.getColors(),onMoodChange:(mood:string)=>this.selectedMood=mood})

MoodPicker 是独立组件——接收当前选中的心情和主题颜色,通过回调返回新选择。这种"受控组件"模式让 MoodPicker 不管理自己的状态——状态由父组件(Index)管理。组件只负责渲染和交互,数据流单向。

六、标签系统

标签的增删逻辑

privateaddTag(tag:string):void{if(tag.trim()&&!this.tags.includes(tag.trim())){this.tags.push(tag.trim())this.newTag=''}}privateremoveTag(index:number):void{this.tags.splice(index,1)}

addTag 的三个检查:

  1. tag.trim()非空——防止添加空白标签
  2. !this.tags.includes(tag.trim())不重复——防止同一标签出现两次

removeTag 按索引删除——因为标签显示用 ForEach,每个标签有唯一的 idx。splice(index, 1)删除指定位置的元素。

标签的 UI 交互

Row(标签容器) ├→ Text('#' + tag) 标签名(主色) └→ Text(' ✕') 删除按钮(灰色)

标签显示#前缀——hashtag 风格,和知识库 App 的标签格式一致。点击调用removeTag(idx)删除。

标签输入的提交方式

TextInput({placeholder:'添加标签...'}).onChange((value:string)=>this.newTag=value).onSubmit(()=>this.addTag(this.newTag))

两种提交方式:键盘回车(onSubmit)和点击"添加"按钮(onClick)。双入口保证用户在任何操作习惯下都能添加标签。

七、CapsuleCard 组件的推断

// 从首页调用推断CapsuleCard({capsule:capsule,// Capsule 数据对象colors:this.getColors(),// 主题颜色onCardClick:(c:Capsule)=>this.handleCardClick(c)// 点击回调})

CapsuleCard 需要展示的信息

根据时间线列表的需求,CapsuleCard 至少需要显示:

信息来源作用
内容预览capsule.content(截取前50字)让用户知道胶囊写了什么
心情capsule.mood视觉化情绪
状态isOpened + unlockAt vs now标记等待中/可开启/已开
标签capsule.tags分类标识
创建时间capsule.createdAt时间参考
解锁倒计时unlockAt - nowCountdownBadge 组件

CountdownBadge 组件

CountdownBadge({capsule:capsule})

倒计时徽章——显示"还剩X天X小时"或"已到期"。在时间线卡片上实时显示每个胶囊的等待状态。

八、数据库层的推断

// 从首页调用推断database.loadData()// 异步加载数据database.getAllCapsules()// 获取所有胶囊database.addCapsule(capsule)// 添加胶囊database.getStats()// 获取统计数据database.clearAll()// 清空所有数据

StatsSummary 接口

// 从首页 loadStats() 推断interfaceStatsSummary{total:number// 总胶囊数opened:number// 已开启数sealed:number// 等待中数(首页用 waiting 展示)ready:number// 可开启数moodCount:Record<string,number>// 心情分布}

首页用StatsSummary.waiting但数据库返回sealed——命名不一致但含义相同。这是两个模块之间的"翻译"——数据库用内部命名(sealed),首页用用户友好命名(waiting)。

数据持久化的可能方案

方案适用场景复杂度
Preferences少量键值对
文件存储JSON 序列化
关系型数据库结构化查询

CapsuleDatabase的类名和loadData()异步方法推断,可能用文件存储(JSON)——先序列化到文件,启动时异步读取。单用户 App 不需要关系型数据库的复杂查询能力。

九、记忆洗牌功能

privategoToShuffle():void{router.pushUrl({url:'pages/MemoryShuffle'})}

记忆洗牌是独立页面——随机展示用户的历史胶囊。名字叫"洗牌"暗示随机性——用户不知道下一张会显示哪个胶囊,增加惊喜感。

和时间线的区别:时间线是按时间排序的完整列表,洗牌是随机抽取的单个胶囊展示。两种方式满足不同的使用场景——“我想按时间回顾” vs “我想随机重逢”。

十、胶囊生命周期的完整数据流

用户在封印 Tab 输入内容 → createCapsule(content, mood, unlockAt, tags) → 生成 id + createdAt + isOpened=false → database.addCapsule(capsule) → capsules 更新 时间线 Tab 显示胶囊 → getFiltered() 根据 activeFilter 筛选 → CapsuleCard 渲染每个胶囊 → CountdownBadge 显示剩余时间 时间到达 → getFiltered() 的 ready 筛选条件自动匹配 → CapsuleCard 显示"可开启"状态 用户点击胶囊 → handleCardClick → router.pushUrl → CapsuleDetail → 用户阅读后胶囊标记为 opened 统计 Tab → database.getStats() 统计三种状态的数量 → 心情分布按 emoji 分组计数

胶囊的生命周期完全由时间驱动——不需要用户手动标记状态。unlockAt时间戳是唯一的状态决定因素。这种"时间即状态"的设计简化了数据模型——没有状态字段,没有状态机,只有一个时间戳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询