鸿蒙【黑马健康App】

原型分析

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
加入一些素材(如:图片,图标,样式等)
在这里插入图片描述

欢迎页面Ul实现

在这里插入图片描述

// src/main/ets/pages/WelcomePage.ets



@Extend(Text) function opacityWhiteText(opacity: number, fontSize: number = 10) {
  .fontSize(fontSize)
  .opacity(opacity)
  .fontColor(Color.White)
}

@Entry
@Component
struct WelcomePage {
  @State message: string = 'Hello World'


  build() {
    Column({space: 10}){
      // 1.中央Slogan
      Row(){
        Image($r('app.media.home_slogan')).width(260)
      }
      .layoutWeight(1)
      // 2.logo
      Image($r('app.media.home_logo')).width(150)
      // 3.文字描述
      Row() {
        Text('黑马健康支持').opacityWhiteText(0.8, 12)
        Text('IPv6')
          .opacityWhiteText(0.8)
          .border({ style: BorderStyle.Solid, width: 1, color: Color.White, radius: 15 })
          .padding({ left: 5, right: 5 })
        Text('网络').opacityWhiteText(0.8, 12)
      }
      Text(`'减更多'指黑马健康App希望通过软件工具的形式,帮助更多用户实现身材管理`)
        .opacityWhiteText(0.6)
      Text('浙ICP备0000000号-36D')
        .opacityWhiteText(0.4)
        .margin({ bottom: 35 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.welcome_page_background'))
  }
}

欢迎页面业务

在这里插入图片描述

在这里插入图片描述

import { CommonConstants } from '../../common/constants/CommonConstants'

// 自定义弹窗
// @Preview  这个注解可以在预览器预览
@CustomDialog
export default struct UserPrivacyDialog {
  controller: CustomDialogController
  confirm: () => void
  cancel: () => void

  build() {
    Column({space: CommonConstants.SPACE_10}){
      // 1.标题
      Text($r('app.string.user_privacy_title'))
        .fontSize(20)
        .fontWeight(CommonConstants.FONT_WEIGHT_700)
      // 2.内容
      Text($r('app.string.user_privacy_content'))
      // 3.按钮
      Button($r('app.string.agree_label'))
        .width(150)
        .backgroundColor($r('app.color.primary_color'))
        .onClick(() => {
          this.confirm()
          this.controller.close()
        })
      Button($r('app.string.refuse_label'))
        .width(150)
        .backgroundColor($r('app.color.lightest_primary_color'))
        .fontColor($r('app.color.light_gray'))
        .onClick(() => {
          this.cancel()
          this.controller.close()
        })
    }
    .width('100%')
    .padding(10)
  }

}

使用弹窗

import common from '@ohos.app.ability.common'
import router from '@ohos.router'
import PreferenceUtil from '../common/utils/PreferenceUtil'
import UserPrivacyDialog from '../view/welcome/UserPrivacyDialog'
@Extend(Text) function opacityWhiteText(opacity: number, fontSize: number = 10) {
  .fontSize(fontSize)
  .opacity(opacity)
  .fontColor(Color.White)
}
const PREF_KEY = 'userPrivacyKey'
@Entry
@Component
struct WelcomePage {
  context = getContext(this) as common.UIAbilityContext
  controller: CustomDialogController = new CustomDialogController({
    builder: UserPrivacyDialog({
      confirm: () => this.onConfirm(),
      cancel: () => this.exitApp()
    })
  })

  async aboutToAppear(){
    // 1.加载首选项
    let isAgree = await PreferenceUtil.getPreferenceValue(PREF_KEY, false)
    // 2.判断是否同意
    if(isAgree){
      // 2.1.同意,跳转首页
      this.jumpToIndex()
    }else{
      // 2.2.不同意,弹窗
      this.controller.open()
    }
  }

  jumpToIndex(){
    setTimeout(() => {
      router.replaceUrl({
        url: 'pages/Index'
      })
    }, 1000)
  }

  onConfirm(){
    // 1.保存首选项
    PreferenceUtil.putPreferenceValue(PREF_KEY, true)
    // 2.跳转到首页
    this.jumpToIndex()
  }

  exitApp(){
    // 退出APP
    this.context.terminateSelf()
  }

  build() {
    Column({ space: 10 }) {
      // 1.中央Slogan
      Row() {
        Image($r('app.media.home_slogan')).width(260)
      }
      .layoutWeight(1)

      // 2.logo
      Image($r('app.media.home_logo')).width(150)
      // 3.文字描述
      Row() {
        Text('黑马健康支持').opacityWhiteText(0.8, 12)
        Text('IPv6')
          .opacityWhiteText(0.8)
          .border({ style: BorderStyle.Solid, width: 1, color: Color.White, radius: 15 })
          .padding({ left: 5, right: 5 })
        Text('网络').opacityWhiteText(0.8, 12)
      }

      Text(`'减更多'指黑马健康App希望通过软件工具的形式,帮助更多用户实现身材管理`)
        .opacityWhiteText(0.6)
      Text('浙ICP备0000000号-36D')
        .opacityWhiteText(0.4)
        .margin({ bottom: 35 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.welcome_page_background'))
  }
}

详细代码在gitee:https://gitee.com/cheng_yong_xu/ho_healthy

首页Tabs

在这里插入图片描述
vertical(false)横向布局 BarPosition.End 就表示下面
vertical(true)纵向布局 BarPosition.End 就表示右

在这里插入图片描述

// src/main/ets/pages/Index.ets

import { CommonConstants } from '../common/constants/CommonConstants'
@Entry
@Component
struct Index {
  @State currentIndex: number = 0

  // 定义一个构建函数,自定义tabar
  @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))
  }
}

  // 选中颜色,根据来显示颜色,选中绿色,未选中灰色
  selectColor(index: number){
    return this.currentIndex === index ? $r('app.color.primary_color') : $r('app.color.gray')
  }

  build() {
    Tabs({barPosition:BarPosition.End}){
      TabContent(){
        Text('饮食记录')
      }
      .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%')
    .onChange(index => this.currentIndex = index)
    .vertical(false)
  }
}

饮食记录

顶部搜索栏

在这里插入图片描述

在这里插入图片描述

// src/main/ets/pages/Index.ets

import { CommonConstants } from '../common/constants/CommonConstants'
import RecordIndex from '../view/record/RecordIndex'


@Entry
@Component
struct Index {
  @State currentIndex: number = 0

  // 定义一个构建函数,自定义tabar
  @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))
  }
}

  // 选中颜色,根据来显示颜色,选中绿色,未选中灰色
  selectColor(index: number){
    return this.currentIndex === index ? $r('app.color.primary_color') : $r('app.color.gray')
  }

  build() {
    Tabs({barPosition:BarPosition.End}){
      TabContent(){
        RecordIndex()
      }
      .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%')
    .onChange(index => this.currentIndex = index)
    .vertical(false)
  }
}

