news 2026/8/5 12:35:49

HarmonyOS7 ArkUI 倒计时应用 - 多计时器管理与节日倒计时实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS7 ArkUI 倒计时应用 - 多计时器管理与节日倒计时实战

文章目录

      • 前言
      • 真实页面会关心什么
      • 列表、状态和反馈
      • 关键片段
        • 片段 1:`formatTime(seconds: number): string {`
        • 片段 2:`getProgress(timer: TimerItem): number {`
      • 使用方式
      • 完整代码
      • 可扩展点

前言

如果刚开始写 HarmonyOS7 页面,我建议多拆这种案例。代码量不夸张,但能把声明式 UI 的味道摸清楚。
它的主线是倒计时应用,细节落在多计时器管理与节日倒计时。这些细节刚好能把页面状态、用户操作和组件刷新串起来。

真实页面会关心什么

CountdownTimerPage不是一个只展示静态内容的页面。它会根据用户点击、输入、切换或计时来改变界面,所以阅读代码时可以先抓三条线:

观察点在这个案例里的表现
页面主题倒计时应用
主要文案番茄时钟,休息时间,会议提醒,元旦,春节,五一劳动节
常用组件Button,Column,Divider,ForEach,Progress,Row,Scroll,Stack,Text

| 适合练习 | 状态驱动 UI、条件样式、列表渲染、事件回调 |

列表、状态和反馈

ArkUI 的页面刷新依赖状态变化。下面这些@State字段就是页面的“开关”和“数据源”,不用手动找 DOM,也不用自己通知某个控件刷新。

状态字段类型我会怎么理解它
activeTabnumber当前选中项,决定高亮和内容切换
timersTimerItem[]记录页面当前选择、输入或展示状态
countdownsCountdownItem[]记录页面当前选择、输入或展示状态

一个经验:先把@State看完,再看build(),页面逻辑会清楚很多。否则很容易被一长串布局代码带偏。

关键片段

