news 2026/7/16 5:22:53

HarmonyOS7 健身打卡应用实战:打卡页要让进度马上可见

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS7 健身打卡应用实战:打卡页要让进度马上可见

文章目录

      • 打卡页的反馈链路
      • 训练数据如何驱动统计
      • 关键代码:环形进度抽成 `Builder`
      • 关键代码:打卡时同步更新总数据
      • 完整代码
      • 可以继续加的一个功能

健身打卡页最重要的是反馈快。用户点一下完成训练,卡路里、分钟数、完成率都应该马上变;否则这个页面就不像“打卡”,更像静态展示。

这个案例用本周日历、环形进度和训练列表做了一个完整的打卡页面,适合练@Builder复用和数组状态更新。

我写这类页面时会先做“点一下有没有反馈”。视觉可以后面慢慢调,反馈链路一定要先通。

打卡页的反馈链路

这个页面的交互链路很短,但很典型:

点击训练项 -> 修改 completed -> 更新总卡路里 / 总分钟 -> 环形进度刷新

页面里的信息可以这样分层:

区域展示内容关键字段
本周打卡一周记录、今天标记weekDays
今日数据卡路里、分钟、完成率totalCaloriestotalMinutesworkouts
训练计划训练名称、时长、月度进度、打卡按钮workouts

| 激励文案 | 底部提醒 | 静态文本 |

训练数据如何驱动统计

workouts是这页的核心数据。它既负责列表展示,也参与计算总消耗和完成率。

interfaceWorkoutRecord{id:numbername:stringicon:stringduration:numbercalories:numbercompleted:booleantargetDays:numberdoneDays:numbercolor:ResourceColor}

其中targetDaysdoneDays用来做月度进度,completed用来控制今日是否完成。一个数据结构同时服务两个维度,页面就不会散。

关键代码:环形进度抽成Builder

三个环形进度长得一样,只是颜色、数值和标题不同,所以这里用@Builder抽出ringProgress

