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

一、从首页代码推断的数据模型
首页 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 接口
| 字段 | 类型 | 来源 | 用途 |
|---|---|---|---|
| id | string | createCapsule 生成 | 唯一标识,路由传参 |
| content | string | 用户输入 | 胶囊内容(写给未来自己的话) |
| mood | string | MoodPicker 选择 | 心情 emoji |
| unlockAt | number | 用户选择 | 解锁时间戳(毫秒) |
| isOpened | boolean | 开启后置 true | 状态标记 |
| tags | string[] | 用户输入 | 标签列表 |
| createdAt | number | createCapsule 生成 | 创建时间戳 |
二、胶囊的三种状态
// 从 getFiltered() 的筛选逻辑推断
enum CapsuleState {
SEALED = 'sealed', // 未开启 && 解锁时间未到
READY = 'ready', // 未开启 && 解锁时间已到
OPENED = 'opened' // 已开启
}

状态转换图
创建胶囊
↓
SEALED(等待中)
↓ 时间到达
READY(可开启)
↓ 用户点击开启
OPENED(已开启)
状态不是存储在 Capsule 对象上的——而是通过计算得出。 首页的 getFiltered() 用 isOpened 和 unlockAt 两个字段推断状态:
| 状态 | 判断条件 |
|---|---|
| SEALED | !isOpened && unlockAt > now |
| READY | !isOpened && unlockAt <= now |
| OPENED | isOpened === true |
为什么不用状态字段? 因为"时间是否到达"是动态的——同一胶囊在不同时间点状态不同。如果存储静态状态字段,需要在每次时间变化时更新。用 unlockAt 时间戳 + 当前时间计算,状态永远是最新的。
三、createCapsule 工厂函数
// 从 sealCapsule() 推断
const capsule = createCapsule(
this.content.trim(), // string — 内容
this.selectedMood, // string — 心情
this.selectedUnlock, // number — 解锁时间戳
this.tags // string[] — 标签
)

工厂函数需要做的事
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 生成唯一 id | 时间戳 + 随机数,保证不重复 |
| 2 | 组装 Capsule 对象 | 填充 7 个字段 |
| 3 | isOpened 默认 false | 新胶囊一定是未开启状态 |
| 4 | createdAt = Date.now() | 记录创建时间 |
id 生成策略的常见方案
| 方案 | 格式 | 优点 | 缺点 |
|---|---|---|---|
| 时间戳+随机数 | 1721088000000_abc |
简单,不重复 | 可读性差 |
| UUID | 550e8400-e29b-41d4-a716-446655440000 |
标准,全局唯一 | 太长 |
| 自增数字 | 1, 2, 3... |
简洁,有序 | 并发冲突 |
时光胶囊是单用户 App,时间戳+随机数已经足够——不需要 UUID 的全局唯一性,也不需要自增数字的严格有序。
四、解锁时间的设计
UnlockOption 接口
// 从 unlockOptions 和 opt.label / opt.getTime() 推断
interface UnlockOption {
label: string // 显示文本,如"1天后"
getTime(): number // 返回解锁时间戳
}
常见的解锁选项
| 选项 | 时间间隔 | 毫秒数 |
|---|---|---|
| 1天后 | 1 × 24h | 86400000 |
| 7天后 | 7 × 24h | 604800000 |
| 30天后 | 30 × 24h | 2592000000 |
| 1年后 | 365 × 24h | 31536000000 |
默认选第二个选项(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 推断
interface MoodOption {
emoji: string // 心情图标,如😊
label: string // 心情名称,如"开心"
}
常见的心情选项
| emoji | 名称 | 使用场景 |
|---|---|---|
| 😊 | 开心 | 高兴的时刻 |
| 😢 | 难过 | 伤感的回忆 |
| 🤔 | 思考 | 有感悟的时候 |
| 😤 | 生气 | 愤怒的记录 |
| 🥳 | 庆祝 | 里程碑事件 |
心情不只是装饰——它在统计 Tab 里生成"心情分布"图表。 每个心情的使用次数被统计,通过进度条可视化。用户能看到"我最常用的心情是开心"——这是一种元认知(对自己情绪的认知)。
MoodPicker 组件
MoodPicker({
selectedMood: this.selectedMood,
colors: this.getColors(),
onMoodChange: (mood: string) => this.selectedMood = mood
})
MoodPicker 是独立组件——接收当前选中的心情和主题颜色,通过回调返回新选择。 这种"受控组件"模式让 MoodPicker 不管理自己的状态——状态由父组件(Index)管理。组件只负责渲染和交互,数据流单向。
六、标签系统
标签的增删逻辑
private addTag(tag: string): void {
if (tag.trim() && !this.tags.includes(tag.trim())) {
this.tags.push(tag.trim())
this.newTag = ''
}
}
private removeTag(index: number): void {
this.tags.splice(index, 1)
}
addTag 的三个检查:
tag.trim()非空——防止添加空白标签!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 - now | CountdownBadge 组件 |
CountdownBadge 组件
CountdownBadge({ capsule: capsule })
倒计时徽章——显示"还剩X天X小时"或"已到期"。 在时间线卡片上实时显示每个胶囊的等待状态。
八、数据库层的推断
// 从首页调用推断
database.loadData() // 异步加载数据
database.getAllCapsules() // 获取所有胶囊
database.addCapsule(capsule) // 添加胶囊
database.getStats() // 获取统计数据
database.clearAll() // 清空所有数据
StatsSummary 接口
// 从首页 loadStats() 推断
interface StatsSummary {
total: number // 总胶囊数
opened: number // 已开启数
sealed: number // 等待中数(首页用 waiting 展示)
ready: number // 可开启数
moodCount: Record<string, number> // 心情分布
}
首页用 StatsSummary.waiting 但数据库返回 sealed——命名不一致但含义相同。 这是两个模块之间的"翻译"——数据库用内部命名(sealed),首页用用户友好命名(waiting)。
数据持久化的可能方案
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| Preferences | 少量键值对 | 低 |
| 文件存储 | JSON 序列化 | 中 |
| 关系型数据库 | 结构化查询 | 高 |
从 CapsuleDatabase 的类名和 loadData() 异步方法推断,可能用文件存储(JSON)——先序列化到文件,启动时异步读取。单用户 App 不需要关系型数据库的复杂查询能力。
九、记忆洗牌功能
private goToShuffle(): 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 时间戳是唯一的状态决定因素。这种"时间即状态"的设计简化了数据模型——没有状态字段,没有状态机,只有一个时间戳。
更多推荐



所有评论(0)