期末项目黑马健康5
文章目录
前言
这篇用来介绍如何实现数据模型–饮食记录、数据模型–通用DB工具
一、项目名称:黑马健康
- “黑马健康”致力于为用户提供个性化的营养饮食指导,帮助用户实现健康饮食。它集成了食物营养和热量数据查询、定制食谱、饮食分析、健康社区等功能,为用户提供全方位的健康管理服务。
二、应用运行过程
1.数据模型–饮食记录
- 我们在设计数据模型的时候,除了要设计饮食记录的模型以外,饮食记录的类型也应该有自己的数据模型,早餐,午餐,晚餐,加餐和运动都是类型。
饮食记录的数据操作模型需要用到数据库。
效果展示
代码
RecordType.ets
代码如下(示例):
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
}
}
RecordType.ets
import RecordItem from './RecordItem'
/**
* 饮食记录的页面数据模型
*/
export default class RecordVO {
/**
* 记录id
*/
id: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 卡路里总数
*/
calorie: number
/**
* 记录中的食物或运动信息
*/
recordItem: RecordItem
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number = 0
}
RecordPO.ets
export default class RecordPO{
/**
* 记录id
*/
id: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 记录中的食物或运动信息
*/
itemId: number
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number
/**
* 记录的日期
*/
createTime: number
}
RecordTypeModel.ets
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}
RecordVO.ets
import RecordItem from './RecordItem'
/**
* 饮食记录的页面数据模型
*/
export default class RecordVO {
/**
* 记录id
*/
id: number
/**
* 饮食记录类型
*/
typeId: number
/**
* 卡路里总数
*/
calorie: number
/**
* 记录中的食物或运动信息
*/
recordItem: RecordItem
/**
* 食物数量或运动时长,如果是运动信息则无
*/
amount: number = 0
}
2.数据模型–通用DB工具
- 来完成饮食记录的数据操作,model的开发。
代码
RecordModel.ets
代码如下(示例):
/**
* 数据库建表语句
*/
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
DbUtil.ts
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)
}
})
})
}
//根据条件删除数据。
// 参数:predicates 是用于定义删除条件的对象。
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)
}
})
})
}
//方法是用来从数据库中查询数据并返回一个指定类型 T 的数组
// 一个 RdbPredicates 对象,定义了查询的条件。
//一个 ColumnInfo 对象的数组,定义了要查询的列信息。
queryForList<T>(predicates: relationalStore.RdbPredicates, columns: ColumnInfo[]): Promise<T[]> {
return new Promise((resolve, reject) => {
//使用 rdbStore.query 方法执行查询。
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))
}
})
})
}
//用于将数据库查询的结果集(ResultSet)解析为指定类型 T 的数组。
parseResultSet<T> (result: relationalStore.ResultSet, columns: ColumnInfo[]): T[] {
// 1.声明最终返回的结果
let arr = []
// 2.判断是否有结果
if (result.rowCount <= 0) {
return arr
}
// 3.处理结果
while (!result.isAtLastRow) {
// 3.1.去下一行 goToNextRow 方法将结果集的指针移动到下一行。
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 中提取与 columns 中列名相关的信息,并将这些信息组织成一个新的对象 value。
buildValueBucket(obj: any, columns: ColumnInfo[]): relationalStore.ValuesBucket {
let value = {}
//forEach 方法遍历 columns 数组中的每一个 ColumnInfo 对象。
columns.forEach(info => {
let val = obj[info.name]
//检查 val 是否为 undefined。如果不是 undefined,则将值添加到 value 对象中。
if (typeof val !== 'undefined') {
value[info.columnName] = val
}
})
return value
}
}
//这段代码定义了一个用于从给定对象中 提取 与指定列相关的信息 并返回一个新对象的函数 buildValueBucket
let dbUtil: DbUtil = new DbUtil();
export default dbUtil as DbUtil
EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import { CommonConstants } from '../common/constants/CommonConstants';
import DateUtil from '../common/utils/DateUtil';
import DbUtil from '../common/utils/DbUtil';
import PreferenceUtil from '../common/utils/PreferenceUtil';
import RecordModel from '../model/RecordModel';
export default class EntryAbility extends UIAbility {
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');
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/WelcomePage', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy() {
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground() {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground() {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
}
3.饮食记录业务层开发
- 利用饮食记录model操作对象的增删改查来完成页面上的互动,实现饮食记录的这种持久化保存。用groupInfo实现数据的分组,StatsInfo来统计当日的卡路里和运动消耗等。
代码
- 实现了把数据库查询出来的数据转换成页面所需要的数据接口。
GroupInfo.ets
export default class GroupInfo<TYPE, ELEMENT> {
/**
* 分组类型
*/
type: TYPE
/**
* 组内数据集合
*/
items: ELEMENT[]
/**
* 组内记录的总热量
*/
calorie: number = 0
constructor(type: TYPE, items: ELEMENT[]) {
this.type = type
this.items = items
}
}
RecordService.ets
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
4.实现数据持久化和页面交互
- 利用前面的功能来实现饮食记录页面的交互,最终完成数据的持久化保存。
效果展示
代码
RecordIndex.ets
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 records: RecordVO[] = []
@Prop @Watch('handlePageShow') isPageShow: boolean
handlePageShow(){
if(this.isPageShow){
this.aboutToAppear()
}
}
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'))
}
}
RecordList.ets
import router from '@ohos.router'
import RecordPO from '../../common/bean/RecordPO'
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordService from '../../service/RecordService'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordType from '../../viewmodel/RecordType'
import RecordVO from '../../viewmodel/RecordVO'
@Extend(Text) function grayText(){//1.分组的标题 中 Text单独样式
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct RecordList {
@Consume @Watch('handleRecordsChange')records:RecordVO[]
@State groups:GroupInfo<RecordType,RecordVO>[]=[]
handleRecordsChange(){
this.groups=RecordService.calculateGroupInfo(this.records)
}
build() {
List({space:CommonConstants.SPACE_10}){//上下有间隔
ForEach(this.groups,(group:GroupInfo<RecordType,RecordVO>)=>{
ListItem(){
Column() {
//1.分组的标题
Row({ space: CommonConstants.SPACE_4 }) {
Image(group.type.icon).width(24)
Text(group.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
Text('建议${group.type.min}~${group.type.min}千卡').grayText()
Blank() //空白
Text('group.calorie.toFixed(0)').fontSize(14).fontColor($r('app.color.light_primary_color'))
Text('千卡').grayText()
Image($r('app.media.ic_public_add_norm_filled'))
.width(20)
.fillColor($r('app.color.primary_color'))
}
.width('100%')
.onClick(()=>{
router.pushUrl({
url:'pages/ItemIndex',
params:{type:group.type}
})
})
//2.组内记录列表
List() {
ForEach(group.items, (item:RecordVO) => {
ListItem(){
Row({space:CommonConstants.SPACE_4}){
Image(item.recordItem.image).width(50)
Column({space:CommonConstants.SPACE_4}){
Text(item.recordItem.name).fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('${item.amount}${item.recordItem.unit}}').grayText()
}
Blank()
Text('${item.calorie}千卡').grayText()
}
.width('100%')
.padding(CommonConstants.SPACE_6)
}.swipeAction({end:this.delectButton.bind(this)})//左滑显示删除按钮
})
}
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(CommonConstants.DEFAULT_18)//边框弧度
.padding(CommonConstants.SPACE_12)//边距 内容不贴边
}
})
}
.width(CommonConstants.THOUSANDTH_940)
.margin({top:10})
.height('100%')
}
@Builder delectButton(){
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.fillColor(Color.Red)
.margin(5)
}
}
- 在RecordList中完成跳转,跳转到ItemIndex
.onClick(()=>{
router.pushUrl({
url:'pages/ItemIndex',
params:{type:group.type}
})
})
- 在ItemIndex中,写取出跳转时传给的参数,要写一个生命周期钩子onPageShow(),去获取传给的参数,通过router.getParams()。
onPageShow(){
// 1.获取跳转时的参数
let params: any = router.getParams()
// 2.获取点击的饮食记录类型
this.type = params.type
this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
}
- 实现持久化保存:
.onClick(() => {
// 1.持久化保存
RecordService.insert(this.type.id, this.item.id, this.amount)
.then(() => {
// 2.关闭弹窗
this.showPanel = false
})
})
总结
1.数据模型–饮食记录
- 根据饮食记录数据模型来编写数据库字段,数据库表结构,建立表。
2.数据模型–通用DB工具
- 可根据日期条件查询出来,代码如下
listByDate(date: number): Promise<RecordPO[]>{
// 1.查询条件
let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
predicates.equalTo(DATE_COLUMN, date)
// 2.查询
return DbUtil.queryForList(predicates, COLUMNS)
}
}
3.饮食记录业务层开发
- 在RecordService.ets中,
将记录列表按照记录类型分组
@param records 记录列表
@returns 分组记录信息
用forEach遍历所有饮食记录
records.forEach(record => {
// 2.1.把每个记录存入其对应类型的分组中
groups[record.typeId].items.push(record)
// 2.2.计算该组的总热量
groups[record.typeId].calorie += record.calorie
})
4.实现数据持久化和页面交互
- handleRecordsChange用于在前端框架中响应某些数据变化(例如,当“records”数据发生变化时)并更新相关的统计信息或视图。
handleRecordsChange(){
this.info = RecordService.calculateStatsInfo(this.records)
}
- 在触发页面渲染时,例如:要把StatsBuilder里面的参数变成传递引用。
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'))
}
}