五.食物列表页
(一)列表页UI设计
1.功能分析
当用户点击饮食记录里的早、午、晚餐的分组时就会跳转到食物列表页,从中添加食物,点击相应食物时会弹出相应面板记录数据。
2.布局分析![](https://img-blog.csdnimg.cn/direct/9700e1470d4c4241ae9b92e34ae04016.png)
3.代码实现
(1)头部导航
@Entry
@Component
struct ItemIndex {
build() {
Column() {
// 1.头部导航
this.Header()
}
.width('100%')
.height('100%')
}
@Builder Header() {
Row() {
Image($r('app.media.ic_public_back'))
.width(24)
.onClick(() => router.back())
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
previewer:
(2)列表
每个tab项都类似所以可以将其中的list封装起来调用
/*ItemList*/
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 条目列表——食物列表
*/
@Component
export default struct ItemList {
build() {
Tabs() {
TabContent() {
this.TabContentBuilder()
}
.tabBar('全部')
TabContent() {
this.TabContentBuilder()
}
.tabBar('主食')
TabContent() {
this.TabContentBuilder()
}
.tabBar('肉蛋奶')
}
.width(CommonConstants.THOUSANDTH_940)
.height('100%')
}
@Builder TabContentBuilder(items: RecordItem[]) {
List({ space: CommonConstants.SPACE_10 }) {
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('91千卡/片').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)
}
})
}
.width('100%')
.height('100%')
}
}
/*ItemIndex*/
import ItemList from '../view/item/ItemList'
@Entry
@Component
struct ItemIndex {
build() {
Column() {
// 1.头部导航
this.Header()
// 2.头部导航
ItemList()
}
.width('100%')
.height('100%')
}
@Builder Header() {
Row() {
Image($r('app.media.ic_public_back'))
.width(24)
.onClick(() => router.back())
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
}
.width(CommonConstants.THOUSANDTH_940)
.height(32)
}
}
previewer:
(二)底部Panel
1.布局分析
自定义生成键盘需要用到Grid组件
底部面板需要用到Panel组件
2.代码实现
(1)面板简单实现
问题:点击时没有出现底部面板
原因及解决方法:面板不占高度,是浮在表面的,它要求它所在的容器的高度与容器里面元素的高度是固定的,但这里面list的高度不固定(动态的),导致底部面板被挤到对下面去了(无法展示出来)。用.layoutWeight(1)去固定list的高度即可解决。
/*ItemIndex*/
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemList from '../view/item/ItemList'
/**
* 条目
*/
@Entry
@Component
struct ItemIndex {
@State showPanel: boolean = false
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this)})
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
Button('关闭').onClick(()=>this.showPanel=false)
}
.mode(PanelMode.Full)//默认完全展出面板
.dragBar(false)//设置可不可以上下拖动顶部边框改变面板大小(这里不可以)
.backgroundMask($r('app.color.light_gray'))//蒙板颜色
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
}
/*ItemList*/
/**
* 条目列表——食物列表
*/
@Component
export default struct ItemList {
showPanel: (item: RecordItem) => void
@Builder TabContentBuilder(items: RecordItem[]) {
List({ space: CommonConstants.SPACE_10 }) {
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('91千卡/片').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%')
}
}
previewer:
(2)顶部日期
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 底部面板——顶部日期
*/
@Component
export default struct ItemPanelHeader {
build() {
Row(){
Text('2024年1月25日 早餐')
.fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_600)
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
}
}
previewer:
(3)记录项卡片
/*ItemCard*/
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordItem from '../../viewmodel/RecordItem'
/**
* 记录项卡片
*/
@Component
export default struct ItemCard {
@Prop amount: number
build() {
Column({space: CommonConstants.SPACE_8}){
// 1.图片
Image($r('app.media.toast')).width(150)
// 2.名称
Row(){
Text('全麦吐司').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( '热量(千卡)', 91.0)
if(this.item.id < 10000){
this.NutrientInfo('碳水(千卡)', 15.5)
this.NutrientInfo( '蛋白质(千卡)', 4.4)
this.NutrientInfo('脂肪(千卡)', 1.3)
}
}
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('片')
.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.toFixed(1)).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)//这里toFixed(1)的作用是将value转成一位小数
}
}
}
/*ItemIndex*/
@Entry
@Component
struct ItemIndex {
@State showPanel: boolean = false
@State amount: number = 1
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this)})
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
if(this.item){
ItemCard({amount: this.amount})
}
}
.mode(PanelMode.Full)//默认完全展出面板
.dragBar(false)//设置可不可以上下拖动顶部边框改变面板大小(这里不可以)
.backgroundMask($r('app.color.light_gray'))//蒙板颜色
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
}
previewer:
(4)数字键盘
需要用到组件Grid,是一种网格容器。
简单实现:
/*NumberKeyboard*/
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 数字键盘
*/
@Component
export default struct NumberKeyboard {
numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.']
@Styles keyBoxStyle(){
.backgroundColor(Color.White)
.borderRadius(8)
.height(60)
}
build() {
Grid(){
ForEach(this.numbers, num => {
GridItem(){
Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
})
GridItem(){
Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
}
.width('100%')
.height(280)
.backgroundColor($r('app.color.index_page_background'))
.columnsTemplate('1fr 1fr 1fr')//网格布局(三列宽度1:1:1)
.columnsGap(8)//网格行间距
.rowsGap(8)//网格列间距
.padding(8)
.margin({top: 10})//外边距
}
}
/*ItemIndex*/
@Entry
@Component
struct ItemIndex {
@State showPanel: boolean = false
@State amount: number = 1
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this)})
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
if(this.item){
ItemCard({amount: this.amount})
}
// 3.3.数字键盘
NumberKeyboard()
}
.mode(PanelMode.Full)//默认完全展出面板
.dragBar(false)//设置可不可以上下拖动顶部边框改变面板大小(这里不可以)
.backgroundMask($r('app.color.light_gray'))//蒙板颜色
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
}
previewer:
传值:
/*ItemIndex*/
@Entry
@Component
struct ItemIndex {
@State amount: number = 1//键盘输出内容
@State value: string = ''//用户键盘输入内容
@State showPanel: boolean = false
onPanelShow(item: RecordItem) {
this.amount = 1
this.value = ''
this.item = item
this.showPanel = true
}
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList({ showPanel: this.onPanelShow.bind(this)})
.layoutWeight(1)
// 3.底部面板
Panel(this.showPanel) {
// 3.1.顶部日期
ItemPanelHeader()
// 3.2.记录项卡片
if(this.item){
ItemCard({amount: this.amount})
}
// 3.3.数字键盘
NumberKeyboard({amount: $amount, value: $value})
}
.mode(PanelMode.Full)//默认完全展出面板
.dragBar(false)//设置可不可以上下拖动顶部边框改变面板大小(这里不可以)
.backgroundMask($r('app.color.light_gray'))//蒙板颜色
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
}
/*NumberKeyboard*/
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 数字键盘
*/
@Component
export default struct NumberKeyboard {
numbers: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.']
@Link amount: number
@Link value: string
@Styles keyBoxStyle(){
.backgroundColor(Color.White)
.borderRadius(8)
.height(60)
}
build() {
Grid(){
ForEach(this.numbers, num => {
GridItem(){
Text(num).fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickNumber(num))
})
GridItem(){
Text('删除').fontSize(20).fontWeight(CommonConstants.FONT_WEIGHT_900)
}
.keyBoxStyle()
.onClick(() => this.clickDelete())
}
.width('100%')
.height(280)
.backgroundColor($r('app.color.index_page_background'))
.columnsTemplate('1fr 1fr 1fr')//网格布局(三列宽度1:1:1)
.columnsGap(8)//网格行间距
.rowsGap(8)//网格列间距
.padding(8)
.margin({top: 10})//外边距
}
clickNumber(num: string){
// 1.拼接用户输入的内容
let val = this.value + num
// 2.校验输入格式是否正确
let firstIndex = val.indexOf('.')//返回第一次出现'.'的角标
let lastIndex = val.lastIndexOf('.')//返回最后一次出现'.'的角标
//如果出现多于一次小数点 或者 在有小数点的情况下等于或超过了2位小数 就算非法输入
if(firstIndex !== lastIndex || (lastIndex != -1 && lastIndex < val.length - 2)){
// 非法输入
return
}
// 3.将字符串转为数值
let amount = this.parseFloat(val)
// 4.保存
//确保没有超出上限
if(amount >= 999.9){
this.amount = 999.0
this.value = '999'
}else{
this.amount = amount
this.value = val
}
}
clickDelete(){
//如果删没了,用户输入置空,网格输出置零。否则用户输入删除最后一位,并将其转换成数值类型赋值给输出。
if(this.value.length <= 0){
this.value = ''
this.amount = 0
return
}
this.value = this.value.substring(0, this.value.length - 1)
this.amount = this.parseFloat(this.value)
}
parseFloat(str: string){
if(!str){
return 0//避免出现NaN
}
//如果字符串最后一位是小数点就将它去除
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}
return parseFloat(str)
}
}
/*ItemCard*/
//营养素信息
@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:
(4)按钮(取消,提交)
提交按钮暂不做处理
/*ItemIndex*/
// 3.4.按钮
Row({space: CommonConstants.SPACE_6}){
Button('取消')
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
.backgroundColor($r('app.color.light_gray'))
.onClick(() => this.showPanel = false)
Button('提交')
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
.backgroundColor($r('app.color.primary_color'))
.onClick(() => this.showPanel = false)
}
.margin({top: 10})
previewer: