鸿蒙原生开发——从零构建倒数日追踪器
一、引言
人是靠期待活着的。生日、纪念日、旅行、毕业、节日——这些未来事件给我们时间感,让平凡的日常有了方向。"倒数日"这种 App 之所以流行,原因在于它把抽象的时间变成了具象的数字:"还有 3 天"比"下周三"更有紧迫感,"已过 12 天"比"两周前"更有纪念意义。
从技术角度看,倒数日涉及的核心问题是日期运算——两个日期之间相差多少天、今天是哪个日期、如何判断某个事件是将来还是过去、如何按距离今天的远近排序。这些运算不依赖后端,全部可以在前端用 JavaScript 的 Date API 完成。
本文将用 ArkUI 从零构建一个倒数日追踪器。功能包括:添加事件(名称 + 图标 + 目标日期)、自动计算剩余天数、四色距离标记(今天/明天红色、3 天内品红、7 天内蓝色、更远绿色)、已过事件灰色标记、自动按距离排序(最近的排最前)。配色严格避免了黄色系(红/品红/蓝/绿四色方案),确保所有文字与背景保持足够对比度。
阅读完本文,你将能够:
- 使用
DateAPI 进行日期差值计算 - 实现四色距离标记系统(根据剩余天数动态着色)
- 构建按距离自动排序的列表
- 处理"已过事件"的特殊显示逻辑
- 构建图标选择器 + 日期输入的表单弹窗
二、数据模型与日期运算
2.1 事件实体
interfaceCountdownEvent{id:number;name:string;icon:string;date:string;// YYYY-MM-DD}日期用YYYY-MM-DD格式存储——与日记本相同策略。8 个预设图标涵盖常见事件类型:生日🎂、纪念日💍、毕业🎓、旅行✈️、搬家🏠、节日🎄、庆祝🎉、自定义📅。
2.2 核心日期函数
两个函数构成日期系统的全部基础:
functiontodayStr():string{constd=newDate();return`${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2,'0')}-${d.getDate().toString().padStart(2,'0')}`;}functiondaysBetween(from:string,to:string):number{constf=newDate(from);constt=newDate(to);returnMath.floor((t.getTime()-f.getTime())/(1000*60*60*24));}todayStr()返回当天日期的字符串(如2026-06-11),用于构建"今天"的参照点。
daysBetween()计算两个日期之间相差的整天数。核心公式:(t.getTime() - f.getTime()) / (1000 * 60 * 60 * 24)。getTime()返回毫秒时间戳,差值除以 86400000(一天的毫秒数)得到天数。Math.floor向下取整——因为我们只关心完整的"天",不关心小时和分钟。
正值表示目标日期在未来(“还有 N 天”),负值表示目标日期已过(“已过 N 天”),零表示今天。
2.3 动态排序
事件列表按距今天的距离排序——最近的排在最前:
sortedEvents():CountdownEvent[]{return[...this.events].sort((a,b)=>{constda=daysBetween(todayStr(),a.date);constdb=daysBetween(todayStr(),b.date);if(da<0&&db>=0)return1;// a 已过,b 将来 → a 排后if(db<0&&da>=0)return-1;// b 已过,a 将来 → a 排前returnda-db;// 都将来或都已过 → 按距离});}排序规则:
- 将来的事件排在已过事件前面
- 将来事件按距离近 → 远排列(今天排第一,28 天后排最后)
- 已过事件按距离近 → 远排列(昨天排最前,1 年前的排最后)
用户打开页面后,最紧迫的事件(今天、明天)自然出现在第一位,不需要手动调整。
三、四色距离标记
3.1 颜色设计
用四种颜色标记事件的距离:
| 距离 | 颜色 | 色值 | 语义 |
|---|---|---|---|
| 已过 | 灰色 | #888899 | “这件事已经发生了” |
| 0-1 天 | 红色 | #FF4D4F | “就是今天/明天!紧迫” |
| 2-3 天 | 品红 | #EB2F96 | “快到了,做好准备” |
| 4-7 天 | 蓝色 | #1677FF | “还有一周,心里有数” |
| 8 天以上 | 绿色 | #52C41A | “还早,轻松等待” |
四种颜色构成了一个情感梯度:红色 = 紧迫感,品红 = 关注,蓝色 = 平常,绿色 = 从容。用户不需要阅读"还有 X 天"的文字,光看颜色就能感知每件事的紧迫程度。
注意这里完全避免了黄色/金色——红色 → 品红 → 蓝色 → 绿色的渐变中,没有黄色参与。品红(#EB2F96)在 2-3 天的位置替代了本来可能使用橙色/黄色的角色,且白字对比度远优于黄色。
3.2 动态文字标签
除了颜色,每个事件还有对应的文字标签:
proximityLabel(days:number):string{if(days<0)return`已过${Math.abs(days)}天`;if(days===0)return'今天!';if(days===1)return'明天';return`还有${days}天`;}"今天!“和"明天"是两个特殊情况——它们给人最强的紧迫感和期待感,所以用特殊的文字而非简单的"还有 0 天"或"还有 1 天”。感叹号增加了情感色彩——今天发生的事值得一个感叹号。
3.3 半透明背景色
每个事件卡片的图标区域也有对应的半透明背景色:
proximityBg(days:number):string{if(days<0)return'#88889915';if(days<=1)return'#FF4D4F15';if(days<=3)return'#EB2F9615';if(days<=7)return'#1677FF15';return'#52C41A15';}15(十六进制)= 约 8% 不透明度。这是一个非常淡的背景色——刚好能感知到色相,但不会和白色卡片背景产生冲突。如果背景色太深(比如 30-50% 不透明度),会和卡片文字争夺视觉注意力。
四、事件管理
4.1 添加事件
点击右下角 FAB("添加事件"蓝色按钮)弹出底部表单,包含三个输入区:
图标选择器:8 个 emoji 图标排列成行。选中态蓝底浅蓝边框(#1677FF22+ 1.5vp 边框),未选中态透明。与日记本的心情选择器不同,这里用边框区分选中态(而非不透明度)——因为图标都需要完整辨认,不适合降低不透明度。
名称输入:TextInput组件,占位文字"事件名称,如’生日派对’"。限制单行。
日期输入:TextInput组件,占位文字"日期,如 2026-12-25"。使用自由文本输入而非 DatePicker 的原因是:DatePicker 选择未来几个月甚至几年的日期需要多次滑动,不如直接输入YYYY-MM-DD快。
底部两个按钮——取消(灰色)和确认添加(蓝色,输入为空时灰色禁用)。
4.2 删除事件
每条事件卡片右侧有 × 按钮。点击直接删除——与日记本的删除逻辑相同,“可见即确认”,不需要额外的确认弹窗。
五、UI 设计
5.1 深色标题栏 + 白色卡片列表
与记账本类似的布局架构:
- 深色标题栏(
#1a1a2e):标题"⏳ 倒数日" + 事件计数 - 浅灰背景(
#F2F3F5):白色事件卡片列表 - 蓝色 FAB:右下角悬浮
5.2 事件卡片布局
每张卡片水平排列四个元素:
- 图标区(56×56vp 圆角方块,淡色背景):大号 emoji
- 信息区(flex 1):事件名称(加粗深色)+ 日期(浅灰小字)
- 天数标签:动态文字 + 动态颜色(红色/品红/蓝色/绿色/灰色)
- 删除按钮:浅灰 ×
卡片之间用 1vp 的细微间隙分隔,第一张卡片顶部与列表顶部对齐(borderRadius特殊处理)。
5.3 空状态
当事件列表为空时,显示空状态引导:📅 emoji + “还没有倒数事件” + “点击右下角 + 添加一个”。空状态是用户教育的一部分——告诉用户这个页面是做什么的、怎么开始使用。
六、完整代码结构
CountdownPage ├── Stack(根容器) │ ├── Column(主界面) │ │ ├── Row(标题栏:⏳ 倒数日 + N 个事件) │ │ └── Scroll(事件列表) │ │ ├── if 空态 → 空状态引导 │ │ └── ForEach → Row(事件卡片) │ │ ├── Column(图标 + 半透明色底) │ │ ├── Column(名称 + 日期) │ │ ├── Text(还有/已过 N 天,动态色) │ │ └── Text(删除 ×) │ ├── Button(FAB:"+ 添加事件") │ └── if showAdd → Column(底部弹窗) │ ├── Row(8 个图标选择) │ ├── TextInput(事件名称) │ ├── TextInput(日期 YYYY-MM-DD) │ └── Row(取消 + 确认添加)七、总结
本文从零构建了一个倒数日追踪器。与前几篇的功能型应用不同,倒数日的核心是日期运算 + 视觉距离编码。
核心要点回顾:
日期运算:
todayStr()返回当天日期字符串,daysBetween()计算两个日期的整天差。全部使用原生DateAPI,无需第三方库。四色距离标记:红色(0-1 天)= 紧迫,品红(2-3 天)= 关注,蓝色(4-7 天)= 平常,绿色(8 天+)= 从容。已过事件灰色。颜色构成情感梯度,用户不需要读文字就能感知紧迫程度。
自动排序:
sortedEvents()将将来事件排在已过事件前面,同类事件按距离升序排列。用户打开页面后最近的事件自然出现在第一位。配色安全:四色方案中完全没有黄色/金色——红色 → 品红 → 蓝色 → 绿色的情感梯度中,品红替代了传统方案中的橙色/黄色位置。所有颜色与白字对比度均达标。
动态标签:"今天!"和"明天"使用特殊文字而非数字,增加情感色彩和紧迫感。
图标选择器:用边框区分选中态(而非不透明度),因为所有图标都需要完整辨认。
倒数日是"小而美"的典型——一个界面、一个列表、几个事件卡片。但日期运算、距离排序、颜色编码、情感标签这些细节组合在一起,形成了一个用户愿意每天打开看的工具。
