经过前几期的不懈努力,健康饮食项目的基本功能已经实现,接下来也就是最后一个问题,那就是数据的持久化和数据的传递以及正确显示。为了解决这两个问题,我们首先需要将各种属性量全都抽象出来,再在前期死数据代码的基础上进行修改(文章里的代码已经是修改完毕的,在此篇章里只展示抽象出来的对象以及数据持久化的实现过程)。
1、记录项类型
/**
* 记录项类型
*/
export default class ItemCategory{
/**
* 类型id
*/
id: number
/**
* 类型名称
*/
name: ResourceStr
constructor(id: number, name: ResourceStr) {
this.id = id
this.name = name
}
}
2、饮食中的记录项
/**
* 饮食记录中的记录项,可以是食物或运动
*/
export default class RecordItem{
/**
* id
*/
id: number
/**
* 名称
*/
name: ResourceStr
/**
* 图片
*/
image: ResourceStr
/**
* 分类id
*/
categoryId: number
/**
* 包含的卡路里
*/
calorie: number
/**
* 单位
*/
unit: ResourceStr
/**
* 碳水含量,单位(克)
*/
carbon: number
/**
* 蛋白质含量,单位(克)
*/
protein: number
/**
* 脂肪含量,单位(克)
*/
fat: number
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、食物类型的枚举
便于获取类型。
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}
4、食物和运动的数据
import RecordItem from '../viewmodel/RecordItem'
import ItemCategory from '../viewmodel/ItemCategory'
import { FoodCategories, FoodCategoryEnum, WorkoutCategories, WorkoutCategoryEnum } from './ItemCategoryModel'
import GroupInfo from '../viewmodel/GroupInfo'
const foods: RecordItem[] = [
new RecordItem(0, '米饭',$r('app.media.rice'),FoodCategoryEnum.STAPLE, '碗', 209, 46.6, 4.7, 0.5),
new RecordItem(1, '馒头',$r('app.media.steamed_bun'),FoodCategoryEnum.STAPLE, '个', 114, 24.0, 3.6, 0.6),
new RecordItem(2, '面包',$r('app.media.bun'),FoodCategoryEnum.STAPLE, '个', 188, 35.2, 5.0, 3.1),
new RecordItem(3, '全麦吐司',$r('app.media.toast'),FoodCategoryEnum.STAPLE, '片', 91, 15.5, 4.4, 1.3),
new RecordItem(4, '紫薯',$r('app.media.purple_potato'),FoodCategoryEnum.STAPLE, '个', 163, 42.0, 1.6, 0.4),
new RecordItem(5, '煮玉米',$r('app.media.corn'),FoodCategoryEnum.STAPLE, '根', 111, 22.6, 4.0, 1.2),
new RecordItem(6, '黄瓜',$r('app.media.cucumber'),FoodCategoryEnum.FRUIT, '根', 29, 5.3, 1.5, 0.4),
new RecordItem(7, '蓝莓',$r('app.media.blueberry'),FoodCategoryEnum.FRUIT, '盒', 71, 18.1, 0.9, 0.4),
new RecordItem(8, '草莓',$r('app.media.strawberry'),FoodCategoryEnum.FRUIT, '颗', 14, 3.1, 0.4, 0.1),
new RecordItem(9, '火龙果',$r('app.media.pitaya'),FoodCategoryEnum.FRUIT, '个', 100, 24.6, 2.2, 0.5),
new RecordItem(10, '奇异果',$r('app.media.kiwi'),FoodCategoryEnum.FRUIT, '个', 25, 8.4, 0.5, 0.3),
new RecordItem(11, '煮鸡蛋',$r('app.media.egg'),FoodCategoryEnum.MEAT, '个', 74, 0.1, 6.2, 5.4),
new RecordItem(12, '煮鸡胸肉',$r('app.media.chicken_breast'),FoodCategoryEnum.MEAT, '克', 1.15, 0.011, 0.236, 0.018),
new RecordItem(13, '煮鸡腿肉',$r('app.media.chicken_leg'),FoodCategoryEnum.MEAT, '克', 1.87, 0.0, 0.243, 0.092),
new RecordItem(14, '牛肉',$r('app.media.beef'),FoodCategoryEnum.MEAT, '克', 1.22, 0.0, 0.23, 0.033),
new RecordItem(15, '鱼肉',$r("app.media.fish"),FoodCategoryEnum.MEAT, '克', 1.04, 0.0, 0.206, 0.024),
new RecordItem(16, '牛奶',$r("app.media.milk"),FoodCategoryEnum.MEAT, '毫升', 0.66, 0.05, 0.03, 0.038),
new RecordItem(17, '酸奶',$r("app.media.yogurt"),FoodCategoryEnum.MEAT, '毫升', 0.7, 0.10, 0.032, 0.019),
new RecordItem(18, '核桃',$r("app.media.walnut"),FoodCategoryEnum.NUT, '颗', 42, 1.2, 1.0, 3.8),
new RecordItem(19, '花生',$r("app.media.peanut"),FoodCategoryEnum.NUT, '克', 3.13, 0.13, 0.12, 0.254),
new RecordItem(20, '腰果',$r("app.media.cashew"),FoodCategoryEnum.NUT, '克', 5.59, 0.416, 0.173, 0.367),
new RecordItem(21, '无糖拿铁',$r("app.media.coffee"),FoodCategoryEnum.OTHER, '毫升', 0.43, 0.044, 0.028, 0.016),
new RecordItem(22, '豆浆',$r("app.media.soybean_milk"),FoodCategoryEnum.OTHER, '毫升', 0.31, 0.012, 0.030, 0.016),
]
const workouts: RecordItem[] = [
new RecordItem(10000, '散步',$r('app.media.ic_walk'), WorkoutCategoryEnum.WALKING, '小时', 111),
new RecordItem(10001, '快走',$r('app.media.ic_walk'), WorkoutCategoryEnum.WALKING, '小时', 343),
new RecordItem(10002, '慢跑',$r('app.media.ic_running'), WorkoutCategoryEnum.RUNNING, '小时', 472),
new RecordItem(10003, '快跑',$r('app.media.ic_running'), WorkoutCategoryEnum.RUNNING, '小时', 652),
new RecordItem(10004, '自行车',$r('app.media.ic_ridding'), WorkoutCategoryEnum.RIDING, '小时', 497),
new RecordItem(10005, '动感单车',$r('app.media.ic_ridding'), WorkoutCategoryEnum.RIDING, '小时', 587),
new RecordItem(10006, '瑜伽',$r('app.media.ic_aerobics'), WorkoutCategoryEnum.AEROBICS, '小时', 172),
new RecordItem(10007, '健身操',$r('app.media.ic_aerobics'), WorkoutCategoryEnum.AEROBICS, '小时', 429),
new RecordItem(10008, '游泳',$r('app.media.ic_swimming'), WorkoutCategoryEnum.SWIMMING, '小时', 472),
new RecordItem(10009, '冲浪',$r('app.media.ic_swimming'), WorkoutCategoryEnum.SWIMMING, '小时', 429),
new RecordItem(10010, '篮球',$r('app.media.ic_basketball'), WorkoutCategoryEnum.BALLGAME, '小时', 472),
new RecordItem(10011, '足球',$r('app.media.ic_football'), WorkoutCategoryEnum.BALLGAME, '小时', 515),
new RecordItem(10012, '排球',$r("app.media.ic_volleyball"), WorkoutCategoryEnum.BALLGAME, '小时', 403),
new RecordItem(10013, '羽毛球',$r("app.media.ic_badminton"), WorkoutCategoryEnum.BALLGAME, '小时', 386),
new RecordItem(10014, '乒乓球',$r("app.media.ic_table_tennis"), WorkoutCategoryEnum.BALLGAME, '小时', 257),
new RecordItem(10015, '哑铃飞鸟',$r("app.media.ic_dumbbell"), WorkoutCategoryEnum.STRENGTH, '小时', 343),
new RecordItem(10016, '哑铃卧推',$r("app.media.ic_dumbbell"), WorkoutCategoryEnum.STRENGTH, '小时', 429),
new RecordItem(10017, '仰卧起坐',$r("app.media.ic_sit_up"), WorkoutCategoryEnum.STRENGTH, '小时', 515),
]
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.创建空的分组
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
5、食物或列表的分类项
export default class GroupInfo<TYPE, ELEMENT> {
/**
* 分组类型
*/
type: TYPE
/**
* 组内数据集合
*/
items: ELEMENT[]
/**
* 组内记录的总热量
*/
calorie: number = 0
constructor(type: TYPE, items: ELEMENT[]) {
this.type = type
this.items = items
}
}
6、饮食记录带统计的页面
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
}
}
7、饮食记录的页面数据模型
import RecordItem from './RecordItem'
/**
* 饮食记录的页面数据模型
*/
export default class RecordVO {
/**
* 记录id
*/
id: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 卡路里总数
*/
calorie: number
/**
* 记录中的食物或运动信息
*/
recordItem: RecordItem
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number = 0
}
8、饮食类型模型
import RecordType from '../viewmodel/RecordType'
enum RecordTypeEnum {
/**
* 早餐
*/
BREAKFAST,
/**
* 午餐
*/
LUNCH,
/**
* 晚餐
*/
DINNER,
/**
* 加餐
*/
EXTRA_MEAL,
/**
* 运动
*/
WORKOUT
}
/**
* 记录类型常量
*/
const RecordTypes: RecordType[] = [
new RecordType(0, $r("app.string.breakfast"), $r("app.media.ic_breakfast"), 423, 592),
new RecordType(1, $r("app.string.lunch"), $r("app.media.ic_lunch"), 592, 761),
new RecordType(2, $r("app.string.dinner"), $r("app.media.ic_dinner"), 423, 592),
new RecordType(3, $r("app.string.extra_meal"), $r("app.media.ic_extra_m"), 0, 169),
new RecordType(4, $r("app.string.workout"), $r("app.media.ic_workout")),
]
export {RecordTypes, RecordTypeEnum}
9、饮食记录模型
/**
* 数据库建表语句
*/
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、饮食类型持久化对象
export default class RecordPO{
/**
* 记录id
*/
id?: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 记录中的食物或运动信息
*/
itemId: number
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number
/**
* 记录的日期
*/
createTime: number
}
11、数据库的数据操作逻辑
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 = 'HeiMaHealthy.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)
})
})
}
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
}
}
let dbUtil: DbUtil = new DbUtil();
export default dbUtil as DbUtil
12、数据库的使用对象
export default class StatsInfo{
/**
* 当日摄入卡路里总量
*/
intake: number = 0
/**
* 当日运动消耗能量
*/
expend: number = 0
/**
* 当日摄入碳水总量
*/
carbon: number = 0
/**
* 当日摄入蛋白总量
*/
protein: number = 0
/**
* 当日摄入脂肪总量
*/
fat: number = 0
constructor(intake: number = 0, expend: number = 0, carbon: number = 0, protein: number = 0, fat: number = 0) {
this.intake = intake
this.expend = expend
this.carbon = carbon
this.protein = protein
this.fat = fat
}
}
13、根据页面修改数据库数据
import RecordPO from '../common/bean/RecordPO'
import DateUtil from '../common/utils/DateUtil'
import ItemModel from '../model/ItemModel'
import RecordModel from '../model/RecordModel'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import GroupInfo from '../viewmodel/GroupInfo'
import RecordType from '../viewmodel/RecordType'
import RecordVO from '../viewmodel/RecordVO'
import StatsInfo from '../viewmodel/StatsInfo'
class RecordService {
/**
* 新增饮食记录
* @param typeId 记录类型id
* @param itemId 记录项id
* @param amount 记录项数量(食物量、运动时长)
* @returns 新增数量
*/
insert(typeId: number, itemId: number, amount: number): Promise<number>{
// 1.获取时间
let createTime = (AppStorage.Get('selectedDate') || DateUtil.beginTimeOfDay(new Date())) as number
// 2.新增
return RecordModel.insert({typeId, itemId, amount, createTime})
}
/**
* 根据id删除饮食记录
* @param id 记录id
* @returns 删除条数
*/
deleteById(id: number): Promise<number>{
return RecordModel.deleteById(id)
}
/**
* 根据日期查询饮食记录列表
* @param date 要查询的日期
* @returns 记录列表
*/
async queryRecordByDate(date: number): Promise<RecordVO[]>{
// 1.查询数据库的RecordPO
let rps = await RecordModel.listByDate(date)
// 2.将RecordPO转为RecordVO
return rps.map(rp => {
// 2.1.获取po中的基本属性
let rv = {id: rp.id, typeId: rp.typeId, amount: rp.amount} as RecordVO
// 2.2.查询记录项
rv.recordItem = ItemModel.getById(rp.itemId, rp.typeId !== RecordTypeEnum.WORKOUT)
// 2.3.计算热量
rv.calorie = rp.amount * rv.recordItem.calorie
return rv
})
}
/**
* 根据记录列表信息统计出热量、营养素信息
* @param records 饮食记录列表
* @returns 热量、营养素信息
*/
calculateStatsInfo(records: RecordVO[]): StatsInfo{
// 1.准备结果
let info = new StatsInfo()
if(!records || records.length <= 0){
return info
}
// 2.计算统计数据
records.forEach(r => {
if(r.typeId === RecordTypeEnum.WORKOUT){
// 运动,累加消耗热量
info.expend += r.calorie
}else{
// 食物,累加摄入热量、蛋白质、碳水、脂肪
info.intake += r.calorie
info.carbon += r.recordItem.carbon
info.protein += r.recordItem.protein
info.fat += r.recordItem.fat
}
})
// 3.返回
return info
}
/**
* 将记录列表按照记录类型分组
* @param records 记录列表
* @returns 分组记录信息
*/
calculateGroupInfo(records: RecordVO[]): GroupInfo<RecordType, RecordVO>[]{
// 1.创建空的记录类型分组
let groups = RecordTypes.map(recordType => new GroupInfo(recordType, []))
if(!records || records.length <= 0){
return groups
}
// 2.遍历所有饮食记录,
records.forEach(record => {
// 2.1.把每个记录存入其对应类型的分组中
groups[record.typeId].items.push(record)
// 2.2.计算该组的总热量
groups[record.typeId].calorie += record.calorie
})
return groups
}
}
let recordService = new RecordService()
export default recordService as RecordService
上述代码就是有关于数据的所有了。
历经千辛万苦,我的健康饮食项目终于完工了(除去我没打算去作的两个页面的功能),不知道有多少小伙伴坚持看到了最后,感谢支持。
通过此次的程序的设计与实现,我的知识点也是呼呼往上涨啊,一些以前从来没用到的组件、组件的新的使用方法等一系列新的信息在程序的实现过程中经过不断的左查右询,并以一种想象不到的方式进入到了我的脑子里。
在此程序中,我们详细分析了该记录列表组件的设计和实现细节。在饮食记录中,可以点击早餐、午餐、晚餐或加餐。进入记录界面后,可以在顶部搜索栏中搜索食物名称,通过详细记录食物种类、摄入量、时间等关键信息,用户能够清晰地了解自己的饮食模式,从而更有效地进行饮食规划和健康管理。用户可以根据自己的喜好和目标,自定义记录的内容和格式,轻松添加备注和标签,以便更准确地追踪和分析饮食情况。记录列表中主要运用了List列表组件实现了遍历循环,List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表,注意List下用ForEach循环数据,列表子项用ListItem组件,组件中再设置布局。在轮播的页面中使用了Tabs组件,还有面板的弹出组件Panel等等。全程序使用了数据库的用户持久化,完善了程序的细节功能。