HarmonyOS7 ArkUI 倒计时应用 - 多计时器管理与节日倒计时实战 文章目录前言真实页面会关心什么列表、状态和反馈关键片段片段 1formatTime(seconds: number): string {片段 2getProgress(timer: TimerItem): number {使用方式完整代码可扩展点前言如果刚开始写 HarmonyOS7 页面我建议多拆这种案例。代码量不夸张但能把声明式 UI 的味道摸清楚。它的主线是倒计时应用细节落在多计时器管理与节日倒计时。这些细节刚好能把页面状态、用户操作和组件刷新串起来。真实页面会关心什么CountdownTimerPage不是一个只展示静态内容的页面。它会根据用户点击、输入、切换或计时来改变界面所以阅读代码时可以先抓三条线观察点在这个案例里的表现页面主题倒计时应用主要文案番茄时钟,休息时间,会议提醒,元旦,春节,五一劳动节常用组件Button,Column,Divider,ForEach,Progress,Row,Scroll,Stack,Text| 适合练习 | 状态驱动 UI、条件样式、列表渲染、事件回调 |列表、状态和反馈ArkUI 的页面刷新依赖状态变化。下面这些State字段就是页面的“开关”和“数据源”不用手动找 DOM也不用自己通知某个控件刷新。状态字段类型我会怎么理解它activeTabnumber当前选中项决定高亮和内容切换timersTimerItem[]记录页面当前选择、输入或展示状态countdownsCountdownItem[]记录页面当前选择、输入或展示状态一个经验先把State看完再看build()页面逻辑会清楚很多。否则很容易被一长串布局代码带偏。关键片段片段 1formatTime(seconds: number): string {这段代码承担的是页面里的“判断”或“动作”把它从布局里抽出来后build()会清爽很多。后面要加新条件也不会到处翻 UI 代码。formatTime(seconds:number):string{consthMath.floor(seconds/3600)constmMath.floor((seconds%3600)/60)constsseconds%60if(h0){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)}}片段 2getProgress(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}EntryComponentstruct CountdownTimerPage{StateactiveTab:number0Statetimers: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:Mapnumber,numbernewMap()formatTime(seconds:number):string{consthMath.floor(seconds/3600)constmMath.floor((seconds%3600)/60)constsseconds%60if(h0){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.timersthis.timers.map((t:TimerItem){if(t.idid){t.isRunning!t.isRunningif(t.isRunning){consthandlesetInterval((){this.timersthis.timers.map((item:TimerItem){if(item.idid){if(item.remaining0){item.remainingitem.remaining-1returnitem}else{clearInterval(this.timerHandles.get(id)asnumber)this.timerHandles.delete(id)item.isRunningfalseitem.isFinishedtruereturnitem}}returnitem})},1000)this.timerHandles.set(id,handle)}else{consththis.timerHandles.get(id)if(h!undefined){clearInterval(h)this.timerHandles.delete(id)}}returnt}returnt})}resetTimer(id:number){consththis.timerHandles.get(id)if(h!undefined){clearInterval(h)this.timerHandles.delete(id)}this.timersthis.timers.map((t:TimerItem){if(t.idid){t.remainingt.totalSeconds t.isRunningfalset.isFinishedfalsereturnt}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.color30})})}}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.activeTabindex?FontWeight.Bold:FontWeight.Normal).fontColor(this.activeTabindex?#1890ff:#999999).padding({top:10,bottom:10})Divider().strokeWidth(2).color(this.activeTabindex?#1890ff:transparent)}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick((){this.activeTabindex})})}.width(100%).backgroundColor(#ffffff).margin({bottom:2})Scroll(){Column({space:0}){if(this.activeTab0){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 更稳。