HarmonyOS7购物车结算页实战:勾选逻辑与金额汇总

发布时间:2026/7/16 9:30:52
HarmonyOS7购物车结算页实战:勾选逻辑与金额汇总 文章目录前言CartItem 同时承担展示和结算信息金额和件数都从 checkedItems 派生全选的判断和动作是两件事单个商品操作都回到 cartItems结算弹窗复用当前合计结果这个示例最值得借的是结算口径完整代码最后总结前言购物车页面最容易出错的地方不是列表怎么画而是“哪些商品参与结算”。用户改数量、取消勾选、全选、删除商品底部合计都要立刻同步。如果金额和勾选状态不同步页面看起来再漂亮也没用。这个示例把购物车的核心闭环写得比较完整商品列表由cartItems驱动结算金额从选中商品派生数量最小值被限制在 1点击结算后弹出一个提交成功的遮罩。它不是完整电商流程但足够用来练购物车里最关键的状态联动。CartItem 同时承担展示和结算信息CartItem_r5tm里既有商品展示字段也有结算字段。name、spec、emoji、shopName负责展示price、qty、checked决定底部金额。interfaceCartItem_r5tm{id:numbername:stringspec:stringprice:numberqty:numberchecked:booleanemoji:stringshopName:string}这里的checked很关键。购物车不是简单把所有商品价格相加而是只统计被选中的商品。把这个状态放在每个商品项里比额外维护一个选中 ID 列表更直观尤其适合教学示例。如果是正式项目我会再补库存、失效状态、优惠信息和店铺维度分组。当前shopName只是展示字段还没有真正把同店铺商品归组。金额和件数都从 checkedItems 派生源码里先通过 getter 拿到选中商品再基于它计算总价和件数。getcheckedItems():CartItem_r5tm[]{returnthis.cartItems.filter(ii.checked)}gettotal():number{returnthis.checkedItems.reduce((s,i)si.price*i.qty,0)}getitemCount():number{returnthis.checkedItems.reduce((s,i)si.qty,0)}旧稿里如果把checkedItems写成普通函数就和源码不一致了。这里它是 getter所以底部直接读this.total、this.itemCount结算弹窗也复用同一套结果。这个结构的好处是数据口径统一列表里无论怎么勾选、改数量、删除商品底部合计和弹窗金额都从cartItems当前状态实时算出来不需要手动维护一个totalPrice状态。全选的判断和动作是两件事allChecked只负责判断当前是否全选toggleAll()才负责改状态。getallChecked():boolean{returnthis.cartItems.every(ii.checked)}toggleAll(){consttoCheck!this.allCheckedthis.cartItemsthis.cartItems.map(i{i.checkedtoCheck;returni})}toggleAll()先取反当前全选状态再把所有商品的checked改成同一个值。这样底部复选框和商品复选框能保持一致。有一个边界值得补充如果cartItems为空every()会返回true。当前示例没有空购物车状态所以看不出来真实项目里要单独处理空列表否则空购物车可能显示成“全选”。单个商品操作都回到 cartItems点击商品前面的勾选框会调用toggleCheck(id)它只翻转对应商品的checked。toggleCheck(id:number){this.cartItemsthis.cartItems.map(i{if(i.idid){i.checked!i.checked}returni})}数量加减走changeQty(id, delta)。这里用了Math.max(1, i.qty delta)保证数量不会减到 0 或负数。changeQty(id:number,delta:number){this.cartItemsthis.cartItems.map(i{if(i.idid){i.qtyMath.max(1,i.qtydelta)}returni})}removeItem(id)目前在源码里实现了但页面上没有绑定删除按钮。也就是说这个函数是扩展点不是当前 UI 已经提供的能力。removeItem(id:number){this.cartItemsthis.cartItems.filter(ii.id!id)}这类地方讲教程时要谨慎函数存在不等于用户已经能操作。当前顶部“编辑”也只是文本没有切换编辑模式。结算弹窗复用当前合计结果点击“去结算”只是把showCheckout改成true并没有真正生成订单或清空购物车。Button(去结算).onClick(()this.showCheckouttrue)弹窗里展示的件数和实付金额依然来自itemCount和total。if(this.showCheckout){Text(共${this.itemCount}件商品)Text(实付金额 ¥${this.total.toFixed(2)})}这意味着如果用户没有选中任何商品当前源码仍然可以弹出“共 0 件商品实付金额 ¥0.00”。正式项目里应该禁用结算按钮或者在点击时判断itemCount 0并给提示。这个示例最值得借的是结算口径我会把这页的核心理解成一句话购物车的主状态只有cartItems其它金额、件数、全选状态都从它推导出来。这个口径一旦稳定UI 怎么换都不容易乱。后续如果扩展成真实购物车优先补空状态、编辑删除、禁用无选中结算、店铺分组和优惠计算。优惠逻辑最好不要塞进total里硬改可以单独拆出商品小计、优惠金额和应付金额避免后面越来越难维护。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。import{router}fromkit.ArkUIinterfaceCartItem_r5tm{id:numbername:stringspec:stringprice:numberqty:numberchecked:booleanemoji:stringshopName:string}EntryComponentstruct ShoppingCartCheckout{StatecartItems:CartItem_r5tm[][{id:1,name:无线蓝牙耳机 Pro,spec:白色 / 标准版,price:299,qty:1,checked:true,emoji:,shopName:数码旗舰店},{id:2,name:运动水杯 800ml,spec:蓝色 / 不锈钢,price:89,qty:2,checked:true,emoji:,shopName:户外生活馆},{id:3,name:机械键盘 TKL,spec:茶轴 / RGB 背光,price:549,qty:1,checked:false,emoji:⌨️,shopName:外设专营店},{id:4,name:竹制笔记本 A5,spec:深棕色 / 200页,price:35,qty:3,checked:true,emoji:,shopName:文具精品屋},{id:5,name:USB-C 多口充电头,spec:65W / 黑色,price:129,qty:1,checked:true,emoji:,shopName:数码旗舰店},]StateshowCheckout:booleanfalsegetcheckedItems():CartItem_r5tm[]{returnthis.cartItems.filter(ii.checked)}gettotal():number{returnthis.checkedItems.reduce((s,i)si.price*i.qty,0)}getitemCount():number{returnthis.checkedItems.reduce((s,i)si.qty,0)}getallChecked():boolean{returnthis.cartItems.every(ii.checked)}toggleAll(){consttoCheck!this.allCheckedthis.cartItemsthis.cartItems.map(i{i.checkedtoCheck;returni})}changeQty(id:number,delta:number){this.cartItemsthis.cartItems.map(i{if(i.idid){i.qtyMath.max(1,i.qtydelta)}returni})}removeItem(id:number){this.cartItemsthis.cartItems.filter(ii.id!id)}toggleCheck(id:number){this.cartItemsthis.cartItems.map(i{if(i.idid){i.checked!i.checked}returni})}build(){Stack(){Column(){Row(){Image($r(app.media.ic_back)).width(24).height(24).onClick(()router.back())Text(购物车).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text(编辑).fontSize(14).fontColor(#007AFF)}.width(100%).padding({left:16,right:16,top:12,bottom:12})// Cart itemsList({space:10}){ForEach(this.cartItems,(item:CartItem_r5tm){ListItem(){Column(){// Shop headerRow(){Text().fontSize(14)Text(item.shopName).fontSize(13).fontColor(#555).margin({left:4}).layoutWeight(1)}.width(100%).margin({bottom:8})Row(){// CheckboxText(item.checked?☑:☐).fontSize(22).fontColor(item.checked?#007AFF:#CCC).margin({right:10}).onClick(()this.toggleCheck(item.id))// Item emojiText(item.emoji).fontSize(44).margin({right:12})// InfoColumn(){Text(item.name).fontSize(14).fontColor(#333).fontWeight(FontWeight.Medium)Text(item.spec).fontSize(12).fontColor(#888).margin({top:4})Text(\¥\${item.price}\).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#FF4757).margin({top:6})}.layoutWeight(1).alignItems(HorizontalAlign.Start)// Qty controlRow(){Text(−).fontSize(18).width(32).height(32).backgroundColor(#F0F0F0).borderRadius(16).textAlign(TextAlign.Center).lineHeight(32).onClick(()this.changeQty(item.id,-1))Text(\\${item.qty}\).fontSize(14).fontWeight(FontWeight.Bold).width(32).textAlign(TextAlign.Center)Text().fontSize(18).width(32).height(32).backgroundColor(#F0F0F0).borderRadius(16).textAlign(TextAlign.Center).lineHeight(32).onClick(()this.changeQty(item.id,1))}}.width(100%)}.width(100%).backgroundColor(#FFF).borderRadius(14).padding(14).shadow({radius:4,color:#0000001A,offsetY:2})}})}.layoutWeight(1).padding({left:16,right:16,top:4})// Bottom barRow(){Row(){Text(this.allChecked?☑:☐).fontSize(22).fontColor(this.allChecked?#007AFF:#CCC).margin({right:6}).onClick(()this.toggleAll())Text(全选).fontSize(14).fontColor(#333)}Blank()Column(){Text(\合计:¥\${this.total.toFixed(2)}\).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#FF4757)Text(\已选 \${this.itemCount}件\).fontSize(12).fontColor(#888)}.alignItems(HorizontalAlign.End).margin({right:12})Button(去结算).backgroundColor(#FF4757).fontColor(#FFF).borderRadius(24).padding({left:24,right:24,top:12,bottom:12}).onClick(()this.showCheckouttrue)}.width(100%).backgroundColor(#FFF).padding({left:16,right:16,top:12,bottom:12}).shadow({radius:8,color:#0000001A,offsetY:-4})}.width(100%).height(100%).backgroundColor(#F5F5F5)// Checkout dialogif(this.showCheckout){Column(){Column(){Text(✅ 订单提交成功).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#2ED573).margin({bottom:8})Text(\共 \${this.itemCount}件商品\).fontSize(14).fontColor(#666).margin({bottom:4})Text(\实付金额 ¥\${this.total.toFixed(2)}\).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FF4757).margin({bottom:20})Button(继续购物).width(160).backgroundColor(#007AFF).fontColor(#FFF).borderRadius(24).onClick(()this.showCheckoutfalse)}.backgroundColor(#FFF).borderRadius(20).padding(28).width(80%).alignItems(HorizontalAlign.Center)}.width(100%).height(100%).backgroundColor(#00000060).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}}}最后总结购物车结算页 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。