组件

// src/main/ets/view/record/RecordIndex.ets

import SearchHeader from './SearchHeader'
@Component
export default struct RecordIndex {
  build() {
    Column(){
      // 1.头部搜索栏
      SearchHeader()
      // 2.统计卡片
      Text('统计卡片')
      // 3.记录列表
      Text('记录列表')
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.index_page_background'))
  }
}

组件

// src/main/ets/view/record/SearchHeader.ets

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,在图标上显示小圆点,小圆点里显示未看消息数量
      Badge({count: 1, position: BadgePosition.RightTop, style: {fontSize: 12}}){
        Image($r('app.media.ic_public_email'))
          .width(24)
      }
    }
    .width(CommonConstants.THOUSANDTH_940)
  }
}

在这里插入图片描述

统计卡片

在这里插入图片描述
上图需要用到日期选择器组件,自定义弹窗,还有AppStorage

在这里插入图片描述
在这里插入图片描述

// src/main/ets/view/record/StatsCard.ets

import DateUtil from '../../common/utils/DateUtil'
import DatePickDialog from './DatePickDialog'
import { CommonConstants } from '../../common/constants/CommonConstants'
import CalorieStats from './CalorieStats'
import NutrientStats from './NutrientStats'

@Component
export default struct StatsCard {
  // 把selectedDate从AppStorage里出去来,这里需要用到 @StorageProp这个注解,同时我们要对取出来的日期处理,因为我们存的是日期毫秒数,这里自己写了个工具类
  @StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())

  // 使用自定义弹窗
  controller: CustomDialogController = new CustomDialogController({
    builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})
  })
  build(){
    Column(){
      // 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'))
      }
      .padding(CommonConstants.SPACE_8)
      .onClick(() => this.controller.open())  // 打开弹窗

      // 2.统计信息
      Swiper(){
        // 2.1.热量统计
        CalorieStats()
        // 2.2.营养素统计
        NutrientStats()
      }
    }
  }
}

