文章目录
前言
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新性的移动应用软件。
一、项目介绍
黑马健康是一款功能全面的健康管理应用,它通过提供个性化的饮食记录、健康评估等功能,帮助用户轻松管理健康,改善饮食和生活习惯。无论是需要减肥塑形,还是关注日常营养摄入,黑马健康都能为用户提供定制化的服务,让健康管理变得简单而有效。
二、应用运行
1.食物列表页
(1)页面整体分析
(2)代码如下(示例)
ItemIndex.ets
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyboard from '../view/item/NumberKeyboard'
@Entry
@Component
struct ItemIndex {
@State amount:number=1//状态变量
@State value:string=''//用户按键的内容记录在value内 amount最终结果
@State showPanel:boolean=false//默认不展示
onPanelShow(){
this.amount=1//每次弹出,需要初始化为原始状态
this.value=''//每次弹出,初始化为原始状态
this.showPanel=true
}
build() {
Column() {
//1.头部导航
this.Header()
//2.列表
ItemList({showPanel:this.onPanelShow.bind(this)})
.layoutWeight(1)//除头部外 剩下都被列表占用 这样高度固定
//3.底部面板
/*Panel 可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切换。
需要传入布尔类型的参数 上方有一个状态变量*/
Panel(this.showPanel){
//3.1.顶部日期
ItemPanelHeader()
//3.2.记录顶卡片
ItemCard({amount:this.amount})
//3.3.数字键盘
NumberKeyboard({amount:$amount,value:$value})
//3.4.按钮
Row({space:CommonConstants.SPACE_6}){
Button('取消')
.width(120)
.backgroundColor($r('app.color.light_gray'))
.type(ButtonType.Normal)
.borderRadius(6)
.onClick(()=>this.showPanel=false)//先关闭窗口
Button('提交')
.width(120)
.backgroundColor($r('app.color.primary_color'))
.type(ButtonType.Normal)
.borderRadius(6)
.onClick(()=>this.showPanel=false)//先关闭窗口
}
.margin({top:10})
}
.mode(PanelMode.Full)//mode是一个枚举 Full默认展现全部
.dragBar(false)//不能调整高度
.backgroundMask($r('app.color.light_gray'))//背后颜色
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
@Builder Header(){
Row(){
Image($r('app.media.ic_public_back'))
.width(24)
.onClick(() => router.back())
Blank()
Text('早餐').fontWeight(CommonConstants.FONT_WEIGHT_500)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
ItemList .ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemList {//导出
//函数 先无参无返回值 将来点击传入
showPanel:()=>void
build() {
Tabs(){//Tabs本身就是个容器 Tab组件可以实现页面内视图内容快速切换,包含Tabar和TabContent两个部分
TabContent() {
this.TabContentBuilder()
}
.tabBar('全部')
TabContent() {
this.TabContentBuilder()
}
.tabBar('主食')
TabContent() {
this.TabContentBuilder()
}
.tabBar('肉蛋奶')
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
}
@Builder TabContentBuilder(){
List({space:CommonConstants.SPACE_10}) {
ForEach([1, 2, 3, 4, 5, 6], (item) => {
ListItem() {
Row({ space: CommonConstants.SPACE_4 }) {
Image($r('app.media.toast')).width(50)
Column({ space: CommonConstants.SPACE_4 }) {
Text('全麦吐司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('91千卡/片').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())
})
}
.width('100%')
.height('100%')
}
}
(3)运行效果截图
2.食物列表-底部Panel
(1)页面整体分析
(2)代码如下(示例)
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'
@Component
export default struct ItemCard {
@Prop amount: number//状态变量 Prop不能初始化
build() {
Column({space:CommonConstants.SPACE_8}){
//1.图片
Image($r('app.media.toast')).width(150)
//2.名称
Row(){
Text('全麦吐司') .fontWeight(CommonConstants.FONT_WEIGHT_700)//为了添加颜色 将其放入Row容器中
}
.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('热量(千卡)',91.0)
this.NutrientInfo('碳水(克)',15.5)
this.NutrientInfo('蛋白质(克)',4.4)
this.NutrientInfo('脂肪(克)',1.3)
}
Divider()//下划线
.width(CommonConstants.THOUSANDTH_940)
.opacity(0.6)//透明度
//4.数量
Row(){
Column({space:CommonConstants.SPACE_4}){
Text(this.amount.toFixed(1))//1位小数
.fontSize(50).fontColor($r('app.color.primary_color'))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
Divider().color($r('app.color.primary_color'))
}
.width(150)
Text('片')
.fontColor($r('app.color.primary_color'))
.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)
}
}
}
(3)运行效果截图
3.食物列表-数字键盘
(1)页面整体分析
主要实现键盘部分
(2)代码如下(示例)
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NumberKeyboard {
numbers: string[]=['1','2','3','4','5','6','7','8','9','0','.']//why?
@Link amount:number//Link双向绑定
@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.clickDelect())
}
.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('.')//记录val里面的小数点 从前往后
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
}
}
clickDelect(){
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)
}
//3.(1) 转换
parseFloat(str:string){
if(!str){
return 0;
}
if(str.endsWith('.')){//是否以'.'结尾
str=str.substring(0,str.length-1)//去除最后一位 从0开始去除 一直到长度减一
}
return parseFloat(str)
}
}
(3)运行效果截图
总结
知识补充
1.Panel
Panel: 可滑动面板,提供一种轻量的内容展示窗口,方便在不同尺寸中切 换。需要传入布尔类型的参数,控制Panel显示或隐藏。
说明:如果设置为false时,则不占位隐藏。Visible.None或者show之间有一个生效时,都会生效不占位隐藏。
2.Grid容器
Grid:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
包含GridItem子组件。
olumnsTemplate ‘1fr 1fr 2fr’ 是将父组件分3列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占2份。
说明:设置为’0fr’时,该列的列宽为0,不显示GridItem。设置为其他非法值时,GridItem显示为固定1列。
问题优化
1.在ItemCard.ets中设置状态变量:@Prop amount:number
用Prop类型原因:
数量是一个动态值,所以定义为状态变量。并且此组件是用来渲染的组件,与键盘不在一个组件里,键盘负责修改,此处负责显示,因而用Prop接收。
2.食物列表页中TabContent代码优化,可将List封装到@Builder中
@Builder TabContentBuilder(){
List({space:CommonConstants.SPACE_10}) {
ForEach([1, 2, 3, 4, 5, 6], (item) => {
ListItem() {
Row({ space: CommonConstants.SPACE_4 }) {
Image($r('app.media.toast')).width(50)
Column({ space: CommonConstants.SPACE_4 }) {
Text('全麦吐司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('1片').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)
} //饮食不需要删除按钮
})
}
.width('100%')
.height('100%')
}
}
3.数字键盘页面中,全部删除后显示NaN
将下面代码
修改为