一個功能完整的鴻蒙健身追蹤器應用,全面展示了ArkTS在健康數據管理、運動記錄、進度追蹤和可視化展示等方面的核心能力。主要功能包括:

  • 運動記錄:記錄步數、跑步距離、卡路里消耗等健康數據
  • 目標設定:設置每日運動目標並追蹤完成進度
  • 數據統計:顯示今日、本週、本月的運動數據統計
  • 健康分析:提供簡單的健康建議和趨勢分析
  • 成就係統:解鎖運動成就,激勵用户堅持鍛鍊
  • 歷史記錄:查看歷史運動數據變化趨勢
  • 個人資料:管理用户基本信息和個人目標

 代碼邏輯分析

應用採用"健康數據驅動UI"的架構設計:

  1. 初始化階段:應用啓動時,加載用户健康數據和運動記錄
  2. 狀態管理:使用多個@State裝飾器管理運動數據、目標設置、統計信息和用户資料
  3. 數據記錄流程
  • 手動記錄運動 → 更新今日數據 → 重新計算統計信息
  • 自動模擬數據 → 定時更新步數 → 實時刷新顯示
  1. 目標追蹤
  • 設置運動目標 → 計算完成進度 → 更新進度顯示
  • 達成目標 → 顯示慶祝效果 → 更新成就狀態
  1. 統計分析
  • 聚合歷史數據 → 計算趨勢變化 → 生成可視化圖表
  • 比較不同週期 → 提供健康建議 → 更新分析結果
  1. 界面更新:所有數據變化實時反映在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 面向對象核心的類和對象、繼承多態,到吃透鴻蒙開發關鍵技能,還能衝刺鴻蒙基礎 +高級開發者證書,更驚喜的是考證成功還送好禮!快加入我的鴻蒙班,一起從入門到精通,班級鏈接:點擊免費進入