代码功能概述

实现了一个功能完整的鸿蒙健身追踪器应用,全面展示了ArkTS在健康数据管理、运动记录、进度追踪和可视化展示等方面的核心能力。主要功能包括:

  • 运动记录:记录步数、跑步距离、卡路里消耗等健康数据
  • 目标设定:设置每日运动目标并追踪完成进度
  • 数据统计:显示今日、本周、本月的运动数据统计
  • 健康分析:提供简单的健康建议和趋势分析
  • 成就系统:解锁运动成就,激励用户坚持锻炼
  • 历史记录:查看历史运动数据变化趋势
  • 个人资料:管理用户基本信息和个人目标

2. 代码逻辑分析

应用采用"健康数据驱动UI"的架构设计:

  1. 初始化阶段:应用启动时,加载用户健康数据和运动记录
  2. 状态管理:使用多个@State装饰器管理运动数据、目标设置、统计信息和用户资料
  3. 数据记录流程
    • 手动记录运动 → 更新今日数据 → 重新计算统计信息
    • 自动模拟数据 → 定时更新步数 → 实时刷新显示
  4. 目标追踪
    • 设置运动目标 → 计算完成进度 → 更新进度显示
    • 达成目标 → 显示庆祝效果 → 更新成就状态
  5. 统计分析
    • 聚合历史数据 → 计算趋势变化 → 生成可视化图表
    • 比较不同周期 → 提供健康建议 → 更新分析结果
  6. 界面更新:所有数据变化实时反映在UI上,提供流畅的用户体验

完整代码

@Entry
@Component
struct FitnessTrackerTutorial {
  @State stepCount: number = 0;
  @State distance: number = 0;
  @State calories: number = 0;
  @State dailyGoal: number = 10000;
  @State workoutHistory: WorkoutRecord[] = [];
  @State currentView: string = 'dashboard';
  @State userProfile: UserProfile = new UserProfile();
  @State achievements: Achievement[] = [];

  aboutToAppear() {
    this.loadSampleData();
    this.startStepSimulation();
  }

  build() {
    Column({ space: 0 }) {
      this.BuildNavigation()
      
      if (this.currentView === 'dashboard') {
        this.BuildDashboard()
      } else if (this.currentView === 'history') {
        this.BuildHistoryView()
      } else if (this.currentView === 'profile') {
        this.BuildProfileView()
      } else if (this.currentView === 'achievements') {
        this.BuildAchievementsView()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F9FA')
  }

  @Builder BuildNavigation() {
    Row({ space: 0 }) {
      this.BuildNavItem('数据', 'dashboard', '📊')
      this.BuildNavItem('历史', 'history', '📈')
      this.BuildNavItem('成就', 'achievements', '🏆')
      this.BuildNavItem('我的', 'profile', '👤')
    }
    .width('100%')
    .height(60)
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 2, color: '#000000', offsetX: 0, offsetY: 1 })
  }

  @Builder BuildNavItem(title: string, view: string, icon: string) {
    Button(icon + '\n' + title)
      .onClick(() => {
        this.currentView = view;
      })
      .backgroundColor(this.currentView === view ? '#4A90E2' : '#FFFFFF')
      .fontColor(this.currentView === view ? '#FFFFFF' : '#666666')
      .fontSize(12)
      .borderRadius(0)
      .layoutWeight(1)
      .height(60)
  }

