黑马健康系统(2)

概要

黑马健康系统:实现了记录页面的数据统计卡片StatsCard,和记录列表RecordList,至此记录页面主页面RecordIndex的基本样式和列表项主页面ItemIndex的样式已经完成。

整体流程

1.预期效果

预期①日期弹窗DatePickDialog

预期②热量统计CalorieStats

预期③营养素统计NutrientStats

预期④记录列表RecordList

预期⑤列表项(食物和运动)主页面ItemIndex

预期⑥具体项的列表ItemList

2.结构框架

3.具体代码

1.预期①
import { CommonConstants } from '../../common/constants/CommonConstants'
@CustomDialog
export default struct DatePickDialog {
  controller: CustomDialogController
  selectedDate: Date = new Date()
  build() {
    Column({space: CommonConstants.SPACE_12}){//上至下布局
      // 1.日期选择器 查看API
      DatePicker({
        start: new Date('2020-01-01'),
        end: new Date(),
        selected: this.selectedDate
      })
        .onChange((value: DatePickerResult) => {//用户选择日期传入
          this.selectedDate.setFullYear(value.year, value.month, value.day)
        })
      // 2.按钮
      Row({space:CommonConstants.SPACE_12}){//按钮放到一行
        Button('取消')
          .width(120)
          .backgroundColor($r('app.color.light_gray'))
          .onClick(() => this.controller.close())
        Button('确定')
          .width(120)
          .backgroundColor($r('app.color.primary_color'))
          .onClick(() => {//日期用处很多
            // 1.保存日期到全局存储
            AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())//存毫秒值,不存日期
            // 2.关闭窗口
            this.controller.close()
          })
      }
    }
    .padding(CommonConstants.SPACE_12)
  }
}
2.预期②
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct CalorieStats {
  @Prop intake: number//摄入
  @Prop expend: number//消耗
  recommend: number = CommonConstants.RECOMMEND_CALORIE//推荐值

  remainCalorie(){
    return this.recommend - this.intake + this.expend
  }//得到还可以吃的

  build() {
    Row({space: CommonConstants.SPACE_6}){
      // 1.饮食摄入
      this.StatsBuilder({label: '饮食摄入', value: this.intake})//传入参数
      // 2.还可以吃
      Stack(){//层叠关系
        // 2.1.进度条
        Progress({
          value: this.intake,//吃了多少
          total: this.recommend,
          type: ProgressType.Ring//环形记录条
        })
          .width(120)
          .style({strokeWidth: CommonConstants.DEFAULT_10})//环的粗细
          .color($r('app.color.primary_color'))
        // 2.2.统计数据
        this.StatsBuilder({label: '还可以吃', value: this.remainCalorie(),tips: `推荐${this.recommend}`})
      }
      // 3.运动消耗
      this.StatsBuilder({label: '运动消耗', value: this.expend})
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)//均匀分配空间
    .padding({top: 30, bottom: 35})//上下分配
  }
//对重复部分进行抽取,文字说明,数字,补充说明(可以不传),传值,不传递引用
  @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'))
      }
    }
  }
}
3.预期③
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NutrientStats {
  @Prop carbon: number//碳水
  @Prop protein: number//蛋白质
  @Prop fat: number//脂肪

  recommendCarbon: number = CommonConstants.RECOMMEND_CARBON
  recommendProtein: number = CommonConstants.RECOMMEND_PROTEIN
  recommendFat: number = CommonConstants.RECOMMEND_FAT

  build() {
    Row({space: CommonConstants.SPACE_6}){
      this.StatsBuilder({
        label: '碳水化合物',
        value: this.carbon,
        recommend: this.recommendCarbon,
        color: $r('app.color.carbon_color')
      })
      this.StatsBuilder({
        label: '蛋白质',
        value: this.protein,
        recommend: this.recommendProtein,
        color: $r('app.color.protein_color')
      })
      this.StatsBuilder({
        label: '脂肪',
        value: this.fat,
        recommend: this.recommendFat,
        color: $r('app.color.fat_color')
      })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)
    .padding({top: 30, bottom: 35})
  }
  //颜色各不相同
  @Builder StatsBuilder($$:{label: string, value: number, recommend: number, color: ResourceStr}){
    Column({space: CommonConstants.SPACE_6}){
      Stack(){
        Progress({
          value: $$.value,
          total: $$.recommend,
          type: ProgressType.Ring
        })
          .width(95)
          .style({strokeWidth: CommonConstants.DEFAULT_6})
          .color($$.color)
        Column({space: CommonConstants.SPACE_6}){
          Text('摄入推荐')
            .fontSize(12)
            .fontColor($r('app.color.gray'))
          Text(`${$$.value.toFixed(0)}/${$$.recommend.toFixed(0)}`)
            .fontSize(18)
            .fontWeight(CommonConstants.FONT_WEIGHT_600)
        }
      }
      Text(`${$$.label}(克)`)
        .fontSize(12)
        .fontColor($r('app.color.light_gray'))
    }
  }
}
4.预期④
import router from '@ohos.router'
import { CommonConstants } from '../../common/constants/CommonConstants'
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>[] = []

  handleRecordsChange(){
    this.groups = RecordService.calculateGroupInfo(this.records)
  }


  build() {
    List({space: CommonConstants.SPACE_10}){//List内部渲染
      ForEach(this.groups, (group: GroupInfo<RecordType, RecordVO>) => {
        ListItem(){
          Column({space: CommonConstants.SPACE_8}){
            // 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.primary_color'))
              Text('千卡').grayText()
              Image($r('app.media.ic_public_add_norm_filled'))
                .width(20)
                .fillColor($r('app.color.primary_color'))//svg图片可添加颜色
            }
            .width('100%')
            .onClick(() => {
              router.pushUrl({
                url: 'pages/ItemIndex',
                params: {type: group.type}
              })
            })
            // 2.组内记录列表
            List(){
              ForEach(group.items, (item: RecordVO) => {
                ListItem(){
                  Row({space: CommonConstants.SPACE_6}){//有很多这样的行
                    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%')
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(CommonConstants.DEFAULT_18)//卡片的基本样式
          .padding(CommonConstants.SPACE_12)
        }
      })
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')//list内部高度
    .margin({top: 10})
  }

  @Builder deleteButton(){
    Image($r('app.media.ic_public_delete_filled'))
      .width(20)
      .fillColor(Color.Red)
      .margin(5)//与其他产生间距
  }
}
5.预期⑤
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'
@Extend(Button) function panelButtonStyle(){
  .width(120)
  .type(ButtonType.Normal)
  .borderRadius(6)
}

@Entry
@Component
struct ItemIndex {
  //需要传递一个状态变量,需要提前声明
  @State amount:number=1
  @State value:string=''
  @State showPanel:boolean=false//什么时候展示面板
  @State item:RecordItem=null
  @State type:RecordType=RecordTypes[0]
  @State isFood:boolean=true

  onPanelShow(item: RecordItem) {
    this.amount = 1
    this.value = ''
    this.item = item
    this.showPanel = true
  }
  //当面板展示的时候
  onPageShow(){
    // 1.获取跳转时的参数
    let params: any = router.getParams()
    // 2.获取点击的饮食记录类型
    this.type = params.type
    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.底部面板
        Panel(this.showPanel){
          //3.1顶部日期
          ItemPanelHeader()


          //3.2记录项卡片
          if(this.item){
            ItemCard({amount:this.amount,item:$item})
          }


          //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)
        .dragBar(false)
        .backgroundMask($r('app.color.light_gray'))
        .backgroundColor(Color.White)
      }
      .width('100%')
      .height('100%')
  }

  @Builder PanelButton(){
    Row({space: CommonConstants.SPACE_6}){
      Button('取消')
        .panelButtonStyle()
        .backgroundColor($r('app.color.light_gray'))
        .onClick(() => this.showPanel = false)
      Button('提交')
        .panelButtonStyle()
        .backgroundColor($r('app.color.primary_color'))
        .onClick(() => {
          // 1.持久化保存
          RecordService.insert(this.type.id, this.item.id, this.amount)
            .then(() => {
              // 2.关闭弹窗
              this.showPanel = false
            })
        })
    }
    .margin({top: 10})
  }
  @Builder Header() {
    Row() {
      Image($r('app.media.ic_public_back'))
        .width(24)
        .onClick(() => router.back())
      Blank()
      Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height(32)
  }
}

6.预期⑥
import { CommonConstants } from '../../common/constants/CommonConstants'
import ItemModel from '../../model/ItemModel'
import GroupInfo from '../../viewmodel/GroupInfo'
import ItemCategory from '../../viewmodel/ItemCategory'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct ItemList {

  showPanel:(item:RecordItem)=>void
  @Prop isFood:boolean

  build() {
    Tabs(){
      TabContent(){
        this.TabContentBuilder(ItemModel.list(this.isFood))
      }
      .tabBar('全部')

      ForEach( ItemModel.listItemGroupByCategory(this.isFood),
        (group: GroupInfo<ItemCategory, RecordItem>) => {
          TabContent() {
            this.TabContentBuilder(group.items)
          }
          .tabBar(group.type.name)
        })
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')

  }

  @Builder TabContentBuilder(items:RecordItem[]){
    List({space:CommonConstants.SPACE_10}){
      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%')
  }
}

实现步骤

1.在记录页面主页面RecordIndex 完成浅绿色日期框,点击时跳到日期选择页面DatePickDialog,所以我们需要开发一个弹窗,弹窗在数据统计卡片StatsCard应用,然后我们对统计卡片添加一个滑动框,包含两部分内容,热量统计和营养素统计,因为这两部分内容比较多,所以我们新建立一个组件热量统计页面CalorieStats去编写,以及营养素统计页面NutrientStats去编写,到此我们记录页面主页面的统计卡片完成。

2.记录页面主页面RecordIndex的第三部分,记录列表RecordList还是单独建立一个组件,来保证记录页面主页面代码的规范性。

3.我们完成了记录页面RecordList的开发,在此页面有不同的记录分组,比如早餐,晚餐.当用户点击某个分组时,就会跳转进入食物列表的页面,在食物列表里面添加饮食记录。食物列表是一个新的页面所以我们要新建ItemIndex(因为我们要添加的信息不止是食物,还有运动所以起名为项的页面,便于理解我们叫做列表项主页面),在pages目录下因为它不属于记录页面主页的。

4.列表项主页面ItemIndex包括头部导航,食物(或运动)列表和弹出底部面板三部分。因为第二部分的列表内容很多,我们在view目录下重新建立一个item文件夹,在文件夹里创建具体项的列表组件ItemList。

新的API

1.DatePicker日期选择器

查看API,其中的属性有lunar代表是否显示农历,选择日期时触发该事件。

// xxx.ets
@Entry
@Component
struct DatePickerExample {
  @State isLunar: boolean = false
  private selectedDate: Date = new Date('2021-08-08')

  build() {
    Column() {
      Button('切换公历农历')
        .margin({ top: 30, bottom: 30 })
        .onClick(() => {
          this.isLunar = !this.isLunar
        })
      DatePicker({
        start: new Date('1970-1-1'),
        end: new Date('2100-1-1'),
        selected: this.selectedDate
      })
        .lunar(this.isLunar)
        .onChange((value: DatePickerResult) => {
          this.selectedDate.setFullYear(value.year, value.month, value.day)
          console.info('select current date is: ' + JSON.stringify(value))
        })

    }.width('100%')
  }
}

2.AppStorage全局存储

里面包含的SetOrCreate<T>函数是Key-Value键值对形式。其中,尽量不要存date对象,将来做状态监控变量时日期date会有问题,存它所对应的毫秒值(是基础类型number可以做状态监控),不存日期

static SetOrCreate<T>(propName: string, newValue: T): void

propName如果已经在AppStorage中存在,则设置propName对应是属性的值为newValue。如果不存在,则创建propName属性,值为newValue。

// 1.保存日期到全局存储
            AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())
//存它所对应的毫秒值(是基础类型number可以做状态监控),不存日期
            

3.Swiper滑动组件

子组件类型:系统组件和自定义组件,不支持渲染控制类型(if/elseForEachLazyForEach)。

接口

Swiper(controller?: SwiperController)

 // 2.统计信息,可滑动的信息
      Swiper(){
        // 2.1.热量统计
        CalorieStats({intake: this.info.intake, expend: this.info.expend})
        // 2.2.营养素统计
        NutrientStats({carbon: this.info.carbon, protein: this.info.protein, fat: this.info.fat})
      }
      .width('100%')//占据整个屏幕
      .backgroundColor(Color.White)
      .borderRadius(CommonConstants.DEFAULT_18)//园边框
      .indicatorStyle({selectedColor: $r('app.color.primary_color')})//穿梭框的颜色

4.Progress进度条

进度条组件,用于显示内容加载或操作处理等进度。因为是两个组件添加,还需要一个Stack堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。

//层叠关系
Stack(){

        // 2.1.进度条
        Progress({
          value: this.intake,//吃了多少
          total: this.recommend,
          type: ProgressType.Ring//环形记录条
        })
          .width(120)
          .style({strokeWidth: CommonConstants.DEFAULT_10})//环的粗细
          .color($r('app.color.primary_color'))

        // 2.2.统计数据
        this.StatsBuilder({label: '还可以吃', value: this.remainCalorie(),tips: `推荐${this.recommend}`})
      }

技术小结

1.读取日期

在数据统计主页要读取日期,因为我们将日期存到了AppStorage中,所有我们用@StorageProp去读取(prop单向绑定,link双向绑定),类型是number类型,必须要做初始化,初始化值是当前日期,默认的日期会加上时分秒,我们存的仅是年月日,我们想要的是这一天的零时零分零秒,用DateUtil来获取这一天的开始日期。同时我们选择的日期要传到这个弹窗里,因为下一次再打开的时候要把这个日期显示出来,叫回显。并且要把日期转换成date类型

class DateUtil{

  formatDate(num: number): string{
    let date = new Date(num)
    let year = date.getFullYear()
    let month = date.getMonth()+1
    let day = date.getDate()
    let m = month < 10 ? '0' + month : month
    let d = day < 10 ? '0' + day : day
    return `${year}/${m}/${d}`
  }

  beginTimeOfDay(date: Date){
    let d = new Date(date.getFullYear(), date.getMonth(), date.getDate())
    return d.getTime()
  }
}

let dateUtil = new DateUtil()

export default dateUtil as DateUtil

   /**
   *日期选择器页面
   */

.onChange((value: DatePickerResult) => {//用户选择日期传入
          this.selectedDate.setFullYear(value.year, value.month, value.day)
        })



  /**
   *数据统计主页
   */
//获取这一天的开始日期
@StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())


controller: CustomDialogController = new CustomDialogController({
    builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})
  })


 // 1.日期信息
      Row(){
//修改
        Text(DateUtil.formatDate(this.selectedDate))
          .fontColor($r('app.color.secondary_color'))
        Image($r('app.media.ic_public_spinner'))
          .width(20)
          .fillColor($r('app.color.secondary_color'))
      }

2.代码抽取

由于第三部分tips不一定有,所以可以不传入这个参数,tips?: string

//对重复部分进行抽取,文字说明,数字,补充说明(可以不传),传值,不传递引用
  @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'))
      }
    }
  }
@Extend(Text) function grayText(){//通用样式
  .fontSize(14)
  .fontColor($r('app.color.light_gray'))
}

3.List列表

列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。

接口

List(value?:{space?: number | string, initialIndex?: number, scroller?: Scroller})

ListItem用来展示列表具体item,必须配合List来使用。

接口

ListItem(value?: string)

// xxx.ets
@Entry
@Component
struct ListItemExample {
  private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  build() {
    Column() {
      List({ space: 20, initialIndex: 0 }) {
        ForEach(this.arr, (item) => {
          ListItem() {
            Text('' + item)
              .width('100%').height(100).fontSize(16)
              .textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
          }
        }, item => item)
      }.width('90%')
    }.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
  }
}

4.Tabs

通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图

接口

Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController})

