【鸿蒙原生应用开发--ArkUI--013】Exercise-tracker 运动记录应用开发教程

Exercise-tracker 运动记录应用开发教程

项目介绍

项目背景

运动记录应用是一个帮助用户记录和追踪运动数据的健康管理工具。随着人们对健康生活方式的重视,运动已经成为日常生活的重要组成部分。然而,很多人在运动时缺乏系统的记录和分析,导致无法准确了解自己的运动状况和进步情况。

运动记录应用通过数字化的方式,帮助用户记录每次运动的类型、时长、消耗的卡路里等数据,并提供统计分析功能,让用户能够清晰地看到自己的运动轨迹和成果。这种可视化的反馈机制能够有效激励用户坚持运动,养成健康的生活习惯。

应用场景

  • 健身记录:记录每次运动的详细数据,包括运动类型、持续时间、消耗卡路里等。用户可以随时查看自己的运动历史。

  • 数据分析:分析运动习惯和趋势,了解哪些运动类型最常进行,哪些时间段运动最频繁。

  • 目标追踪:设定运动目标并追踪完成情况,如每周运动次数、每月运动时长等。

  • 健康监测:长期记录运动数据,为健康管理提供参考依据。

功能特性

  1. 运动记录:记录运动类型、时长、消耗卡路里和备注信息。

  2. 类型选择:支持多种运动类型,如跑步、游泳、骑行、健身、瑜伽等。

  3. 数据统计:统计本周运动次数、总时长和总消耗卡路里。

  4. 历史记录:查看历史运动记录,支持删除操作。

  5. 图标显示:根据运动类型显示相应的图标,直观易识别。

最终效果

应用采用绿色主题,象征着健康和活力。主界面包含:

  • 顶部标题栏和添加按钮
  • 本周统计卡片,显示运动次数、总时长和总消耗
  • 运动记录列表,显示每条记录的详情

技术栈

  • 开发框架:HarmonyOS NEXT (API 20+)
  • 编程语言:ArkTS
  • UI框架:ArkUI 声明式 UI
  • 核心组件:Column, Row, List, Button, TextInput, Select

知识点讲解

1. 数据统计

使用 reduce 方法进行数据统计,计算总和、平均值等。

typescript 复制代码
// 计算本周统计数据
private getWeeklyStats() {
  const today = new Date()
  const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
  
  // 过滤本周的记录
  const weeklyRecords = this.records.filter(record => {
    const recordDate = new Date(record.date)
    return recordDate >= weekAgo && recordDate <= today
  })
  
  // 计算统计数据
  return {
    // 运动次数
    count: weeklyRecords.length,
    // 总时长(使用 reduce 累加)
    totalDuration: weeklyRecords.reduce((sum, record) => sum + record.duration, 0),
    // 总消耗卡路里
    totalCalories: weeklyRecords.reduce((sum, record) => sum + record.calories, 0)
  }
}

2. 图标映射

使用对象映射实现图标选择,根据运动类型返回对应的图标。

typescript 复制代码
// 定义运动类型图标映射
private readonly typeIcons: Record<string, string> = {
  '跑步': '🏃',
  '游泳': '🏊',
  '骑行': '🚴',
  '健身': '💪',
  '瑜伽': '🧘',
  '跳绳': '⚡',
  '篮球': '🏀',
  '羽毛球': '🏸',
  '足球': '⚽',
  '网球': '🎾'
}

// 获取运动类型图标
private getTypeIcon(type: string): string {
  return this.typeIcons[type] || '🏃'  // 默认图标
}

3. 日期处理

获取和格式化日期,用于记录和筛选运动数据。

typescript 复制代码
// 获取今天的日期字符串
private getTodayString(): string {
  const today = new Date()
  const year = today.getFullYear()
  const month = (today.getMonth() + 1).toString().padStart(2, '0')
  const day = today.getDate().toString().padStart(2, '0')
  return `${year}-${month}-${day}`
}

// 格式化日期显示
private formatDate(dateStr: string): string {
  const date = new Date(dateStr)
  const month = (date.getMonth() + 1).toString().padStart(2, '0')
  const day = date.getDate().toString().padStart(2, '0')
  return `${month}月${day}日`
}

