HarmonyOS5 运动健康app(一):健康饮食(附代码)

发布于:2025-06-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、核心数据模型设计

代码通过两个接口构建了饮食管理的基础数据结构:

interface footItem {
  name: string; // 营养名称(蛋白质/碳水/脂肪)
  weight: number; // 重量(克)
}

interface DietItem {
  name: string; // 食物名称
  image: string; // 图片路径(如app.media.mantou)
  weight: number; // 单份重量(克)
  calorie: number; // 单份卡路里
  count: number; // 食用数量
  nengliang: string; // 主要营养素类型
}
  • 营养元素模型(footItem):将蛋白质、碳水、脂肪抽象为基础单元,用于统计每日摄入量。
  • 食物模型(DietItem):每个食物对象包含物理属性(重量、卡路里)和营养属性(nengliang),如:
{ name: '鸡蛋', image: 'app.media.egg', weight: 13, calorie: 200, count: 0, nengliang: '蛋白质' }

其中nengliang字段建立了食物与基础营养素的映射关系,为后续营养统计提供依据。

二、主组件架构:Index组件的状态与布局
@Entry
@Component
export struct Index {
  @State progressIndex: number = 0 // 总卡路里
  @State dbzIndex: number = 0 // 总蛋白质
  @State tsIndex: number = 0 // 总碳水
  @State zfIndex: number = 0 // 总脂肪

  // 头部统计组件
  @Builder
  toubu() {
    Column({ space: 15 }) {
      // 环形卡路里进度条
      Stack() {
        Progress({ value: this.progressIndex, total: 20000, type: ProgressType.Ring })
          .width(90).height(90).style({ strokeWidth: 10 })
          .color('#4CD964').backgroundColor('#e0e0e0')
        Text(`${this.progressIndex} kcal`).fontSize(14).fontWeight(FontWeight.Bold)
      }
      // 营养素统计行
      Row() {
        ForEach(footData, (item) => {
          Column() {
            Text(this.getItemWeight(item).toString()).fontSize(18).fontColor('#333')
            Text(item.name).fontSize(14).fontColor('#666')
          }.width('30%')
        })
      }
    }
    .padding({ top: 20, bottom: 15 }).width('100%')
  }

  // 营养素与状态映射
  private getItemWeight(item: footItem): number {
    switch (item.name) {
      case '蛋白质': return this.dbzIndex
      case '碳水': return this.tsIndex
      case '脂肪': return this.zfIndex
      default: return 0
    }
  }

  build() {
    Column({ space: 15 }) {
      this.toubu()
      Text('饮食内容').fontSize(20).margin({ left: 20 })
      List() {
        ForEach(dietData, (item) => {
          ListItem() { foods({ item, progressIndex: this.progressIndex, ... }) }
        })
      }
    }
    .width('100%').padding({ left: 10, right: 10 })
  }
}
  • 状态管理:通过@State定义四大核心状态,分别追踪总卡路里和三类营养素摄入量,形成数据中枢。
  • 头部组件(toubu)
    • 环形进度条使用Progress组件,以20000kcal为目标值,绿色进度随progressIndex动态变化;
    • 营养素统计行通过ForEach遍历footData,将dbzIndex等状态映射为界面数值,实现"蛋白质:18g"等展示效果。

三、可复用组件:foods组件的交互逻辑
@Reusable
@Component
export struct foods {
  @State ifjiajian: boolean = false // 操作类型(增减)
  @Prop item: DietItem // 食物对象(只读)
  @Link progressIndex: number // 绑定总卡路里状态
  @Link dbzIndex: number // 绑定蛋白质状态(双向同步)
  
  // 卡路里计算
  calorieNUm() {
    const num = this.ifjiajian 
      ? this.item.calorie * this.item.count 
      : -this.item.calorie * (this.item.count + 1)
    this.progressIndex += num
  }

