鸿蒙期末个人项目-黑马健康——项目阶段七:饮食记录业务层开发&数据持久化和页面交互

目录

项目介绍:

分析:

饮食记录业务层开发

数据持久化和页面交互

页面实现效果:

出现的问题:

问题1:

问题2:

问题3:

问题4:

阶段项目代码:

饮食记录业务层开发关键代码

数据持久化和页面交互关键代码:


项目介绍:

黑马健康软件是一款基于全民健康的软件,主要有三个页面组成,分别是欢迎页面,统计记录页面,食物列表页面。

分析:

饮食记录业务层开发
  • 本项目主要解决的问题是如何把数据库中的数据写入页面中。
  • 在viewmodel中导入已经封装好的StatsInfo.ets,作为当日统计信息的数据模型;将写死的分组信息类GroupInfo.ets改成通用的;完成食物列表的业务逻辑层,从数据库到页面数据的转换过程,需要在ets文件夹下新建service文件夹,再新建RecordService.ets文件;
数据持久化和页面交互
  • 将查询的任务放在RecordIndex.ets中,只查询一次,并且该查询任务在build函数加载之前就开始加载:
  async aboutToAppear(){
    this.records = await RecordService.queryRecordByDate(this.selectedDate)
  }
  •  供给统计卡片 StatsCard()和 记录列表 RecordList()使用
  • 父子组件的值传递采用@Prop装饰器,父子组件单向传值,父组件向子组件传递值,子组件向父组件不传回值。
  • 其中,统计卡片 StatsCard()中有统计热量和营养素,统计热量需要从父组件StatsCard传值过来,传递总摄入热量和总消耗热量;统计营养素需要从父组件StatsCard传递值过去,传递 碳水、蛋白质和脂肪的值;记录列表 RecordList()采用和上面的方式一样在页面中渲染。
  • 页面跳转设置:当点击食物列表页中的“早餐”、“加餐”、“运动”等标签时,应实现页面跳转到ItemPage;
  • 并且判断类型的isFood也由前面的父组件ItemIndex传递过来,父组件通过生命周期函数获取页面跳转传递过来的值时,通过传递过来的点击饮食距记录的类型来更爱isFood的类型
//生命周期钩子,取出页面跳转时传递过来的值
onPageShow(){
  //1.获取页面跳转时的参数
 let params: any =  router.getParams()
  //2.获取点击的饮食记录类型
  this.type = params.type
  //3.更改isFood的类型,不是运动就等于true
  this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
}
  • 当弹出了panel框后,用户更改好信息,点击“提交”,将数据持久化保存到数据库。
 .onClick(()=>{
              //点击提交后信息持久化保存到数据库
              //1.持久化保存
              RecordService.insert(this.type.id,this.item.id,this.amount)
                .then(()=>{
                  //2.新增成功,关闭弹窗
                  this.showPanel = false
                })

            })

页面实现效果:

实现了持久化操作以及页面的交互:

当点击食物项时,图标会变绿,且页面初始时,不会变绿

出现的问题:

问题1:

点击一个食物列表组别,所有组别都变色了

解决办法:

