鸿蒙 ArkTS 实战:节日倒计时 Countdown
2026/8/7 12:20:02 网站建设 项目流程

引言

如果说示例 41「世界时钟」关注的是「每个时刻的当前时间」,那么示例 44「节日倒计时」关注的就是「从当前时刻到目标日期的剩余时间」。用户可以在列表中看到 6 个节日的剩余天数,点击某个节日后,顶部卡片会以大号字体展示到该节日的精确倒计时(天/小时/分/秒)。

这个应用的实现核心是每秒定时器驱动倒计时刷新——nowTick状态每秒 +1 触发 UI 重绘,重绘时动态计算到各节日的剩余时间。设计上的亮点包括:节日列表与主显示区的联动(点击列表项切换主显示)、日期跨年处理(当年已过则顺延明年)、以及秒级精度的倒计时展示。

1. 应用概述与功能

「节日倒计时」页面自上而下分为:顶部返回栏、主显示倒计时卡片、全部节日剩余天数列表。

1.1 核心功能清单

  • 6 个预置节日:元旦、春节、劳动节、国庆节、圣诞节、七夕。
  • 秒级倒计时:实时展示到目标节日的精确剩余时间(天/小时/分/秒)。
  • 主显示切换:点击列表中的节日条目,顶部卡片切换显示该节日的倒计时。
  • 自动跨年:如果当年节日已过,自动顺延到明年。
  • 春节特殊处理:春节固定为 2027-02-06(示例),到期后顺延到 2028。
  • 列表剩余天数:列表中每个节日显示剩余整天数。

1.2 技术要点一览

  1. Festival 接口:定义节日数据结构(名称、月份、日期)。
  2. 定时器驱动刷新setInterval每秒更新nowTick状态,触发所有倒计时重新计算。
  3. 动态日期计算getTarget()方法处理节日已过/未过的逻辑,返回正确的目标日期。
  4. 剩余时间格式化remainText()将毫秒差格式化为「天 小时 分 秒」的文本。
  5. ForEach 列表渲染:节日列表与主显示区联动,点击切换当前节日。

2. 核心知识点

2.1 Festival 接口

interfaceFestival{name:string;month:number;day:number;}
  • name:节日中文名。
  • month:月份(1-12)。
  • day:日期(1-31)。

6 个节日的预置数据覆盖了全年主要节日。

2.2 @State 状态变量

@Statefestivals:Festival[]=[...]@StatecurrentName:string='元旦';@StatenowTick: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{constnow:Date=newDate();if(f.name==='春节'){constd:Date=newDate(2027,1,6);if(d.getTime()>now.getTime()){returnd;}returnnewDate(2028,1,6);}constyear:number=now.getFullYear();constd:Date=newDate(year,f.month-1,f.day);if(d.getTime()>now.getTime()){returnd;}returnnewDate(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{constdiff:number=this.getTarget(f).getTime()-newDate().getTime();constsec:number=Math.max(0,Math.floor(diff/1000));constdays:number=Math.floor(sec/86400);consthours:number=Math.floor((sec%86400)/3600);constminutes:number=Math.floor((sec%3600)/60);constseconds:number=sec%60;returndays+' 天 '+hours+' 小时 '+minutes+' 分 '+seconds+' 秒';}

计算步骤:

  1. 毫秒差:目标日期时间戳 - 当前时间戳 = 剩余毫秒数。
  2. 转秒Math.floor(diff / 1000),用Math.max(0, ...)防止负数。
  3. 分解为天/小时/分/秒
    • 天:Math.floor(sec / 86400)(86400 = 24 × 3600)
    • 小时:Math.floor((sec % 86400) / 3600)
    • 分钟:Math.floor((sec % 3600) / 60)
    • 秒:sec % 60
  4. 拼接字符串'X 天 X 小时 X 分 X 秒'

2.6 remainDays:剩余整天数

remainDays(f:Festival):number{constdiff:number=this.getTarget(f).getTime()-newDate().getTime();returnMath.max(0,Math.ceil(diff/(1000*3600*24)));}

Math.ceil向上取整。例如剩余 2 天 1 小时,显示 3 天。这比Math.floor更符合「还剩几天」的直觉——用户更愿意看到「还有 3 天」而不是「还有 2 天」。

2.7 current:获取当前节日

current():Festival{letcur:Festival=this.festivals[0];for(leti:number=0;i<this.festivals.length;i++){if(this.festivals[i].name===this.currentName){cur=this.festivals[i];}}returncur;}

遍历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 运行步骤

  1. 从入口页导航到「节日倒计时」页面。
  2. 页面打开后立即显示当前默认节日(元旦)的精确倒计时。
  3. 点击列表中的其他节日,切换主显示。
  4. 倒计时每秒自动刷新。

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 交互的标准模式。
  • 跨年处理:自动将已过节日顺延到明年。

这些技术在后续的番茄钟、计时器等示例中会被复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询