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

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

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

首页 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()isOpenedunlockAt 两个字段推断状态:

状态 判断条件
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 的三个检查:

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

Logo

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

更多推荐