  @Builder BuildDashboard() {
    Scroll() {
      Column({ space: 20 }) {
        this.BuildTodayOverview()
        this.BuildGoalProgress()
        this.BuildQuickActions()
        this.BuildHealthTips()
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .layoutWeight(1)
  }

  @Builder BuildTodayOverview() {
    Column({ space: 15 }) {
      Text('今日运动')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .alignSelf(ItemAlign.Start)
      
      Row({ space: 20 }) {
        this.BuildMetricCard('步数', this.stepCount.toString(), '👣')
        this.BuildMetricCard('距离', this.distance.toFixed(1) + 'km', '🛣️')
        this.BuildMetricCard('卡路里', this.calories.toString(), '🔥')
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildMetricCard(title: string, value: string, icon: string) {
    Column({ space: 8 }) {
      Text(icon)
        .fontSize(24)
      
      Text(value)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      
      Text(title)
        .fontSize(12)
        .fontColor('#666666')
    }
    .layoutWeight(1)
    .padding(15)
    .backgroundColor('#F8F9FA')
    .borderRadius(12)
  }

  @Builder BuildGoalProgress() {
    const progress = Math.min(this.stepCount / this.dailyGoal, 1);
    
    Column({ space: 15 }) {
      Row({ space: 10 }) {
        Text('今日目标')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1A1A1A')
          .layoutWeight(1)
        
        Text(`${this.stepCount} / ${this.dailyGoal}`)
          .fontSize(16)
          .fontColor('#666666')
      }
      .width('100%')
      
      Stack() {
        Rect()
          .width('100%')
          .height(12)
          .fill('#E9ECEF')
          .borderRadius(6)
        
        Rect()
          .width(`${progress * 100}%`)
          .height(12)
          .fill(progress >= 1 ? '#28A745' : '#4A90E2')
          .borderRadius(6)
      }
      .width('100%')
      .height(12)
      
      if (progress >= 1) {
        Text('🎉 恭喜完成今日目标!')
          .fontSize(14)
          .fontColor('#28A745')
          .fontWeight(FontWeight.Medium)
      }
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildQuickActions() {
    Column({ space: 15 }) {
      Text('快速记录')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')
        .alignSelf(ItemAlign.Start)
      
      Row({ space: 15 }) {
        Button('步行\n+1000步')
          .onClick(() => {
            this.addSteps(1000);
          })
          .backgroundColor('#4A90E2')
          .fontColor('#FFFFFF')
          .borderRadius(12)
          .layoutWeight(1)
          .height(80)
        
        Button('跑步\n+2km')
          .onClick(() => {
            this.addWorkout('running', 2000, 120);
          })
          .backgroundColor('#FF6B6B')
          .fontColor('#FFFFFF')
          .borderRadius(12)
          .layoutWeight(1)
          .height(80)
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildHealthTips() {
    const tip = this.getHealthTip();
    
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Text('💡')
          .fontSize(18)
        
        Text('健康建议')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1A1A1A')
          .layoutWeight(1)
      }
      .width('100%')
      
      Text(tip)
        .fontSize(14)
        .fontColor('#666666')
        .lineHeight(20)
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#E7F3FF')
    .borderRadius(16)
  }

  @Builder BuildHistoryView() {
    Column({ space: 20 }) {
      Text('运动历史')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .margin({ top: 20 })
      
      if (this.workoutHistory.length === 0) {
        this.BuildEmptyState('暂无运动记录', '开始你的第一次运动吧!')
      } else {
        List() {
          ForEach(this.workoutHistory.slice().reverse(), (record: WorkoutRecord) => {
            ListItem() {
              this.BuildHistoryItem(record)
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(Color.Transparent)
      }
    }
    .width('100%')
    .padding(20)
  }

  @Builder BuildHistoryItem(record: WorkoutRecord) {
    Row({ space: 15 }) {
      Column({ space: 5 }) {
        Text(record.date)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1A1A1A')
          .alignSelf(ItemAlign.Start)
        
        Text(`${record.steps} 步 · ${record.distance}km · ${record.calories}卡`)
          .fontSize(14)
          .fontColor('#666666')
          .alignSelf(ItemAlign.Start)
      }
      .layoutWeight(1)
      
      Text(this.getWorkoutIcon(record.type))
        .fontSize(20)
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ bottom: 10 })
  }

  @Builder BuildProfileView() {
    Scroll() {
      Column({ space: 20 }) {
        this.BuildUserInfo()
        this.BuildGoalSetting()
        this.BuildStatistics()
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .layoutWeight(1)
  }

  @Builder BuildUserInfo() {
    Column({ space: 15 }) {
      Text('个人资料')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .alignSelf(ItemAlign.Start)
      
      Row({ space: 15 }) {
        Column({ space: 8 }) {
          Text(this.userProfile.name)
            .fontSize(18)
            .fontWeight(FontWeight.Medium)
            .fontColor('#1A1A1A')
          
          Text(`${this.userProfile.age}岁 · ${this.userProfile.height}cm · ${this.userProfile.weight}kg`)
            .fontSize(14)
            .fontColor('#666666')
        }
        .layoutWeight(1)
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildGoalSetting() {
    Column({ space: 15 }) {
      Text('目标设置')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')
        .alignSelf(ItemAlign.Start)
      
      Row({ space: 10 }) {
        Text('每日步数目标:')
          .fontSize(16)
          .fontColor('#666666')
          .layoutWeight(1)
        
        TextInput({ text: this.dailyGoal.toString() })
          .onChange((value: string) => {
            const newGoal = parseInt(value) || 10000;
            this.dailyGoal = Math.max(1000, Math.min(50000, newGoal));
          })
          .type(InputType.Number)
          .width(100)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F8F9FA')
          .borderRadius(8)
          .padding(8)
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildAchievementsView() {
    Column({ space: 20 }) {
      Text('运动成就')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .margin({ top: 20 })
      
      if (this.achievements.length === 0) {
        this.BuildEmptyState('暂无成就', '开始运动解锁成就吧!')
      } else {
        Grid() {
          ForEach(this.achievements, (achievement: Achievement) => {
            GridItem() {
              this.BuildAchievementItem(achievement)
            }
          })
        }
        .columnsTemplate('1fr 1fr')
        .rowsTemplate('1fr 1fr')
        .columnsGap(15)
        .rowsGap(15)
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .padding(20)
  }

  @Builder BuildAchievementItem(achievement: Achievement) {
    Column({ space: 10 }) {
      Text(achievement.unlocked ? '🏆' : '🔒')
        .fontSize(24)
      
      Text(achievement.title)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')
        .textAlign(TextAlign.Center)
      
      Text(achievement.description)
        .fontSize(12)
        .fontColor('#666666')
        .textAlign(TextAlign.Center)
        .maxLines(2)
      
      if (!achievement.unlocked) {
        Text(`进度: ${achievement.progress}%`)
          .fontSize(10)
          .fontColor('#999999')
      }
    }
    .width('100%')
    .height(120)
    .padding(15)
    .backgroundColor(achievement.unlocked ? '#E7F3FF' : '#F8F9FA')
    .borderRadius(12)
  }

  @Builder BuildEmptyState(title: string, message: string) {
    Column({ space: 15 }) {
      Text('📊')
        .fontSize(48)
        .opacity(0.5)
      
      Text(title)
        .fontSize(18)
        .fontColor('#666666')
      
      Text(message)
        .fontSize(14)
        .fontColor('#999999')
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height(300)
    .justifyContent(FlexAlign.Center)
  }

  @Builder BuildStatistics() {
    Column({ space: 15 }) {
      Text('数据统计')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')
        .alignSelf(ItemAlign.Start)
      
      Row({ space: 15 }) {
        this.BuildStatCard('平均步数', this.getAverageSteps().toString())
        this.BuildStatCard('总距离', this.getTotalDistance().toFixed(1) + 'km')
      }
      .width('100%')
      
      Row({ space: 15 }) {
        this.BuildStatCard('总卡路里', this.getTotalCalories().toString())
        this.BuildStatCard('运动天数', this.getWorkoutDays().toString())
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }

  @Builder BuildStatCard(title: string, value: string) {
    Column({ space: 8 }) {
      Text(value)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#4A90E2')
      
      Text(title)
        .fontSize(14)
        .fontColor('#666666')
        .textAlign(TextAlign.Center)
    }
    .layoutWeight(1)
    .padding(15)
    .backgroundColor('#F8F9FA')
    .borderRadius(12)
  }

  private loadSampleData(): void {
    // 加载示例数据
    this.workoutHistory = [
      { date: '2024-01-15', steps: 12560, distance: 8.2, calories: 420, duration: 90, type: 'walking' },
      { date: '2024-01-14', steps: 9870, distance: 6.5, calories: 320, duration: 75, type: 'running' },
      { date: '2024-01-13', steps: 11340, distance: 7.8, calories: 380, duration: 85, type: 'walking' }
    ];
    
    this.achievements = [
      { id: '1', title: '起步者', description: '首次记录运动', unlocked: true, progress: 100 },
      { id: '2', title: '万步达人', description: '单日步数超过10000', unlocked: true, progress: 100 },
      { id: '3', title: '运动健将', description: '连续7天运动', unlocked: false, progress: 60 },
      { id: '4', title: '马拉松', description: '累计跑步100公里', unlocked: false, progress: 25 }
    ];
    
    // 更新今日数据
    this.updateTodayData();
  }

  private startStepSimulation(): void {
    // 模拟步数更新
    setInterval(() => {
      if (this.currentView === 'dashboard') {
        this.addSteps(Math.floor(Math.random() * 10) + 1);
      }
    }, 5000);
  }

  private addSteps(steps: number): void {
    this.stepCount += steps;
    this.distance += steps * 0.0007; // 假设步幅0.7米
    this.calories += Math.floor(steps * 0.04); // 假设每步消耗0.04卡路里
    this.updateTodayData();
  }

  private addWorkout(type: string, steps: number, duration: number): void {
    const distance = type === 'running' ? steps * 0.001 : steps * 0.0007;
    const calories = Math.floor(steps * (type === 'running' ? 0.08 : 0.04));
    
    this.stepCount += steps;
    this.distance += distance;
    this.calories += calories;
    
    const today = new Date().toISOString().split('T')[0];
    this.workoutHistory.push({
      date: today,
      steps: steps,
      distance: distance,
      calories: calories,
      duration: duration,
      type: type
    });
    
    this.updateTodayData();
  }

  private updateTodayData(): void {
    const today = new Date().toISOString().split('T')[0];
    const todayRecord = this.workoutHistory.find(record => record.date === today);
    
    if (todayRecord) {
      todayRecord.steps = this.stepCount;
      todayRecord.distance = this.distance;
      todayRecord.calories = this.calories;
    } else {
      this.workoutHistory.push({
        date: today,
        steps: this.stepCount,
        distance: this.distance,
        calories: this.calories,
        duration: 0,
        type: 'walking'
      });
    }
    
    this.checkAchievements();
  }

  private checkAchievements(): void {
    // 检查并更新成就状态
    this.achievements.forEach(achievement => {
      if (!achievement.unlocked) {
        switch(achievement.id) {
          case '3':
            achievement.progress = this.getConsecutiveDays();
            if (achievement.progress >= 7) achievement.unlocked = true;
            break;
          case '4':
            achievement.progress = Math.min(this.getTotalDistance(), 100);
            if (achievement.progress >= 100) achievement.unlocked = true;
            break;
        }
      }
    });
  }

  private getHealthTip(): string {
    if (this.stepCount < 5000) {
      return '今天运动量较少,建议多走动,可以尝试每小时站起来活动5分钟。';
    } else if (this.stepCount < 10000) {
      return '继续保持!适当增加运动量有助于提高新陈代谢。';
    } else {
      return '很棒!你已达成今日目标,继续保持健康的生活习惯。';
    }
  }

  private getWorkoutIcon(type: string): string {
    switch(type) {
      case 'running': return '🏃';
      case 'walking': return '🚶';
      default: return '🏃';
    }
  }

  private getAverageSteps(): number {
    if (this.workoutHistory.length === 0) return 0;
    const total = this.workoutHistory.reduce((sum, record) => sum + record.steps, 0);
    return Math.floor(total / this.workoutHistory.length);
  }

  private getTotalDistance(): number {
    return this.workoutHistory.reduce((sum, record) => sum + record.distance, 0);
  }

  private getTotalCalories(): number {
    return this.workoutHistory.reduce((sum, record) => sum + record.calories, 0);
  }

  private getWorkoutDays(): number {
    return this.workoutHistory.length;
  }

  private getConsecutiveDays(): number {
    // 简化实现,返回最近连续运动天数
    return Math.min(this.workoutHistory.length, 7);
  }
}

class WorkoutRecord {
  date: string = '';
  steps: number = 0;
  distance: number = 0;
  calories: number = 0;
  duration: number = 0;
  type: string = 'walking';
}

class UserProfile {
  name: string = '用户';
  age: number = 25;
  weight: number = 65;
  height: number = 170;
  dailyStepGoal: number = 10000;
}

class Achievement {
  id: string = '';
  title: string = '';
  description: string = '';
  unlocked: boolean = false;
  progress: number = 0;
}

想入门鸿蒙开发又怕花冤枉钱?别错过!现在能免费系统学 -- 从 ArkTS 面向对象核心的类和对象、继承多态,到吃透鸿蒙开发关键技能,还能冲刺鸿蒙基础 +高级开发者证书,更惊喜的是考证成功还送好礼!快加入我的鸿蒙班,一起从入门到精通,班级链接:点击免费进入

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