# 鸿蒙 ArkTS 实战:节日倒计时 Countdown(示例 44)

引言
如果说示例 41「世界时钟」关注的是「每个时刻的当前时间」,那么示例 44「节日倒计时」关注的就是「从当前时刻到目标日期的剩余时间」。用户可以在列表中看到 6 个节日的剩余天数,点击某个节日后,顶部卡片会以大号字体展示到该节日的精确倒计时(天/小时/分/秒)。
这个应用的实现核心是每秒定时器驱动倒计时刷新——nowTick 状态每秒 +1 触发 UI 重绘,重绘时动态计算到各节日的剩余时间。设计上的亮点包括:节日列表与主显示区的联动(点击列表项切换主显示)、日期跨年处理(当年已过则顺延明年)、以及秒级精度的倒计时展示。
1. 应用概述与功能
「节日倒计时」页面自上而下分为:顶部返回栏、主显示倒计时卡片、全部节日剩余天数列表。
1.1 核心功能清单
- 6 个预置节日:元旦、春节、劳动节、国庆节、圣诞节、七夕。
- 秒级倒计时:实时展示到目标节日的精确剩余时间(天/小时/分/秒)。
- 主显示切换:点击列表中的节日条目,顶部卡片切换显示该节日的倒计时。
- 自动跨年:如果当年节日已过,自动顺延到明年。
- 春节特殊处理:春节固定为 2027-02-06(示例),到期后顺延到 2028。
- 列表剩余天数:列表中每个节日显示剩余整天数。
1.2 技术要点一览
- Festival 接口:定义节日数据结构(名称、月份、日期)。
- 定时器驱动刷新:
setInterval每秒更新nowTick状态,触发所有倒计时重新计算。 - 动态日期计算:
getTarget()方法处理节日已过/未过的逻辑,返回正确的目标日期。 - 剩余时间格式化:
remainText()将毫秒差格式化为「天 小时 分 秒」的文本。 - ForEach 列表渲染:节日列表与主显示区联动,点击切换当前节日。
2. 核心知识点
2.1 Festival 接口
interface Festival {
name: string;
month: number;
day: number;
}
name:节日中文名。month:月份(1-12)。day:日期(1-31)。
6 个节日的预置数据覆盖了全年主要节日。
2.2 @State 状态变量
@State festivals: Festival[] = [...]
@State currentName: string = '元旦';
@State nowTick: number = 0;
festivals:节日数据数组,标记为@State是因为需要ForEach遍历渲染(尽管数据不变,但ForEach要求数据源是响应式的)。currentName:当前主显示的节日名称,初始为「元旦」。nowTick:每秒 +1 的计数器,用于触发 UI 刷新(因为倒计时数据是在渲染时动态计算的,不需要存储为状态)。
2.3 定时器驱动
aboutToAppear(): void {
this.timer = setInterval(() => {
this.nowTick = this.nowTick + 1;
}, 1000);
}
与示例 41 不同,这里定时器只做一件事:将 nowTick 状态 +1。UI 上显示的倒计时不是存储在 @State 中,而是在渲染时通过 remainText() 和 remainDays() 动态计算。这种设计的好处是:不需要在定时器回调中计算和存储大量状态,只需触发一次重绘即可。
2.4 getTarget:动态目标日期
getTarget(f: Festival): Date {
const now: Date = new Date();
if (f.name === '春节') {
const d: Date = new Date(2027, 1, 6);
if (d.getTime() > now.getTime()) {
return d;
}
return new Date(2028, 1, 6);
}
const year: number = now.getFullYear();
const d: Date = new Date(year, f.month - 1, f.day);
if (d.getTime() > now.getTime()) {
return d;
}
return new Date(year + 1, f.month - 1, f.day);
}
日期处理逻辑:
- 春节特殊处理:因为春节是农历节日,公历日期每年不同,所以用固定示例日期(2027-02-06),过期后顺延到 2028-02-06。
- 其他节日:默认使用当年的日期。如果当年节日已过(
d.getTime() > now.getTime()为false),则顺延到明年。
month - 1 是因为 Date 构造函数中月份从 0 开始(0=一月,11=十二月)。
2.5 remainText:剩余时间格式化
remainText(f: Festival): string {
const diff: number = this.getTarget(f).getTime() - new Date().getTime();
const sec: number = Math.max(0, Math.floor(diff / 1000));
const days: number = Math.floor(sec / 86400);
const hours: number = Math.floor((sec % 86400) / 3600);
const minutes: number = Math.floor((sec % 3600) / 60);
const seconds: number = sec % 60;
return days + ' 天 ' + hours + ' 小时 ' + minutes + ' 分 ' + seconds + ' 秒';
}
计算步骤:
- 毫秒差:目标日期时间戳 - 当前时间戳 = 剩余毫秒数。
- 转秒:
Math.floor(diff / 1000),用Math.max(0, ...)防止负数。 - 分解为天/小时/分/秒:
- 天:
Math.floor(sec / 86400)(86400 = 24 × 3600) - 小时:
Math.floor((sec % 86400) / 3600) - 分钟:
Math.floor((sec % 3600) / 60) - 秒:
sec % 60
- 天:
- 拼接字符串:
'X 天 X 小时 X 分 X 秒'
2.6 remainDays:剩余整天数
remainDays(f: Festival): number {
const diff: number = this.getTarget(f).getTime() - new Date().getTime();
return Math.max(0, Math.ceil(diff / (1000 * 3600 * 24)));
}
Math.ceil 向上取整。例如剩余 2 天 1 小时,显示 3 天。这比 Math.floor 更符合「还剩几天」的直觉——用户更愿意看到「还有 3 天」而不是「还有 2 天」。
2.7 current:获取当前节日
current(): Festival {
let cur: Festival = this.festivals[0];
for (let i: number = 0; i < this.festivals.length; i++) {
if (this.festivals[i].name === this.currentName) {
cur = this.festivals[i];
}
}
return cur;
}
遍历 festivals 找到 name 匹配 currentName 的节日。使用变量而非索引是为了展示遍历查找的模式,实际可以用索引直接访问。
2.8 列表点击切换
.onClick(() => {
this.currentName = item.name;
})
点击列表项时,将 currentName 更新为点击的节日名称,触发主显示卡片刷新。这是「主从联动」的标准实现。
3. 源码逐段解析
3.1 数据初始化
6 个节日覆盖了公历和农历节日:元旦(1/1)、春节(农历,固定示例)、劳动节(5/1)、国庆节(10/1)、圣诞节(12/25)、七夕(农历,按公历 8/19 示例)。
3.2 主显示卡片
主显示卡片使用白色圆角卡片,居中对齐,顶部显示「距 XXX 还有」,下方是 20fp 蓝色加粗的精确倒计时文本。
3.3 节日列表
列表使用 List + ForEach,每个条目显示节日名和剩余天数(红色强调)。layoutWeight(1) 占据剩余空间。
3.4 整体布局
与系列其他示例一致:顶部返回栏 → 主内容区 → 浅灰背景。
4. 关键实现细节分析
4.1 动态计算 vs 存储状态
倒计时文本是在渲染时通过方法动态计算的,而不是存储在 @State 中。这种设计的优势是:
- 代码简洁:不需要在定时器回调中更新多个状态变量。
- 数据一致性:所有倒计时都基于同一个
nowTick驱动的重绘,不会出现不同步。 - 缺点:每次重绘都要重新计算所有节日的倒计时,但 6 个节日的计算量很小,性能完全没问题。
4.2 nowTick 的作用
nowTick 本身没有业务含义,它的唯一作用是「触发 UI 重绘」。每当 nowTick +1,@State 系统通知 UI 重新渲染,渲染过程中 remainText() 和 remainDays() 被调用,计算出最新的倒计时数值。
4.3 跨年处理
getTarget() 中的跨年逻辑非常重要:
- 如果今年的节日还没过 → 用今年的日期。
- 如果今年的节日已过 → 用明年的日期。
这确保了倒计时永远指向「下一次」该节日,而不是已经过去的那一次。
4.4 春节的农历日期问题
春节是农历节日,公历日期不固定。当前实现使用了固定日期(2027-02-06)作为示例。更精确的实现需要一个农历到公历的转换表或算法。
4.5 性能考虑
每秒调用 remainText() 和 remainDays() 各 7 次(6 个节日 + 1 个当前节日),对于手机来说毫无压力。即使扩展到 100 个节日也不会有性能问题。
5. 运行效果与操作指南
5.1 运行步骤
- 从入口页导航到「节日倒计时」页面。
- 页面打开后立即显示当前默认节日(元旦)的精确倒计时。
- 点击列表中的其他节日,切换主显示。
- 倒计时每秒自动刷新。
5.2 使用场景
- 节日等待:期待某个节日的到来。
- 活动倒计时:自定义节日用于活动倒计时。
- 学期/假期倒计时:学生可以用它来期待假期。
6. 可扩展方向
6.1 添加自定义节日
允许用户添加自定义节日(如生日、纪念日、考试日期等)。
6.2 节日持久化
使用 @StorageLink 保存自定义节日列表。
6.3 节日提醒
在节日前 N 天发送通知。
6.4 动画效果
倒计时数字变化时添加过渡动画。
6.5 分类展示
按类别分组节日(传统节日、法定假日、西方节日等)。
7. 常见问题与调试技巧
7.1 倒计时不刷新
检查定时器是否启动。在 aboutToAppear 中添加日志确认。
7.2 倒计时显示负数
检查 remainText() 和 remainDays() 中的 Math.max(0, ...) 是否正确使用。
7.3 日期跨年不正确
检查 getTarget() 的逻辑。确认当年节日已过时正确顺延到明年。
7.4 春节日期不准确
当前使用固定示例日期。对于真实春节日期,需要实现农历转换。
8. 总结
示例 44「节日倒计时」用约 152 行代码,展示了 ArkTS 在「定时刷新 + 动态列表」场景下的实现。重点回顾:
- 定时器驱动重绘:
nowTick每秒 +1 触发 UI 重绘,渲染时动态计算倒计时。 - 动态日期计算:
getTarget()处理节日已过/未过的逻辑。 - 时间格式化:
remainText()将毫秒差格式化为「天/小时/分/秒」。 - 主从联动:点击列表切换主显示,是 ArkUI 交互的标准模式。
- 跨年处理:自动将已过节日顺延到明年。
这些技术在后续的番茄钟、计时器等示例中会被复用。
更多推荐



所有评论(0)