【HarmonyOS开发】案例:黑马健康:首页Tabs、饮食记录-顶部搜索栏及统计卡片:

一、项目介绍:

该项目是一个关于健身运动、健康饮食的黑马健身移动应用软件;主要包括三个页面,分别是欢迎页面、统计记录页面、食物列表页面。主要实现的功能多端部署,可以在不同的设备上根据屏幕的大小判断进行页面分布;实现数据库持久化和页面交互。欢迎页面有自动弹窗,点击同意方可进入,首次进入点击后,不需要二次点击,点击不同意,则退出app。统计记录页面,分别有早餐、午餐、晚餐、加餐、运动;根据你输入的食物统计摄入的卡路里你输入的运动量统计消耗的卡路里;根据身高体重来推荐你还能摄入的碳水化合物、蛋白质、脂肪,或者应该消耗多少卡路里。食物列表页面(运动列表页面),根据父组件判断是食物列表页面(true)/运动列表页面(false),默认为食物列表页面;食物列表页面/运动列表页面还进行了食物或运动的分类。


第二天项目成果:

3、首页Tabs:

通过Tabs组件可以实现页面内视图内容快速切换,完成了首页底部的Panel设计,

运行成果:


Index:
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 //用来记录index
  //不是静态,需要初始化
  private breakpointSystem:BreakpointSystem=new BreakpointSystem()
  //然后页面利用StorageProp进行读取
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM

  @State isPageShow:boolean=false
  onPageShow(){
    this.isPageShow=true
  }
  onPageHide(){
    this.isPageShow=false
  }


  @Builder TabsBarBuilder(title:ResourceStr,image:ResourceStr,index:number){//自定义TabsBarBuilder函数来自定义Bar样式
    Column({space:CommonConstants.SPACE_8}){//index角标来判断切换到哪一个Bar
      Image(image)
        .width(22)
        .fillColor(this.selectColor(index))
      Text(title)
        .fontSize(14)
        .fontColor(this.selectColor(index))
    }
  }
  aboutToAppear(){
    this.breakpointSystem.register()
  }
  aboutToDisappear(){//取消注册
    this.breakpointSystem.unregister()
  }

  selectColor(index:number){
    return this.currentIndex===index ? $r('app.color.primary_color'):$r('app.color.gray')
  }

  build(){
    Tabs({barPosition:BreakpointConstants.BAR_POSITION.getValue(this.currentBreakpoint)}){//控制Bar位置,即底下三个按钮
      TabContent(){
        RecordIndex({isPageShow: this.isPageShow})
      }
      .tabBar(this.TabsBarBuilder($r('app.string.tab_record'),$r('app.media.ic_calendar'),0))

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

      TabContent(){
        Text('我的主页')
      }
      .tabBar(this.TabsBarBuilder($r('app.string.tab_user'),$r('app.media.ic_user_portrait'),2))
    }
    .width('100%')
    .height('100%')
    .onChange(index=>this.currentIndex=index)
    .vertical(new BreakpointType({
      //判断布局样式,false垂直布局,true纵向布局,实现不同宽度,不同结果
      sm:false,
      md:true,
      lg:true,
    }).getValue(this.currentBreakpoint))
  }
}

4、饮食记录-顶部搜索栏:

通过search和image组件完成了顶部搜索栏的设计。

运行成果:


源码:
RecordIndex:
import defaultAppManager from '@ohos.bundle.defaultAppManager'
import { CommonConstants } from '../../common/constants/CommonConstants'
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')
  @Watch('aboutToAppear')//监控日期变更,加载时查询一次,日期变更时在查一次
  selectedDate:number=DateUtil.beginTimeOfDay(new Date())
  //在这查询饮食记录列表一次,方便下面的查询,把查到的结果分给儿子们使用,需要用provide
  @Provide records:RecordVO[]=[]

  //当发现isPageShow更新时,说明页面加载了
  @Prop @Watch('handlePageShow') isPageShow: boolean

  //true时查询,false不查询
  handlePageShow(){
    if(this.isPageShow){
      this.aboutToAppear()
    }
  }

  //在build执行前查询一次
  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'))
  }
}
SearchHeader:
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct SearchHeader{
  build(){
    Row({space:CommonConstants.SPACE_6}){
      Search({placeholder:'搜索饮食或运动信息'})
        .textFont({size:18})
        .layoutWeight(1)
      Badge({count:1,position:BadgePosition.RightTop,style:{fontSize:14}}){//数字角标,显示邮件个数
        Image($r('app.media.ic_public_email'))
          .width(24)
      }

    }
    .width(CommonConstants.THOUSANDTH_940)
  }
}


