上一期完成了黑马健康软件主页功能的实现,这一期我们继续完成对黑马健康软件的创建开发,这一期我们要完成的食物列表页的开发实现。
食物列表页实现代码参照“黑马程序员—实战案例—食物列表页”,“黑马程序员—实战案例—食物列表—底部Panel”及“黑马程序员—实战案例—数字键盘”三个视频。
食物列表页分为三个部分,头部导航,列表及底部面板,接下来我们根据视频讲解,逐步完成食物列表页的创建。
首先创建食物列表页头部导航,实现代码如下:
@Builder Header() { Row() { Image($r('app.media.ic_public_back')) .width(24) .onClick(() => router.back()) Blank() Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600) } .width(CommonConstants.THOUSANDTH_940) .height(32) } }
进行对列表的创建,列表部分是由tabs组件和列表部分组成,主要也是用foreach循环来实现列表创建的。列表的“+”号图片添加一个showpanel弹窗动作。实现代码如下:
ItemList.ets:
import { CommonConstants } from '../../common/constants/CommonConstants' import ItemModel from '../../model/ItemModel' import GroupInfo from '../../viewmodel/GroupInfo' import ItemCategory from '../../viewmodel/ItemCategory' import RecordItem from '../../viewmodel/RecordItem' @Component export default struct ItemList { showPanel: (item: RecordItem) => void @Prop isFood: boolean build() { Tabs() { TabContent() { this.TabContentBuilder(ItemModel.list(this.isFood)) } .tabBar('全部') ForEach( ItemModel.listItemGroupByCategory(this.isFood), (group: GroupInfo<ItemCategory, RecordItem>) => { TabContent() { this.TabContentBuilder(group.items) } .tabBar(group.type.name) }) } .width(CommonConstants.THOUSANDTH_940) .height('100%') .barMode(BarMode.Scrollable) } @Builder TabContentBuilder(items: RecordItem[]) { List({ space: CommonConstants.SPACE_10 }) { ForEach(items, (item: RecordItem) => { ListItem() { Row({ space: CommonConstants.SPACE_6 }) { Image(item.image).width(50) Column({ space: CommonConstants.SPACE_4 }) { Text(item.name).fontWeight(CommonConstants.FONT_WEIGHT_500) Text(`${item.calorie}千卡/${item.unit}`).fontSize(14).fontColor($r('app.color.light_gray')) }.alignItems(HorizontalAlign.Start) Blank() Image($r('app.media.ic_public_add_norm_filled')) .width(18) .fillColor($r('app.color.primary_color')) } .width('100%') .padding(CommonConstants.SPACE_6) } .onClick(() => this.showPanel(item)) }) } .width('100%') .height('100%') } }
接下来是对底部弹窗面板部分的实现,框架及代码如下:
// 3.底部面板 Panel(this.showPanel) { // 3.1.顶部日期 ItemPanelHeader() // 3.2.记录项卡片 if(this.item){ ItemCard({amount: this.amount, item: $item}) } // 3.3.数字键盘 NumberKeyboard({amount: $amount, value: $value}) // 3.4.按钮 this.PanelButton() }
顶部日期 ItemPanelHeader.ets:
import { CommonConstants } from '../../common/constants/CommonConstants' @Component export default struct ItemPanelHeader { build() { Row(){ Text('2024年1月25日 早餐') .fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600) Image($r('app.media.ic_public_spinner')) .width(20) .fillColor(Color.Black) } } }
记录项卡片 ItemCard.ets:
import { CommonConstants } from '../../common/constants/CommonConstants' import RecordItem from '../../viewmodel/RecordItem' @Component export default struct ItemCard { @Prop amount: number @Link item: RecordItem build() { Column({space: CommonConstants.SPACE_8}){ // 1.图片 Image(this.item.image).width(150) // 2.名称 Row(){ Text(this.item.name).fontWeight(CommonConstants.FONT_WEIGHT_700) } .backgroundColor($r('app.color.lightest_primary_color')) .padding({top: 5, bottom: 5, left: 12, right: 12}) Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6) // 3.营养素 Row({space: CommonConstants.SPACE_8}){ this.NutrientInfo({label: '热量(千卡)', value: this.item.calorie}) if(this.item.id < 10000){ this.NutrientInfo({label: '碳水(千卡)', value: this.item.carbon}) this.NutrientInfo({label: '蛋白质(千卡)', value: this.item.protein}) this.NutrientInfo({label: '脂肪(千卡)', value: this.item.fat}) } } Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6) // 4.数量 Row(){ Column({space: CommonConstants.SPACE_4}){ Text(this.amount.toFixed(1)) .fontSize(50).fontColor($r('app.color.primary_color')) .fontWeight(CommonConstants.FONT_WEIGHT_600) Divider().color($r('app.color.primary_color')) } .width(150) Text(this.item.unit) .fontColor($r('app.color.light_gray')) .fontWeight(CommonConstants.FONT_WEIGHT_600) } } } @Builder NutrientInfo($$:{label: string, value: number}){ Column({space: CommonConstants.SPACE_8}){ Text($$.label).fontSize(14).fontColor($r('app.color.light_gray')) Text(($$.value * this.amount).toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700) } } }
数字键盘 NumberKeyboard.ets:
import { CommonConstants } from '../../common/constants/CommonConstants' @Component export default struct NumberKeyboard { numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.'] @Link amount: number @Link value: string @Styles keyBoxStyle(){ .backgroundColor(Color.White) .borderRadius(8) .height(60) } build() { Grid(){ ForEach(this.numbers, num => { GridItem(){ Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900) } .keyBoxStyle() .onClick(() => this.clickNumber(num)) }) GridItem(){ Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900) } .keyBoxStyle() .onClick(() => this.clickDelete()) } .width('100%') .height(280) .backgroundColor($r('app.color.index_page_background')) .columnsTemplate('1fr 1fr 1fr') .columnsGap(8) .rowsGap(8) .padding(8) .margin({top: 10}) } clickNumber(num: string){ // 1.拼接用户输入的内容 let val = this.value + num // 2.校验输入格式是否正确 let firstIndex = val.indexOf('.') let lastIndex = val.lastIndexOf('.') if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){ // 非法输入 return } // 3.将字符串转为数值 let amount = this.parseFloat(val) // 4.保存 if(amount >= 999.9){ this.amount = 999.0 this.value = '999' }else{ this.amount = amount this.value = val } } clickDelete(){ if(this.value.length <= 0){ this.value = '' this.amount = 0 return } this.value = this.value.substring(0, this.value.length - 1) this.amount = this.parseFloat(this.value) } parseFloat(str: string){ if(!str){ return 0 } if(str.endsWith('.')){ str = str.substring(0, str.length - 1) } return parseFloat(str) } }
按钮 PanelButton:
@Builder PanelButton(){ Row({space: CommonConstants.SPACE_6}){ Button('取消') .panelButtonStyle() .backgroundColor($r('app.color.light_gray')) .onClick(() => this.showPanel = false) Button('提交') .panelButtonStyle() .backgroundColor($r('app.color.primary_color')) .onClick(() => { // 1.持久化保存 RecordService.insert(this.type.id, this.item.id, this.amount) .then(() => { // 2.关闭弹窗 this.showPanel = false }) }) } .margin({top: 10}) }
其中数字键盘部分较为困难,它是一个Grid容器,每一个按键都是由griditem组件组成,数字部分的按键可以由foreach循环对于我们定义的数组来实现。
最终将这些模块汇集到一起,已完成数据持久化和页面交互的功能:
Itemindex.ets:
import router from '@ohos.router' import { CommonConstants } from '../common/constants/CommonConstants' import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel' import RecordService from '../service/RecordService' import ItemCard from '../view/item/ItemCard' import ItemList from '../view/item/ItemList' import ItemPanelHeader from '../view/item/ItemPanelHeader' import NumberKeyboard from '../view/item/NumberKeyboard' import RecordItem from '../viewmodel/RecordItem' import RecordType from '../viewmodel/RecordType' @Extend(Button) function panelButtonStyle(){ .width(120) .type(ButtonType.Normal) .borderRadius(6) } @Entry @Component struct ItemIndex { @State amount: number = 1 @State value: string = '' @State showPanel: boolean = false @State item: RecordItem = null @State type: RecordType = RecordTypes[0] @State isFood: boolean = true onPanelShow(item: RecordItem) { this.amount = 1 this.value = '' this.item = item this.showPanel = true } onPageShow(){ // 1.获取跳转时的参数 let params: any = router.getParams() // 2.获取点击的饮食记录类型 this.type = params.type this.isFood = this.type.id !== RecordTypeEnum.WORKOUT } build() { Column() { // 1.头部导航 this.Header() // 2.列表 ItemList({ showPanel: this.onPanelShow.bind(this), isFood: this.isFood }) .layoutWeight(1) // 3.底部面板 Panel(this.showPanel) { // 3.1.顶部日期 ItemPanelHeader() // 3.2.记录项卡片 if(this.item){ ItemCard({amount: this.amount, item: $item}) } // 3.3.数字键盘 NumberKeyboard({amount: $amount, value: $value}) // 3.4.按钮 this.PanelButton() } .mode(PanelMode.Full) .dragBar(false) .backgroundMask($r('app.color.light_gray')) .backgroundColor(Color.White) } .width('100%') .height('100%') } @Builder PanelButton(){ Row({space: CommonConstants.SPACE_6}){ Button('取消') .panelButtonStyle() .backgroundColor($r('app.color.light_gray')) .onClick(() => this.showPanel = false) Button('提交') .panelButtonStyle() .backgroundColor($r('app.color.primary_color')) .onClick(() => { // 1.持久化保存 RecordService.insert(this.type.id, this.item.id, this.amount) .then(() => { // 2.关闭弹窗 this.showPanel = false }) }) } .margin({top: 10}) } @Builder Header() { Row() { Image($r('app.media.ic_public_back')) .width(24) .onClick(() => router.back()) Blank() Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600) } .width(CommonConstants.THOUSANDTH_940) .height(32) } }
便完成了黑马健康软件食物列表页的全部内容。
这期讲述了黑马健康软件食物列表页面的创建,我们攻克了“数字键盘”这一难题,对我们的开发软件的能力又提升了一步,下一期讲述如何实现“数据模型—记录项”。