  // 营养素重量计算
  weightNUm() {
    const amount = this.ifjiajian ? this.item.count : -(this.item.count + 1)
    const weightChange = 13 * amount
    switch (this.item.nengliang) {
      case '蛋白质': this.dbzIndex += weightChange
      case '碳水': this.tsIndex += weightChange
      case '脂肪': this.zfIndex += weightChange
    }
  }

  build() {
    Row() {
      Image($r(this.item.image)).width(60).height(60).borderRadius(8)
      Column() {
        Text(this.item.name).fontSize(16).fontWeight(FontWeight.Bold)
        Text(`${this.item.weight} 克`).fontSize(14).fontColor('#777')
      }.width('40%')
      
      Column() {
        Text(`${this.item.calorie * this.item.count} 卡`).fontSize(16)
        Row() {
          Text('-').onClick(() => {
            if (this.item.count > 0) {
              this.item.count--; this.ifjiajian = false
              this.calorieNUm(); this.weightNUm()
            }
          })
          Text(`${this.item.count}`).width(30).textAlign(TextAlign.Center)
          Text('+').onClick(() => {
            this.item.count++; this.ifjiajian = true
            this.calorieNUm(); this.weightNUm()
          })
        }.width(90)
      }.width('40%')
    }
    .width('100%').padding({ left: 10, right: 10 })
  }
}
  • 状态绑定:通过@Link实现与主组件状态的双向同步,点击"+/-"按钮时,progressIndex和营养素状态会实时更新。
  • 交互逻辑
    • ifjiajian标记操作类型,增加时calorieNUm()计算正卡路里值,减少时计算负值;
    • weightNUm()根据nengliang属性(如"蛋白质")更新对应营养素总量,1份食物默认增加13克重量(与item.weight一致)。

四、数据流转与业务闭环
  1. 用户操作:点击食物卡片的"+"按钮 → item.count自增 → ifjiajian设为true
  2. 数据计算
    • calorieNUm()计算新增卡路里(如鸡蛋200卡×1份),累加到progressIndex
    • weightNUm()根据nengliang(蛋白质)计算13克重量,累加到dbzIndex
  1. 界面更新:主组件的环形进度条和营养素数值通过状态响应式机制自动刷新,形成"操作-计算-展示"的闭环。

五、附:代码


interface footItem {
  name: string; // 营养名称
  weight: number; // 重量
}

interface DietItem {
  name: string; // 食物名称
  image: string; // 食物图片路径(本地或网络,这里用占位示意)
  weight: number; // 重量
  calorie: number; // 卡路里
  count: number; // 食用数量
  nengliang: string; // 营养名称(蛋白质、脂肪、碳水)
}

const footData: footItem[] = [
  { name: '蛋白质', weight: 0 },
  { name: '碳水', weight: 0 },
  { name: '脂肪', weight: 0 },
];

const dietData: DietItem[] = [
  { name: '馒头', image: 'app.media.mantou', weight: 13, calorie: 100, count: 0, nengliang: '蛋白质' },
  { name: '油条', image: 'app.media.youtiao', weight: 13, calorie: 200, count: 0, nengliang: '脂肪' },
  { name: '豆浆', image: 'app.media.doujiang', weight: 13, calorie: 300, count: 0, nengliang: '碳水' },
  { name: '稀饭', image: 'app.media.xifan', weight: 13, calorie: 300, count: 0, nengliang: '碳水' },
  { name: '鸡蛋', image: 'app.media.egg', weight: 13, calorie: 200, count: 0, nengliang: '蛋白质' },
];

@Entry
@Component
export struct Index {

  @State progressIndex: number = 0 // 进度条进度(总大卡数)
  @State dbzIndex: number = 0 // 总蛋白质
  @State tsIndex: number = 0 // 总碳水
  @State zfIndex: number = 0 // 总脂肪

  // 头部组件
  @Builder
  toubu() {
    Column({ space: 15 }) {
      Stack() {
        Progress({
          value: this.progressIndex,
          total: 20000,
          type: ProgressType.Ring
        })
          .width(90)
          .height(90)
          .style({ strokeWidth: 10 })
          .color('#4CD964')
          .backgroundColor('#e0e0e0');
        Text(`${this.progressIndex} kcal`)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 5 })
      }