组件

// src/main/ets/view/record/DatePickDialog.ets

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官方组件
      DatePicker({
        start: new Date('2020-01-01'),
        end: new Date(),
        selected: this.selectedDate
      })
        .onChange((value: DatePickerResult) => {
          // 注意这里,选择的结果是年月日,没有时分秒,默认是当日的0时,0分,0秒
          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是一个全局的存储空间,getTime()获取的是日期毫秒数,以为存一个日期对象,会不好处理
            AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())
            // 2.关闭窗口
            this.controller.close()
          })

      }

    }
    .padding(CommonConstants.SPACE_12)
  }
}

组件

// src/main/ets/view/record/CalorieStats.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct CalorieStats {
  // 饮食摄入
  intake: number = 192
  // 消耗
  expend: number = 150
  //推荐的
  recommend: number = CommonConstants.RECOMMEND_CALORIE

  // 计算还可以吃的量
  remainCalorie(){
    return this.recommend - this.intake + this.expend
  }
  build() {
    Row({space: CommonConstants.SPACE_6}){
      // 1.饮食摄入
      this.StatsBuilder('饮食摄入', 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('还可以吃', this.remainCalorie(), `推荐${this.recommend}`)
      }
      // 3.运动消耗
      this.StatsBuilder('运动消耗', 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'))
      }
    }
  }
}

组件

// src/main/ets/view/record/NutrientStats.ets