@BuilderringProgress(progress:number,color:ResourceColor,size:number,label:string,value:string){Stack({alignContent:Alignment.Center}){Progress({value:0,total:100,type:ProgressType.Ring}).width(size).height(size).color('#1A1A2E').style({strokeWidth:size*0.1})Progress({value:progress,total:100,type:ProgressType.Ring}).width(size).height(size).color(color).style({strokeWidth:size*0.1})Column({space:2}){Text(value).fontSize(size*0.2).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')Text(label).fontSize(size*0.12).fontColor('#94A3B8')}}}

这个Builder的好处很明显:页面里调用三次就能得到三种进度,不用复制三段Stack + Progress

关键代码:打卡时同步更新总数据

toggleWorkout负责切换单个训练项,同时增减总卡路里和总分钟数。

toggleWorkout(id:number){this.workouts=this.workouts.map(w=>{if(w.id===id){constnewCompleted=!w.completedthis.totalCalories+=newCompleted?w.calories:-w.caloriesthis.totalMinutes+=newCompleted?w.duration:-w.duration w.completed=newCompletedreturnw}returnw})}

这里有一个可以继续优化的点:当前代码在map里修改了w.completed。学习示例能看懂,但真实项目里我更倾向返回新对象,状态变化会更干净。


完整代码

interfaceWorkoutRecord{id:numbername:stringicon:stringduration:numbercalories:numbercompleted:booleantargetDays:numberdoneDays:numbercolor:ResourceColor}interfaceWeekDay{day:stringlabel:stringhasRecord:booleanisToday:boolean}@Entry@Componentstruct FitnessCheckinPage{@StatetotalCalories:number=0@StatetotalMinutes:number=0@StatestreakDays:number=7@Stateworkouts:WorkoutRecord[]=[{id:1,name:'晨跑',icon:'🏃',duration:30,calories:280,completed:true,targetDays:20,doneDays:14,color:'#FF6B35'},{id:2,name:'核心训练',icon:'💪',duration:20,calories:150,completed:false,targetDays:15,doneDays:8,color:'#4ECDC4'},{id:3,name:'瑜伽',icon:'🧘',duration:45,calories:120,completed:true,targetDays:30,doneDays:22,color:'#A78BFA'},{id:4,name:'游泳',icon:'🏊',duration:60,calories:400,completed:false,targetDays:10,doneDays:5,color:'#3B82F6'}]@StateweekDays:WeekDay[]=[{day:'一',label:'Mon',hasRecord:true,isToday:false},{day:'二',label:'Tue',hasRecord:true,isToday:false},{day:'三',label:'Wed',hasRecord:true,isToday:false},{day:'四',label:'Thu',hasRecord:false,isToday:false},{day:'五',label:'Fri',hasRecord:true,isToday:false},{day:'六',label:'Sat',hasRecord:true,isToday:false},{day:'日',label:'Sun',hasRecord:false,isToday:true}]aboutToAppear(){this.workouts.forEach(w=>{if(w.completed){this.totalCalories+=w.caloriesthis.totalMinutes+=w.duration}})}toggleWorkout(id:number){this.workouts=this.workouts.map(w=>{if(w.id===id){constnewCompleted=!w.completedthis.totalCalories+=newCompleted?w.calories:-w.caloriesthis.totalMinutes+=newCompleted?w.duration:-w.duration w.completed=newCompletedreturnw}returnw})}@BuilderringProgress(progress:number,color:ResourceColor,size:number,label:string,value:string){Stack({alignContent:Alignment.Center}){Progress({value:0,total:100,type:ProgressType.Ring}).width(size).height(size).color('#1A1A2E').style({strokeWidth:size*0.1})Progress({value:progress,total:100,type:ProgressType.Ring}).width(size).height(size).color(color).style({strokeWidth:size*0.1})Column({space:2}){Text(value).fontSize(size*0.2).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')Text(label).fontSize(size*0.12).fontColor('#94A3B8')}}}build(){Column(){// 顶部标题栏Row(){Column({space:2}){Text('健身打卡').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')Text('今天是你坚持的第 '+this.streakDays+' 天 🔥').fontSize(13).fontColor('#94A3B8')}.alignItems(HorizontalAlign.Start)Blank()Text('🏆').fontSize(32)}.width('100%').padding({left:20,right:20,top:50,bottom:16})Scroll(){Column({space:16}){// 本周打卡日历Column({space:12}){Text('本周打卡').fontSize(15).fontWeight(FontWeight.Medium).fontColor('#94A3B8').alignSelf(ItemAlign.Start)Row({space:8}){ForEach(this.weekDays,(day:WeekDay)=>{Column({space:6}){Text(day.day).fontSize(12).fontColor(day.isToday?'#FFFFFF':'#64748B')Stack(){Circle({width:36,height:36}).fill(day.isToday?'#FF6B35':day.hasRecord?'#22C55E':'#1E293B')if(day.hasRecord&&!day.isToday){Text('✓').fontSize(14).fontColor('#FFFFFF')}elseif(day.isToday){Text('今').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)}}}})}.justifyContent(FlexAlign.SpaceBetween).width('100%')}.width('100%').padding(16).backgroundColor('#0F172A').borderRadius(16)// 今日数据环形图Row({space:20}){this.ringProgress(this.totalCalories/6,'#FF6B35',90,'卡路里',this.totalCalories+'')this.ringProgress(this.totalMinutes/0.9,'#4ECDC4',90,'分钟',this.totalMinutes+'')this.ringProgress(this.workouts.filter(w=>w.completed).length/this.workouts.length*100,'#A78BFA',90,'完成率',this.workouts.filter(w=>w.completed).length+'/'+this.workouts.length)}.justifyContent(FlexAlign.SpaceAround).width('100%').padding(20).backgroundColor('#0F172A').borderRadius(16)// 训练项目列表Column({space:12}){Text('今日训练计划').fontSize(15).fontWeight(FontWeight.Medium).fontColor('#94A3B8').alignSelf(ItemAlign.Start)ForEach(this.workouts,(workout:WorkoutRecord)=>{Row({space:14}){Text(workout.icon).fontSize(28).width(52).height(52).textAlign(TextAlign.Center).backgroundColor('#1E293B').borderRadius(14)Column({space:6}){Row(){Text(workout.name).fontSize(16).fontWeight(FontWeight.Medium).fontColor('#FFFFFF')Blank()Text(workout.duration+' 分钟').fontSize(12).fontColor('#64748B')}.width('100%')// 月度进度条Row({space:8}){Progress({value:workout.doneDays,total:workout.targetDays,type:ProgressType.Linear}).width(120).height(6).color(workout.color).backgroundColor('#1E293B')Text(workout.doneDays+'/'+workout.targetDays+'天').fontSize(11).fontColor('#94A3B8')}Text('消耗 '+workout.calories+' 千卡').fontSize(12).fontColor('#64748B')}.layoutWeight(1).alignItems(HorizontalAlign.Start)// 打卡按钮Stack(){Circle({width:40,height:40}).fill(workout.completed?'#22C55E':'#1E293B').stroke(workout.completed?'#22C55E':'#334155').strokeWidth(1.5)Text(workout.completed?'✓':'○').fontSize(16).fontColor(workout.completed?'#FFFFFF':'#475569')}.onClick(()=>this.toggleWorkout(workout.id))}.width('100%').padding(14).backgroundColor('#0F172A').borderRadius(14)})}.width('100%')// 底部激励文字Row(){Text('💬').fontSize(20)Text(' 坚持就是胜利!每一次训练都让你更强大 💪').fontSize(13).fontColor('#94A3B8').layoutWeight(1)}.width('100%').padding(16).backgroundColor('#0F172A').borderRadius(12).margin({bottom:30})}.padding({left:16,right:16})}.layoutWeight(1)}.width('100%').height('100%').backgroundColor('#020617')}}

可以继续加的一个功能

这个案例很适合继续加“今日目标”。比如设置目标卡路里和目标分钟数,再让环形进度基于目标计算,会比现在的演示数据更接近真实打卡应用。

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

从零实现C++矩阵类:设计原理、性能优化与工程实践

1. 项目概述:为什么我们需要自己造一个矩阵计算的轮子?在C的世界里,提到矩阵计算,大家的第一反应往往是Eigen、OpenCV的Mat,或者Armadillo这些久经沙场的第三方库。它们功能强大、性能卓越,是工业级项目的首…

作者头像 李华
网站建设 2026/7/16 5:16:39

WindTerm深度解析:为什么它能替代iTerm2、PowerShell和Warp

1. 这不是又一个终端,而是我三年来第一次主动卸载 iTerm2 的理由“再见了 iTerm2、PowerShell、Warp!”——这句话我写在 WindTerm 首次成功连接生产数据库的截图下方,发在了公司内部 DevOps 群里。没人回“1”,但第二天&#xff…

作者头像 李华
网站建设 2026/7/16 5:16:31

构建低时延协议栈:事件驱动、无锁与用户态架构实战

1. 项目概述:为什么我们需要亲手打造低时延协议栈?如果你是一名C后端开发,或者正在从事游戏服务器、高频交易、实时音视频这类对网络延迟有“变态”要求的领域,那么“网络协议栈”这个词对你来说,可能既是老朋友&#…

作者头像 李华
网站建设 2026/7/16 5:12:43

如何高效管理知识星球内容:专业开发者的完整PDF电子书制作指南

如何高效管理知识星球内容:专业开发者的完整PDF电子书制作指南 【免费下载链接】zsxq-spider 爬取知识星球内容,并制作 PDF 电子书。 项目地址: https://gitcode.com/gh_mirrors/zs/zsxq-spider 你是否在知识星球上订阅了多个优质专栏&#xff0c…

作者头像 李华