一、核心数据模型设计
代码通过两个接口构建了饮食管理的基础数据结构:
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)
}
}