import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NutrientStats {
  // 碳水化合物
  carbon: number = 10
  // 蛋白质
  protein: number = 20
  // 脂肪
  fat: number  = 20

  // 推荐摄入的碳水化合物,蛋白质,脂肪
  recommendCarbon: number = CommonConstants.RECOMMEND_CARBON
  recommendProtein: number = CommonConstants.RECOMMEND_PROTEIN
  recommendFat: number = CommonConstants.RECOMMEND_FAT


  build() {
    Row({space: CommonConstants.SPACE_6}){
      this.StatsBuilder(
        '碳水化合物',
        this.carbon,
        this.recommendCarbon,
        $r('app.color.carbon_color')
      )

      this.StatsBuilder(
        '蛋白质',
        this.protein,
        this.recommendProtein,
        $r('app.color.protein_color')
      )
      this.StatsBuilder(
        '脂肪',
        this.fat,
        this.recommendFat,
        $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'))
    }
  }
}

在这里插入图片描述

记录列表

在这里插入图片描述

// src/main/ets/view/record/RecordIndex.ets
import RecordList from './RecordList'
import SearchHeader from './SearchHeader'
import StatsCard from './StatsCard'

@Component
export default struct RecordIndex {
  build() {
    Column(){
      // 1.头部搜索栏
      SearchHeader()
      // 2.统计卡片
      StatsCard()
      // 3.记录列表
      RecordList()
        .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.index_page_background'))
  }
}

组件

// src/main/ets/view/record/RecordList.ets

import { CommonConstants } from '../../common/constants/CommonConstants'

@Extend(Text) function grayText() {
  .fontSize(14)
  .fontColor($r('app.color.light_gray'))
}

@Component
export default struct RecordList {
  build() {
 List({space: CommonConstants.SPACE_10}){
   ForEach([1,2,3,4,5,6], (item) => {
     ListItem() {
       Column({ space: CommonConstants.SPACE_8 }) {
         // 1.分组的标题
         Row({space: CommonConstants.SPACE_4}){
           Image($r('app.media.ic_breakfast')).width(24)
           Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
           Text('建议423-592千卡').grayText()
           Blank()
           Text('190').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'))
         }
         .width('100%')
         // 2.组内记录列表
         List(){
           ForEach([1,2], (item) => {
             ListItem(){
               Row({space: CommonConstants.SPACE_6}){
                 Image($r('app.media.toast')).width(50)
                 Column({space: CommonConstants.SPACE_4}){
                   Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
                   Text(`1片`).grayText()
                 }
                 Blank()
                 Text('91千卡').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%')
 .margin({top: 10})
  }

  @Builder deleteButton() {
    Image($r('app.media.ic_public_delete_filled'))
      .width(20)
      .height(20)
      .fillColor(Color.Red)
      .margin(5)
  }
}

食物列表页

在这里插入图片描述

// src/main/ets/pages/ItemIndex.ets

import { CommonConstants } from '../common/constants/CommonConstants'
import ItemList from '../view/record/ItemList'
@Entry
@Component
struct ItemIndex {
  @State message: string = 'Hello World'

  build() {
    Column() {
      // 1.头部导航
      this.Header()

      // 2.列表
      ItemList()

    }
  }

  @Builder Header() {
    Row() {
      Image($r('app.media.ic_public_back'))
        .width(24)
      Blank()
      Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height(32)
    }

}

组件

// src/main/ets/view/record/ItemList.ets

import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemList {
  build() {
    Tabs() {
      TabContent() {
        List({space: CommonConstants.SPACE_10}){
          ForEach([1,2,3,4,5,6], (item) => {
            ListItem(){
              Row({space: CommonConstants.SPACE_6}){
                Image($r('app.media.toast')).width(50)
                Column({space: CommonConstants.SPACE_4}){
                  Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
                  Text(`1片`).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)
            }
          })
        }
        .width('100%')
        .height('100%')
      }
      .tabBar('全部')
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')
  }
}```

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2e6d69cefd8f48a5b02e04e9bb9c4368.png)
封装

```javascript
// src/main/ets/view/record/ItemList.ets

import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct ItemList {
  build() {
    Tabs() {
      TabContent() {
        this.TabContentBuilder()
      }
      .tabBar('全部')
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height('100%')
  }

  @Builder TabContentBuilder() {
    List({space: CommonConstants.SPACE_10}){
      ForEach([1,2,3,4,5,6], (item) => {
        ListItem(){
          Row({space: CommonConstants.SPACE_6}){
            Image($r('app.media.toast')).width(50)
            Column({space: CommonConstants.SPACE_4}){
              Text('全麦土司').fontWeight(CommonConstants.FONT_WEIGHT_500)
              Text(`1片`).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)
        }
      })
    }
    .width('100%')
    .height('100%')
  }
}

食物列表底部Panel

在这里插入图片描述

源码在gitee

在这里插入图片描述

食物列表数字键盘

在这里插入图片描述

// src/main/ets/pages/ItemIndex.ets

import { CommonConstants } from '../common/constants/CommonConstants'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyboard from '../view/item/NumberKeyboard'

@Extend(Button) function panelButtonStyle(){
  .width(120)
  .type(ButtonType.Normal)
  .borderRadius(6)
}


@Entry
@Component
struct ItemIndex {
  @State showPanel: boolean = false
  @State amount: number = 1
  @State value: string = ''

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

  build() {
    Column() {
      // 1.头部导航
      this.Header()

      // 2.列表
      ItemList({showPanel: this.onPanelShow.bind(this)})
        .layoutWeight(1)  // 这里注意,ItemList作为Panel的容器,必须要有高度
      // 3.底部面板
      Panel(this.showPanel) {
        // 3.1.顶部日期
        ItemPanelHeader()
        // 3.2.记录项卡片
        ItemCard({amount: this.amount})
        // 3.3.数字键盘
        NumberKeyboard({amount: $amount, value: $value})
        // 3.4.按钮
        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(() => 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(24)
      Blank()
      Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
    }
    .width(CommonConstants.THOUSANDTH_940)
    .height(32)
    }

}
// src/main/ets/view/item/NumberKeyboard.ets

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

  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(280)
    .backgroundColor($r('app.color.index_page_background'))
    .columnsTemplate('1fr 1fr 1fr')  // 分成3列
    .columnsGap(8)  // 列间距
    .rowsGap(8)  // 行间距
    .padding(8)
    .margin(10)
  }

  clickNumber(num: string){
    // 1.拼接用户输入的内容
    let val = this.value + num
    // 2.校验输入格式是否正确
    let firstIndex = val.indexOf('.')  // 获得最前面的小数点
    let lastIndex = val.lastIndexOf('.')  // 获得最前后的小数点
    if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){
      // 非法输入
      // firstIndex !== lastIndex 说明有两个及以上个小数点
      // lastIndex != -1 && lastIndex < val.length - 2  有小数点,且小数长度不大于2,就是保留一位小数
      return
    }
    // 3.将字符串转为数值
    let amount = this.parseFloat(val)
    // 4.保存
    if(amount >= 999.9){
      this.amount = 999.0
      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
    }
    // 如果最后有一个小数点,比如这样的 23. 就把最后的小数点去掉
    if(str.endsWith('.')){
      str = str.substring(0, str.length - 1)
    }
    return parseFloat(str)
  }
}

多设备响应式布局

我们写的是手机的样式和布局
放大会变形
在这里插入图片描述
应该根据不同的设备动态的调整布局

在这里插入图片描述

媒体查询技术
在这里插入图片描述
源码在gitee:https://gitee.com/cheng_yong_xu/ho_healthy

在这里插入图片描述

数据模型

记录项

我们把饮食和运动,定义到一个模型里
思路我们从,最单元开始,建立数据模型,比如显示食物和运动,再去建立分类,时间等等的数据模型
在这里插入图片描述

源码在gitee

饮食记录

在这里插入图片描述

数据模型通用DB工具

通用DB工具都可以用,可以简化我们对关系数据库的增删改查

定义数据模型
// 记录的数据
// 注意和数据库字段名字不一样,所以还需要定义一个,数据库字段和数据记录数据相对应的类ColumnInfo

export default class RecordPO{
  /**
   * 记录id
   */
  id?: number
  /**
   * 饮食记录类型
   */
  typeId: number
  /**
   * 记录中的食物或运动信息
   */
  itemId: number

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number
  /**
   * 记录的日期
   */
  createTime: number
}
export class ColumnInfo{
  name: string
  columnName: string
  type: ColumnType
}

export enum ColumnType{
  LONG,
  DOUBLE,
  STRING,
  BLOB
}
工具类
import common from '@ohos.app.ability.common';  // 系统工具
import relationalStore from '@ohos.data.relationalStore';  // 系统数据库
import { ColumnInfo, ColumnType } from '../bean/ColumnInfo';
import Logger from './Logger';  // 自定义打印类

const DB_FILENAME: string = 'HealthyApp.db'

class DbUtil {
  rdbStore: relationalStore.RdbStore

  initDB(context: common.UIAbilityContext): Promise<void> {
    let config: relationalStore.StoreConfig = {
      name: DB_FILENAME,
      securityLevel: relationalStore.SecurityLevel.S1
    }
    return new Promise<void>((resolve, reject) => {
      relationalStore.getRdbStore(context, config)
        .then(rdbStore => {
          this.rdbStore = rdbStore
          Logger.debug('rdbStore 初始化完成!')
          resolve()
        })
        .catch(reason => {
          Logger.debug('rdbStore 初始化异常', JSON.stringify(reason))
          reject(reason)
        })
    })
  }

  createTable(createSQL: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.rdbStore.executeSql(createSQL)
        .then(() => {
          Logger.debug('创建表成功', createSQL)
          resolve()
        })
        .catch(err => {
          Logger.error('创建表失败,' + err.message, JSON.stringify(err))
          reject(err)
        })
    })
  }
  // obj是匿名对象{id: 12, name:zs,age: 18}
  insert(tableName: string, obj: any, columns: ColumnInfo[]): Promise<number> {
    return new Promise((resolve, reject) => {
      // 1.构建新增数据
      let value = this.buildValueBucket(obj, columns)
        this.rdbStore.insert(tableName, value, (err, id) => {
           if (err) {
             Logger.error('新增失败!', JSON.stringify(err))
             reject(err)
           }else {
             Logger.debug('新增成功!新增id:', id.toString())
             resolve(id)
           }
        })
    })
  }

  delete(predicates: relationalStore.RdbPredicates): Promise<number> {
    return new Promise((resolve, reject) => {
       this.rdbStore.delete(predicates, (err, rows) => {
         if (err) {
           Logger.error('删除失败!', JSON.stringify(err))
           reject(err)
         } else {
           Logger.debug('删除成功!删除行数:', rows.toString())
           resolve(rows)
         }
       })
    })
  }

  // columns 把你要查询的字段告诉我
  // 以后调用的时候知道类型
  queryForList<T>(predicates: relationalStore.RdbPredicates, columns: ColumnInfo[]): Promise<T[]> {
    return new Promise((resolve, reject) => {
      this.rdbStore.query(predicates, columns.map(info => info.columnName), (err, result) => {  // map将对象里的属性映射成数组
        if (err) {
          Logger.error('查询失败!', JSON.stringify(err))
          reject(err)
        } else {
          Logger.debug('查询成功!查询行数:', result.rowCount.toString())
          resolve(this.parseResultSet(result, columns))   // 由于查询结果是一个ResultSet,我们解析成数组,以后调用的时候方便
        }
      })
    })
  }

  // 解析函数
  parseResultSet<T>(result: relationalStore.ResultSet, columns: ColumnInfo[]){
    // 1.声明最终返回的结果
    let arr = []
    if (result.rowCount <= 0) {
      return arr
    }
    // 3.处理结果
    while (!result.isAtLastRow) {
      // 3.1.去下一行
      result.goToNextRow()
      // 3.2.解析这行数据,转为对象
      let obj = {}
      columns.forEach(info => {
        let val = null
         switch (info.type) {  // 判断类型
           case ColumnType.LONG:  // 如果是LONG
             val = result.getLong(result.getColumnIndex(info.columnName))
             break
           case ColumnType.DOUBLE: // 如果是DOUBLE
             val = result.getDouble(result.getColumnIndex(info.columnName))
             break
           case ColumnType.STRING:  // 如果是STRING
             val = result.getString(result.getColumnIndex(info.columnName))
             break
           case ColumnType.BLOB:  // 如果是BLOB
             val = result.getBlob(result.getColumnIndex(info.columnName))
             break
         }
        obj[info.name] = val
      })
      // 3.3.将对象填入结果数组
      arr.push(obj)
      Logger.debug('查询到数据:', JSON.stringify(obj))
    }
    return arr
  }

  buildValueBucket(obj: any, columns: ColumnInfo[]): relationalStore.ValuesBucket{
    let value = {}
    columns.forEach(info => {
      let val = obj[info.name]
      if (typeof val !== 'undefined') {
        value[info.columnName] = val  // 数据库的字段名和我们添加时候写的属性名称对应起来
      }
    })
    return value
  }
}

let dbUtil: DbUtil = new DbUtil();

export default dbUtil as DbUtil
数据查询
/**
 * 数据库建表语句
 */
import relationalStore from '@ohos.data.relationalStore'
import { ColumnInfo, ColumnType } from '../common/bean/ColumnInfo'
import RecordPO from '../common/bean/RecordPO'
import DbUtil from '../common/utils/DbUtil'

const CREATE_TABLE_SQL: string = `
 CREATE TABLE IF NOT EXISTS record (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   type_id INTEGER NOT NULL,
   item_id INTEGER NOT NULL,
   amount DOUBLE NOT NULL,
   create_time INTEGER NOT NULL
 )
 `

const COLUMNS: ColumnInfo[] = [
 {name: 'id', columnName: 'id', type: ColumnType.LONG},
 {name: 'typeId', columnName: 'type_id', type: ColumnType.LONG},
 {name: 'itemId', columnName: 'item_id', type: ColumnType.LONG},
 {name: 'amount', columnName: 'amount', type: ColumnType.DOUBLE},
 {name: 'createTime', columnName: 'create_time', type: ColumnType.LONG}
]

// 表名
const TABLE_NAME = 'record'
const ID_COLUMN = 'id'
const DATE_COLUMN = 'create_time'

class RecordModel {
 getCreateTableSql(): string {
  return CREATE_TABLE_SQL
 }

 insert(record: RecordPO): Promise<number>{
  // TABLE_NAM  E表名
  // record     插入的数据
  // COLUMNS    数据库字段名
  return DbUtil.insert(TABLE_NAME, record, COLUMNS)
 }

 deleteById(id: number): Promise<number>{
  // 1.删除条件
  let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
  predicates.equalTo(ID_COLUMN, id)
  // 2.删除
  return DbUtil.delete(predicates)
 }

 listByDate(date: number): Promise<RecordPO[]>{
  // 1.查询条件
  let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
  // predicates查询条件
  predicates.equalTo(DATE_COLUMN, date)
  // 2.查询
  // predicates查询条件, 字段
  return DbUtil.queryForList(predicates, COLUMNS)
 }
}

let recordModel = new RecordModel()

export default recordModel as RecordModel

业务层开发

我们上面把数据从数据库中查出来了,但是我们页面需要的数据,还有些差别,我们需要处理下

在这里插入图片描述

从数据库里查询的内容,应该先做分组在渲染

在这里插入图片描述
在这里插入图片描述
gitee 源码:
https://gitee.com/cheng_yong_xu/ho_healthy

实现数据持久化和页面交互

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值