黑马健康App

七、数据模型

到目前为止我们已经完成了饮食记录功能相关页面的开发,但目前这些页面中我们用到的数据都是假数据,食物信息只有一个吐司,饮食记录也没有办法持久保存,所以接下来我们需要去分析饮食记录的数据模型,然后基于这些数据模型去提供真实数据,去完成页面渲染并实现饮食记录的持久化保存。

(一)记录项

1.数据模型分析

2.编写模型

目录:

/*ItemCategory*/
/**
 * 记录项类型
 */
export default class ItemCategory{
  /**
   * 类型id
   */
  id: number
  /**
   * 类型名称
   */
  name: ResourceStr

  constructor(id: number, name: ResourceStr) {
    this.id = id
    this.name = name
  }
}

/*RecordItem*/
/**
 * 饮食记录中的记录项,可以是食物或运动
 */
export default class RecordItem{
  /**
   * id
   */
  id: number
  /**
   * 名称
   */
  name: ResourceStr
  /**
   * 图片
   */
  image: ResourceStr
  /**
   * 分类id
   */
  categoryId: number
  /**
   * 包含的卡路里
   */
  calorie: number
  /**
   * 单位
   */
  unit: ResourceStr
  /**
   * 碳水含量,单位(克)
   */
  carbon: number
  /**
   * 蛋白质含量,单位(克)
   */
  protein: number
  /**
   * 脂肪含量,单位(克)
   */
  fat: number

  //将营养素信息设置默认值为0,也就是可选的。
  constructor(id: number, name: ResourceStr, image: ResourceStr,
              categoryId: number, unit: ResourceStr, calorie: number,
              carbon: number = 0, protein: number = 0, fat: number = 0) {
    this.id = id
    this.name = name
    this.image = image
    this.categoryId = categoryId
    this.unit = unit
    this.calorie = calorie
    this.protein = protein
    this.fat = fat
    this.carbon = carbon
  }
}

3.代码实现

(1)编写数据操作接口

这里设置枚举作用是简化取记录项类型数组和记录项数组的操作(因为枚举项数值与对应数组元素角标一致)。

问题:查询运动时类型部分变成省略号不现实
原因:因为类型太多放不开了
解决方法:给Tabs加.barMode(BarMode.Scrollable)属性,使元素超出宽度时可滑动显示。

/*ItemCategoryModel*/
import ItemCategory from '../viewmodel/ItemCategory'

/**
 * 食物类型的枚举
 */
enum FoodCategoryEnum{
  /**
   * 主食
   */
  STAPLE,
  /**
   * 蔬果
   */
  FRUIT,
  /**
   * 肉蛋奶
   */
  MEAT,
  /**
   * 坚果
   */
  NUT,
  /**
   * 其它
   */
  OTHER,
}

/**
 * 食物类型数组
 */
let FoodCategories = [
  new ItemCategory(0, $r('app.string.staple')),
  new ItemCategory(1, $r('app.string.fruit')),
  new ItemCategory(2, $r('app.string.meat')),
  new ItemCategory(3, $r('app.string.nut')),
  new ItemCategory(4, $r('app.string.other_type')),
]

/**
 * 运动类型枚举
 */
enum WorkoutCategoryEnum {
  /**
   * 走路
   */
  WALKING,
  /**
   * 跑步
   */
  RUNNING,
  /**
   * 骑行
   */
  RIDING,
  /**
   * 跳操
   */
  AEROBICS,
  /**
   * 游泳
   */
  SWIMMING,
  /**
   * 打球
   */
  BALLGAME,
  /**
   * 力量训练
   */
  STRENGTH
}

/**
 * 运动类型数组
 */
let WorkoutCategories = [
  new ItemCategory(0, $r('app.string.walking_type')),
  new ItemCategory(1, $r('app.string.running')),
  new ItemCategory(2, $r('app.string.riding')),
  new ItemCategory(3, $r('app.string.aerobics')),
  new ItemCategory(4, $r('app.string.swimming')),
  new ItemCategory(5, $r('app.string.ballgame')),
  new ItemCategory(6, $r('app.string.strength')),
]

export {FoodCategories , WorkoutCategories , FoodCategoryEnum, WorkoutCategoryEnum}

/*ItemModel*/
//查询接口
class ItemModel{

  getById(id: number, isFood: boolean = true){
    return isFood ? foods[id] : workouts[id - 10000]
  }

  //查出所有
  list(isFood: boolean = true): RecordItem[]{
    return isFood ? foods : workouts
  }

  //按需求查询
  listItemGroupByCategory(isFood: boolean = true){
    // 1.判断要处理的是食物还是运动
    let categories = isFood ? FoodCategories : WorkoutCategories
    let items = isFood ? foods: workouts
    // 2.创建空的分组(map是映射,作用是遍历前面的数组中的每一个元素,把它们转变成另外一种元素)
    let groups = categories.map(itemCategory => new GroupInfo(itemCategory, []))
    // 3.遍历记录项列表,将食物添加到对应的分组
    items.forEach(item => groups[item.categoryId].items.push(item))
    // 4.返回结果
    return groups
  }
}

let itemModel = new ItemModel()
export default itemModel as ItemModel
(2)修改食物列表
/*GroupInfo*/
export default class GroupInfo<TYPE, ELEMENT> {
  /**
   * 分组类型
   */
  type: TYPE
  /**
   * 组内数据集合
   */
  items: ELEMENT[]

