八、应用运行
1.饮食记录业务层开发
(1)页面整体分析
通过整合饮食记录管理系统的功能,我们实现了一个能够支持用户与页面进行实时互动的系统。该系统通过运用模型(Model)层中的增删改查操作,确保用户的饮食记录能够被持久化保存并随时更新。具体来说,我们采用了饮食记录model作为核心数据操作对象,以支持用户在界面上创建新的饮食记录、修改现有记录、删除不再需要的记录以及查询特定时间段内的记录。
为了提升数据管理的效率和用户体验,我们引入了groupInfo来实现数据的分组功能。通过分组,用户能够更直观地查看和管理自己的饮食记录,如按餐次、食品类别或日期等方式进行分类。此外,我们还利用StatsInfo模块来统计和展示用户当日的卡路里摄入、运动消耗等关键信息,帮助用户更好地监控和调整自己的饮食习惯,以达到健康管理的目的。
(2)代码如下(示例)
/*GroupInfo.ets*/
export default class GroupInfo<TYPE,ELEMENT>{//泛型 可读性会更好一点
/**
* 分组类型
*/
type:TYPE
/**
* 组内数据集合
*/
items:ELEMENT[]
/**
* 组内记录的总热量
*/
calorie:number=0
constructor(type:TYPE,items:ELEMENT[]) {
this.type=type
this.items=items
}
}
/*RecordService.ets*/
import RecordPO from '../common/bean/RecordPO'
import DateUtil from '../common/utils/DateUtil'
import ItemModel from '../model/ItemModel'
import RecordModel from '../model/RecordModel'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import GroupInfo from '../viewmodel/GroupInfo'
import RecordType from '../viewmodel/RecordType'
import RecordVO from '../viewmodel/RecordVO'
import StatsInfo from '../viewmodel/StatsInfo'
class RecordService {
/**
* 新增饮食记录
* @param typeId 记录类型id
* @param itemId 记录项id
* @param amount 记录项数量(食物量、运动时长)
* @returns 新增数量
*/
insert(typeId: number, itemId: number, amount: number): Promise<number>{
// 1.获取时间
let createTime = (AppStorage.Get('selectedDate') || DateUtil.beginTimeOfDay(new Date())) as number//numbe类型
// 2.新增
return RecordModel.insert({typeId, itemId, amount, createTime})//id可选,自增长
}
/**
* 根据id删除饮食记录
* @param id 记录id
* @returns 删除条数
*/
deleteById(id: number): Promise<number>{
return RecordModel.deleteById(id)
}
/**
* 根据日期查询饮食记录列表
* @param date 要查询的日期
* @returns 记录列表
*/
async queryRecordByDate(date: number): Promise<RecordVO[]>{
// 1.查询数据库的RecordPO
let rps = await RecordModel.listByDate(date)
// 2.将RecordPO转为RecordVO
return rps.map(rp => {
// 2.1.获取po中的基本属性
let rv = {id: rp.id, typeId: rp.typeId, amount: rp.amount} as RecordVO
// 2.2.查询记录项
rv.recordItem = ItemModel.getById(rp.itemId, rp.typeId !== RecordTypeEnum.WORKOUT)
// 2.3.计算热量
rv.calorie = rp.amount * rv.recordItem.calorie
return rv
})
}
/**
* 根据记录列表信息统计出热量、营养素信息
* @param records 饮食记录列表
* @returns 热量、营养素信息
*/
calculateStatsInfo(records: RecordVO[]): StatsInfo{
// 1.准备结果
let info = new StatsInfo()
if(!records || records.length <= 0){//没有值或长度小于零
return info
}
// 2.计算统计数据
records.forEach(r => {
if(r.typeId === RecordTypeEnum.WORKOUT){//判断是什么类型 枚举WORKOUT代表运动
// 运动,累加消耗热量
info.expend += r.calorie
}else{
// 食物,累加摄入热量、蛋白质、碳水、脂肪
info.intake += r.calorie
info.carbon += r.recordItem.carbon
info.protein += r.recordItem.protein
info.fat += r.recordItem.fat
}
})
// 3.返回
return info
}
/**
* 将记录列表按照记录类型分组
* @param records 记录列表
* @returns 分组记录信息
*/
calculateGroupInfo(records: RecordVO[]): GroupInfo<RecordType, RecordVO>[]{//分组
// 1.创建空的记录类型分组
let groups = RecordTypes.map(recordType => new GroupInfo(recordType, []))
if(!records || records.length <= 0){
return groups
}
// 2.遍历所有饮食记录,
records.forEach(record => {
// 2.1.把每个记录存入其对应类型的分组中
groups[record.typeId].items.push(record)
// 2.2.计算该组的总热量
groups[record.typeId].calorie += record.calorie
})
return groups
}
}
let recordService = new RecordService()
export default recordService as RecordService
//数据库查询的数据转换成页面需要的数据的接口全部写好
//新增 删除已全部完成
2.实现数据持久化和页面交互
(1)页面整体分析
利用前面页面的内容,实现饮食记录页面的用户交互,从而确保数据的持久化保存。这些功能包括通过增删改查操作对饮食记录进行实时管理,使用户能够轻松地实现相应功能。同时,我们还引入了数据分组(如通过groupInfo)的功能,使得用户能够更加清晰地组织和管理他们的饮食和运动记录。此外,利用StatsInfo进行卡路里摄入和运动消耗的统计,为用户提供了一个直观的数据概览,帮助他们更好地理解和控制自己的饮食习惯。总之,通过整合这些功能,我们为用户打造了一个交互性强、数据保存可靠的记录系统。
(2)代码如下(示例)
/*RecordIndex.ets*/
import DateUtil from '../../common/utils/DateUtil'
import RecordService from '../../service/RecordService'
import RecordVO from '../../viewmodel/RecordVO'
import RecordList from './RecordList'
import SearchHeader from './SearchHeader'
import StatsCard from './StatsCard'
@Component
export default struct RecordIndex {
@StorageProp('selectDate')
@Watch('aboutToAppear')//监控日期变更
selectedDate:number=DateUtil.beginTimeOfDay(new Date())
@Provide records:RecordVO[]=[]//父
@Prop @Watch('handlePageShow') isPageShow:boolean
handlePageShow(){
if(this.isPageShow){
this.aboutToAppear()
}
}
//页面加载时会查询 变更时也会查询
async aboutToAppear(){
this.records=await RecordService.queryRecordByDate(this.selectedDate)//默认今天 若用户选择日期,利用上方内容
}
build() {
Column(){
//1.头部搜索栏
SearchHeader()//定义单独组件写
//2.统计卡片
StatsCard()
//3.记录列表
RecordList()
.layoutWeight(1)//除上面 剩下的内容
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_page_background'))
}
}
/*RecordList.ets*/
import router from '@ohos.router'
import { CommonConstants } from '../../common/constants/CommonConstants'
import { RecordTypeEnum } from '../../model/RecordTypeModel'
import RecordService from '../../service/RecordService'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordType from '../../viewmodel/RecordType'
import RecordVO from '../../viewmodel/RecordVO'
@Extend(Text) function grayText(){//1.分组的标题 中 Text单独样式
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList {
@Consume @Watch('handleRecordsChanges')records:RecordVO[]//接收父提供的数据
@State groups:GroupInfo<RecordType,RecordVO>[]=[]
handleRecordsChanges(){
this.groups=RecordService.calculateGroupInfo(this.records)
}
build() {
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.light_primary_color'))
Text('千卡').grayText()
Image($r('app.media.ic_public_add_norm_filled'))
.width(20)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.onClick(()=>{
router.pushUrl({
url:'pages/ItemIndex',
params:{type:group.type}
})
})
//2.组内记录列表
List() {
ForEach(group.items, (item:RecordVO) => {
ListItem(){
Row({space:CommonConstants.SPACE_4}){
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)}千卡`).grayText()
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}.swipeAction({end:this.deleteButton.bind(this)})//左滑显示删除按钮
})
}
}
.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)
}
}
九、代码优化
(1)RecordList.ets
下面红线代码就可以修改
改为:
继续修改
下面
修改为
此处
修改为
(2)RecordList页面中,在分组标题下面添加跳转事件,跳转到ItemIndex页面
继续优化,ItemList页面中,此处代码优化
优化为

(3)信息持久化保存到数据库
修改下面代码
修改为
从而实现持久化保存。
(4)在虚拟机中添加相关项目,主页面信息不变
解决方法:在Index中定义一个@State isPageShow:boolean=false
添加此代码
接着对首页RecordIndex组件修改
改为
接着从RecordIndex中定义一个@Prop接收对象并定义一个handlePageShow函数
(5)主页面上方信息不变,未触发视图的重新渲染,小数未取整
优化:
添加取整
信息未变的原因:
Bulider函数不会触发视图重新渲染
在CalorieStats
修改下面代码
修改为
这样里面内容就变成引用,就可以触发渲染
同时,上方传值也进行修改
改为
以此类推,此页面下方类似代码也进行相关修改
同样,NutrientStats页面也进行相关操作
(6)食物页面热量一样
在ItemCard里面也是因为Builder
对ItemCard页面进行修改
十、运行效果

总结
问题修改
(1)
修改:拼写错误 StatsInfo
(2)在虚拟机运行出现闪退
修改:未对calorie初始化,将其初始化为零。