HarmonyOS7 购物车看着简单最容易翻车:增删改、全选、价格计算一篇讲透
文章目录
- 前言
- 购物车数据模型
- 全选与反选逻辑
- 数量步进器
- 滑动删除
- 价格计算
- 完整页面拼装
- 一些实用建议
前言
购物车这个页面,看着简单,做起来坑真不少。增删改查、全选反选、滑动删除、实时价格计算——每个功能单独拎出来都不难,凑一块儿状态管理就容易乱。今天把鲜选商城的购物车完整跑通,顺便聊聊我踩过的那些坑。
购物车数据模型
先把数据结构定好,后面所有逻辑都围绕它转。购物车里的每一条商品需要记录商品基本信息、选中状态和购买数量。
// CartItem 数据模型@ObservedclassCartItem{id:stringgoodsId:stringname:stringcoverUrl:stringspecText:string// 规格描述,如"红色/XL"price:number// 单价(分)originalPrice:number// 原价quantity:numberchecked:booleanstock:number// 库存上限shopId:stringconstructor(partial:Partial<CartItem>){this.id=partial.id??''this.goodsId=partial.goodsId??''this.name=partial.name??''this.coverUrl=partial.coverUrl??''this.specText=partial.specText??''this.price=partial.price??0this.originalPrice=partial.originalPrice??0this.quantity=partial.quantity??1this.checked=partial.checked??falsethis.stock=partial.stock??99this.shopId=partial.shopId??''}}有个经验:价格用"分"而不是"元"存储。浮点数加减乘除会出精度问题,比如0.1 + 0.2 = 0.30000000000000004,用整数算完再除以 100 显示,稳得多。
全选与反选逻辑
全选按钮的状态分三种:全不选、部分选、全选。我用一个计算属性来搞定:
// 全选状态:三种态getcheckedState():CheckboxState{constcheckedItems=this.cartList.filter(item=>item.checked)if(checkedItems.length===0)returnCheckboxState.Uncheckedif(checkedItems.length===this.cartList.length)returnCheckboxState.CheckedreturnCheckboxState.Indeterminate// 半选态}// 全选/取消全选toggleAll(){constnewChecked=this.checkedState!==CheckboxState.Checkedthis.cartList.forEach(item=>item.checked=newChecked)}这里容易犯的错误是直接拿布尔值做判断,忽略了"半选"状态。鸿蒙的Checkbox支持CheckboxState.Indeterminate,用上它全选按钮才有那味儿。
数量步进器
鸿蒙自带Stepper组件,但它默认样式比较朴素,购物车里通常需要自定义。我包了一层:
@Componentstruct QuantityStepper{@Prop@Watch('onValueChange')value:number=1min:number=1max:number=99onValueChange?:(val:number)=>voidbuild(){Row(){Button('-').fontSize(16).width(28).height(28).backgroundColor('#F5F5F5').enabled(this.value>this.min).opacity(this.value>this.min?1:0.4).onClick(()=>{if(this.value>this.min)this.value--})Text(`${this.value}`).fontSize(14).width(40).textAlign(TextAlign.Center)Button('+').fontSize(16).width(28).height(28).backgroundColor('#F5F5F5').enabled(this.value<this.max).opacity(this.value<this.max?1:0.4).onClick(()=>{if(this.value<this.max)this.value++})}}}@Watch装饰器是关键——外部传入onValueChange回调,数量变了自动通知父组件更新购物车数据和价格。
滑动删除
鸿蒙的ListItem配合swipeAction属性,滑动删除几行代码就搞定:
ForEach(this.cartList,(item:CartItem)=>{ListItem(){CartItemCard({item:item})}.swipeAction({end:this.buildDeleteAction(item)})},(item:CartItem)=>item.id)// 滑出来的删除按钮@BuilderbuildDeleteAction(item:CartItem){Row(){Button('删除').backgroundColor('#FF4D4F').fontColor('#FFFFFF').height('100%').width(80).onClick(()=>{this.removeItem(item.id)})}}批量删除也不复杂,底部栏加个"删除"按钮,点击时把checked === true的全干掉。记得加个确认弹窗,不然用户误操作就炸了。
价格计算
价格计算我抽成独立方法,所有需要总价的地方都调它:
// 计算选中商品总价calcSelectedTotal():PriceBreakdown{constselected=this.cartList.filter(item=>item.checked)consttotalOriginal=selected.reduce((sum,item)=>sum+item.originalPrice*item.quantity,0)consttotalSale=selected.reduce((sum,item)=>sum+item.price*item.quantity,0)constdiscount=totalOriginal-totalSaleconstcount=selected.reduce((sum,item)=>sum+item.quantity,0)return{totalAmount:totalSale,// 实付金额totalOriginal:totalOriginal,discount:discount,// 优惠金额selectedCount:count}}这里有个细节:@Observed修饰的CartItem,内部属性变化能触发 UI 刷新。但如果你的数组很深(比如嵌套了店铺分组),得注意@Observed只监听第一层属性,深层嵌套需要用@ObjectLink传递。
完整页面拼装
把上面的模块拼到一起,页面结构大概是这样的:
@Componentstruct CartPage{@StatecartList:CartItem[]=[]@StatepriceBreakdown:PriceBreakdown=newPriceBreakdown()build(){Column(){// 顶部导航NavBar({title:'购物车'})// 商品列表(按店铺分组)List(){ForEach(this.groupByShop(),(group:ShopGroup)=>{ListItemGroup({header:this.buildShopHeader(group)})ForEach(group.items,(item:CartItem)=>{ListItem(){CartItemCard({item:item})}.swipeAction({end:this.buildDeleteAction(item)})})})}.width('100%').layoutWeight(1).scrollBar(BarState.Off)// 底部结算栏BottomBar({checkedState:this.checkedState,onToggleAll:()=>this.toggleAll(),breakdown:this.priceBreakdown,onCheckout:()=>this.goCheckout()})}.width('100%').height('100%').backgroundColor('#F5F5F5')}}购物车页面状态比较多,我一开始想着全用@State管理,结果发现状态之间互相影响——改了数量要更新总价,改了选中要更新底部栏。后来改成用@Observed+@ObjectLink的方式,每个CartItem自己管理自己的状态,总价通过计算方法实时算,清爽很多。
一些实用建议
空状态别忘了。购物车为空的时候显示个占位图 + "去逛逛"按钮,别让用户看到一个空白页面。
本地缓存很重要。用户加购了商品、改过数量,突然杀进程再进来发现购物车空了,体验极差。用Preferences或者relationalStore做个本地持久化,数据回来体验好很多。
步进器的库存校验。用户点"+"的时候不仅要判断不超过max,还要跟库存stock对比。库存不足的时候弹个 Toast 提示,比让用户到结算页才发现买不了强。
购物车这个页面就是细节多,每个小功能都不难,但状态流转一多就容易出 bug。我的建议是先把数据模型和状态流转图画清楚,再动手写代码,效率反而更高。下一篇我们搞 SKU 选择器,那个规格矩阵算法才是真的烧脑,不过搞懂了会觉得挺有意思。
