前言
前面提到了各种通用组件的构建,其中包括:顶栏组件 Header.ets、提醒弹窗组件 Dialog.ets、结算弹窗组件 JieSuanDialog.ets等,接下来将继续介绍主页面的构建,包括:主页面-点餐页 HomePage_Order.ets、主页面-购物车页 HomePage_BuyCar.ets、主页面-订单页 HomePage_Bill、主页面-我的页 HomePage_Me等。
一、关键技术
- 数据库操作(包括增、删、改、查)。
- @State、@Link、@Prop、@Provide、@Consume、@Observed、@ObjectLink等装饰器的使用。
- Tabs 组件的使用。
- List 组件的使用。
- Panel组件的使用。
- 通过systemDateTime库获取系统当前时间。
- @Extend修饰器全局通用样式的使用。
- @Styles修饰器的使用。
二、实验步骤
1.主页面-点餐页 HomePage_Order.ets
使用到的主要组件包括Tabs、List、Button、image、text等等,但是:在做自定义导航栏时,无法实现点击切换页签后导航图标对应的变化,在图标加上点击事件后点击切换页签的功能却又失效。
检查后发现不应该在图标上加点击事件,而是应该在Tabs上添加监听事件onChange,并接受index。
同时这里要说一下购物车饮品的图片地址存储方法(要达到程序与数据库相联系的目的),在Tea类里加入no属性代表在数组中的位置一并存入数据库,当读取数据库时再拿no直接在数组里取出tea,并获取其imag属性(自己定义的, Resource类型)
再者,搜索功能的逻辑比较特殊,由于鸿蒙的模拟器上无法输入中文汉字,故用拼音首字母代替,在Tea类里加入拼音首字母属性用于保存当前茶品的名称的拼音首字母。首先清空当前数组,监视输入框,当输入框发生改变时,读取里面的内容并循环遍历数组teas,若有符合条件的条目则加入当前数组。若输入框为空则当前数组恢复(即当前数组=StaticValue.teas)
效果如图:
代码如下:
import { OrderModel } from '../view/OrderModel'
import { StaticValue } from '../viewModel/StaticValue'
import { Tea } from '../viewModel/Tea'
@Component
export struct HomePage_Order {
// 饮品总集合(包括四类)
@Provide teas: Tea[] = []
@State currentIndex: number = 1
build() {
Column(){
// 搜索框
SearchModel({initTeas: this.initTeas.bind(this)})
// 左侧导航栏及右侧内容模块
LeftBarModel({currentIndex: $currentIndex})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
}
// 初始化数组
initTeas(){
StaticValue.teas = []
StaticValue.teas.push(
new Tea(
'牧场酸酪牛油果',
$r('app.media.niuYouGuo'),
'定制牧场奶源酸酪·百分百进口牛油果鲜果·不使用过你,清爽顺滑',
// 价格
23,
// 类别
1,
// 数量
undefined,
// 备注
undefined,
// 规格
undefined,
'mcslnyg',
// 标记
0
),
new Tea(
'喜悦黄果茶',
$r('app.media.yellow_sq'),
'匠心甄选黄色系水果·当季芒果·鲜制橙丁百香果,真果无香精',
19,
1,
undefined,
undefined,
undefined,
'xyhgc',
1
),
new Tea(
'东坡荔枝生椰露',
$r('app.media.coco_sq'),
'当季新鲜荔枝果肉·定制生椰乳·每日现制西米,椰椰荔香清甜交融',
19,
1,
undefined,
undefined,
undefined,
'dplzsyl',
2
),
new Tea(
'水牛乳·粉黛玫影',
$r('app.media.pinkmilk_square'),
'无香精[玫影]玫瑰红茶·优选广西水牛乳调制奶底',
15,
2,
undefined,
undefined,
undefined,
'snrfdmy',
3
),
new Tea(
'水牛乳双拼波波',
$r('app.media.black_sq'),
'优选广西牧场水牛乳·水牛乳冻·慢数黑糖波波,口感甜腻不喜慎点',
19,
2,
undefined,
undefined,
undefined,
'snrspbb',
4
),
new Tea(
'轻波波牛乳茶',
$r('app.media.bobo_sq'),
'人气轻波波牛乳灵感延伸·慢熬黑糖波波,口感香醇,真牛乳无奶精',
15,
2,
undefined,
undefined,
undefined,
'qbbnrc',
5
),
new Tea(
'芋泥牛乳满贯',
$r('app.media.yuni_sq'),
'芋泥系列大满贯版,5重口感,浓浓芋香,轻盈不腻',
18,
2,
undefined,
undefined,
undefined,
'ynnrmg',
6
),
new Tea(
'烤黑糖波波牛乳茶',
$r('app.media.black_sq'),
'65分钟慢熬黑糖波波·真牛乳·定制嫣红茶底,口感浓厚不喜慎点',
19,
2,
undefined,
undefined,
undefined,
'khtbbnrc',
7
),
new Tea(
'多肉桃李',
$r('app.media.peach_square'),
'当季三华李与当季黄油桃,脆、鲜、甜层层递进',
15,
3,
undefined,
undefined,
undefined,
'drtl',
8
),
new Tea(
'芝芝多肉桃桃',
$r('app.media.pinkpeach_sq'),
'优选当季新鲜水蜜桃·新岩岚,岩茶·醇香芝士,不添加香精色素',
28,
3,
undefined,
undefined,
undefined,
'zzdrtt',
9
),
new Tea(
'芝芝多肉青提',
$r('app.media.grape_sq'),
'优选阳光玫瑰青提·鲜果颗颗去皮·无奶精芝士,甜脆香郁。',
28,
3,
undefined,
undefined,
undefined,
'zzdrqt',
10
),
new Tea(
'芝芝莓莓',
$r('app.media.strawberry_sq'),
'当季新鲜草莓·定制绿妍茶底·无奶精芝士,奶香浓醇,莓香满溢',
28,
3,
undefined,
undefined,
undefined,
'zzmm',
11
),
new Tea(
'大桶鸭屎香柠茶',
$r('app.media.lemond_square'),
'暴打新鲜柠檬·甄选无香精鸭屎香单从茶,超大桶的清爽更解腻',
18,
4,
undefined,
undefined,
undefined,
'dtysxnc',
12
),
new Tea(
'芝芝玫影',
$r('app.media.redtea_sq'),
'全新[玫影]玫瑰红茶,无香精自然玫瑰香·无奶精芝士,甜醇顺滑',
13,
4,
undefined,
undefined,
undefined,
'zzmy',
13
),
new Tea(
'纯绿妍茶后',
$r('app.media.greentea_sq'),
'甄选茶园定制绿妍茶底,淡雅芳幽,默认不加糖,0糖0卡轻负担',
8,
4,
undefined,
undefined,
undefined,
'clych',
14
)
)
this.teas = StaticValue.teas
}
aboutToAppear(){
// 运行时先把饮品加载进去
this.initTeas()
}
}
// 搜索框
@Component
struct SearchModel {
@Consume teas: Tea[]
initTeas: ()=> void
// 搜索函数
searchWay(message: string){
if(message == ''){
this.teas = StaticValue.teas
}
else{
this.teas = []
StaticValue.teas.forEach((tea: Tea,index) => {
if(tea.firstI.includes(message)){
this.teas.push(tea)
}
})
}
}
build() {
Row(){
Text('类别')
.width('15%')
.textAlign(TextAlign.Center)
.fontColor(Color.Gray)
Row(){
Image($r('app.media.search'))
.height('60%')
TextInput({placeholder: '搜饮品(请输入拼音首字母)'})
// 搜索函数
.onChange(message => {
this.searchWay(message)
})
.backgroundColor('#0000')
}
.width('75%')
.padding({left: 10})
.backgroundColor('#d6b98d')
.borderRadius(20)
.height('90%')
}
.width('100%')
.height(40)
.margin({top: 5})
}
}
// 左侧导航栏及右侧内容块
@Component
struct LeftBarModel {
@Link currentIndex: number
// 通用样式
@Styles barStyles(){
.border({
width: {left: 1},
color: Color.Gray
})
}
build() {
Tabs({barPosition: BarPosition.Start}){
TabContent(){
OrderModel({currentType: 1})
}
.tabBar(this.TabBuilder('灵感上新',1,$r('app.media.lingGanZhiShang')))
.barStyles()
TabContent(){
OrderModel({currentType: 2})
}
.tabBar(this.TabBuilder('浓郁牛乳',2,$r('app.media.nongYu')))
.barStyles()
TabContent(){
OrderModel({currentType: 3})
}
.tabBar(this.TabBuilder('时令鲜果',3,$r('app.media.shiLingXianGuo')))
.barStyles()
TabContent(){
OrderModel({currentType: 4})
}
.tabBar(this.TabBuilder('简单茗茶',4,$r('app.media.jianDanMingCha')))
.barStyles()
}
.vertical(true)
.barMode(BarMode.Scrollable)
.animationDuration(100)
.onChange((index:number) => {
this.currentIndex = index + 1
})
}
@Builder TabBuilder(title: string, targetIndex: number, normalImg: Resource) {
Column() {
Image(normalImg)
.size({ width: 25, height: 25 })
Text(title)
.textAlign(TextAlign.Center)
.width('80%')
.fontColor(this.currentIndex === targetIndex ? '#ff000000' : '#6B6B6B')
}
.backgroundColor(this.currentIndex === targetIndex ? '#eaeaea' : '#fafafa')
.width('100%')
.height('12%')
.justifyContent(FlexAlign.Center)
}
}
2.主页面-购物车页 HomePage_BuyCar.ets
以上是有东西的时候(相比没有东西多了下面两行:信息填充行、结算操作行)
这里是有一个判断的,如果信息填充行没有填信息那么点击结算会提醒填信息。
服务费是固定0.2元(购物车不为空的时候),当然在静态类StaticValue里也能改。
注意这里涉及了修改数组对象的属性的操作,故若用一般的监视器则不会重发视图的更新,需要使用@Observed进行监测,但:报错了~
检查发现:ts文件后缀不是写组件的文件,故无法使用组件的监视器,将文件后缀ts改为ets即可使用(是的,直接改后缀)。
点击结算后会跳出收款码弹窗:
代码如下:
import promptAction from '@ohos.promptAction'
import dataCtrl from '../dataModel/DataCtrl'
import { Dialog } from '../view/Dialog'
import { jieSuanDialog } from '../view/jieSuanDialog'
import { Tea } from '../viewModel/Tea'
import { User } from '../viewModel/User'
@Component
export struct HomePage_BuyCar {
@Link teas: Tea[]
// 桌号
@State Btno: number = 0
// 人数
@State Bsum: number = 0
// 外带
@State Btype: boolean = false
// 服务费
@Consume sumF_: number
// 总费用
@Consume sumF: number
// 计算总费用方法
jisuanSumF: () => void
build() {
Column(){
if(this.teas.length == 0){
Text('购物车空空如也~')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.fontSize(30)
.fontColor(Color.Gray)
}
List(){
ForEach(this.teas,(tea: Tea,index) => {
ListItem(){
showTea({tea: tea,jisuanSumF: this.jisuanSumF.bind(this),teas: $teas})
}
.width('100%')
.height('20%')
.padding(5)
})
}
.width('100%')
.layoutWeight(1)
.padding({top: 10})
// 结算操作栏
Ok_buy({teas: $teas,jisuanSumF: this.jisuanSumF.bind(this),Btno: $Btno,Bsum: $Bsum,Btype: $Btype})
}
.width('100%')
.height('100%')
}
}
@Component
struct showTea {
controllerTo: CustomDialogController = new CustomDialogController({
builder: Dialog({
message: '确定要从购物车移除此项饮品吗?',
onYes: () => {
// 先从数据库中删除,再从列表中删除
dataCtrl.deleteBuyCarTo(this.tea.Ctime,User.Uno)
let index = this.teas.indexOf(this.tea)
this.teas.splice(index,1)
this.jisuanSumF()
this.controllerTo.close()
},
onNo: () => {
this.controllerTo.close()
}
})
})
@ObjectLink tea: Tea
@Link teas: Tea[]
jisuanSumF: () => void
build() {
Row(){
Image(this.tea.image as Resource)
.height('100%')
Column(){
Text(this.tea.name)
.width('100%')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Start)
Row({space: 5}){
if(this.tea.norms['cup'] == 1){
Text('小杯')
.fontColor(Color.Gray)
}
else if(this.tea.norms['cup'] == 2){
Text('中杯')
.fontColor(Color.Gray)
}
else{
Text('大杯')
.fontColor(Color.Gray)
}
if(this.tea.norms['temp'] == 1){
Text('全冰')
.fontColor(Color.Gray)
}
else if(this.tea.norms['temp'] == 2){
Text('少冰')
.fontColor(Color.Gray)
}
else if(this.tea.norms['temp'] == 3){
Text('去冰')
.fontColor(Color.Gray)
}
else {
Text('常温')
.fontColor(Color.Gray)
}
if(this.tea.norms['sweetness'] == 1){
Text('全糖')
.fontColor(Color.Gray)
}
else if(this.tea.norms['temp'] == 2){
Text('少糖')
.fontColor(Color.Gray)
}
else if(this.tea.norms['temp'] == 3){
Text('半糖')
.fontColor(Color.Gray)
}
else {
Text('微糖')
.fontColor(Color.Gray)
}
}
.width('100%')
Blank()
Row(){
Text(`¥${this.tea.price}`)
.fontColor('#327c6b')
.fontSize(17)
Blank()
// 计数器
Counter(){
Text(this.tea.sum.toString())
}
.onInc(async () => {
// 注意先加价格再加数量,否则逻辑错误
this.tea.price += this.tea.price / this.tea.sum
this.tea.sum++
this.jisuanSumF()
await dataCtrl.updateBuyCar(User.Uno,this.tea.Ctime,this.tea.sum)
})
.onDec(async () => {
// 只有大于1时才可以减
console.log('08808','--------')
if(this.tea.sum > 1){
this.tea.price -= this.tea.price / this.tea.sum
this.tea.sum--
this.jisuanSumF()
await dataCtrl.updateBuyCar(User.Uno,this.tea.Ctime,this.tea.sum)
}
else if (this.tea.sum == 1){
this.controllerTo.open()
}
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
.padding({left: 5,top: 5,right: 10})
.height('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
@Component
struct Ok_buy {
@Consume sumF: number
@Consume sumF_: number
@Link teas: Tea[]
jisuanSumF: () => void
// 桌号
@Link Btno: number
// 人数
@Link Bsum: number
// 外带
@Link Btype: boolean
controller: CustomDialogController = new CustomDialogController({
builder: jieSuanDialog({teas: $teas,Btno: this.Btno,Bsum: this.Bsum,Btype: this.Btype})
})
controllerT: CustomDialogController = new CustomDialogController({
builder: Dialog({message: '确定要清空购物车吗?',onYes: () => {
this.teas = []
dataCtrl.deleteBuyCar()
this.jisuanSumF()
this.controllerT.close()
},
onNo: () => {
this.controllerT.close()
}})
})
build() {
Column({space: 10}){
Row({space: 1}){
Image($r('app.media.zhuo_hao'))
.height('100%')
Text('桌号:')
TextInput()
.onChange(message => {
this.Btno = parseInt(message)
})
.width(50)
.backgroundColor('#0000')
.borderRadius(0)
.border({
width: {bottom: 1},
color: Color.Gray
})
Image($r('app.media.ren_shu'))
.height('100%')
Text('人数:')
TextInput()
.onChange(message => {
this.Bsum = parseInt(message)
})
.width(50)
.backgroundColor('#0000')
.borderRadius(0)
.border({
width: {bottom: 1},
color: Color.Gray
})
Checkbox()
.onChange(yn => {
if(yn){
this.Btype = true
}
else {
this.Btype = false
}
})
Image($r('app.media.wai_dai'))
.height('100%')
Text('外带')
}
.height(40)
Row({space: 5}){
Image($r('app.media.shop_'))
.height('100%')
Column(){
Text(`饮料费:¥${this.sumF}`)
.width('100%')
.textAlign(TextAlign.Start)
.fontSize(20)
.fontColor(Color.Gray)
Text(`服务费:¥${this.sumF_}`)
.width('100%')
.textAlign(TextAlign.Start)
.fontSize(20)
.fontColor(Color.Gray)
}
.height('100%')
.layoutWeight(1)
// 清空购物车
Image($r('app.media.shan_chu'))
.onClick(() => {
this.controllerT.open()
})
.height('100%')
// 结算
Button('结算')
.onClick(() => {
if(this.Btno != 0 && this.Bsum != 0){
this.controller.open()
}
else{
promptAction.showToast({
message: '请先将信息补充完整!',
duration: 1500
})
}
})
.type(ButtonType.Normal)
.backgroundColor('#d6b98d')
.fontColor(Color.White)
.fontSize(20)
.height('100%')
.width(150)
}
.width('100%')
.height(50)
}
}
}
3.主页面-订单页 HomePage_Bill
效果如下:
点击订单项后会有详细订单信息,效果如下:
订单左滑可以删除,效果如下:
代码如下:
import dataCtrl from '../dataModel/DataCtrl'
import { Dialog } from '../view/Dialog'
import { Bill } from '../viewModel/Bill'
@Component
export struct HomePage_Bill {
@Link bills: Bill[]
@State showPanel: boolean = false
@State bill: Bill = new Bill('','','','','','')
// 用作储存需要删除记录项的下标
index: number = 0
controllerTo: CustomDialogController = new CustomDialogController({
builder: Dialog({
message: '确定要删除此项订单记录吗?',
onYes: () => {
dataCtrl.deleteBill(this.bills[this.index].Bno)
this.bills.splice(this.index,1)
this.controllerTo.close()
},
onNo: () => {
this.controllerTo.close()
}
})
})
build() {
Column(){
if(this.bills.length == 0){
Text('订单空空如也~')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.fontSize(30)
.fontColor(Color.Gray)
}
List(){
ForEach(this.bills,(bill: Bill,index) => {
ListItem(){
Column({space: 5}){
Text(`订单编号:${bill.Bno}`)
.width('100%')
.fontSize(17)
.fontWeight(FontWeight.Bold)
Text(bill.Btime)
.width('100%')
.fontSize(Color.Gray)
Row(){
Text(bill.Btype)
.fontSize(17)
.fontWeight(FontWeight.Bold)
Blank()
Text(`总价:¥${bill.Bprice}`)
.fontSize(17)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.padding({bottom: 5})
.width('100%')
.border({
width: {bottom: 1},
color: Color.Gray
})
}
.onClick(() => {
this.bill = bill
this.showPanel = true
})
.swipeAction({end: this.left_delete_button(index)})
.padding(10)
})
}
.layoutWeight(1)
.chainAnimation(true)
Panel(this.showPanel){
Column(){
Column(){
Image($r('app.media.bill_touxiang'))
.borderRadius(50)
.height(50)
Text('ABC')
.margin({bottom: 10})
Text(`-${this.bill.Bprice}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.border({
width: {bottom: 1},
color: Color.Gray
})
.padding(30)
.alignItems(HorizontalAlign.Center)
Row(){
Column({space: 5}){
Text('当前状态')
.text1Styles()
Text('支付时间')
.text1Styles()
Text('商品')
.text1Styles()
Text('商品全称')
.text1Styles()
Text('收单机构')
.text1Styles()
Text('支付方式')
.text1Styles()
Text('交易单号')
.text1Styles()
Text('商户单号')
.text1Styles()
Text('订单类型')
.text1Styles()
Text('桌号')
.text1Styles()
Text('人数')
.text1Styles()
}
.height('100%')
Column({space: 5}){
Text('支付成功')
.text2Styles()
Text(this.bill.Btime)
.text2Styles()
Text('松达的茶-消费')
.text2Styles()
Text('松达的茶')
.text2Styles()
Text('中国建设银行股份有限公司深圳市分行')
.text2Styles()
Text('零钱')
.text2Styles()
Text(this.bill.Bno)
.text2Styles()
Text('可在支持的商户扫码退款')
.text2Styles()
Text(this.bill.Btype)
.text2Styles()
Text(this.bill.Bdno.toString())
.text2Styles()
Text(this.bill.Bsum.toString())
.text2Styles()
}
.height('100%')
}
.height('40%')
.width('100%')
.padding(10)
Button('关闭')
.backgroundColor('#d5be8e')
.margin({top: 15})
.onClick(() => {
this.showPanel = false
})
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.padding(10)
}
.height('100%')
.mode(PanelMode.Full)
.dragBar(false)
.backgroundMask(Color.Gray)
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
// 左滑删除按钮定义
@Builder left_delete_button(index: number){
Button('删除')
.onClick(() => {
this.index = index
this.controllerTo.open()
})
.backgroundColor(Color.Red)
.margin(10)
}
}
@Extend(Text) function text1Styles(){
.width('25%')
.fontColor(Color.Gray)
.textAlign(TextAlign.Start)
}
@Extend(Text) function text2Styles(){
.width('75%')
.textAlign(TextAlign.Start)
}
4.主页面-我的页 HomePage_Me
效果如下:
代码如下:
import router from '@ohos.router'
import { Dialog } from '../view/Dialog'
import { User } from '../viewModel/User'
// @Preview
@Component
export struct HomePage_Me {
controllerLogOut: CustomDialogController = new CustomDialogController({
builder: Dialog({
message: '确定要退出登陆吗(下次需要重新登陆)?',
onYes: () => {
router.replaceUrl(
{
url: 'pages/LogIn'
},
router.RouterMode.Single,
err => {
if(err){
console.log('08808','退出登录路由失败')
}
}
)
this.controllerLogOut.close()
},
onNo: () => {
this.controllerLogOut.close()
}
})
})
build() {
Column(){
Image($r('app.media.tou_xiang'))
.width('30%')
Text('UserName: ' + User.Uname)
.fontSize(20)
Text('UserNo: ' + User.Uno)
.fontSize(20)
Button('修改密码')
.onClick(() => {
router.pushUrl(
{
url: 'pages/UpdatePassword'
},
router.RouterMode.Single,
err => {
if(err){
console.log('08808','跳转到密码修改页失败')
}
}
)
})
.margin({top: 100})
.backgroundColor('#d6b98d')
Button('退出登录')
.onClick(() => {
this.controllerLogOut.open()
})
.margin({top: 15})
.backgroundColor('#d6b98d')
}
.alignItems(HorizontalAlign.Center)
.padding({top: 200,left: 90,right: 90})
.width('100%')
.height('100%')
}
}
三、遇到的问题
问题1:有关饮品的搜索问题,其一是鸿蒙文本输入框无法输入中文;再一个是搜索的算法实现问题。
解决方案:在饮品的类定义(Tea)里添加首字母变量,在搜索时提醒用户输入首字母进行检索;首先清空当前数组,监视输入框,当输入框发生改变时,读取里面的内容并循环遍历数组teas,若有符合条件的条目则加入当前数组。若输入框为空则当前数组恢复(即当前数组=StaticValue.teas)
问题2:在为TabContent设置样式时使用了@Styles装饰器定义组建的通用样式,但是报错。
解决方案:@Styles通用样式函数需要写在使用它之前。
问题3:图片地址如何传入?
解决方案:在Tea类中图片地址使用resourceManager.Resource类型,然后在image组件中通过tea.image as Resource进行读取。
问题4:对Button的样式使用@Styles封装时提醒.type非公共样式,故使用@Extend(Button),但是依旧报红。
解决方案:@Extend只能放在全局中使用。
总结
以上就是本章的主要内容,包括了主页面-点餐页 HomePage_Order.ets、主页面-购物车页 HomePage_BuyCar.ets、主页面-订单页 HomePage_Bill、主页面-我的页 HomePage_Me等。