5、饮食记录-统计卡片:

完成了统计卡片的时间记录,可以查看历史记录的饮食运动数据;饮食摄入,运动消耗的统计卡片;推荐摄入碳水化合物、蛋白质、脂肪的统计卡片。

运行成果:


源码:
StatsCard:
import DatePickDialog from './DatePickDialog'
import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import CalorieStats from './CalorieStats'
import NutrientStats from './NutrientStats'
import BreakpointType from '../../common/bean/BreanpointType'
import BreakpointConstants from '../../common/constants/BreakpointConstants'
import RecordVO from '../../viewmodel/RecordVO'
import StatsInfo from '../../viewmodel/StatsInfo'
import RecordService from '../../service/RecordService'

@Component
export default struct StatsCard{

  //StorageProp单向绑定,StorageLink双向绑定,这里只加载并读取,所以用StorageProp单向绑定
  @StorageProp('selectedDate') selectedDate:number=DateUtil.beginTimeOfDay(new Date())
  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM
  @Consume @Watch('handleRecordsChange')records:RecordVO[] //拿到父亲提供的数据
  @State info:StatsInfo=new StatsInfo()

  //处理Record变更
  handleRecordsChange(){
    this.info = RecordService.calculateStatsInfo(this.records)//在watch监控record// 时进行统计信息
  }
  controller:CustomDialogController=new CustomDialogController({
    builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})//下次打开还要回显,所以还要把数据传回去。
  })
  build(){
    Column(){
      //1.日期信息
      Row(){
        Text(DateUtil.formatDate(this.selectedDate))//格式化,使用formatDate接受毫秒值。
          .fontColor($r('app.color.secondary_color'))
        Image($r('app.media.ic_public_spinner'))
          .width(20)
          .fillColor($r('app.color.secondary_color'))
      }
      .padding(CommonConstants.SPACE_8)
      .onClick(()=>this.controller.open())
      //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)//圆角18
      .indicatorStyle({selectedColor:$r('app.color.primary_color')})
      .displayCount(new BreakpointType({//1为滑动,2为平铺
        sm:1,
        md:1,
        lg:2,
      }).getValue(this.currentBreakpoint))//在不同屏幕下改动,在大屏幕下改成平铺
    }
    .width(CommonConstants.THOUSANDTH_940)
    .backgroundColor($r('app.color.stats_title_bgc'))
    .borderRadius(CommonConstants.DEFAULT_18)
  }
}
DatePickDialog:
import { CommonConstants } from '../../common/constants/CommonConstants'
@CustomDialog
export default struct DatePickDialog{
  controller:CustomDialogController
  selectedDate:Date=new Date() //定义结束日期为今天
  build(){
    Column({space:CommonConstants.SPACE_12}){
      //1.日期选择器
      //日期选择器组件,用于根据指定日期范围创建日期滑动选择器。
      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())//不存日期Date对象,将来做状态变量日期监控会有问题,存毫秒值,毫秒值和日期之间可以任意转换。
            //2.关闭窗口
            this.controller.close()
          })
      }
    }
    .padding(CommonConstants.SPACE_12)
  }
}
CalorieStats:
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'))
      }
    }
  }
}

总结:

了解以下知识点:

1、Tabs组件可以实现页面内视图内容快速切换,包含TabBar和TabContent两个部分,不支持图标。

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

3、Tabcontent:仅在Tabs中使用,对应一个切换页签的内容视图。

4、Progress:进度条组件,用于显示内容加载或操作处理等进度。

5、DatePicker:日期选择器组件,用于根据指定日期范围创建日期滑动选择器。

6、Swiper:滑块视图容器,提供子组件滑动轮播显示的能力。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值