// 计算相对日期
private getRelativeDate(dateStr: string): string {
  const today = this.getTodayString()
  if (dateStr === today) return '今天'
  
  const yesterday = new Date()
  yesterday.setDate(yesterday.getDate() - 1)
  if (dateStr === this.formatDateToString(yesterday)) return '昨天'
  
  return this.formatDate(dateStr)
}

4. 表单处理

处理添加运动记录的表单数据,包括输入验证和数据格式化。

typescript 复制代码
// 表单状态
@State newType: string = '跑步'
@State newDuration: string = '30'
@State newCalories: string = '200'
@State newNotes: string = ''

// 添加运动记录
private addRecord() {
  // 验证输入
  const duration = parseInt(this.newDuration)
  const calories = parseInt(this.newCalories)
  
  if (isNaN(duration) || duration <= 0) {
    // 提示错误
    return
  }
  
  if (isNaN(calories) || calories <= 0) {
    // 提示错误
    return
  }
  
  // 创建新记录
  const newRecord: 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.showAddRecord = false
}

// 清空表单
private clearForm() {
  this.newType = '跑步'
  this.newDuration = '30'
  this.newCalories = '200'
  this.newNotes = ''
}

5. 列表渲染

使用 List 和 ForEach 渲染运动记录列表。

typescript 复制代码
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. 统计卡片

显示本周运动统计数据。

typescript 复制代码
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. 删除记录

删除不需要的运动记录。

typescript 复制代码
private deleteRecord(id: number) {
  this.records = this.records.filter(record => record.id !== id)
}

8. 类型选择

使用 Select 组件选择运动类型。

typescript 复制代码
Select(this.exerciseTypes.map(type => ({ value: type })))
  .value(this.newType)
  .width('100%')
  .height(44)
  .onSelect((index: number) => {
    this.newType = this.exerciseTypes[index]
  })

9. 输入验证

验证用户输入的数据是否有效。

typescript 复制代码
private validateDuration(input: string): boolean {
  const duration = parseInt(input)
  return !isNaN(duration) && duration > 0 && duration <= 480  // 最大8小时
}

private validateCalories(input: string): boolean {
  const calories = parseInt(input)
  return !isNaN(calories) && calories > 0 && calories <= 5000  // 最大5000千卡
}

10. 数据格式化

格式化显示数据,使其更加易读。

typescript 复制代码
// 格式化时长显示
private formatDuration(minutes: number): string {
  if (minutes < 60) {
    return `${minutes}分钟`
  }
  const hours = Math.floor(minutes / 60)
  const mins = minutes % 60
  return mins > 0 ? `${hours}小时${mins}分钟` : `${hours}小时`
}

// 格式化卡路里显示
private formatCalories(calories: number): string {
  if (calories >= 1000) {
    return `${(calories / 1000).toFixed(1)}千卡`
  }
  return `${calories}千卡`
}

完整代码解析

页面结构

复制代码
┌─────────────────────────────────┐
│  [运动记录]            [+]      │
├─────────────────────────────────┤
│  ┌───────────────────────────┐  │
│  │       本周统计            │  │
│  │  3次运动  135分钟  1000千卡│  │
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│  运动记录                       │
│  ┌───────────────────────────┐  │
│  │ 🏃 跑步                   │  │
│  │    45分钟 · 350千卡        │  │
│  │    晨跑5公里               │  │
│  │    今天            [×]    │  │
│  └───────────────────────────┘  │
│  ┌───────────────────────────┐  │
│  │ 💪 健身                   │  │
│  │    60分钟 · 400千卡        │  │
│  │    上肢训练                │  │
│  │    昨天            [×]    │  │
│  └───────────────────────────┘  │
│  ┌───────────────────────────┐  │
│  │ 🏊 游泳                   │  │
│  │    30分钟 · 250千卡        │  │
│  │    自由泳1000米            │  │
│  │    01月18日        [×]    │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

核心方法

