目录
b 日期组件--返回选择日期,返回开始日期 DateUtil.ets
3. AppStorage.SetOrCreate('selectedDate', this.selectedDate)发生错误。
一、 效果展示
二、 功能概述
1. 顶部搜索栏
页面里边包含一个头部搜索栏组件,一个统计卡片和食物记录列表。头部搜索栏组件由一行布局(Row) 组成,包含一个搜索栏和一个实现带有角标/小数字显示的信封图标,显示有多少未读消息。
2. 统计卡片
然后搜索栏下边是一个统计卡片组件,包括顶部有一个日期信息显示和下面的热量统计和营养素统计。卡片上方是一个日期选择器,用户可以滑动选择日期。然后下面是两个可以横向滑动切换的视图--热量统计视图和营养素统计视图。热量统计视图显示饮食摄入、还可以吃和运动消耗的统计信息。使用环形进度条直观显示摄入量和推荐值的比例。计算还可以吃多少,并显示在界面上。营养素统计视图显示碳水化合物、蛋白质和脂肪的摄入量和推荐值的比例,用环形进度条显示摄入量和推荐值的比例。
3. 记录列表
记录列表里面包含餐食类别(早餐,晚餐等),每种类别下面有对应的食物种类。每个食物记录项包括一个分组标题和组内记录列表(食物种类)。组内记录列表展示了具体的食物名称、数量和热量。向左拉食物选项会显示删除按钮。
三、 组件实现
1. 顶部搜索栏
整个组件包含在一个Row容器里面,罕有一个Search搜索栏组件,和一个信封图标且带有小角标(角标/小数字显示使用Badge组件),为了显示有多少未读消息。
2. 统计卡片
在统计卡片视图StatsCard组件里边用Column容器包住日期选择组件DatePickDialog,和两个统计视图组件切换。日期弹窗DatePickDialog来滑动选择日期,同时用DateUtil日期组件来返回传递日期。然后是Swiper()组件切换两个统计视图--热量统计组件CalorieStats与营养统计组件NutrientStats。热量统计组件CalorieStats里边remainCalorie()方法用于计算还可以摄入多少热量,通过推荐值减去摄入量再加上运动消耗量得出结果。StatsBuilder()方法用于统一构建展示统计数据的样式,包括标签、数值和提示信息的显示。营养统计组件NutrientStats里边build()方法包括碳水化合物、蛋白质和脂肪的展示。用StatsBuilder()方法来实现展示营养素统计数据的样式。
3. 记录列表
使用ForEach循环生成多个ListItem,每个ListItem包含分组标题(早餐等)和组内记录列表(食物种类)。分组标题使用Row和Text组件展示食物类别、建议热量和总热量,并设置相应的样式。组内记录列表使用嵌套的List和ForEach循环生成多个记录项,每个记录项包含食物图片、名称、数量和热量。每个记录项右侧设置一个删除按钮,点击按钮触发deleteButton函数,生成一个带有删除图标的Image组件。
四、 完整代码展示
1. 食物记录页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')
selectedDate: number = DateUtil.beginTimeOfDay(new Date())
build() {
Column(){
// 1.头部搜索栏
SearchHeader()
// 2.统计卡片
StatsCard()
// 3.记录列表
RecordList()
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_page_background'))
}
}
2. 头部搜索栏SearchHeader.ets
/**
* 头部搜索栏组件
*/
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct SearchHeader {
build() {
Row({space: CommonConstants.SPACE_6}){
/**
* 搜索栏组件Search和TextInput使用相似
*/
Search({placeholder: '搜索饮食或运动信息'})//搜索提示
.textFont({size: 18})//字体信息
.layoutWeight(1)//占据剩余空间
/**
* 组件Bage 角标/小数字显示
*/
Badge({count: 1, position: BadgePosition.RightTop, style: {fontSize: 12}}){
Image($r('app.media.ic_public_email'))
.width(24)
}
}
.width(CommonConstants.THOUSANDTH_940)
}
}
3. 统计卡片 StatCard.ets
(1) 统计卡片总视图StatsCard.ets
/**
* 统计卡片组件
*/
import BreakpointType from '../../common/bean/BreanpointType'
import BreakpointConstants from '../../common/constants/BreakpointConstants'
import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import CalorieStats from './CalorieStats'
import DatePickDialog from './DatePickDialog'
import NutrientStats from './NutrientStats'
/*
import RecordService from '../../service/RecordService'
import RecordVO from '../../viewmodel/RecordVO'
import StatsInfo from '../../viewmodel/StatsInfo'
* */
@Component
export default struct StatsCard {
//读取AppStorage存储的值
@StorageProp('selectedDate') selectedDate: number = DateUtil.beginTimeOfDay(new Date())//DateUtil.beginTimeOfDay获取开始日期的功能
@StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM
/*
@Consume @Watch('handleRecordsChange') records: RecordVO[]
@State info: StatsInfo = new StatsInfo()
handleRecordsChange(){
this.info = RecordService.calculateStatsInfo(this.records)
}
*/
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()
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(CommonConstants.DEFAULT_18)//边框圆角18
.indicatorStyle({selectedColor: $r('app.color.primary_color')})//穿梭框的样式,颜色从蓝色改为绿色
.displayCount(new BreakpointType({
sm: 1,
md: 1,
lg: 2
}).getValue(this.currentBreakpoint))
}
.width(CommonConstants.THOUSANDTH_940)//常量值
.backgroundColor($r('app.color.stats_title_bgc'))//颜色
.borderRadius(CommonConstants.DEFAULT_18)//边框角度
}
}
(2) 日期信息
a 日期滑动选择组件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({
start: new Date('2020-01-01'),
end: new Date(),
selected: this.selectedDate
})
.onChange((value: DatePickerResult) => {
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.SetOrCreate('selectedDate', this.selectedDate.getTime())//这里不要存日期data对象,因为日期data对象将来去作为状态变量去监控的时候会有问题,就是用@State/@Prop等对日期对象进行监控的时候会出现错误。
//gettime是一个时间number可以监控。
// 2.关闭窗口
this.controller.close()
})
}
}
.padding(CommonConstants.SPACE_12)
}
}
b 日期组件--返回选择日期,返回开始日期 DateUtil.ets
/**
* 日期组件
*/
class DateUtil{
//返回显示日期
formatDate(num: number): string{
let date = new Date(num)
let year = date.getFullYear()//获取年份
let month = date.getMonth()+1//获取月份 data是0-11月
let day = date.getDate()//获取日
let m = month < 10 ? '0' + month : month//三目选择语句
let d = day < 10 ? '0' + day : day
return `${year}/${m}/${d}`
}
//返回开始日期。
beginTimeOfDay(date: Date){
let d = new Date(date.getFullYear(), date.getMonth(), date.getDate())
return d.getTime()
}
}
let dateUtil = new DateUtil()
export default dateUtil as DateUtil
(3)统计信息
热量统计组件CalorieStats.ets与营养统计组件NutrientStats. 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(//进度条组件
this.intake,//摄入
this.recommend,//推荐值
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})
}
//统一样式
//label文字说明
@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'))
}
}
}
}
/**
* 营养素统计组件
*/
import { CommonConstants } from '../../common/constants/CommonConstants'
@Component
export default struct NutrientStats {
//碳水、蛋白质、脂肪
carbon: number =23
protein: number=9
fat: number =7
//推荐碳水化合物
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}){
@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'))
}
}
}
4. 记录列表 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],(item) => {
ListItem(){
Column(){
// 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(item.recordItem.image).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)
.fillColor(Color.Red)
.margin(5)
}
}
五、 问题记录与收获
1. 日期选择器DatePicker组件学习
DatePicker日期选择器组件,用于根据指定日期范围创建日期滑动选择器。接口:DatePicker(options?: {start?: Date, end?: Date, selected?: Date})
satrt :指定选择器的起始日期。end:指定选择器的结束日期。selected:
设置选中项的日期。默认值:当前系统日期
然后它还有一个属性lunar用来选择是否展示农历。
2. 如何在滑动选择日期后将日期全局保存下来?
在Tabs组件里使用onChange方法记录传入tabBar的角标值,根据记录的角标值重新渲染。
3. AppStorage.SetOrCreate('selectedDate', this.selectedDate)发生错误。
//应用的内部存储
这里不要存日期data对象,因为日期data对象将来去作为状态变量去监控的时候会有问题,就是用@State/@Prop等对日期对象进行监控的时候会出现错误。
而gettime是返回一个时间 number类型 可以监控。
所以这里程序修改为AppStorage.SetOrCreate('selectedDate', this.selectedDate.getTime()).
4. Swiper滑块视图显示蓝色,无法修改背景色为白色
Swiper滑块视图容器,提供子组件滑动轮播显示的能力。
接口:Swiper(controller?: SwiperController)
初步代码:
效果
解决:回顾视频发现,整个蓝色背景色是文本自带的背景色,删除文本样式backgroundColor(0xAFEEEE)后,视图变为白色。