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

一、核心数据模型设计

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

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一致)。

四、数据流转与业务闭环

用户操作:点击食物卡片的"+"按钮 → item.count自增 → ifjiajian设为true。 

数据计算: 

calorieNUm()计算新增卡路里(如鸡蛋200卡×1份),累加到progressIndex; 

weightNUm()根据nengliang(蛋白质)计算13克重量,累加到dbzIndex。

界面更新:主组件的环形进度条和营养素数值通过状态响应式机制自动刷新,形成"操作-计算-展示"的闭环。

五、附:代码

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)

  }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容