一、项目介绍:
该项目是一个关于健身运动、健康饮食的黑马健身移动应用软件;主要包括三个页面,分别是欢迎页面、统计记录页面、食物列表页面。主要实现的功能多端部署,可以在不同的设备上根据屏幕的大小判断进行页面分布;实现数据库持久化和页面交互。欢迎页面有自动弹窗,点击同意方可进入,首次进入点击后,不需要二次点击,点击不同意,则退出app。统计记录页面,分别有早餐、午餐、晚餐、加餐、运动;根据你输入的食物统计摄入的卡路里你输入的运动量统计消耗的卡路里;根据身高体重来推荐你还能摄入的碳水化合物、蛋白质、脂肪,或者应该消耗多少卡路里。食物列表页面(运动列表页面),根据父组件判断是食物列表页面(true)/运动列表页面(false),默认为食物列表页面;食物列表页面/运动列表页面还进行了食物或运动的分类。
第三天项目成果:
6、饮食记录-记录列表:
通过使用List组件完成了食物和运动的记录列表。
运行成果:
相关源码:
RecordList:
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordService from '../../service/RecordService'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordType from '../../viewmodel/RecordType'
import RecordVO from '../../viewmodel/RecordVO'
import StatsInfo from '../../viewmodel/StatsInfo'
import router from '@ohos.router'
@Extend(Text) function grayText(){
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList{
//当监控到record饮食变化,去进行统计
@Consume @Watch('handleRecordsChange') records: RecordVO[]
@State groups: GroupInfo<RecordType, RecordVO>[] = []
handleRecordsChange(){
this.groups = RecordService.calculateGroupInfo(this.records)
}
build(){//只有一组的样式,用循环遍历样式ui实现很多组
List({space:CommonConstants.SPACE_10}){
ForEach(this.groups,(group: GroupInfo<RecordType, RecordVO>)=>{
ListItem(){
Column(){
//1.分组的标题
Row({space:CommonConstants.SPACE_4}){
Image(group.type.icon)
.width(24)
Text(group.type.name)
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
Text(`建议${group.type.min}-${group.type.max}千卡`)
.grayText()
Blank()
Text(group.calorie.toFixed(0))
.fontSize(14)
.fontColor($r('app.color.primary_color'))
Text('千卡').grayText()
Image($r('app.media.ic_public_add_norm_filled'))
.width(20)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
//点击分组标题,跳转到对应的页面
.onClick(()=>{//跳到itemindex
router.pushUrl({
url:'pages/ItemIndex',
params:{type:group.type}
})
})
//2.组内记录列表
List(){
ForEach(group.items,(item:RecordVO)=>{
ListItem(){
Row({space:CommonConstants.SPACE_6}){
Image(item.recordItem.image)
.width(50)
Column({space:CommonConstants.SPACE_4}){
Text(item.recordItem.name)
.fontWeight(CommonConstants.FONT_WEIGHT_500)
Text(`${item.amount}${item.recordItem.unit}`)
.grayText()
}
Blank()
Text(`${item.calorie.toFixed(0)}千卡`)//toFixed(0)取整
.grayText()
}
.width('100%')
.padding(CommonConstants.DEFAULT_6)
}.swipeAction({end:this.deleteButton.bind(this)})
//滑动删除功能
})
}
.width('100%')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(CommonConstants.DEFAULT_18)
.padding(CommonConstants.SPACE_12)
}
})
}
.width(CommonConstants.THOUSANDTH_940)
.margin({top:10})
.height('100%')
}
@Builder deleteButton(){
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.fillColor(Color.Red)
.margin(5)
}
}
7、食物列表页:
通过Row()容器完成了食物列表的样式。
运行成果:
相关源码:
ItemList:
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
//改成true为食物,改成false为运动
@Prop isFood:boolean
build(){
Tabs(){//只设计一种样式,不同的分类再由for渲染展示。
TabContent(){
this.TabContentBuilder(ItemModel.list(this.isFood))
}
.tabBar('全部')
ForEach(ItemModel.listItemGroupByCategory(this.isFood),
(group:GroupInfo<ItemCategory, RecordItem>)=>{ //得到上面一组一组的信息
TabContent(){//由于页面是通用页面,所以不能写出各个bar进行各个接口的连接
this.TabContentBuilder(group.items)
}
.tabBar(group.type.name)//获得上面的数组
})
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
//Scrollable均使用实际布局宽度,超过总宽度可滑动,fixed平均分布宽度
.barMode(BarMode.Scrollable)
}
@Builder TabContentBuilder(items:RecordItem[]){//抽取封装在builder组件内
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'))
}
Blank()
Image($r('app.media.ic_public_add_norm_filled'))
.width(18)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.padding(CommonConstants.DEFAULT_6)
}
//需要定义一个showPanel函数声明,由父组件调用时,覆盖这个声明
//点击时传给父组件ItemIndex,再由父组件ItemIndex传给ItemCard
.onClick(()=>this.showPanel(item))//当前RecordItem对象
})
}
.width('100%')
.height('100%')
}
}
8、食物列表-底部Panel:
通过Panel组件,完成了再点击添加食物或运动记录时,弹出弹出面板,并添加了简单的面板样式。
运行成果:
相关源码:
ItemPanelHeader:
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:
import { CommonConstants } from '../common/constants/CommonConstants'
import RecordItem from '../viewmodel/RecordItem';
@Component
export default struct ItemCard{
//由于需要用的值是动态的会变,所以需要定义成状态变量,不用step的原因是,该组件负责渲染,不和键盘一个组件
//所以修改时,只需显示,所以用Prop来接受
@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'))
//输入的amount值改变,所以用传入的值乘以数量
Text(($$.value * this.amount).toFixed(1))//转成一位小数
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
}
}
}
总结:
了解以下知识点:
1、List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
2、ListItem;用来展示列表具体item,必须配合List来使用。
3、Blank:空白填充组件,在容器主轴方向上,空白填充组件具有自动填充容器空余部分的能力。仅当父组件为Row/Column时生效。
4、TabContent:仅在Tabs中使用,对应一个切换页签的内容视图。
5、Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
6、List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
7、ListItem:用来展示列表具体item,必须配合List来使用。