
时光胶囊的核心是一个有生命周期的数据对象——胶囊被封印后进入等待状态时间到了才能开启。这个生命周期决定了数据模型的设计它需要存储内容、心情、解锁时间、标签还需要一个状态标记来区分等待中“可开启”已开启三种状态。完整效果一、从首页代码推断的数据模型首页 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 选择心情 emojiunlockAtnumber用户选择解锁时间戳毫秒isOpenedboolean开启后置 true状态标记tagsstring[]用户输入标签列表createdAtnumbercreateCapsule 生成创建时间戳二、胶囊的三种状态// 从 getFiltered() 的筛选逻辑推断enumCapsuleState{SEALEDsealed,// 未开启 解锁时间未到READYready,// 未开启 解锁时间已到OPENEDopened// 已开启}状态转换图创建胶囊 ↓ SEALED等待中 ↓ 时间到达 READY可开启 ↓ 用户点击开启 OPENED已开启状态不是存储在 Capsule 对象上的——而是通过计算得出。首页的getFiltered()用isOpened和unlockAt两个字段推断状态状态判断条件SEALED!isOpened unlockAt nowREADY!isOpened unlockAt nowOPENEDisOpened true为什么不用状态字段因为时间是否到达是动态的——同一胶囊在不同时间点状态不同。如果存储静态状态字段需要在每次时间变化时更新。用unlockAt时间戳 当前时间计算状态永远是最新的。三、createCapsule 工厂函数// 从 sealCapsule() 推断constcapsulecreateCapsule(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 × 24h864000007天后7 × 24h60480000030天后30 × 24h25920000001年后365 × 24h31536000000默认选第二个选项7天后。首页初始化代码this.unlockOptions[1]证实了这一点。7天是一个不长不短的心理预期——足够期待又不至于等太久。Flex 布局的解锁选项Flex({wrap:FlexWrap.Wrap}){ForEach(this.unlockOptions,(opt:UnlockOption){Text(opt.label).fontColor(this.selectedUnlockopt.getTime()?#FFFFFF:this.getColors().text).backgroundColor(this.selectedUnlockopt.getTime()?this.getColors().primary:this.getColors().surface).border({width:1,color:this.selectedUnlockopt.getTime()?this.getColors().primary:this.getColors().border}).onClick(()this.selectedUnlockopt.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.selectedMoodmood})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.newTagvalue).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:Recordstring,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 isOpenedfalse → database.addCapsule(capsule) → capsules 更新 时间线 Tab 显示胶囊 → getFiltered() 根据 activeFilter 筛选 → CapsuleCard 渲染每个胶囊 → CountdownBadge 显示剩余时间 时间到达 → getFiltered() 的 ready 筛选条件自动匹配 → CapsuleCard 显示可开启状态 用户点击胶囊 → handleCardClick → router.pushUrl → CapsuleDetail → 用户阅读后胶囊标记为 opened 统计 Tab → database.getStats() 统计三种状态的数量 → 心情分布按 emoji 分组计数胶囊的生命周期完全由时间驱动——不需要用户手动标记状态。unlockAt时间戳是唯一的状态决定因素。这种时间即状态的设计简化了数据模型——没有状态字段没有状态机只有一个时间戳。