  /**
   * 组内记录的总热量
   */
  calorie: number = 0

  constructor(type: TYPE, items: ELEMENT[]) {
    this.type = type
    this.items = items
  }
}

/*ItemList*/
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
  @State isFood: boolean=true

  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%')
    .barMode(BarMode.Scrollable)
  }
}

previewer: isFood为true时显示食物,为false时显示运动。

(3)弹窗的改进
/*ItemList*/
  showPanel: (item: RecordItem) => void

  @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'))
            }.alignItems(HorizontalAlign.Start)

            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))//被点击时展示底层面板,将item传给父组件i、Itemindex
      })
    }
    .width('100%')
    .height('100%')
  }

/*ItemIndex*/
  @State item: RecordItem = null

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

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

/*ItemCard*/
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}){
      // 1.图片
      Image(this.item.image).width(150)
      // 2.名称
      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)//下划线,opacity调整透明度
      // 3.营养素
      Row({space: CommonConstants.SPACE_8}){
        this.NutrientInfo( '热量(千卡)',this.item.calorie)
        //判断是否展示营养值(食物展示,运动不展示),id<10000为食物,id>10000为运动
        if(this.item.id < 10000){
          this.NutrientInfo('碳水(千卡)',  this.item.carbon)
          this.NutrientInfo('蛋白质(千卡)',  this.item.protein)
          this.NutrientInfo( '脂肪(千卡)', this.item.fat)
        }
      }
      Divider().width(CommonConstants.THOUSANDTH_940).opacity(0.6)
      // 4.数量
      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)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)//这里toFixed(1)的作用是将value转成一位小数
    }
  }
}

previewer:

(二)饮食记录

1.数据模型分析


2.编写模型

/*RecordType*/
/**
 *饮食记录类型的页面数据模型
 */
export default class RecordType{
  /**
   * 类型id
   */
  id: number
  /**
   * 类型名称
   */
  name: ResourceStr
  /**
   * 类型图标
   */
  icon: ResourceStr
  /**
   * 类型推荐最小卡路里
   */
  min: number
  /**
   * 类型推荐最大卡路里
   */
  max: number

  constructor(id: number, name: ResourceStr, icon: ResourceStr, min: number = 0, max: number = 0) {
    this.id = id
    this.name = name
    this.icon = icon
    this.min = min
    this.max = max
  }
}

/*RecordVO*/
import RecordItem from './RecordItem'

/**
 * 饮食记录的页面数据模型
 */
export default class RecordVO {
  /**
   * 记录id
   */
  id: number
  /**
   * 饮食记录类型
   */
  typeId: number

  /**
   * 卡路里总数
   */
  calorie: number

  /**
   * 记录中的食物或运动信息
   */
  recordItem: RecordItem

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number = 0
}

3.代码实现

(1)数据库模型
export default class RecordPO{
  /**
   * 记录id
   */
  id?: number
  /**
   * 饮食记录类型
   */
  typeId: number
  /**
   * 记录中的食物或运动信息
   */
  itemId: number

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number
  /**
   * 记录的日期
   */
  createTime: number
}
(2)数据操作接口
/*EntryAbility*/
  async onCreate(want, launchParam) {
    // 1.加载用户首选项
    PreferenceUtil.loadPreference(this.context)
    // 2.初始化日期
    AppStorage.SetOrCreate(CommonConstants.RECORD_DATE, DateUtil.beginTimeOfDay(new Date()))
    // 3.初始化RDB工具
    await DbUtil.initDB(this.context)//异步方法,需等待
    // 4.创建record表
    DbUtil.createTable(RecordModel.getCreateTableSql())

    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

/*ColumnInfo*/
export interface ColumnInfo{
  name: string
  columnName: string
  type: ColumnType
}

export enum ColumnType{
  LONG,
  DOUBLE,
  STRING,
  BLOB
}

/*DbUtil*/
  //新增
  insert(tableName: string, obj: any, columns: ColumnInfo[]): Promise<number> {
    return new Promise((resolve, reject) => {
      // 1.构建新增数据
      let value = this.buildValueBucket(obj, columns)
      // 2.新增
      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)
        }
      })
    })
  }

  //需传入查询条件和查询字段
  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) => {
        if (err) {
          Logger.error('查询失败!', JSON.stringify(err))
          reject(err)
        } else {
          Logger.debug('查询成功!查询行数:', result.rowCount.toString())
          resolve(this.parseResultSet(result, columns))
        }
      })
    })
  }

  //解析查询结果
  parseResultSet<T> (result: relationalStore.ResultSet, columns: ColumnInfo[]): T[] {
    // 1.声明最终返回的结果
    let arr = []
    // 2.判断是否有结果
    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:
            val = result.getLong(result.getColumnIndex(info.columnName))
            break
          case ColumnType.DOUBLE:
            val = result.getDouble(result.getColumnIndex(info.columnName))
            break
          case ColumnType.STRING:
            val = result.getString(result.getColumnIndex(info.columnName))
            break
          case ColumnType.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
  }
}

/*RecordModel*/
/**
 * 数据库建表语句
 */
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>{
    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.equalTo(DATE_COLUMN, date)
    // 2.查询
    return DbUtil.queryForList(predicates, COLUMNS)
  }
}

let recordModel = new RecordModel()
export default recordModel as RecordModel

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值