目录
一、食物列表
1、食物列表页面结构:
食物列表逻辑结构
标题结构
图片是一个返回箭头,点击时回退上个页面。Text定义选择记录项目是早中晚餐。
标题实现代码
@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)
}
运行效果:
代码如下:
//ItemIndex.ets
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemList from '../view/item/ItemList'
import router from '@ohos.router'
@Extend(Button) function panelButtonStyle(){
.width(120)
.type(ButtonType.Normal)
.borderRadius(6)
}
@Entry
@Component
struct ItemIndex {
build() {
Column() {
// 1.头部导航
this.Header()
// 2.列表
ItemList()
.layoutWeight(1)
// 3.底部面板
}
.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)
}
}
//ItemList.ets
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() {
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'))
}
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%')
}
}
2、底部Panel
Panel结构
关键技术:
Panel组件
底部弹出面板,列式布局,在食物列表页设置点击事件,当点击时弹出来
onPanelShow
方法用于控制面板的显示状态,通过修改showPanel
状态来实现面板的展开和隐藏。.mode(PanelMode.Full)
指定了面板的模式为全屏模式自定义面板行为:
.dragBar(false)
禁用了面板的拖动条,.backgroundMask
和.backgroundColor
分别设置了面板的遮罩颜色和背景颜色。
布局权重.layoutWeight(1)
确保ItemList
组件填满可用的垂直空间。
运行效果:
代码如下:
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemList from '../view/item/ItemList'
import router from '@ohos.router'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import ItemCard from '../view/item/ItemCard'
@Entry
@Component
struct ItemIndex {
@State amount: number = 1
@State showPanel: boolean = false
onPanelShow() {
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.记录项卡片
ItemCard({amount: this.amount})
// 3.3.数字键盘
// 3.4.按钮
}
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask($r('app.color.light_gray'))
.backgroundColor(Color.White)
}
.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)
}
}
//ItemCard.ets
import { CommonConstants } from '../../common/constants/CommonConstants'
@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)
// 3.营养素
Row({space: CommonConstants.SPACE_8}){
this.NutrientInfo('热量(千卡)',91.0)
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)
}
}
}
3、数字键盘
关键技术:Grid组件
columnsTemplate('1fr 1fr 1fr')
定义了网格的列宽,使用fr
单位来表示相对宽度,使得每列宽度自适应。columnsGap(8)
和rowsGap(8)
设置了列与列之间、行与行之间的间距。GridItem()
用于定义网格中的单个项,可以包含文本、按钮或其他组件。.onClick(() => this.clickNumber(num))
绑定了点击事件,当用户点击某个数字或操作按钮时触发。ForEach(this.numbers, num => {...})
用于遍历数字数组,并为每个数字创建一个网格项。- 使用
@Link
装饰器将NumberKeyboard
组件的状态与父组件的状态链接起来,实现双向数据绑定。 - 在
clickNumber
方法中,实现了对用户输入的验证逻辑,确保输入的数字格式正确,并且不超过最大值。
运行效果:
代码如下:
// 3.3.数字键盘
NumberKeyboard({amount:$amount,value:$value})
// 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})
}
//NumberKeyboard.ets
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')
.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('.')
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
}
if(str.endsWith('.')){
str = str.substring(0, str.length - 1)
}
return parseFloat(str)
}
}
二、遇到问题
问题1:弹出面板未正确弹出
解决方法:面板并不占用高度,浮在表面,,要求它所在的容器组件高度固定,需在列表中添加权重(1)
问题2:如何实现数字输入与页面元素改变的联动
解决方法:①定义状态变量来存储当前的输入值。在 NumberKeyboard
组件中,使用 @Link
装饰器将 amount
和 value
与父组件的状态进行双向绑定。②在 NumberKeyboard
组件中,为每个按键和删除操作提供事件处理函数,如 clickNumber
和 clickDelete
。③当用户点击数字或删除按钮时,更新 value
字符串,并重新计算 amount
数值。使用 parseFloat
函数来确保数值格式正确。④确保 NumberKeyboard
组件的 amount
和 value
属性与父组件中的状态是绑定的,这样任何更改都会同步到父组件。
三、总结
通过本次开发,我们深入理解了食物列表页面的设计和实现,掌握了关键技术的应用,如列表组件、Panel组件和Grid组件。Panel组件提供了一个可展开和收起的底部面板,用于展示额外的操作或信息。在食物列表页面中,Panel组件可用于编辑、删除食物项或进行其他高级操作。Grid组件实现了数字键盘的网格布局,允许用户输入数量。通过Grid组件,我们创建了一个直观且响应式的用户界面,便于用户进行数字输入。