HarmonyOS时光胶囊——MoodPicker与CountdownBadge组件设计
时光胶囊 App 有两个高频使用的小组件——心情选择器和倒计时徽章。它们分别在封印 Tab 和时间线列表里反复出现。一个负责让用户快速选择情绪另一个实时显示距离解锁还有多久。看似简单但要做到交互流畅、视觉统一、状态同步每个组件都有值得拆解的设计细节。完整效果一、MoodPicker 的调用方式从首页封印 Tab 的代码可以还原 MoodPicker 的完整用法MoodPicker({selectedMood:this.selectedMood,// 当前选中的心情colors:this.getColors(),// 主题颜色对象onMoodChange:(mood:string){// 选中回调this.selectedMoodmood}})三个参数的设计理由参数类型作用为什么这样传selectedMoodstring当前选中的 emoji组件不管理状态由父组件控制colorsThemeColors主题颜色保证切换主题后心情选择器同步变色onMoodChange回调函数通知父组件选中变化单向数据流——组件只通知不修改状态这是典型的受控组件模式——组件本身不持有状态所有状态由父组件管理。心情选择器只负责展示当前选中的是什么和用户点了哪个至于当前选中的是什么这个数据存在 Index 的State selectedMood里。二、MoodPicker 的内部实现推断MOOD_OPTIONS 常量从opt.emoji和opt.label推断MoodPicker 依赖一个固定的心情选项列表// 从 model/Capsule.ets 导入exportconstMOOD_OPTIONS:MoodOption[][{emoji:,label:开心},{emoji:,label:难过},{emoji:,label:思考},{emoji:,label:生气},{emoji:,label:庆祝}]五个选项是固定值——不从数据库读取不支持自定义。原因心情选项需要和统计 Tab 的柱状图对齐。如果用户自定义心情统计图表的维度会不可控。渲染结构推断基于选中/未选中的视觉效果MoodPicker 内部大概率是这样的结构Componentstruct MoodPicker{selectedMood:stringcolors:ThemeColorsgetThemeColors(ThemeType.CAPSULE)onMoodChange:(mood:string)void(){}build(){Flex({wrap:FlexWrap.Wrap}){ForEach(MOOD_OPTIONS,(opt:MoodOption){Column(){Text(opt.emoji).fontSize(28)Text(opt.label).fontSize(10)}.width(56).padding({top:8,bottom:8}).borderRadius(BorderRadius.md).backgroundColor(this.selectedMoodopt.emoji?this.colors.primary20:transparent).border({width:1.5,color:this.selectedMoodopt.emoji?this.colors.primary:this.colors.border}).onClick(()this.onMoodChange(opt.emoji))})}.width(100%)}}选中状态的视觉编码状态背景色边框色文字选中primary ‘20’20% 透明度primaryemoji label未选中transparentborderemoji label选中状态用 20% 透明度的主色背景——比笔记本卡片的 8% 更深因为心情选择器的点击目标更小需要更强的视觉反馈。边框从 1px 加粗到 1.5px进一步强化选中感。为什么用 Flex 而不是 RowFlex({wrap:FlexWrap.Wrap})// 自动换行// vsRow({space:8})// 不换行可能溢出五个心情选项在窄屏手机上一行放不下——Row 会溢出Flex 会自动换行。和首页统计 Tab 的 2×2 网格用同样的 FlexWrap.Wrap 策略。三、CountdownBadge 的调用方式// 从 CapsuleCard 调用推断CountdownBadge({capsule:capsule})只有一个参数——整个 Capsule 对象。组件自己从胶囊上读取unlockAt和isOpened来计算状态不需要父组件预处理。三种显示状态根据胶囊的生命周期CountdownBadge 有三种显示模式状态判断条件显示内容颜色等待中!isOpened unlockAt now“还剩 X天X小时”主色可开启!isOpened unlockAt now“✨ 可开启”绿色已开启isOpened true“已开启”灰色倒计时的计算逻辑privategetTimeLeft(unlockAt:number):string{constdiffunlockAt-Date.now()if(diff0)return可开启constdaysMath.floor(diff/86400000)consthoursMath.floor((diff%86400000)/3600000)if(days0)return还剩${days}天${hours}小时return还剩${hours}小时}时间单位只显示天和小时——不显示分钟和秒。原因胶囊的解锁周期通常是天级别1天/7天/30天/1年精确到分钟没有实际意义反而增加视觉噪音。渲染结构推断Componentstruct CountdownBadge{capsule:Capsule{}asCapsulebuild(){Text(this.badgeText()).fontSize(10).fontColor(this.badgeColor()).padding({left:6,right:6,top:2,bottom:2}).borderRadius(BorderRadius.full).backgroundColor(this.badgeBgColor())}}Badge 样式——小字号 圆角背景 文字颜色区分状态。这是移动端常见的标签徽章模式用于在有限空间内传递状态信息。四、CapsuleCard 的调用方式CapsuleCard({capsule:capsule,colors:this.getColors(),onCardClick:(c:Capsule){this.handleCardClick(c)}})三个参数参数类型作用capsuleCapsule胶囊数据对象colorsThemeColors主题颜色onCardClick回调函数点击卡片时触发传递整个胶囊对象CapsuleCard 同样是受控组件——不管理自己的状态数据和交互都由父组件控制。卡片需要展示的信息信息来源布局位置内容预览capsule.content截取前50字卡片主体心情capsule.mood右上角标签capsule.tags内容下方创建时间capsule.createdAt卡片底部状态标记isOpened unlockAtCountdownBadge卡片的视觉结构推断Row卡片 ├→ Column左侧状态/时间 │ ├→ CountdownBadge 状态徽章 │ └→ Text(createdAt) 创建时间 └→ Column右侧内容 ├→ Row标题行 │ ├→ Text(content) 内容预览截取50字 │ └→ Text(mood) 心情 emoji └→ Row标签行 └→ ForEach(tags) 标签列表五、组件间的颜色传递为什么传 colors 而不是让组件自己获取// 方案 A当前父组件传颜色MoodPicker({colors:this.getColors(),...})// 方案 B组件自己获取颜色MoodPicker({...})// 组件内部getThemeColors(currentTheme)方案 A 更好——因为颜色来自父组件的 currentTheme 状态。如果组件自己获取颜色需要访问父组件的 currentTheme增加了组件间的耦合。传 colors 参数让组件完全独立——不关心当前是什么主题只用传进来的颜色渲染。主题切换时的刷新链路用户点击主题标签 → saveTheme() 更新 currentTheme → build 重新执行 → getColors() 返回新主题颜色 → 传给 MoodPicker/CapsuleCard 的 colors 更新 → 组件重新渲染透明度后缀的统一规则组件透明度用途笔记本图标背景‘15’15%轻度背景心情选中背景‘20’20%中度背景快捷操作图标‘12’12%轻度背景标签输入背景‘15’15%轻度背景所有透明度都在 10%~20% 之间——太浅看不见太深抢内容的视觉权重。MoodPicker 的 20% 最深因为它是核心交互元素需要最明显的选中反馈。六、受控组件模式的统一整个 App 的组件都遵循同一个模式父组件管理状态 传递数据 接收回调 子组件接收数据 渲染 UI 触发回调组件状态归属数据输入交互输出MoodPickerIndex StateselectedMood, colorsonMoodChangeCountdownBadge无状态capsule无CapsuleCard无状态capsule, colorsonCardClickCountdownBadge 是最简单的——纯展示无交互。它不需要回调因为用户不会和倒计时徽章交互点击是整个卡片的事件。MoodPicker 需要回调——因为用户会和它交互。每次点击都触发 onMoodChange通知父组件更新状态。七、组件文件的组织方式ets/ components/ CapsuleCard.ets 卡片组件 MoodPicker.ets 心情选择器 CountdownBadge.ets 倒计时徽章 pages/ Index.ets 首页调用上述组件 CapsuleDetail.ets 详情页 MemoryShuffle.ets 记忆洗牌 model/ Capsule.ets 数据模型 Database.ets 数据库 utils/ Theme.ets 主题系统components 目录放可复用组件pages 目录放页面model 放数据层utils 放工具函数。四层分离——每个目录的职责清晰。组件复用的可能性组件当前使用位置可复用位置MoodPicker封印 TabCapsuleDetail编辑时可改心情CountdownBadgeCapsuleCardCapsuleDetail详情页显示倒计时CapsuleCard时间线列表MemoryShuffle洗牌展示卡片MoodPicker 在详情页可能需要——如果允许用户修改已封印胶囊的心情。CountdownBadge 在详情页也合理——显示当前胶囊的等待状态。CapsuleCard 在洗牌页可以复用——每张洗出来的牌就是一张胶囊卡片。八、组件与页面的边界什么逻辑放在组件里什么放在页面里逻辑位置原因心情选择 UIMoodPicker 组件纯 UI 渲染可复用选中状态管理Index 页面状态属于封印表单的一部分倒计时计算CountdownBadge 组件计算逻辑和展示绑定卡片点击跳转Index 页面路由逻辑属于页面层卡片内容渲染CapsuleCard 组件纯 UI 渲染可复用组件只管怎么显示页面管显示什么和点了之后做什么。这是组件化开发的核心原则——组件是 UI 的封装不是业务逻辑的封装。九、组件的类型安全回调函数的类型定义// MoodPicker 的回调类型onMoodChange:(mood:string)void// CapsuleCard 的回调类型onCardClick:(c:Capsule)void回调参数有明确的类型——不是(data: any) void。ArkTS 的类型系统在编译期检查参数类型防止运行时错误。mood是 stringc是 Capsule——类型不匹配时编译器直接报错。组件属性的类型定义// CountdownBadge 的 capsule 属性capsule:Capsule{}asCapsule默认值用{} as Capsule——空对象类型断言为 Capsule。这是 ArkTS 处理可选属性的常见模式。组件被调用时一定会传 capsule但类型系统需要默认值。十、组件设计的三个原则1. 单一职责每个组件只做一件事组件职责MoodPicker展示心情选项 接收选择CountdownBadge计算并展示剩余时间CapsuleCard渲染一张胶囊卡片不做超出职责的事。MoodPicker 不负责保存数据CountdownBadge 不负责点击跳转CapsuleCard 不负责筛选逻辑。2. 受控优先组件不管理自己的 State——所有状态由父组件控制。这让数据流单向父组件 → 组件 → 用户交互 → 回调 → 父组件。调试时只需要追踪父组件的 State 变化。3. 主题可配置所有组件通过 colors 参数接收主题颜色——不在组件内部硬编码颜色值。主题切换时父组件传入新的 colors组件自动刷新。这保证了整个 App 的视觉一致性。

相关新闻