时光胶囊的核心是一个有"生命周期"的数据对象——胶囊被封印后进入等待状态,时间到了才能开启。这个生命周期决定了数据模型的设计:它需要存储内容、心情、解锁时间、标签,还需要一个状态标记来区分"等待中"“可开启”"已开启"三种状态。
完整效果
一、从首页代码推断的数据模型
首页 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() 的筛选逻辑推断enumCapsuleState{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() 推断constcapsule=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() 推断interfaceUnlockOption{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 推断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 的三个检查:
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() 推断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时间戳是唯一的状态决定因素。这种"时间即状态"的设计简化了数据模型——没有状态字段,没有状态机,只有一个时间戳。