一、项目目标
综合运用本学期所学内容及个人自学知识,使用HarmonyOS 4.0及以上版本开发一款具有实用性和创新 性的移动应用软件。
二、项目介绍
黑马健康是一款功能全面的健康管理应用,它通过提供个性化的饮食记录、健康评估等功能,帮助用户轻松管理健康,改善饮食和生活习惯。无论是需要减肥塑形,还是关注日常营养摄入,黑马健康都能为用户提供定制化的服务,让健康管理变得简单而有效。
三、实验步骤
统计卡片
1.
2.
3.
4.
5.
6.CalorieStats组件
实现效果
饮食记录页面
列表
列表页UI设计
食物列表Tabs项开发
食物列表—底部Panel
底部弹窗
底部弹窗的头部设计
中间列表设计
四、遇到的问题及解决方案
1.开发饮食记录UI界面时,实现顶部搜索栏使用日期滑动选择器
2.如何把日期选择器的日期保存到全局
// 1.保存日期到全局存储 AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime())
getTime()转成ms值去计算,selectedDate存到AppStorage
3.日期不一致
@StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())
DateUtil.beginTimeOfDay(new Date()获取开始日期
把日期回显回去,在对话框补全,读取对话款的选中日期
builder: DatePickDialog({selectedDate: new Date(this.selectedDate)})
日期格式化展示时,使用selectedDate
Text(DateUtil.formatDate(this.selectedDate)) 把ms格式化成日期
4.如何让设置进度条
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({label: '还可以吃', value: this.remainCalorie(),tips: `推荐${this.recommend}`}) }
使用Stack,为环绕关系
5.如何使用Text的通用样式
@Extend(Text) function grayText(){ .fontSize(14) .fontColor($r('app.color.light_gray')) }
后面直接调用.grayText
6.列表页UI设计时,食物列表样式一样,需要很多tapBar如何简化
TabContent() { List() { 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).fontSize($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) } }) }
使用@Builder封装List部分
7.滑动面板如何实现
使用Panel组件,需要给每个List项设置点击事件,如何控制父组件弹出,定义showPanel函数来控制,父组件调用函数时,覆盖
8.底部面板不显示
build() { Column() { // 1.头部导航 this.Header() // 2.列表 ItemList({showPanel:this.onPanelShow.bind(this)}) //3.底部面板 Panel(this.showPanel) Button('关闭').onClick(() => this.showPanel = false) } .width('100%') .height('100%') }
使用.layoutWeight(1)除了头部导航,其余全是列表,这样Panel高度固定,底部面板渲染
9.数字键盘如何让实现
示例代码
// xxx.ets class MyDataSource implements IDataSource { private list: number[] = [] private listener: DataChangeListener constructor(list: number[]) { this.list = list } totalCount(): number { return this.list.length } getData(index: number): any { return this.list[index] } registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener } unregisterDataChangeListener() { } } @Entry @Component struct SwiperExample { private swiperController: SwiperController = new SwiperController() private data: MyDataSource = new MyDataSource([]) aboutToAppear(): void { let list = [] for (var i = 1; i <= 10; i++) { list.push(i.toString()); } this.data = new MyDataSource(list) } build() { Column({ space: 5 }) { Swiper(this.swiperController) { LazyForEach(this.data, (item: string) => { Text(item).width('90%').height(160).backgroundColor(0xAFEEEE).textAlign(TextAlign.Center).fontSize(30) }, item => item) } .cachedCount(2) .index(1) .autoPlay(true) .interval(4000) .indicator(true) .loop(true) .duration(1000) .itemSpace(0) .curve(Curve.Linear) .onChange((index: number) => { console.info(index.toString()) }) Row({ space: 12 }) { Button('showNext') .onClick(() => { this.swiperController.showNext() }) Button('showPrevious') .onClick(() => { this.swiperController.showPrevious() }) }.margin(5) }.width('100%') .margin({ top: 5 }) } }
饮食记录页面
组内记录列表
删除按钮设置,使用@Builder
swipeAction({end: this.deleteButton.bind(this)})
@Builder deleteButton(){ Image($r('app.media.ic_public_delete_filled')) .width(20) .fillColor(Color.Red) .margin(5)
代码:
ItemIndex
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'
@Extend(Button) function panelButtonStyle(){
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
}
@Entry
@Component
struct ItemIndex {
@State amount: number = 1
@State value: string = ''
@State showPanel: boolean = false
@State item: RecordItem = null
@State type: RecordType = RecordTypes[0]
@State isFood: boolean = true
onPanelShow(item: RecordItem) {
this.amount = 1
this.value = ''
this.item = item
this.showPanel = true
}
onPageShow(){
// 1.获取跳转时的参数
let params: any = router.getParams()
// 2.获取点击的饮食记录类型
this.type = params.type
this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
}
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this), isFood: this.isFood })
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
if(this.item){
ItemCard({amount: this.amount, item: $item})
}
// 3.3.数字键盘
NumberKeyboard({amount: $amount, value: $value})
// 3.4.按钮
this.PanelButton()
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray'))
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
@Builder PanelButton(){
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(() => {
// 1.持久化保存
RecordService.insert(this.type.id, this.item.id, this.amount)
.then(() => {
// 2.关闭弹窗
this.showPanel = false
})
})
}
.margin({top: 10})
}
@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)
}
}
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
@Prop isFood: boolean
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)
}
@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))
})
}
.width('100%')
.height('100%')
}
}
ItemCard
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.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)
}
@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))
})
}
.width('100%')
.height('100%')
}
}
NumberKeyboard
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.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)
}
@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))
})
}
.width('100%')
.height('100%')
}
}