(三)鸿蒙实战案例 - 黑马健康生活应用

第三天

前言

综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新性的移动应用软件。


项目要求

  1. 技术栈:HarmonyOS 4.0,DevEco Studio。
  2. 实用性与创意:软件需解决具体问题,具备用户友好界面,创新性需体现在功能或用户体验上。
  3. 代码质量:遵守HarmonyOS编码规范,代码需模块化,易于维护。

食物列表页

代码:

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 amount:number = 1
  @State showPanel:boolean = false
  @State value:string = ''
  @State item:RecordItem = null
  @State type:RecordType = RecordTypes[0]
  @State isFood : boolean = true

  onPanelShow(item:RecordItem){
    this.showPanel = true
    this.amount = 1
    this.item = item
    this.value = ''
  }

  onPageShow(){
    //获取跳转参数
    let params : any = router.getParams()
    //获取点击记录类型
    this.type = params.type
    //获取isFood
    this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
  }

  build() {
      Column() {
        //头部标题
        this.Header()
        //列表
        ItemList({showPanel:this.onPanelShow.bind(this),isFood : this.isFood })
          .layoutWeight(1)//高度固定,底部就能弹出
        //底部面板
        /*可滑动面板*/
        Panel(this.showPanel){

          //顶部日期弹窗
          itemPanelHeader()

          //记录详细信息,数量
          if(this.item){
            itemCard({ amount: this.amount,item: $item})//对象只能link传
          }

          //键盘
          NumberKeyboard({amount:$amount,value:$value})

          //按钮
          Row({space:CommonConstants.SPACE_6}){

            Button('提交')
              .width(120)
              .backgroundColor($r('app.color.primary_color'))
              .type(ButtonType.Normal)
              .borderRadius(6)
              .onClick(()=> {
                //持久化
                RecordService.insert(this.type.id,this.item.id,this.amount)
                  .then(() => {
                    //关闭弹窗
                    this.showPanel = false
                  })

              })


            Button('关闭')
              .width(120)
              .backgroundColor($r('app.color.light_gray'))
              .type(ButtonType.Normal)
              .borderRadius(6)
              .onClick(()=>this.showPanel=false)
          }
          .padding({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(24)
        .onClick(()=> router.back())
      Blank()
      Text(this.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height(32)
  }
}

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.TabsList(ItemModel.list(this.isFood))
      }
      .tabBar('全部')
      
      
      ForEach(ItemModel.listItemGroupByCategory(this.isFood),
        (group:GroupInfo<ItemCategory,RecordItem>) =>
      {
        TabContent(){
          this.TabsList(ItemModel.list(this.isFood))
        }
        .tabBar(group.type.name)
      })

    }
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')
    .barMode(BarMode.Scrollable)//超出可以滚动
  }

  @Builder TabsList(items:RecordItem[]){
    List({space:CommonConstants.SPACE_10}){
      ForEach(items,
        (item:RecordItem)=>{
          ListItem(){
            Row(){
              Image(item.image)
                .width(50)
              Column(){
                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%')
}
}
该代码实现了食物列表页面的布局和样式

底部Panel弹窗

顶部日期弹窗

代码:

import { CommonConstants } from '../../common/constants/CommonConstants'
@CustomDialog
export default struct DatePickDialog{
  controller:CustomDialogController//控制器
  selectedDate:Date = new Date()
  build(){
    Column(){
      //1、日期选择
      DatePicker({
        start: new Date('2024-01-17'),
        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.primary_color'))
          .onClick(()=>{
            //保存到全局
            AppStorage.SetOrCreate('selectedDate',this.selectedDate.getTime())//全局保存


            this.controller.close()
          })
        Button('取消')
          .width(120)
          .backgroundColor($r('app.color.light_gray'))
          .onClick(()=>this.controller.close())
      }
    }
    .padding(CommonConstants.SPACE_12)
  }
}

import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import itemPanelDialog from './itemPanelDialog'
@Component
export default struct itemPanelHeader {

  @StorageProp('selectedDate') selectedDate:number = DateUtil.beginTimeOfDay(new Date())

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

  build() {
    Row(){
      Text(`${DateUtil.formatDate(this.selectedDate)} 早餐`)
        .fontSize(18)
        .fontWeight(CommonConstants.FONT_WEIGHT_600)
      Image($r('app.media.ic_public_spinner'))
        .width(20)
        .fillColor(Color.Black)
    }
    .width(CommonConstants.THOUSANDTH_940)
    .justifyContent(FlexAlign.Center)
    .onClick(()=>this.controller.open())
  }
}

实现了底部Panel滑动面板中的日期选择功能

 Panel内容

代码:


import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordItem from '../../viewmodel/RecordItem'
@Component
export default struct itemCard {

  @Prop amount:number
  @Link item :RecordItem

  build() {
    Column({space:CommonConstants.SPACE_8}){
      Image(this.item.image)
        .width(150)

      Row()
      {
        Text(this.item.name)
          .fontWeight(CommonConstants.FONT_WEIGHT_700)
      }
      .backgroundColor($r('app.color.lightest_primary_color'))
      .padding({top:5,bottom:5,left:12,right:12})
      /*下划线*/
      Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)

      Row({space:CommonConstants.SPACE_8}){
        this.NutrientInfo({
          label:'热量(千卡)',
          value:this.item.calorie
        })
        if(this.item.id<10000)
        {
          this.NutrientInfo({
            label:'碳水(克)',
            value:this.item.carbon
          })
          this.NutrientInfo({
            label:'蛋白(克)',
            value:this.item.protein
          })
          this.NutrientInfo({
            label:'脂肪(克)',
            value:this.item.fat
          })
        }
      }
      Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)

      Row(){
        Column({space:CommonConstants.SPACE_4}){
          Text(this.amount.toFixed(1))
            .fontSize(50)
            .fontColor($r('app.color.primary_color'))
            .fontWeight(CommonConstants.FONT_WEIGHT_600)
          Divider().color($r('app.color.primary_color'))
        }
        .width(150)
        Text(this.item.unit)
          .fontColor($r('app.color.light_gray'))
          .fontWeight(CommonConstants.FONT_WEIGHT_600)
      }
    }
  }


  @Builder NutrientInfo($$:{label:string,value:number}){
    Column({space:CommonConstants.SPACE_8}){
      Text($$.label)
        .fontSize(14)
        .fontColor($r('app.color.light_gray'))

      Text(($$.value*this.amount).toFixed(1))//1位小数
        .fontWeight(CommonConstants.FONT_WEIGHT_700)
    }
  }
}

该代码实现了Panel中热量,碳水等信息内容

 数字键盘

代码:


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)
    .height(60)
    .borderRadius(8)

}

  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(300)
    .backgroundColor($r('app.color.index_page_background'))
    .columnsTemplate('1fr 1fr 1fr')//列数
    .columnsGap(8)//列间距
    .rowsGap(8)
    .padding(8)
    .margin({top:10})
  }


  clickNumber(num:string){
    //拼接
    let val = this.value + num

    //处理
    let firstIndex = val.indexOf('.')
    let lastIndex = val.lastIndexOf('.')
    if(firstIndex!==lastIndex||(lastIndex !=-1 && lastIndex<val.length-2)){
      return
    }

    //转数值

    let amount = this.parseFloat(val)

    if(amount>=999.9){
      this.amount = 999
      this.value = '999'
    }else{
      this.amount = amount
      this.value = val
    }
  }

  clickDelete(){
    if(this.value.length<=0){
      this.value = ''
      this.amount = 0
      return
    }
    this.value = this.value.substring(0,this.value.length-1)
    this.amount = this.parseFloat(this.value)
  }

  parseFloat(str:string){
    if(!str)
    {
      return 0
    }
    if(str.endsWith('.')){
      str = str.substring(0,str.length-1)
    }
    return parseFloat(str)

  }

}

 该代码实现了,Panel中数字键盘,输入的数传到信息内容和统计信息中

 

总结

我了解并学会了如何使用Panel滑动面板,Divider()下划线,Grid()网格容器,将网格内的变量设置成字符串,进行ForEach循环输出。

在拼接,处理,转换数字时,我知道了,通过indexOf和lastIndexOf来判断 ‘ . ’的首位和尾位是否是相同的下标,不能出现两个‘ . ’,或有小数点的情况下,判断小数点后的位数不能超过两位。最后判断格式,不符合的通过substring函数来删除最后一位。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值