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 选择器,那个规格矩阵算法才是真的烧脑,不过搞懂了会觉得挺有意思。