通过forEach循环结构获取点击的食物列表组的下标,通过这个下标与当前正在渲染的下标相比较,如果相等就填充为主题色,不等就填充为浅灰色;


  //定义状态变量来记录当前的组别的下标
  @State currentItemIndex:number = 0
  //封装颜色选择
  selectColor(itemIndex){
    return this.currentItemIndex ===  itemIndex ? $r('app.color.primary_color'):$r('app.color.gray')
  }
  build() {
    List({space:CommonConstants.SPACE_12}){
      ForEach(this.groups,(group:GroupInfo<RecordType,RecordVO>,index: number)=>{
        ListItem(){
          Column(){
            // 1.分组的标题
            Row({space:CommonConstants.SPACE_4}){
              Image(group.type.icon).width(27)
                .fillColor(this.selectColor(index))
              Text(group.type.name).fontWeight(CommonConstants.FONT_WEIGHT_700).fontSize(18)
              Text(`建议${group.type.min}~${group.type.max}千卡`).grayText()
              Blank()
              Text(group.calorie.toFixed(0)).fontColor($r('app.color.primary_color')).fontSize(14)
              Text('千卡').grayText()
              Image($r('app.media.ic_public_add_norm_filled')).width(20).fillColor($r('app.color.primary_color'))
                .onClick(()=>{
                  //TODO 页面跳转
                  router.pushUrl({
                    url: "pages/ItemIndex",
                    params: {type: group.type}//传递类型参数
                  })
                  //当点击后,该图标的图像就会发生变化
                  this.currentItemIndex = index
                })
            }.width('100%')
            .onClick(()=>{
              //当点击后,该图标的颜色就会发生变化
              this.currentItemIndex = index

            })

更改之后:

问题2:

当选择的几项饮食之后,再返回主页面,却发现没有上面的Swiper组件里面没有数据

出错原因:用来查询数据———页面交互渲染的数据来源,生命周期函数只在build函数执行之前进行触发,导致跳转页面选择几项饮食之后,页面不会重新触发组件,也就不会显示数据。

aboutToAppear(){
  //在build函数加载前就注册设备监听
  this.breakpointSystem.register()
}

修改:想要在返回index时就加载数据,就要在index页面上响应,定义状态变量来判断当前是否加载的这个index页面

  @State isPageShow:boolean = false

  onPageShow(){
    this.isPageShow = true
  }
  onPageHide(){
    this.isPageShow = false
  }

之后将这个状态参数传递给RecordIndex({isPageShow:this.isPageShow})

在RecordIndex里面监听当isPageShow==true的变化,之后触发生命周期函数,重新查询数据,渲染页面。

实现效果:

问题3:

但是上面的Swiper并没有发生更新

原因:数据经过再次查询是传递过去了,但是没有进行页面渲染。因为Swiper组件所在的函数StatsCard使用了@Builder构建内部函数进行页面渲染,Builder函数默认情况下只传递的值而不是传递的引用,导致当值发生变更时,页面不会重新渲染。

修改:将builder函数的参数类型变成引用类型,在参数前面统一加上$$。

@Builder statsBuilder($$:{label:string, value:number, tips?:string}){
    Column({space:CommonConstants.SPACE_6}){
      Text($$.label)
        .fontColor($r('app.color.gray'))
        .fontWeight(CommonConstants.FONT_WEIGHT_600)
      Text($$.value.toFixed(0))
        .fontSize(20)
        .fontWeight(CommonConstants.FONT_WEIGHT_700)
      if($$.tips){
        Text($$.tips)
          .fontSize(12)
          .fontColor($r('app.color.light_gray'))

      }

    }
  }

并且传递参数值的时候就改成传递对象:

this.statsBuilder({label:'饮食摄入',value:this.intake})

同理也将ItemCard中的builder的参数等改成引用类型。

效果展示:

问题4:

尝试自己写如何删除饮食列表中已经添加的食物:

在原来渲染这个页面的基础上,将原来的删除按钮部分改成下面这种形式,最初的尝试是括号里面传递的是这个ForEach数组的下标,结果删不掉;追踪RecordService.deleteById(id:number)事件,发现需要传递的id不能是数组的下标,需要传递的是已经构建的饮食记录的页面数据模型的id,这个id才是真正映射到表的id,才能在数据库中将其删除。

.swipeAction({end:this.deleteButton(item.id)})

删除按钮的构造函数里面改成:

@Builder deleteButton(index:number){
  Image($r('app.media.ic_public_delete_filled'))
    .fillColor(Color.Red)
    .width(25)
    .margin(5)
    .onClick(()=>{
     //点击删除图标之后,执行数据的删除
      RecordService.deleteById(index)
        .then(()=>{
         //删除成功,
          console.log('删除成功','testTag')
        })

    })
}

删除之前:

删除午餐中的全麦面包之后:

阶段项目代码:

饮食记录业务层开发关键代码

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 {
  /**
   * 新增功能
   * 传递typeId
   *    itemId
   *    amount
   */
  insert(typeId:number,itemId:number,amount:number){
    // 1.获取时间,如果没取到就获取当前的时间
    let createTime =  (AppStorage.Get('selectedDate') || DateUtil.beginTimeOfDay(new Date()) ) as number
    // 2.新增,使用匿名对象
    return RecordModel.insert({typeId,itemId,amount,createTime})
  }
  /**
   * 删除操作
   */
  deleteById(id:number){
    return RecordModel.deleteById(id)
  }
  /**
   * 查询操作,封装一个查询方法,按照日期查询
   * 数据查询一遍就可以,查询保存在RecordVO[]中
   */
 async  queryRecordByDate(date:number): Promise<RecordVO[]>{//TODO 注意这里不能直接返回因为是异步,要加上Promise
    // 1.查询数据库的RecordPO
    let rps = await RecordModel.listByDate(date)
    // 2.将recordPO转换为recordVO
    return rps.map(rp =>{
      // 2.1 获取PO和VO中共同的
      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
    })
  }

  /**
   * 把RecordVO[]数组中的数据转成StatsInfo
   *
   *
   */
  calculateStatsInfo(records:RecordVO[]):StatsInfo{
      // 1.准备结果
      let info = new StatsInfo()
      // 2.计算统计数据
      if(!records || records.length<=0){
        return info
      }
      records.forEach(r=>{
          //累加
          if(r.typeId === RecordTypeEnum.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
  }

  /**
   * 把RecordVO[]数组中的数据转成GroupInfo,相当于将查到的数据分组
   *
   */
    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
数据持久化和页面交互关键代码:
/**
 * 首页开发
 */
import BreakpointType from '../common/bean/BreanpointType'
import BreakpointConstants from '../common/constants/BreakpointConstants'
import { CommonConstants } from '../common/constants/CommonConstants'
import BreakpointSystem from '../common/utils/BreakpointSystem'
import RecordIndex from '../view/record/RecordIndex'

@Entry
@Component
struct Index {

  //定义状态变量来记录当前的页面下标
  @State currentIndex:number = 0

  private breakpointSystem:BreakpointSystem = new BreakpointSystem()
  //定义当前的设备并初始化
  @StorageProp('currentBreakpoint') currentBreakpoint:string = BreakpointConstants.BREAKPOINT_SM
  //
  @State isPageShow:boolean = false

  onPageShow(){
    this.isPageShow = true
  }
  onPageHide(){
    this.isPageShow = false
  }
  //定义内部构造函数,参数直接传题目,图片,下标
  @Builder TabBarBuilder(title:ResourceStr,image:ResourceStr, index:number){
    Column({space:CommonConstants.SPACE_8}){
      Image(image)
        .width(22)
        .fillColor(this.selectColor(index))
      Text(title)
        .fontSize(14)
        .fontColor(this.selectColor(index))
    }
  }

  aboutToAppear(){
    //在build函数加载前就注册设备监听
    this.breakpointSystem.register()
  }
  aboutToDisappear(){
    //在退出应用时取消注册
    this.breakpointSystem.unregister()
  }
  //封装颜色选择
  selectColor(index){
    return this.currentIndex ===  index ? $r('app.color.primary_color'):$r('app.color.gray')
  }

  build() {
    //让导航栏位于页面底部
    //根据设备选择导航的布局
    Tabs({barPosition: BreakpointConstants.BAR_POSITION.getValue(this.currentBreakpoint)}){
      TabContent(){
        // 饮食记录页面
        RecordIndex({isPageShow:this.isPageShow})

      }.tabBar(this.TabBarBuilder($r('app.string.tab_record'),$r('app.media.ic_calendar'),0))
      TabContent(){
        Text('发现页面')
      }.tabBar(this.TabBarBuilder($r('app.string.tab_discover'),$r('app.media.discover'),1))

      TabContent(){
        Text('我的主页')
      }.tabBar(this.TabBarBuilder($r('app.string.tab_user'),$r('app.media.ic_user_portrait'),2))



    }
    .width('100%')
    .height('100%')
    .vertical(new BreakpointType({
      //传参
      sm:false,
      md:true,
      lg:true
    }).getValue(this.currentBreakpoint))
    .onChange(index => this.currentIndex = index)//也可以直接对TabBarBuilder加点击事件
  }
}
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import RecordService from '../service/RecordService'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyBoard from '../view/item/NumberKeyBoard'
import RecordItem from '../viewmodel/RecordItem'
import RecordType from '../viewmodel/RecordType'
/**
 * 食物记录页面
 */
@Entry
@Component
struct ItemIndex {
  @State value: string = ''//记录按下的键的内容
  @State amount: number  = 1 //需要和其他组件互动
  @State showPanel:boolean = false
  @State item:RecordItem = null //并且将数组传入的数据项记录下来
  @State type:RecordType = RecordTypes[0]
  @State isFood:boolean =  true

  onPanelShow(item:RecordItem){
    this.value = ''
    this.amount = 1
    this.item = item
    this.showPanel = true
  }
  //生命周期钩子,取出页面跳转时传递过来的值
  onPageShow(){
    //1.获取页面跳转时的参数
   let params: any =  router.getParams()
    //2.获取点击的饮食记录类型
    this.type = params.type
    //3.更改isFood的类型,不是运动就等于true
    this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
  }
  build() {
   Column(){
     // 1.头部导航栏
       this.Header()
     // 2. 列表
       ItemList({showPanel:this.onPanelShow.bind(this),isFood:this.isFood})
        .layoutWeight(1)
     // 3.TODO 底部面板
      Panel(this.showPanel) {
        // 3.1顶部日期
            ItemPanelHeader()
        // 3.2记录项信息卡片
        if(this.item){
          ItemCard({amount:this.amount,item:$item})
        }

        // 3.3数字键盘
           NumberKeyBoard({amount:$amount,value:$value})//对象传递值时使用$(link),不用this,
        // 3.4按钮
        Row({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(()=>{
              //点击提交后信息持久化保存到数据库
                  //1.持久化保存,插入是往表中插入
              RecordService.insert(this.type.id,this.item.id,this.amount)
                .then(()=>{
                  //2.新增成功,关闭弹窗
                  this.showPanel = false

                })

            })
        }.margin({top:10})

      }.mode(PanelMode.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(25)
        .onClick(()=> router.back())
        Blank()
        Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
    }.width('94%')
    .height(36)
  }
}

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('selectedDate') selectedDate:number = DateUtil.beginTimeOfDay(new Date())
  //另外监控日期的变更,当日期发生变化的时候再次查询一遍
  @Watch('aboutToAppear')

  @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'))
  }

}

import router from '@ohos.router';
import { CommonConstants } from '../../common/constants/CommonConstants'
import { RecordTypes } 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(){
  .fontSize(14)
  .fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList {
  @Consume @Watch('handleRecordsChange') records: RecordVO[]
  @State groups:GroupInfo<RecordType,RecordVO>[] = []

  //定义状态变量来记录当前的组别的下标
  @State currentItemIndex:number = -1

  handleRecordsChange(){
    this.groups = RecordService.calculateGroupInfo(this.records)
  }
  //封装颜色选择
  selectColor(itemIndex){
    return this.currentItemIndex ===  itemIndex ? $r('app.color.primary_color'):$r('app.color.gray')
  }
  build() {
    List({space:CommonConstants.SPACE_12}){
      ForEach(this.groups,(group:GroupInfo<RecordType,RecordVO>,index: number)=>{
        ListItem(){
          Column(){
            // 1.分组的标题
            Row({space:CommonConstants.SPACE_4}){
              Image(group.type.icon).width(27)
                .fillColor(this.selectColor(index))
              Text(group.type.name).fontWeight(CommonConstants.FONT_WEIGHT_700).fontSize(18)
              Text(`建议${group.type.min}~${group.type.max}千卡`).grayText()
              Blank()
              Text(group.calorie.toFixed(0)).fontColor($r('app.color.primary_color')).fontSize(14)
              Text('千卡').grayText()
              Image($r('app.media.ic_public_add_norm_filled')).width(20).fillColor($r('app.color.primary_color'))
                .onClick(()=>{
                  //TODO 页面跳转
                  router.pushUrl({
                    url: "pages/ItemIndex",
                    params: {type: group.type}//传递类型参数
                  })
                  //当点击后,该图标的图像就会发生变化
                  this.currentItemIndex = index
                })
            }.width('100%')
            .onClick(()=>{
              //当点击后,该图标的颜色就会发生变化
              this.currentItemIndex = index
            })

            // 2.组内记录列表
            List(){
              ForEach(group.items,(item:RecordVO,index:number)=>{
                ListItem(){
                  Row({space:CommonConstants.SPACE_6}){
                    Image(item.recordItem.image).width(50)
                    Column(){
                      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(item.id)})// TODO 学习

              })
            }

          }
          .backgroundColor(Color.White)
          .width('100%')
          .borderRadius(CommonConstants.DEFAULT_18)
          .padding(CommonConstants.SPACE_12)
        }

      })
    }.width(CommonConstants.THOUSANDTH_940)
    .margin({top:10})

  }
  @Builder deleteButton(index:number){
    Image($r('app.media.ic_public_delete_filled'))
      .fillColor(Color.Red)
      .width(25)
      .margin(5)
      .onClick(()=>{
       //点击删除图标之后,执行数据的删除
        RecordService.deleteById(index)
          .then(()=>{
           //删除成功,
            console.log('删除成功','testTag')
          })

      })
  }

}

参考黑马课堂老师的讲解,欢迎大家的批评和指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值