一、项目介绍
该项目是一个关于健身运动、健康饮食的黑马健身移动应用软件;主要包括三个页面,分别是欢迎页面、统计记录页面、食物列表页面。主要实现的功能多端部署,可以在不同的设备上根据屏幕的大小判断进行页面分布;实现数据库持久化和页面交互。欢迎页面有自动弹窗,点击同意方可进入,首次进入点击后,不需要二次点击,点击不同意,则退出app。统计记录页面,分别有早餐、午餐、晚餐、加餐、运动;根据你输入的食物统计摄入的卡路里你输入的运动量统计消耗的卡路里;根据身高体重来推荐你还能摄入的碳水化合物、蛋白质、脂肪,或者应该消耗多少卡路里。食物列表页面(运动列表页面),根据父组件判断是食物列表页面(true)/运动列表页面(false),默认为食物列表页面;食物列表页面/运动列表页面还进行了食物或运动的分类。
第四天项目成果:
9、食物列表-数字键盘:
通过使用Grid组件完成了弹出面板的数字键盘和提交按钮的样式。
运行成果:
相关源码:
NumberKeyboard:
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)
.rowsGap(8)
.margin({top:10})
}
clickNumber(num:string){//输入数字点击
//1.拼接用户输入的内容
let val=this.value+num
//2.校验输入格式是否正确
let firstIndex=val.indexOf('.')//记录小数点角标
let lastIndex=val.lastIndexOf('.')//从后往前数小数点角标
//1.1.两个小数点
//2.2有小数点,但小数点位数不超过两位
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//如果超过这个值,则被赋值为999,
this.value='999'//value也被赋值为999
}else{//如果没有超过这个值,则amount和value为输入的值
this.amount=amount
this.value=val
}
}
clickDelete(){//删除点击
if(this.value.length<=0){
//如果删没了,把value置为空,把amount置0
this.value=''
this.amount=0
return
}//如果删除没有到0,则保留这一部分,从0删到length-1
this.value=this.value.substring(0,this.value.length-1)
//因为没有删完,所以amount置为0
this.amount=this.parseFloat(this.value)
}
parseFloat(str:string){
if(!str){//如果全部删完返回0
return 0
}
if(str.endsWith('.')){//判断是否以.结尾,如果是,则去除
str=str.substring(0,str.length-1)
}//去掉小数点后再去转为数值
return parseFloat(str)
}
}
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)
}
}
}
10、一次开发多端部署:
多设备响应式布局,通过不同设备的屏幕大小来进行自定义样式布局。
运行成果:
相关源码:
BreakpointSystem:
import mediaQuery from '@ohos.mediaquery'
import BreakpointConstants from '../constants/BreakpointConstants'
export default class BreakpointSystem{
//三种监听器类型的定义,用mediaQuery来获取,ts文件不能导ts文件,需要改成ets
//用MediaQuery设置媒体查询条件
//当媒体宽度发生改变时,会触发三个监听器,然后在那个范围匹配对应的回调函数
private smListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_SM)
private mdListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_MD)
private lgListener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync(BreakpointConstants.RANGE_LG)
//为监听器绑定回调函数
smListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){ //判断是否匹配,如果匹配到则进行存储
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_SM)
}
}
mdListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_MD)
}
}
lgListenerCallback(result: mediaQuery.MediaQueryResult){
if(result.matches){
this.updateCurrentBreakpoint(BreakpointConstants.BREAKPOINT_LG)
}
}
//进行抽取,帮助更新存储,
updateCurrentBreakpoint(breakpoint: string){ //然后将状态标记储存到全局存储
AppStorage.SetOrCreate(BreakpointConstants.CURRENT_BREAKPOINT, breakpoint)
}
//将回调函数与三种Listener进行绑定,on绑定监听函数
register(){
this.smListener.on('change', this.smListenerCallback.bind(this))
this.mdListener.on('change', this.mdListenerCallback.bind(this))
this.lgListener.on('change', this.lgListenerCallback.bind(this))
}
//将来在系统退出时,取消回调函数,off取消回调函数
unregister(){
this.smListener.off('change', this.smListenerCallback.bind(this))
this.mdListener.off('change', this.mdListenerCallback.bind(this))
this.lgListener.off('change', this.lgListenerCallback.bind(this))
}
}
BreakpointConstants:
import BreakpointType from '../bean/BreanpointType';
export default class BreakpointConstants {
/**
* 小屏幕设备的 Breakpoints 标记.
*/
//每个范围都指定一个状态,分别为sm,ms,lg
static readonly BREAKPOINT_SM: string = 'sm';
/**
* 中等屏幕设备的 Breakpoints 标记.
*/
static readonly BREAKPOINT_MD: string = 'md';
/**
* 大屏幕设备的 Breakpoints 标记.
*/
static readonly BREAKPOINT_LG: string = 'lg';
/**
* 当前设备的 breakpoints 存储key,用来存储到appstore全局存储。
*/
//不同范围定义不同条件
static readonly CURRENT_BREAKPOINT: string = 'currentBreakpoint';
/**
* 小屏幕设备宽度范围.
*/
static readonly RANGE_SM: string = '(320vp<=width<600vp)';
/**
* 中屏幕设备宽度范围.
*/
static readonly RANGE_MD: string = '(600vp<=width<840vp)';
/**
* 大屏幕设备宽度范围.
*/
static readonly RANGE_LG: string = '(840vp<=width)';
static readonly BAR_POSITION: BreakpointType<BarPosition> = new BreakpointType({//提前定义好常量
sm: BarPosition.End,
md: BarPosition.Start,
lg: BarPosition.Start,
})
}
11、数据模型-记录项:
食物或运动的页面列表;通过点击添加记录,在弹出的面板中通过数字键盘进行食物或运动记录的添加。
运行成果:
相关源码:
RecordItem:
/**
* 饮食记录中的记录项,可以是食物或运动
*/
export default class RecordItem{
/**
* id
*/
id: number
/**
* 名称
*/
name: ResourceStr
/**
* 图片
*/
image: ResourceStr
/**
* 分类id
*/
categoryId: number
/**
* 包含的卡路里
*/
calorie: number
/**
* 单位
*/
unit: ResourceStr
/**
* 碳水含量,单位(克)
*/
carbon: number
/**
* 蛋白质含量,单位(克)
*/
protein: number
/**
* 脂肪含量,单位(克)
*/
fat: number
constructor(id: number, name: ResourceStr, image: ResourceStr,
categoryId: number, unit: ResourceStr, calorie: number,
carbon: number = 0, protein: number = 0, fat: number = 0) {//营养素的信息设置为默认值,可选
this.id = id
this.name = name
this.image = image
this.categoryId = categoryId
this.unit = unit
this.calorie = calorie
this.protein = protein
this.fat = fat
this.carbon = carbon
}
}
ItemCategoryModel:
import ItemCategory from '../viewmodel/ItemCategory'
/**
* 食物类型的枚举
*/
enum FoodCategoryEnum{//没有赋值时,默认角标为0,1,2,3,4,5
/**
* 主食
*/
STAPLE,
/**
* 蔬果
*/
FRUIT,
/**
* 肉蛋奶
*/
MEAT,
/**
* 坚果
*/
NUT,
/**
* 其它
*/
OTHER,
}
/**
* 食物类型数组
*/
let FoodCategories = [
new ItemCategory(0, $r('app.string.staple')),
new ItemCategory(1, $r('app.string.fruit')),
new ItemCategory(2, $r('app.string.meat')),
new ItemCategory(3, $r('app.string.nut')),
new ItemCategory(4, $r('app.string.other_type')),
]
/**
* 运动类型枚举
*/
enum WorkoutCategoryEnum {//没有赋值时,默认角标为0,1,2,3,4,5
/**
* 走路
*/
WALKING,
/**
* 跑步
*/
RUNNING,
/**
* 骑行
*/
RIDING,
/**
* 跳操
*/
AEROBICS,
/**
* 游泳
*/
SWIMMING,
/**
* 打球
*/
BALLGAME,
/**
* 力量训练
*/
STRENGTH
}
/**
* 运动类型数组
*/
let WorkoutCategories = [
new ItemCategory(0, $r('app.string.walking_type')),
new ItemCategory(1, $r('app.string.running')),
new ItemCategory(2, $r('app.string.riding')),
new ItemCategory(3, $r('app.string.aerobics')),
new ItemCategory(4, $r('app.string.swimming')),
new ItemCategory(5, $r('app.string.ballgame')),
new ItemCategory(6, $r('app.string.strength')),
]
export {FoodCategories , WorkoutCategories , FoodCategoryEnum, WorkoutCategoryEnum}
ItemModel:
import RecordItem from '../viewmodel/RecordItem'
import ItemCategory from '../viewmodel/ItemCategory'
import { FoodCategories, FoodCategoryEnum, WorkoutCategories, WorkoutCategoryEnum } from './ItemCategoryModel'
import GroupInfo from '../viewmodel/GroupInfo'
const foods: RecordItem[] = [//id,name,图片,食物所属类型
new RecordItem(0, '米饭',$r('app.media.rice'),FoodCategoryEnum.STAPLE, '碗', 209, 46.6, 4.7, 0.5),
new RecordItem(1, '馒头',$r('app.media.steamed_bun'),FoodCategoryEnum.STAPLE, '个', 114, 24.0, 3.6, 0.6),
new RecordItem(2, '面包',$r('app.media.bun'),FoodCategoryEnum.STAPLE, '个', 188, 35.2, 5.0, 3.1),
new RecordItem(3, '全麦吐司',$r('app.media.toast'),FoodCategoryEnum.STAPLE, '片', 91, 15.5, 4.4, 1.3),
new RecordItem(4, '紫薯',$r('app.media.purple_potato'),FoodCategoryEnum.STAPLE, '个', 163, 42.0, 1.6, 0.4),
new RecordItem(5, '煮玉米',$r('app.media.corn'),FoodCategoryEnum.STAPLE, '根', 111, 22.6, 4.0, 1.2),
new RecordItem(6, '黄瓜',$r('app.media.cucumber'),FoodCategoryEnum.FRUIT, '根', 29, 5.3, 1.5, 0.4),
new RecordItem(7, '蓝莓',$r('app.media.blueberry'),FoodCategoryEnum.FRUIT, '盒', 71, 18.1, 0.9, 0.4),
new RecordItem(8, '草莓',$r('app.media.strawberry'),FoodCategoryEnum.FRUIT, '颗', 14, 3.1, 0.4, 0.1),
new RecordItem(9, '火龙果',$r('app.media.pitaya'),FoodCategoryEnum.FRUIT, '个', 100, 24.6, 2.2, 0.5),
new RecordItem(10, '奇异果',$r('app.media.kiwi'),FoodCategoryEnum.FRUIT, '个', 25, 8.4, 0.5, 0.3),
new RecordItem(11, '煮鸡蛋',$r('app.media.egg'),FoodCategoryEnum.MEAT, '个', 74, 0.1, 6.2, 5.4),
new RecordItem(12, '煮鸡胸肉',$r('app.media.chicken_breast'),FoodCategoryEnum.MEAT, '克', 1.15, 0.011, 0.236, 0.018),
new RecordItem(13, '煮鸡腿肉',$r('app.media.chicken_leg'),FoodCategoryEnum.MEAT, '克', 1.87, 0.0, 0.243, 0.092),
new RecordItem(14, '牛肉',$r('app.media.beef'),FoodCategoryEnum.MEAT, '克', 1.22, 0.0, 0.23, 0.033),
new RecordItem(15, '鱼肉',$r("app.media.fish"),FoodCategoryEnum.MEAT, '克', 1.04, 0.0, 0.206, 0.024),
new RecordItem(16, '牛奶',$r("app.media.milk"),FoodCategoryEnum.MEAT, '毫升', 0.66, 0.05, 0.03, 0.038),
new RecordItem(17, '酸奶',$r("app.media.yogurt"),FoodCategoryEnum.MEAT, '毫升', 0.7, 0.10, 0.032, 0.019),
new RecordItem(18, '核桃',$r("app.media.walnut"),FoodCategoryEnum.NUT, '颗', 42, 1.2, 1.0, 3.8),
new RecordItem(19, '花生',$r("app.media.peanut"),FoodCategoryEnum.NUT, '克', 3.13, 0.13, 0.12, 0.254),
new RecordItem(20, '腰果',$r("app.media.cashew"),FoodCategoryEnum.NUT, '克', 5.59, 0.416, 0.173, 0.367),
new RecordItem(21, '无糖拿铁',$r("app.media.coffee"),FoodCategoryEnum.OTHER, '毫升', 0.43, 0.044, 0.028, 0.016),
new RecordItem(22, '豆浆',$r("app.media.soybean_milk"),FoodCategoryEnum.OTHER, '毫升', 0.31, 0.012, 0.030, 0.016),
]
const workouts: RecordItem[] = [//id,name,图片,运动所属类型
new RecordItem(10000, '散步',$r('app.media.ic_walk'), WorkoutCategoryEnum.WALKING, '小时', 111),
new RecordItem(10001, '快走',$r('app.media.ic_walk'), WorkoutCategoryEnum.WALKING, '小时', 343),
new RecordItem(10002, '慢跑',$r('app.media.ic_running'), WorkoutCategoryEnum.RUNNING, '小时', 472),
new RecordItem(10003, '快跑',$r('app.media.ic_running'), WorkoutCategoryEnum.RUNNING, '小时', 652),
new RecordItem(10004, '自行车',$r('app.media.ic_ridding'), WorkoutCategoryEnum.RIDING, '小时', 497),
new RecordItem(10005, '动感单车',$r('app.media.ic_ridding'), WorkoutCategoryEnum.RIDING, '小时', 587),
new RecordItem(10006, '瑜伽',$r('app.media.ic_aerobics'), WorkoutCategoryEnum.AEROBICS, '小时', 172),
new RecordItem(10007, '健身操',$r('app.media.ic_aerobics'), WorkoutCategoryEnum.AEROBICS, '小时', 429),
new RecordItem(10008, '游泳',$r('app.media.ic_swimming'), WorkoutCategoryEnum.SWIMMING, '小时', 472),
new RecordItem(10009, '冲浪',$r('app.media.ic_swimming'), WorkoutCategoryEnum.SWIMMING, '小时', 429),
new RecordItem(10010, '篮球',$r('app.media.ic_basketball'), WorkoutCategoryEnum.BALLGAME, '小时', 472),
new RecordItem(10011, '足球',$r('app.media.ic_football'), WorkoutCategoryEnum.BALLGAME, '小时', 515),
new RecordItem(10012, '排球',$r("app.media.ic_volleyball"), WorkoutCategoryEnum.BALLGAME, '小时', 403),
new RecordItem(10013, '羽毛球',$r("app.media.ic_badminton"), WorkoutCategoryEnum.BALLGAME, '小时', 386),
new RecordItem(10014, '乒乓球',$r("app.media.ic_table_tennis"), WorkoutCategoryEnum.BALLGAME, '小时', 257),
new RecordItem(10015, '哑铃飞鸟',$r("app.media.ic_dumbbell"), WorkoutCategoryEnum.STRENGTH, '小时', 343),
new RecordItem(10016, '哑铃卧推',$r("app.media.ic_dumbbell"), WorkoutCategoryEnum.STRENGTH, '小时', 429),
new RecordItem(10017, '仰卧起坐',$r("app.media.ic_sit_up"), WorkoutCategoryEnum.STRENGTH, '小时', 515),
]
class ItemModel{
getById(id: number, isFood: boolean = true){//首选判断,判断查询是为什么,true为食物,false为运动
return isFood ? foods[id] : workouts[id - 10000] //通过角标去找,由于运动角标从10000开始,所以减去10000就从0开始了
}
//查询所有
list(isFood: boolean = true): RecordItem[]{//根据判断查询true为食物,false为运动
return isFood ? foods : workouts
}
//按组查询,自动封装到一组一组
listItemGroupByCategory(isFood: boolean = true){//进行分组,传入true为食物,传入false为运动
// 1.判断要处理的是食物还是运动
let categories = isFood ? FoodCategories : WorkoutCategories
let items = isFood ? foods: workouts
// 2.创建空的分组,map映射遍历前面数组ItemGroup元素转成另一种groupInfo元素
let groups = categories.map(itemCategory => new GroupInfo(itemCategory, []))
// 3.遍历记录项列表,将食物添加到对应的分组,根据类型枚举,进行食物分组,根据角标去取食物并放到那一组
items.forEach(item => groups[item.categoryId].items.push(item))
// 4.返回结果
return groups
}
}
let itemModel = new ItemModel()
export default itemModel as ItemModel
总结:
了解以下知识点
1、Grid:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
2、GrigItem:网格容器中单项内容容器
3、indexOf:查找当前对象中第一次出现value的索引,如果不包含value,则为-1。
4、endsWith:endsWith(field: string, value: string): DataAbilityPredicates,配置谓词以匹配数据类型为string且值以指定字符串结尾的字段。
4、List:获取指定路径下全部文件的列表。