
文章目录前言购物车数据模型全选与反选逻辑数量步进器滑动删除价格计算完整页面拼装一些实用建议前言购物车这个页面看着简单做起来坑真不少。增删改查、全选反选、滑动删除、实时价格计算——每个功能单独拎出来都不难凑一块儿状态管理就容易乱。今天把鲜选商城的购物车完整跑通顺便聊聊我踩过的那些坑。购物车数据模型先把数据结构定好后面所有逻辑都围绕它转。购物车里的每一条商品需要记录商品基本信息、选中状态和购买数量。// CartItem 数据模型ObservedclassCartItem{id:stringgoodsId:stringname:stringcoverUrl:stringspecText:string// 规格描述如红色/XLprice:number// 单价分originalPrice:number// 原价quantity:numberchecked:booleanstock:number// 库存上限shopId:stringconstructor(partial:PartialCartItem){this.idpartial.id??this.goodsIdpartial.goodsId??this.namepartial.name??this.coverUrlpartial.coverUrl??this.specTextpartial.specText??this.pricepartial.price??0this.originalPricepartial.originalPrice??0this.quantitypartial.quantity??1this.checkedpartial.checked??falsethis.stockpartial.stock??99this.shopIdpartial.shopId??}}有个经验价格用分而不是元存储。浮点数加减乘除会出精度问题比如0.1 0.2 0.30000000000000004用整数算完再除以 100 显示稳得多。全选与反选逻辑全选按钮的状态分三种全不选、部分选、全选。我用一个计算属性来搞定// 全选状态三种态getcheckedState():CheckboxState{constcheckedItemsthis.cartList.filter(itemitem.checked)if(checkedItems.length0)returnCheckboxState.Uncheckedif(checkedItems.lengththis.cartList.length)returnCheckboxState.CheckedreturnCheckboxState.Indeterminate// 半选态}// 全选/取消全选toggleAll(){constnewCheckedthis.checkedState!CheckboxState.Checkedthis.cartList.forEach(itemitem.checkednewChecked)}这里容易犯的错误是直接拿布尔值做判断忽略了半选状态。鸿蒙的Checkbox支持CheckboxState.Indeterminate用上它全选按钮才有那味儿。数量步进器鸿蒙自带Stepper组件但它默认样式比较朴素购物车里通常需要自定义。我包了一层Componentstruct QuantityStepper{PropWatch(onValueChange)value:number1min:number1max:number99onValueChange?:(val:number)voidbuild(){Row(){Button(-).fontSize(16).width(28).height(28).backgroundColor(#F5F5F5).enabled(this.valuethis.min).opacity(this.valuethis.min?1:0.4).onClick((){if(this.valuethis.min)this.value--})Text(${this.value}).fontSize(14).width(40).textAlign(TextAlign.Center)Button().fontSize(16).width(28).height(28).backgroundColor(#F5F5F5).enabled(this.valuethis.max).opacity(this.valuethis.max?1:0.4).onClick((){if(this.valuethis.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{constselectedthis.cartList.filter(itemitem.checked)consttotalOriginalselected.reduce((sum,item)sumitem.originalPrice*item.quantity,0)consttotalSaleselected.reduce((sum,item)sumitem.price*item.quantity,0)constdiscounttotalOriginal-totalSaleconstcountselected.reduce((sum,item)sumitem.quantity,0)return{totalAmount:totalSale,// 实付金额totalOriginal:totalOriginal,discount:discount,// 优惠金额selectedCount:count}}这里有个细节Observed修饰的CartItem内部属性变化能触发 UI 刷新。但如果你的数组很深比如嵌套了店铺分组得注意Observed只监听第一层属性深层嵌套需要用ObjectLink传递。完整页面拼装把上面的模块拼到一起页面结构大概是这样的Componentstruct CartPage{StatecartList:CartItem[][]StatepriceBreakdown:PriceBreakdownnewPriceBreakdown()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管理结果发现状态之间互相影响——改了数量要更新总价改了选中要更新底部栏。后来改成用ObservedObjectLink的方式每个CartItem自己管理自己的状态总价通过计算方法实时算清爽很多。一些实用建议空状态别忘了。购物车为空的时候显示个占位图 去逛逛按钮别让用户看到一个空白页面。本地缓存很重要。用户加购了商品、改过数量突然杀进程再进来发现购物车空了体验极差。用Preferences或者relationalStore做个本地持久化数据回来体验好很多。步进器的库存校验。用户点的时候不仅要判断不超过max还要跟库存stock对比。库存不足的时候弹个 Toast 提示比让用户到结算页才发现买不了强。购物车这个页面就是细节多每个小功能都不难但状态流转一多就容易出 bug。我的建议是先把数据模型和状态流转图画清楚再动手写代码效率反而更高。下一篇我们搞 SKU 选择器那个规格矩阵算法才是真的烧脑不过搞懂了会觉得挺有意思。