片段 1:formatTime(seconds: number): string {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

formatTime(seconds:number):string{consth=Math.floor(seconds/3600)constm=Math.floor((seconds%3600)/60)consts=seconds%60if(h>0){return`${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`}return`${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`}
片段 2:getProgress(timer: TimerItem): number {

这段代码承担的是页面里的“判断”或“动作”,把它从布局里抽出来后,build()会清爽很多。后面要加新条件,也不会到处翻 UI 代码。

getProgress(timer:TimerItem):number{return(timer.totalSeconds-timer.remaining)/timer.totalSeconds}

使用方式

把下面代码放进 ArkTS 页面文件中即可运行。用于正式项目时,我会继续把数据模型、卡片 Builder 和页面事件拆出去,页面文件只负责组合。

建议练习顺序:先改状态字段 -> 再改交互事件 -> 最后调整 UI 样式 检查重点:点击是否刷新、列表 key 是否稳定、条件样式是否集中管理
这份代码可以继续扩展的方向
  • 把模拟数据替换成接口数据
  • 抽出复用卡片组件
  • 增加空状态和异常状态
  • 补充深色模式或主题色适配
  • 对输入类场景增加校验提示

完整代码

// CountdownTimerPage - 倒计时应用 - 多计时器管理与节日倒计时interfaceCountdownItem{id:numbertitle:stringtargetDate:stringemoji:stringcolor:stringdaysLeft:number}interfaceTimerItem{id:numberlabel:stringtotalSeconds:numberremaining:numberisRunning:booleanisFinished:boolean}@Entry@Componentstruct CountdownTimerPage{@StateactiveTab:number=0@Statetimers:TimerItem[]=[{id:1,label:'番茄时钟',totalSeconds:1500,remaining:1500,isRunning:false,isFinished:false},{id:2,label:'休息时间',totalSeconds:300,remaining:300,isRunning:false,isFinished:false},{id:3,label:'会议提醒',totalSeconds:3600,remaining:3600,isRunning:false,isFinished:false},]@Statecountdowns:CountdownItem[]=[{id:1,title:'元旦',targetDate:'2026-01-01',emoji:'🎆',color:'#ff4d4f',daysLeft:209},{id:2,title:'春节',targetDate:'2026-02-17',emoji:'🧨',color:'#fa8c16',daysLeft:256},{id:3,title:'五一劳动节',targetDate:'2026-05-01',emoji:'🌸',color:'#52c41a',daysLeft:329},{id:4,title:'中秋节',targetDate:'2026-09-25',emoji:'🌕',color:'#1890ff',daysLeft:476},{id:5,title:'国庆节',targetDate:'2026-10-01',emoji:'🇨🇳',color:'#eb2f96',daysLeft:482},]privatetimerHandles:Map<number,number>=newMap()formatTime(seconds:number):string{consth=Math.floor(seconds/3600)constm=Math.floor((seconds%3600)/60)consts=seconds%60if(h>0){return`${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`}return`${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`}getProgress(timer:TimerItem):number{return(timer.totalSeconds-timer.remaining)/timer.totalSeconds}toggleTimer(id:number){this.timers=this.timers.map((t:TimerItem)=>{if(t.id===id){t.isRunning=!t.isRunningif(t.isRunning){consthandle=setInterval(()=>{this.timers=this.timers.map((item:TimerItem)=>{if(item.id===id){if(item.remaining>0){item.remaining=item.remaining-1returnitem}else{clearInterval(this.timerHandles.get(id)asnumber)this.timerHandles.delete(id)item.isRunning=falseitem.isFinished=truereturnitem}}returnitem})},1000)this.timerHandles.set(id,handle)}else{consth=this.timerHandles.get(id)if(h!==undefined){clearInterval(h)this.timerHandles.delete(id)}}returnt}returnt})}resetTimer(id:number){consth=this.timerHandles.get(id)if(h!==undefined){clearInterval(h)this.timerHandles.delete(id)}this.timers=this.timers.map((t:TimerItem)=>{if(t.id===id){t.remaining=t.totalSeconds t.isRunning=falset.isFinished=falsereturnt}returnt})}aboutToDisappear(){this.timerHandles.forEach((handle:number)=>clearInterval(handle))}@BuilderTimerTab(){Column({space:16}){ForEach(this.timers,(item:TimerItem)=>{Column({space:16}){Row(){Text(item.label).fontSize(16).fontWeight(FontWeight.Bold).fontColor(item.isFinished?'#52c41a':'#1a1a1a')Blank()if(item.isFinished){Text('✅ 完成!').fontSize(13).fontColor('#52c41a').fontWeight(FontWeight.Medium)}}.width('100%')// 圆形进度Stack(){// 背景圆Circle().width(120).height(120).fill('none').stroke('#f0f0f0').strokeWidth(8)Text(this.formatTime(item.remaining)).fontSize(22).fontWeight(FontWeight.Bold).fontColor(item.isFinished?'#52c41a':'#1a1a1a')}.width(120).height(120)// 线性进度条Column({space:4}){Progress({value:this.getProgress(item)*100,total:100,type:ProgressType.Linear}).width('100%').height(6).color(item.isFinished?'#52c41a':'#1890ff').backgroundColor('#f0f0f0')Text(`${Math.floor(this.getProgress(item)*100)}%`).fontSize(12).fontColor('#999999').width('100%').textAlign(TextAlign.End)}// 控制按钮Row({space:12}){Button(item.isRunning?'暂停':(item.isFinished?'已完成':'开始')).layoutWeight(1).height(40).fontSize(14).backgroundColor(item.isFinished?'#f0f0f0':(item.isRunning?'#fa8c16':'#1890ff')).fontColor(item.isFinished?'#999999':'#ffffff').enabled(!item.isFinished).onClick(()=>this.toggleTimer(item.id))Button('重置').width(80).height(40).fontSize(14).backgroundColor('#f5f5f5').fontColor('#444444').onClick(()=>this.resetTimer(item.id))}.width('100%')}.width('100%').padding(20).backgroundColor('#ffffff').borderRadius(16).shadow({radius:6,color:'#00000010',offsetX:0,offsetY:2}).alignItems(HorizontalAlign.Center)})}}@BuilderCountdownTab(){Column({space:12}){ForEach(this.countdowns,(item:CountdownItem)=>{Row({space:16}){Text(item.emoji).fontSize(36)Column({space:4}){Text(item.title).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')Text(item.targetDate).fontSize(12).fontColor('#999999')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Column({space:2}){Text(`${item.daysLeft}`).fontSize(28).fontWeight(FontWeight.Bold).fontColor(item.color)Text('天后').fontSize(12).fontColor('#999999')}.alignItems(HorizontalAlign.Center)}.width('100%').padding({left:20,right:20,top:16,bottom:16}).backgroundColor('#ffffff').borderRadius(16).border({width:2,color:item.color+'30'})})}}build(){Column({space:0}){// 标题栏Row(){Text('倒计时').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')Blank()Text('+ 新建').fontSize(14).fontColor('#1890ff')}.width('100%').padding({left:20,right:20,top:16,bottom:16}).backgroundColor('#ffffff')// Tab 切换Row(){ForEach(['计时器','节日倒计时'],(tab:string,index:number)=>{Column(){Text(tab).fontSize(15).fontWeight(this.activeTab===index?FontWeight.Bold:FontWeight.Normal).fontColor(this.activeTab===index?'#1890ff':'#999999').padding({top:10,bottom:10})Divider().strokeWidth(2).color(this.activeTab===index?'#1890ff':'transparent')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>{this.activeTab=index})})}.width('100%').backgroundColor('#ffffff').margin({bottom:2})Scroll(){Column({space:0}){if(this.activeTab===0){this.TimerTab()}else{this.CountdownTab()}}.padding({left:16,right:16,top:16,bottom:32})}.layoutWeight(1).backgroundColor('#f5f7fa')}.width('100%').height('100%').backgroundColor('#f5f7fa')}}

可扩展点

这个案例可以当成一个小模板:先把页面会变的东西放进状态,再用函数或 Builder 处理重复逻辑,最后让布局只关心展示。写 HarmonyOS7 页面时,这个顺序通常比一上来堆 UI 更稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/5 12:33:05

拼多多商家推流方法|免费 + 付费全链路

做拼多多&#xff0c;流量决定店铺生死。不少新手商家上来就猛开全站、搜索推广&#xff0c;商品没有基础销量、没有评价&#xff0c;直接导致投产崩盘&#xff0c;预算烧完没拿到有效订单&#xff1b;纯靠自然流量&#xff0c;新品又会陷入零访客、零转化的死循环。拼多多流量…

作者头像 李华
网站建设 2026/8/5 12:31:03

EuroSAT遥感数据集终极指南:从零开始掌握卫星图像分类

EuroSAT遥感数据集终极指南&#xff1a;从零开始掌握卫星图像分类 【免费下载链接】EuroSAT EuroSAT: Land Use and Land Cover Classification with Sentinel-2 项目地址: https://gitcode.com/gh_mirrors/eu/EuroSAT 还在为卫星遥感图像分类项目寻找高质量数据集而烦恼…

作者头像 李华
网站建设 2026/8/5 12:30:45

安全生产许可与特种设备台账平台:用 Python 搭一套许可、设备、检验结果三表关联采集工程

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~ ㊙️本期爬虫难度指数:⭐⭐⭐⭐⭐(专家级) 🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。 全文目录: 🌟…

作者头像 李华
网站建设 2026/8/5 12:30:26

机构、资产与公告数据质量引擎:从爬虫采集到数据治理落地的 Python 实战

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~ ㊙️本期爬虫难度指数:⭐⭐⭐⭐⭐(专家级) 🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。 全文目录: 🌟…

作者头像 李华
网站建设 2026/8/5 12:28:33

SAP权限管理实战:从权限对象、角色创建到用户分配全流程详解

1. 项目概述&#xff1a;从零构建SAP权限体系的核心操作在SAP项目实施和日常运维中&#xff0c;权限管理是保障系统安全、数据隔离和业务流程合规的基石。无论是新用户入职、岗位变动&#xff0c;还是业务流程调整&#xff0c;都离不开权限角色的创建与分配。很多刚接触SAP BAS…

作者头像 李华
网站建设 2026/8/5 12:28:10

使用Python实现盲水印:保护你的数字资产不被盗用

使用Python实现盲水印&#xff1a;保护你的数字资产不被盗用 【免费下载链接】BlindWaterMark 盲水印 by python 项目地址: https://gitcode.com/gh_mirrors/bli/BlindWaterMark 你是否曾担心自己精心设计的图片在网络上被他人随意盗用&#xff1f;或者想要在分享作品时…

作者头像 李华