// xxx.ets
@Entry
@Component
struct TabsExample {
  @State fontColor: string = '#182431'
  @State selectedFontColor: string = '#007DFF'
  @State currentIndex: number = 0
  private controller: TabsController = new TabsController()

  @Builder TabBuilder(index: number, name: string) {
    Column() {
      Text(name)
        .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
        .fontSize(16)
        .fontWeight(this.currentIndex === index ? 500 : 400)
        .lineHeight(22)
        .margin({ top: 17, bottom: 7 })
      Divider()
        .strokeWidth(2)
        .color('#007DFF')
        .opacity(this.currentIndex === index ? 1 : 0)
    }.width('100%')
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
        TabContent() {
          Column().width('100%').height('100%').backgroundColor('#00CB87')
        }.tabBar(this.TabBuilder(0, 'green'))

        TabContent() {
          Column().width('100%').height('100%').backgroundColor('#007DFF')
        }.tabBar(this.TabBuilder(1, 'blue'))


      }
      .vertical(false)
      .barMode(BarMode.Fixed)
      .barWidth(360)
      .barHeight(56)
      .animationDuration(400)
      .onChange((index: number) => {
        this.currentIndex = index
      })
      .width(360)
      .height(296)
      .margin({ top: 52 })
      .backgroundColor('#F1F3F5')
    }.width('100%')
  }
}

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值