      Row() {
        ForEach(footData, (item: footItem) => {
          Column() {
            Text(this.getItemWeight(item).toString())
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333')
            Text(item.name)
              .fontSize(14)
              .fontColor('#666')
          }
          .width('30%')
        }, (item: footItem) => JSON.stringify(item))
      }
    }
    .padding({ top: 20, bottom: 15 })
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }

  // 获取对应的营养值
  private getItemWeight(item: footItem): number {
    switch (item.name) {
      case '蛋白质':
        return this.dbzIndex;
      case '碳水':
        return this.tsIndex;
      case '脂肪':
        return this.zfIndex;
      default:
        return 0;
    }
  }

  build() {
    Column({ space: 15 }) {
      this.toubu()
      Text('饮食内容')
        .fontSize(20)
        .fontColor('#555')
        .width('100%')
        .margin({ left: 20 })
      List({ space: 10 }) {
        ForEach(dietData, (item: DietItem) => {
          ListItem() {
            foods({
              item: item,
              progressIndex: this.progressIndex,
              dbzIndex: this.dbzIndex,
              tsIndex: this.tsIndex,
              zfIndex: this.zfIndex
            })
          }
        }, (item: DietItem) => JSON.stringify(item))
      }
    }
    .width('100%')
    .padding({ left: 10,right: 10 })
  }
}

// 饮食内容组件
@Reusable
@Component
export struct foods {
  @State ifjiajian: boolean = false
  @Prop item: DietItem
  @Link progressIndex: number
  @Link dbzIndex: number
  @Link tsIndex: number
  @Link zfIndex: number

  // 统计大卡数
  calorieNUm() {
    let num = this.ifjiajian ?
      this.item.calorie * this.item.count :
      -this.item.calorie * (this.item.count + 1);
    this.progressIndex += num;
  }

  // 统计能量
  weightNUm() {
    const amount = this.ifjiajian ? this.item.count : -(this.item.count + 1);
    const weightChange = 13 * amount;

    switch (this.item.nengliang) {
      case '蛋白质':
        this.dbzIndex += weightChange;
        break;
      case '碳水':
        this.tsIndex += weightChange;
        break;
      case '脂肪':
        this.zfIndex += weightChange;
        break;
    }
  }

  build() {
    Row() {
      Image($r(this.item.image))
        .width(60)
        .height(60)
        .borderRadius(8)
      Column({ space: 6 }) {
        Text(this.item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
        Text(`${this.item.weight} 克`)
          .fontSize(14)
          .fontColor('#777')
      }
      .width('40%')
      .alignItems(HorizontalAlign.Start)

      Column({ space: 6 }) {
        Text(`${this.item.calorie * this.item.count} 卡`)
          .fontSize(16)
          .fontColor('#555')

        Row() {
          Text('-')
            .fontSize(20)
            .width(25)
            .height(25)
            .textAlign(TextAlign.Center)
            .borderRadius(4)
            .border({ width: 1, color: '#ccc' })
            .onClick(() => {
              if (this.item.count > 0) {
                this.item.count--;
                this.ifjiajian = false;
                this.calorieNUm();
                this.weightNUm();
              }
            })
          Text(`${this.item.count}`)
            .fontSize(16)
            .width(30)
            .textAlign(TextAlign.Center)
          Text('+')
            .fontSize(20)
            .width(25)
            .height(25)
            .textAlign(TextAlign.Center)
            .borderRadius(4)
            .border({ width: 1, color: '#ccc' })
            .onClick(() => {
              this.item.count++;
              this.ifjiajian = true;
              this.calorieNUm();
              this.weightNUm();
            })
        }
        .justifyContent(FlexAlign.SpaceAround)
        .width(90)
      }
      .width('40%')
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .padding({ left: 10, right: 10 })
    .justifyContent(FlexAlign.SpaceBetween)
  }
}