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

完整效果
在这里插入图片描述
在这里插入图片描述

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

首页 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() 的筛选逻辑推断
enum CapsuleState {
  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() 推断
const capsule = 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() 推断
interface UnlockOption {
  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 推断
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 的三个检查:

  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() 推断
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 时间戳是唯一的状态决定因素。这种"时间即状态"的设计简化了数据模型——没有状态字段,没有状态机,只有一个时间戳。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