1. 添加记录
typescript 复制代码
private addRecord() {
  const duration = parseInt(this.newDuration)
  const calories = parseInt(this.newCalories)
  
  if (isNaN(duration) || duration <= 0) {
    return
  }
  
  if (isNaN(calories) || calories <= 0) {
    return
  }
  
  const newRecord: 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.showAddRecord = false
}
2. 获取本周统计
typescript 复制代码
private getWeeklyStats() {
  const today = new Date()
  const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
  
  const weeklyRecords = this.records.filter(record => {
    const recordDate = new Date(record.date)
    return recordDate >= weekAgo && recordDate <= today
  })
  
  return {
    count: weeklyRecords.length,
    totalDuration: weeklyRecords.reduce((sum, record) => sum + record.duration, 0),
    totalCalories: weeklyRecords.reduce((sum, record) => sum + record.calories, 0)
  }
}
3. 删除记录
typescript 复制代码
private deleteRecord(id: number) {
  this.records = this.records.filter(record => record.id !== id)
}

常见问题与解决方案

问题1:输入验证不严格

现象:可以输入负数或非数字字符。

解决方案

typescript 复制代码
const duration = parseInt(this.newDuration)
if (isNaN(duration) || duration <= 0) {
  // 显示错误提示
  return
}

问题2:日期过滤不准确

现象:本周统计包含了非本周的记录。

解决方案

typescript 复制代码
// 确保正确计算一周的时间范围
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)

// 使用日期对象进行比较
const weeklyRecords = this.records.filter(record => {
  const recordDate = new Date(record.date)
  return recordDate >= weekAgo && recordDate <= today
})

问题3:统计数据不更新

现象:添加或删除记录后,统计数据没有变化。

解决方案

typescript 复制代码
// 确保 getWeeklyStats() 方法在每次渲染时重新计算
// 不要缓存统计数据,而是每次调用时重新计算
private getWeeklyStats() {
  // 每次调用都重新计算
  // ...
}

扩展学习

可添加功能

  1. GPS定位

    • 记录运动轨迹
    • 计算运动距离
    • 显示运动路线地图
  2. 心率监测

    • 连接心率设备
    • 记录心率数据
    • 心率区间分析
  3. 运动计划

    • 制定运动计划
    • 计划完成追踪
    • 智能推荐运动
  4. 社交分享

    • 分享运动成果
    • 好友排行榜
    • 运动挑战
  5. 数据分析

    • 运动趋势图表
    • 消耗卡路里分析
    • 运动效果评估

总结

通过本教程,您学会了:

  1. 数据统计:如何使用 reduce 方法进行数据累加和统计。

  2. 图标映射:如何使用对象映射实现图标选择。

  3. 日期处理:如何获取、格式化和比较日期。

  4. 表单处理:如何处理用户输入和表单验证。

  5. 列表渲染:如何使用 List 和 ForEach 渲染数据列表。

  6. 数据格式化:如何格式化显示数据,使其更加易读。

这些知识点可以应用于各种数据记录和统计分析类应用的开发。

相关推荐
想你依然心痛1 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“图谱智脑“——PC端AI智能体沉浸式知识图谱构建工作台
人工智能·ar·知识图谱·harmonyos·智能体
想你依然心痛2 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“律界智脑“——PC端AI智能体沉浸式法律文档智能审查工作台
人工智能·华为·ar·harmonyos·智能体
特立独行的猫a2 小时前
鸿蒙 PC 平台 Python 第三方库移植全景指南
python·华为·harmonyos·三方库移植·鸿蒙pc
大雷神2 小时前
第31篇|位置信息写入照片记录:为什么拍照时要带上地点
harmonyos
Goway_Hui2 小时前
【鸿蒙原生应用开发--ArkUI--012】Currency-converter 汇率转换应用开发教程
华为·harmonyos
李二。3 小时前
鸿蒙 HarmonyOS 校园风登录页面开发实战 —— 基于 ArkTS 的 Stage 模型完整教程
华为·harmonyos
大雷神3 小时前
第30篇|图片文件落盘:沙箱路径、Uri 与后续读取
harmonyos
枫叶丹43 小时前
【HarmonyOS 6.0】Live View Kit 实况窗开发详解:进度胶囊支持副文本功能探究
开发语言·华为·harmonyos
想你依然心痛3 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR城市地下管网运维中心
运维·ar·harmonyos·智能体