一、核心數據模型設計
代碼通過兩個接口構建了飲食管理的基礎數據結構:
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 // 總脂肪// 頭部統計組件@Buildertoubu() {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.dbzIndexcase '碳水': return this.tsIndexcase '脂肪': return this.zfIndexdefault: 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 * amountswitch (this.item.nengliang) {case '蛋白質': this.dbzIndex += weightChangecase '碳水': this.tsIndex += weightChangecase '脂肪': 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 = falsethis.calorieNUm(); this.weightNUm()}})Text(`${this.item.count}`).width(30).textAlign(TextAlign.Center)Text('+').onClick(() => {this.item.count++; this.ifjiajian = truethis.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 // 總脂肪// 頭部組件@Buildertoubu() {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)}
}