目录
前言
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新 性的移动应用软件。
一、项目目标
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新 性的移动应用软件。
二、项目介绍
黑马健康是一款功能全面的健康管理应用,它通过提供个性化的饮食记录、健康评估等功能,帮助用户轻松管理健康,改善饮食和生活习惯。无论是需要减肥塑形,还是关注日常营养摄入,黑马健康都能为用户提供定制化的服务,让健康管理变得简单而有效。
三、具体页面实现
1.饮食记录页面--记录列表
(1)页面分析:
本页面每个卡片都相似,可以编写一个卡片使用ForEach和List列表循环遍历渲染
(2)页面实现代码:
import { CommonConstants } from '../../common/constants/CommonConstants'
@Extend(Text) function grayText(){
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList {
build() {
List({space:CommonConstants.SPACE_10}){
ForEach([1,2,3,4],(item)=>{
ListItem(){
Column(){
//标题
Row({space:CommonConstants.SPACE_4}){
Image($r('app.media.ic_breakfast'))
.width(24)
Text('早餐')
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
Text('建议....')
.grayText()
Blank()
Text('212')
.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%')
//记录列表
List(){
ForEach([1,2],(item)=>{
ListItem(){
Row({space:CommonConstants.SPACE_6}){
Image($r('app.media.toast'))
.width(50)
Column({space:CommonConstants.SPACE_4}){
Text('全麦吐司')
.fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('一片')
.grayText()
}
Blank()
Text('91千卡')
.grayText()
}
.width('100%')
.padding(CommonConstants.SPACE_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)
.height('100%')
.margin({top:10})
}
@Builder deleteButton(){
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.height(20)
.fillColor(Color.Red)
.margin(5)
}
}
(3)运行截图:
2.食物列表页
(1)页面分析:
食物列表页面实现记录列表页面,用于提供各种食物,并且点击右方的绿色加号按钮就会跳转到食物列表页,显示出要添加食物的具体信息。
(2)页面实现代码:
ItemList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
import ItemModel from '../../model/ItemModel'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemList {
showPanel:(item:RecordItem) => void
@State isFood:boolean =true
build() {
Tabs(){
TabContent(){
this.TabContentBuilder(ItemModel.list(this.isFood))
}
.tabBar('全部')
ForEach(ItemModel.listItemGroupByCategory(this.isFood),(group:GroupInfo)=>{
TabContent(){
this.TabContentBuilder(group.items)
}
.tabBar(group.type.name)
})
}
.width('94%')
.height('100%')
.barMode(BarMode.Scrollable)
}
@Builder TabContentBuilder(items:RecordItem[]){
List({space:CommonConstants.SPACE_8}){
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.SPACE_6)
}
.onClick(() => this.showPanel(item))
})
}
.width('100%')
.height('100%')
}
}
(3)运行截图:
3.食物列表-底部Panel
(1)页面分析:
当你点击你想了解的具体食物信息以及添加食物数量时,会从页面底部弹出一个面板,该面板是由Panel组件实现的,该面板页面主要分为四大块,顶部日期,记录项卡片,数字键盘,以及按钮。
食物列表页面实现记录列表页面点击右方的绿色加号按钮就会会从页面底部弹出一个面板,显示出要添加食物的具体信息。面板使用Panel组件实现。面板包括日期,食物详细信息,数字键盘,按钮。
(2)页面实现代码:
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:8}){
//图片
Image(this.item.image)
.width(150)
//名称
Row() {
Text(this.item.name)
.fontWeight(CommonConstants.FONT_WEIGHT_700)
}
.backgroundColor($r('app.color.lightest_primary_color'))
.padding({top:5,bottom:5,left:10,right:10})
Divider()
.width(CommonConstants.THOUSANDTH_940)
.opacity(0.6)
//营养素
Row({space:8}){
this.NutrientInfo('热量(千卡)',this.item.calorie)
if (this.item.id<10000){
this.NutrientInfo('碳水(克)',this.item.carbon)
this.NutrientInfo('蛋白质(克)',this.item.protein)
this.NutrientInfo('脂肪(克)',this.item.fat)
}
}
Divider()
.width(CommonConstants.THOUSANDTH_940)
.opacity(0.6)
//数量
Row(){
Column({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(lable:string,value:number){
Column({space:8}){
Text(lable)
.fontSize(14)
.fontColor($r('app.color.light_gray'))
Text((value*this.amount).toFixed(1))
.fontWeight(CommonConstants.FONT_WEIGHT_700)
}
}
}
ItemList.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
import ItemModel from '../../model/ItemModel'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemList {
showPanel:(item:RecordItem) => void
@State isFood:boolean =true
build() {
Tabs(){
TabContent(){
this.TabContentBuilder(ItemModel.list(this.isFood))
}
.tabBar('全部')
ForEach(ItemModel.listItemGroupByCategory(this.isFood),(group:GroupInfo)=>{
TabContent(){
this.TabContentBuilder(group.items)
}
.tabBar(group.type.name)
})
}
.width('94%')
.height('100%')
.barMode(BarMode.Scrollable)
}
@Builder TabContentBuilder(items:RecordItem[]){
List({space:CommonConstants.SPACE_8}){
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.SPACE_6)
}
.onClick(() => this.showPanel(item))
})
}
.width('100%')
.height('100%')
}
}
ItemPanelHeader.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import DatePickDialog from '../record/DatePickDialog'
@Component
export default struct ItemPanelHeader {
@StorageProp ('selectedDate') selectedDate : number = DateUtil.beginTimeOfDay(new Date())
controller:CustomDialogController = new CustomDialogController({
builder:DatePickDialog({selectedDate:new Date(this.selectedDate)})
})
build() {
Row({space:10}){
Text(DateUtil.formatDate(this.selectedDate))
.onClick(()=>this.controller.open())
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_600)
Text('早餐')
.fontSize(18)
.fontWeight(CommonConstants.FONT_WEIGHT_600)
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
}
}
NumberKeyboard.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NumberKeyboard {
@Link amount:number
@Link value:string
numbers:string[] = ['1','2','3','4','5','6','7','8','9','0','.']
@Styles keyboxStyle(){
.backgroundColor(Color.White)
.height(60)
.borderRadius(8)
}
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){
//拼接
let val = this.value+num
//校验
let firstIndex = val.indexOf('.')
let lastIndex = val.lastIndexOf('.')
if (firstIndex!==lastIndex||(lastIndex!=-1&&lastIndex<val.length-2)) {
return
}
//转为数值
let amount = this.parseFloat(val)
//保存
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)
}
}
(3)运行截图:
总结
通过对黑马程序员鸿蒙实战项目相关课程的学习,加深了对List列表,Tabs组件的认识;更好的学会了Panel组件与Grid组件的使用方法,成功使用相关组件解决了底部弹窗的问题。