目录
项目介绍:
黑马健康软件是一款基于全民健康的软件,主要有三个页面组成,分别是欢迎页面,统计记录页面,食物列表页面。
页面布局:
分析:
食物列表页UI设计:
- 列式结构,第一行为导航条,左侧为返回按钮,右侧是提示的标签文本;剩下的部分就是一个Tabs组件
食物列表-底部Panel:
- 列式布局,第一行是Text文本和Image组件,第二行Image组件,第三行是一个Text组件,第四行是食物的详细信息,用Text组件构成,第五行是食物的数量(左侧是列式布局,首行是一个Text文本组件,第二行是一个Divder,右侧是一个Text文本);第六行是键盘,Grid组件;最后一行是两个按钮;
食物列表-数字键盘:
- Grid布局加上最下面的一行按钮,Grid布局用来加载数字键盘。
步骤:
- 在pages文件夹下新建ItemIndex.ets文件,用来渲染食物列表页面;
- 在view文件夹下面新建item文件夹,在其中新ItemList.ets文件,用来渲染列表展示的列表项;新建ItemCard.ets文件,用来渲染Panel的表项;新建ItemPanelHeader.ets用来渲染Panel的首部;
- 在item文件夹下新建NumberKeyBoard.ets文件,用来渲染数字键盘
页面实现效果:
食物列表的效果展示
键盘的效果展示
上面营养素的信息也随着数量的变化而变化
出现的问题与解决:
问题1:
当添加多个tabBar时,就会出现下图的这种情况,长度长的文字会被呈现省略号的效果,查阅TabsAPI文档,发现有一个BarMode属性,其中默认的是Fixed模式
Fixed | 所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度)。 |
还有一个模式是Scrollable模式,
Scrollable | 每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。 |
当选用Scrollable模式,文字内容就会显示全:
问题2:
出现问题:当点击列表的一行时,不显示弹窗
改进:因为面板虽然不占用高度,浮在表面上,但是要求容器所在的高度以及内部元素的高度是固定的,所以需要固定上部分组件ItemList()的高度,用.layoutWeight(1)
问题3:
Link修饰的变量在传递参数的时候不能用this,应该使用$amount
问题4:
这里显示的@Link装饰的value变量必须被初始化,经过检查已经初始化,并且在这个问题函数内部也没有赋值
问题出现的原因:直接忘记传值
阶段项目代码:
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
import ItemCard from '../view/item/ItemCard'
import ItemList from '../view/item/ItemList'
import ItemPanelHeader from '../view/item/ItemPanelHeader'
import NumberKeyBoard from '../view/item/NumberKeyBoard'
/**
* 食物记录页面
*/
@Entry
@Component
struct ItemIndex {
@State value: string = ''//记录按下的键的内容
@State amount: number = 1 //需要和其他组件互动
@State showPanel:boolean = false
onPanelShow(){
this.showPanel = true
}
build() {
Column(){
// 1.头部导航栏
this.Header()
// 2.TODO 列表
ItemList({showPanel:this.onPanelShow.bind(this)})
.layoutWeight(1)
// 3.TODO 底部面板
Panel(this.showPanel) {
// 3.1顶部日期
ItemPanelHeader()
// 3.2记录项信息卡片
ItemCard({amount:this.amount})
// 3.3数字键盘
NumberKeyBoard({amount:$amount,value:$value})//link传递值时使用$,不用this
// 3.4按钮
Row({space:6}){
Button('取消')
.width(120)
.backgroundColor($r('app.color.light_gray'))
.type(ButtonType.Normal)
.borderRadius(6)
.onClick(()=>this.showPanel = false)
Button('提交')
.width(120)
.backgroundColor($r('app.color.primary_color'))
.type(ButtonType.Normal)
.borderRadius(6)
.onClick(()=>this.showPanel = false)
}.margin({top:10})
}.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(25)
.onClick(()=>{
router.back()
})
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
}.width('94%')
.height(36)
}
}
食物列表页代码:
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 列表展示Tabs
*/
@Extend(Text) function grayText(){
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct ItemList {
build() {
Tabs(){
TabContent(){
this.TabContentBuilder()
}.tabBar('全部')
TabContent(){
this.TabContentBuilder()
}.tabBar('主食')
TabContent(){
this.TabContentBuilder()
}.tabBar('果蔬')
TabContent(){
this.TabContentBuilder()
}.tabBar('肉蛋奶')
TabContent(){
this.TabContentBuilder()
}.tabBar('坚果')
TabContent(){
this.TabContentBuilder()
}.tabBar('其它')
}
.barMode(BarMode.Scrollable)
.barWidth(700)
.width('100%')
.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(){
Text('全麦吐司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('90千卡/片').grayText()
}
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%')
}
}
import router from '@ohos.router'
import { CommonConstants } from '../common/constants/CommonConstants'
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(25)
.onClick(()=>{
router.back()
})
Blank()
Text('早餐').fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
}.width('94%')
.height(36)
}
}
食物列表-底部Panel:
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* 列表展示Tabs
* 注意这里控制父组件的状态改变的话,需要用一个函数就覆盖
*/
@Extend(Text) function grayText(){
.fontSize(14)
.fontColor($r('app.color.light_gray'))
}
@Component
export default struct ItemList {
showPanel:() => void
build() {
Tabs(){
TabContent(){
this.TabContentBuilder()
}.tabBar('全部')
TabContent(){
this.TabContentBuilder()
}.tabBar('主食')
TabContent(){
this.TabContentBuilder()
}.tabBar('果蔬')
TabContent(){
this.TabContentBuilder()
}.tabBar('肉蛋奶')
TabContent(){
this.TabContentBuilder()
}.tabBar('坚果')
TabContent(){
this.TabContentBuilder()
}.tabBar('其它')
}
.barMode(BarMode.Scrollable)
.barWidth(700)
.width('100%')
.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(){
Text('全麦吐司').fontWeight(CommonConstants.FONT_WEIGHT_500)
Text('90千卡/片').grayText()
}
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())
})
}
.width('100%')
.height('100%')
}
}
import { CommonConstants } from '../../common/constants/CommonConstants'
import DateUtil from '../../common/utils/DateUtil'
import DatePickDialog from '../record/DatePickDialog'
/**
*
* Panel的顶部样式
*/
@Component
export default struct ItemPanelHeader {
@StorageProp('selectedDate') selectedDate:number = DateUtil.beginTimeOfDay(new Date())
controller:CustomDialogController = new CustomDialogController({
builder:DatePickDialog({selectedDate:new Date(this.selectedDate)})
})
build() {
Row(){
Text(DateUtil.formatDate(this.selectedDate))
.fontWeight(CommonConstants.FONT_WEIGHT_600)
.fontSize(18)
.margin({left: 80,right:13})
.onClick(()=>{
this.controller.open()
})
Text('早餐')
Image($r('app.media.ic_public_spinner'))
.width(20)
.fillColor(Color.Black)
}
.width(CommonConstants.THOUSANDTH_940)
}
}
import { CommonConstants } from '../../common/constants/CommonConstants'
/**
* Panel信息部分
*/
@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:10,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('蛋白质(克)',10.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)
}
}
}
食物列表源码-键盘:
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)
.height(60)
.borderRadius(8)
}
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('.')//从后往前查找小数点的下标
if(firstIndex !== lastIndex || (lastIndex != -1&&lastIndex<val.length-2) ){
//需要排除掉的情况:2.1小数点不止有一位,2.2小数点的位数超过2位
return
}
// 3.将输入字符串转为数值
let amount = this.parseFloat(val)
// 4.保存
if(amount>=999.9){
this.amount = 999
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)
}
}
参考黑马课堂老师的讲解,欢迎大家的批评和指正。