Exercise-tracker 运动记录应用开发教程项目介绍项目背景运动记录应用是一个帮助用户记录和追踪运动数据的健康管理工具。随着人们对健康生活方式的重视运动已经成为日常生活的重要组成部分。然而很多人在运动时缺乏系统的记录和分析导致无法准确了解自己的运动状况和进步情况。运动记录应用通过数字化的方式帮助用户记录每次运动的类型、时长、消耗的卡路里等数据并提供统计分析功能让用户能够清晰地看到自己的运动轨迹和成果。这种可视化的反馈机制能够有效激励用户坚持运动养成健康的生活习惯。应用场景健身记录记录每次运动的详细数据包括运动类型、持续时间、消耗卡路里等。用户可以随时查看自己的运动历史。数据分析分析运动习惯和趋势了解哪些运动类型最常进行哪些时间段运动最频繁。目标追踪设定运动目标并追踪完成情况如每周运动次数、每月运动时长等。健康监测长期记录运动数据为健康管理提供参考依据。功能特性运动记录记录运动类型、时长、消耗卡路里和备注信息。类型选择支持多种运动类型如跑步、游泳、骑行、健身、瑜伽等。数据统计统计本周运动次数、总时长和总消耗卡路里。历史记录查看历史运动记录支持删除操作。图标显示根据运动类型显示相应的图标直观易识别。最终效果应用采用绿色主题象征着健康和活力。主界面包含顶部标题栏和添加按钮本周统计卡片显示运动次数、总时长和总消耗运动记录列表显示每条记录的详情技术栈开发框架HarmonyOS NEXT (API 20)编程语言ArkTSUI框架ArkUI 声明式 UI核心组件Column, Row, List, Button, TextInput, Select知识点讲解1. 数据统计使用reduce方法进行数据统计计算总和、平均值等。// 计算本周统计数据privategetWeeklyStats(){consttodaynewDate()constweekAgonewDate(today.getTime()-7*24*60*60*1000)// 过滤本周的记录constweeklyRecordsthis.records.filter(record{constrecordDatenewDate(record.date)returnrecordDateweekAgorecordDatetoday})// 计算统计数据return{// 运动次数count:weeklyRecords.length,// 总时长使用 reduce 累加totalDuration:weeklyRecords.reduce((sum,record)sumrecord.duration,0),// 总消耗卡路里totalCalories:weeklyRecords.reduce((sum,record)sumrecord.calories,0)}}2. 图标映射使用对象映射实现图标选择根据运动类型返回对应的图标。// 定义运动类型图标映射privatereadonlytypeIcons:Recordstring,string{跑步:,游泳:,骑行:,健身:,瑜伽:,跳绳:⚡,篮球:,羽毛球:,足球:⚽,网球:}// 获取运动类型图标privategetTypeIcon(type:string):string{returnthis.typeIcons[type]||// 默认图标}3. 日期处理获取和格式化日期用于记录和筛选运动数据。// 获取今天的日期字符串privategetTodayString():string{consttodaynewDate()constyeartoday.getFullYear()constmonth(today.getMonth()1).toString().padStart(2,0)constdaytoday.getDate().toString().padStart(2,0)return${year}-${month}-${day}}// 格式化日期显示privateformatDate(dateStr:string):string{constdatenewDate(dateStr)constmonth(date.getMonth()1).toString().padStart(2,0)constdaydate.getDate().toString().padStart(2,0)return${month}月${day}日}// 计算相对日期privategetRelativeDate(dateStr:string):string{consttodaythis.getTodayString()if(dateStrtoday)return今天constyesterdaynewDate()yesterday.setDate(yesterday.getDate()-1)if(dateStrthis.formatDateToString(yesterday))return昨天returnthis.formatDate(dateStr)}4. 表单处理处理添加运动记录的表单数据包括输入验证和数据格式化。// 表单状态StatenewType:string跑步StatenewDuration:string30StatenewCalories:string200StatenewNotes:string// 添加运动记录privateaddRecord(){// 验证输入constdurationparseInt(this.newDuration)constcaloriesparseInt(this.newCalories)if(isNaN(duration)||duration0){// 提示错误return}if(isNaN(calories)||calories0){// 提示错误return}// 创建新记录constnewRecord:ExerciseRecord{id:Date.now(),type:this.newType,duration:duration,calories:calories,date:this.getTodayString(),notes:this.newNotes.trim()}// 添加到列表this.records.unshift(newRecord)// 清空表单this.clearForm()// 关闭表单this.showAddRecordfalse}// 清空表单privateclearForm(){this.newType跑步this.newDuration30this.newCalories200this.newNotes}5. 列表渲染使用 List 和 ForEach 渲染运动记录列表。List(){ForEach(this.records,(record:ExerciseRecord){ListItem(){Row(){// 运动类型图标Text(this.getTypeIcon(record.type)).fontSize(32).margin({right:12})// 记录信息Column(){Text(record.type).fontSize(16).fontWeight(FontWeight.Medium).fontColor(#1e293b)Text(${record.duration}分钟 ·${record.calories}千卡).fontSize(12).fontColor(#64748b).margin({top:2})if(record.notes!){Text(record.notes).fontSize(12).fontColor(#9ca3af).margin({top:2}).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})}}.width(60%)// 日期和删除按钮Column(){Text(this.getRelativeDate(record.date)).fontSize(12).fontColor(#64748b)Button(){Text(×).fontSize(16).fontColor(#9ca3af)}.width(24).height(24).backgroundColor(transparent).margin({top:4}).onClick((){this.deleteRecord(record.id)})}.width(20%).alignItems(HorizontalAlign.End)}.width(100%).padding(16).backgroundColor(#ffffff).borderRadius(12).margin({bottom:8})}})}.width(100%).layoutWeight(1)6. 统计卡片显示本周运动统计数据。Column(){Text(本周统计).fontSize(16).fontColor(#64748b).margin({bottom:12})Row(){// 运动次数Column(){Text(${this.getWeeklyStats().count}).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#10b981)Text(次运动).fontSize(12).fontColor(#64748b)}.width(33%).alignItems(HorizontalAlign.Center)// 总时长Column(){Text(${this.getWeeklyStats().totalDuration}).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#10b981)Text(分钟).fontSize(12).fontColor(#64748b)}.width(33%).alignItems(HorizontalAlign.Center)// 总消耗Column(){Text(${this.getWeeklyStats().totalCalories}).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#10b981)Text(千卡).fontSize(12).fontColor(#64748b)}.width(33%).alignItems(HorizontalAlign.Center)}.width(100%)}.width(100%).padding(20).backgroundColor(#ffffff).borderRadius(16).margin({left:16,right:16,bottom:16})7. 删除记录删除不需要的运动记录。privatedeleteRecord(id:number){this.recordsthis.records.filter(recordrecord.id!id)}8. 类型选择使用 Select 组件选择运动类型。Select(this.exerciseTypes.map(type({value:type}))).value(this.newType).width(100%).height(44).onSelect((index:number){this.newTypethis.exerciseTypes[index]})9. 输入验证验证用户输入的数据是否有效。privatevalidateDuration(input:string):boolean{constdurationparseInt(input)return!isNaN(duration)duration0duration480// 最大8小时}privatevalidateCalories(input:string):boolean{constcaloriesparseInt(input)return!isNaN(calories)calories0calories5000// 最大5000千卡}10. 数据格式化格式化显示数据使其更加易读。// 格式化时长显示privateformatDuration(minutes:number):string{if(minutes60){return${minutes}分钟}consthoursMath.floor(minutes/60)constminsminutes%60returnmins0?${hours}小时${mins}分钟:${hours}小时}// 格式化卡路里显示privateformatCalories(calories:number):string{if(calories1000){return${(calories/1000).toFixed(1)}千卡}return${calories}千卡}完整代码解析页面结构┌─────────────────────────────────┐ │ [运动记录] [] │ ├─────────────────────────────────┤ │ ┌───────────────────────────┐ │ │ │ 本周统计 │ │ │ │ 3次运动 135分钟 1000千卡│ │ │ └───────────────────────────┘ │ ├─────────────────────────────────┤ │ 运动记录 │ │ ┌───────────────────────────┐ │ │ │ 跑步 │ │ │ │ 45分钟 · 350千卡 │ │ │ │ 晨跑5公里 │ │ │ │ 今天 [×] │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 健身 │ │ │ │ 60分钟 · 400千卡 │ │ │ │ 上肢训练 │ │ │ │ 昨天 [×] │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 游泳 │ │ │ │ 30分钟 · 250千卡 │ │ │ │ 自由泳1000米 │ │ │ │ 01月18日 [×] │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘核心方法1. 添加记录privateaddRecord(){constdurationparseInt(this.newDuration)constcaloriesparseInt(this.newCalories)if(isNaN(duration)||duration0){return}if(isNaN(calories)||calories0){return}constnewRecord:ExerciseRecord{id:Date.now(),type:this.newType,duration:duration,calories:calories,date:this.getTodayString(),notes:this.newNotes.trim()}this.records.unshift(newRecord)this.clearForm()this.showAddRecordfalse}2. 获取本周统计privategetWeeklyStats(){consttodaynewDate()constweekAgonewDate(today.getTime()-7*24*60*60*1000)constweeklyRecordsthis.records.filter(record{constrecordDatenewDate(record.date)returnrecordDateweekAgorecordDatetoday})return{count:weeklyRecords.length,totalDuration:weeklyRecords.reduce((sum,record)sumrecord.duration,0),totalCalories:weeklyRecords.reduce((sum,record)sumrecord.calories,0)}}3. 删除记录privatedeleteRecord(id:number){this.recordsthis.records.filter(recordrecord.id!id)}常见问题与解决方案问题1输入验证不严格现象可以输入负数或非数字字符。解决方案constdurationparseInt(this.newDuration)if(isNaN(duration)||duration0){// 显示错误提示return}问题2日期过滤不准确现象本周统计包含了非本周的记录。解决方案// 确保正确计算一周的时间范围constweekAgonewDate(today.getTime()-7*24*60*60*1000)// 使用日期对象进行比较constweeklyRecordsthis.records.filter(record{constrecordDatenewDate(record.date)returnrecordDateweekAgorecordDatetoday})问题3统计数据不更新现象添加或删除记录后统计数据没有变化。解决方案// 确保 getWeeklyStats() 方法在每次渲染时重新计算// 不要缓存统计数据而是每次调用时重新计算privategetWeeklyStats(){// 每次调用都重新计算// ...}扩展学习可添加功能GPS定位记录运动轨迹计算运动距离显示运动路线地图心率监测连接心率设备记录心率数据心率区间分析运动计划制定运动计划计划完成追踪智能推荐运动社交分享分享运动成果好友排行榜运动挑战数据分析运动趋势图表消耗卡路里分析运动效果评估总结通过本教程您学会了数据统计如何使用 reduce 方法进行数据累加和统计。图标映射如何使用对象映射实现图标选择。日期处理如何获取、格式化和比较日期。表单处理如何处理用户输入和表单验证。列表渲染如何使用 List 和 ForEach 渲染数据列表。数据格式化如何格式化显示数据使其更加易读。这些知识点可以应用于各种数据记录和统计分析类应用的开发。