HarmonyOS Next中实现嵌套购物车功能(上) 原创
一、背景
最近逛论坛和各种技术站发现大家对商城类应用搜索的频率非常高,在商城开发领域深耕多年的我因为业务需要也想要找一份购物车的代码来模仿一下,逛遍了论坛发现,有关购物车的文章都是简单的单列表展示、展示、结算,有那么个意思但是不符合我的需求,所以我想要自己实现一个包含店铺和商品功能的购物车模块,具体效果我们就直接看淘宝的图来进行效果实现,首先上淘宝的效果图:
二、功能分析
通过以上图片我们可以看到一个相对完整的购物车页面,那么这个购物车页面也是分为了好多块逻辑,下面我们对要实现的功能进行一个全面的分析
首先把页面拆分成块级,我们看一条数据的显示模式
小范围全选:
一条数据包含了店铺的信息和店铺是否选中的状态,店铺下方是当前店铺的商品以及选中状态,当前的选中状态是全选的,然后我们来看一条数据的另一种状态
选中某几条:
当我们取消了店铺下商品的选中后,可以看到店铺的选中状态也随之取消了,这里的店铺选中其实就是一个小范围的全选功能,最后我们看初始状态
初始状态:
可以看到在初始状态下,我们的商品和店铺都是未选中状态,经过分析之后我们已经对我们要处理的数据有了清晰的理解和认知,下面我们来实现一下代码
三、商品列表展示
首先定义好我们需要的变量,一个商品总价和商品流的详情,因为我们需要改变数组中对象的数据所以我们需要用到@ObservedV2和@Trace装饰器,两者的配合增强状态管理框架对类对象中属性的观测能力我们只需要把状态和子类,以及子类中的状态和数量进行修饰,就可以实现深层次的监听非常的方便
@ObservedV2
export class Test{
pos:number=0
name:string=''
@Trace check:boolean=false
@Trace childList:ChileBean[]=[]
}
@ObservedV2
export class ChileBean{
child_pos:number=0//关联标记
name:string=''//商品名称
price:number=0//商品原价
img:string=''//商品图
@Trace child_check:boolean=false//选中状态
@Trace num:number=1//数量
maxAmount:number=0//最大有效可购买数量
activitiesAmount:number=0//活动商品数量
couponPrice:number=0//券后价
constructor(child_pos?: number, name?: string, price?: number,couponPrice?:number,img?:string, child_check?: boolean,num?:number,maxAmount?:number,activitiesAmount?:number) {
this.child_pos = child_pos!
this.name = name!
this.price = price!
this.couponPrice= couponPrice!
this.img=this.img!
this.child_check = child_check!
this.num=num!
this.maxAmount=maxAmount!
this.activitiesAmount=activitiesAmount!
}
}
我们创建好数据后,在页面的生命周期中填充数据
aboutToAppear() {
// 创建数码电器的子项
const digitalAppliancesChildren:ChileBean[] = [
new ChileBean(0, "相机", 1998,1500,'https://tse3-mm.cn.bing.net/th/id/OIP-C.0OLkB4kpqaftDNKhgjOvfQHaE7?w=285&h=190&c=7&r=0&o=5&dpr=2&pid=1.7',false,1,100,5),
new ChileBean(0, "电视", 6889,3500,'https://tse4-mm.cn.bing.net/th/id/OIP-C.2fzOsOjmQAfnu6hVBLSw5gAAAA?w=231&h=180&c=7&r=0&o=5&dpr=2&pid=1.7', false,1,10,3),
new ChileBean(0, "手机", 9999,8888,'https://tse1-mm.cn.bing.net/th/id/OIP-C.JgrkZoJeG_fL9ODJbChWCQHaE8?w=287&h=191&c=7&r=0&o=5&dpr=2&pid=1.7', false,1,100,-1)
];
// 创建食品生鲜的子项
const freshFoodChildren:ChileBean[] = [
new ChileBean(1, "带鱼", 27,25,'https://tse4-mm.cn.bing.net/th/id/OIP-C.hR-iiysZZAZD0X79TqXwQwHaEc?w=255&h=183&c=7&r=0&o=5&dpr=2&pid=1.7', false,1,10,2),
new ChileBean(1, "螃蟹", 36,30, 'https://tse4-mm.cn.bing.net/th/id/OIP-C.R7CUxWDPlLs1o7-SVn_ymwHaE7?w=254&h=180&c=7&r=0&o=5&dpr=2&pid=1.7',false,1,10,3),
new ChileBean(1, "大虾", 19,15,'https://tse2-mm.cn.bing.net/th/id/OIP-C.QLsczzx_AB_Az0W8ZDFE2gHaHz?w=168&h=180&c=7&r=0&o=5&dpr=2&pid=1.7',false,1,20,10)
];
// 创建并添加数码电器和食品生鲜到数组
this.arrAy.push(this.createTest(0, "数码电器", digitalAppliancesChildren));
this.arrAy.push(this.createTest(1, "食品生鲜", freshFoodChildren));
}
数据准备好后使用list组件进行数据的填充并且加入布局展示数据(代码如下)
List({space:10}) {
ForEach(this.arrAy, (item: Test, index: number) => {
ListItem() {
Column() {
Row() {
Checkbox({ name: 'checkbox1' + index, group: 'checkboxGroup'+index})
.selectedColor("#ffd95112")
.select(item.check)
.shape(CheckBoxShape.CIRCLE)
.onChange((value: boolean) => {
})
.width(20)
.height(20)
Text(item.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.color_red'))
.width('100%')
.onClick(() => {
showToast(item.name)
})
}
List({space:10}) {
ForEach(item.childList, (child: ChileBean, child_index: number) => {
ListItem() {
Row() {
Checkbox({ name: 'checkbox1' + child_index, group: 'checkboxGroup' + child_index })
.select(child.child_check)
.selectedColor("#ffd95112")
.shape(CheckBoxShape.CIRCLE)
.onChange((value: boolean) => {
})
.width(20)
.height(20)
Image(child.img)
.height(90)
.width(90)
.border({width:1,color:Color.Blue,radius:10})
Column({space:10}){
Text(child.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.tab_text_normal'))
Row(){
Text(){
Span('¥')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor("#ffe5570b")
Span(""+child.couponPrice)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor("#ffe5570b")
}
if (child.activitiesAmount>-1){
if (child.num>child.activitiesAmount) {
Text("x"+child.activitiesAmount)
}
}
}
if (child.activitiesAmount>-1){
if (child.num>child.activitiesAmount) {
Row(){
Text(){
Span('¥')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor("#ffe5570b")
Span(""+child.price)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor("#ffe5570b")
}
Text("x"+Number(child.num-child.activitiesAmount))
}
}
}
}
.margin({left:10})
.alignItems(HorizontalAlign.Start)
Blank()
}
.padding({left:10,right:20})
.alignItems(VerticalAlign.Center)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
})
}
}
}
})
}
这时候执行我们的项目可以看到数据列表展示,运行效果如下
可以看到我们的数据是一个条目里分了两层,一层是商品类型,一层是该类型的所有商品
四、自定义结算组件
已经实现了商品信息的展示之后,当我们选中商品,或者取消商品的时候,我们需要对价格进行一个展示和计算,下面我们来实现一个自定义的结算模块,结算模块我们需要有一个全选按钮,一个提交按钮,一个商品价格计算的组件,当我们没有商品选择的时候,给提交按钮一个置灰和点击添加商品提示。
实现代码如下:
Row(){
Checkbox({ name: 'checkbox_all' , group: 'checkboxGroup_all'})
.selectedColor("#ffd95112")
.select(this.isALlCheck())
.shape(CheckBoxShape.CIRCLE)
.width(25)
.height(25)
.onChange((value: boolean) => {
})
全选")
.fontSize(16)
Blank()
提交")
.border({radius:20})
.width(90)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.backgroundColor(this.getSelectedChildCount()>0?"#ffe5570b":"#ffc3bdbd")
.height(45)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.onClick(()=>{
未选择商品")
})
.margin({right:10})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
这时候我们执行代码就可以看到商品列表和底部的结算组件了,效果如下:
赶紧试试吧!