文章目录前言页面数据来源收货地址选择优惠券选择支付方式选择价格明细完整页面组装一些建议前言订单确认页是下单流程的最后一道关卡信息量大、交互多但不算太难。核心就三块地址、优惠券、价格明细。今天一次性把它搭完。页面数据来源订单确认页的数据从购物车过来——用户在购物车页勾选了商品点击去结算把选中商品的信息传过来。我用了一个OrderConfirmData来承载ObservedclassOrderConfirmData{items:OrderItem[][]// 商品列表address:AddressInfo|nullnullavailableCoupons:CouponInfo[][]selectedCouponId:stringpayMethod:PayMethodPayMethod.HuaweiPay freight:number0// 运费分constructor(items:OrderItem[]){this.itemsitems}}interfaceOrderItem{skuId:stringgoodsName:stringcoverUrl:stringspecText:stringprice:numberquantity:number}收货地址选择地址区域放在页面最上面点击弹出地址列表。鸿蒙的ActionSheet做这种底部弹窗很合适// 地址卡片Componentstruct AddressCard{Propaddress:AddressInfo|nullonChoose?:()voidbuild(){Row(){if(this.address){Column(){Row(){Text(this.address.name).fontSize(16).fontWeight(FontWeight.Bold)Text(this.address.phone).fontSize(14).fontColor(#666).margin({left:12})}Text(this.address.provincethis.address.citythis.address.districtthis.address.detail).fontSize(13).fontColor(#999).margin({top:4})}.alignItems(HorizontalAlign.Start).layoutWeight(1)}else{Text(请选择收货地址).fontSize(14).fontColor(#999).layoutWeight(1)}Image($r(app.media.icon_arrow_right)).width(16).height(16)}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12).onClick(()this.onChoose?.())}}地址列表弹窗我用CustomDialog实现里面放一个 ListCustomDialogstruct AddressPickerDialog{controller:CustomDialogController addressList:AddressInfo[][]onSelect?:(addr:AddressInfo)voidonAddNew?:()voidbuild(){Column(){Text(选择收货地址).fontSize(18).fontWeight(FontWeight.Bold).padding({top:20,bottom:16})List(){ForEach(this.addressList,(addr:AddressInfo){ListItem(){Row(){Column(){Text(${addr.name}${addr.phone}).fontSize(15)Text(addr.fullAddress).fontSize(13).fontColor(#666).margin({top:4})}.alignItems(HorizontalAlign.Start).layoutWeight(1)Image($r(app.media.icon_arrow_right)).width(16).height(16)}.padding(12)}.onClick((){this.onSelect?.(addr)this.controller.close()})})}.layoutWeight(1)Button( 新增收货地址).width(92%).height(44).margin({bottom:20}).onClick((){this.controller.close()this.onAddNew?.()})}.width(100%).height(70%)}}优惠券选择优惠券这块逻辑稍微有点绕。可用优惠券要满足门槛条件不满足的归到不可用里// 优惠券分类getCouponGroups():CouponGroups{constorderAmountthis.calcGoodsTotal()constavailable:CouponInfo[][]constunavailable:CouponInfo[][]this.allCoupons.forEach(coupon{if(coupon.typeCouponType.Fixed){// 满减券订单金额 门槛if(orderAmountcoupon.threshold){available.push(coupon)}else{unavailable.push({...coupon,reason:还差${this.yuan(coupon.threshold-orderAmount)}元可用})}}elseif(coupon.typeCouponType.Percent){// 折扣券available.push(coupon)}})return{available,unavailable}}// 计算优惠金额calcCouponDiscount(coupon:CouponInfo,orderAmount:number):number{if(coupon.typeCouponType.Fixed){returncoupon.discount// 满减直接减}// 折扣券算出省了多少constsavedMath.round(orderAmount*(1-coupon.discount/100))returnMath.min(saved,coupon.maxDiscount)// 有封顶}优惠券选择页面可以用Navigation跳转也可以用bindSheet底部弹出。我选了后者体验更连贯。支付方式选择支付方式就几个 Radio 选项简单直接Componentstruct PayMethodSelector{PropWatch(onMethodChange)selected:PayMethodPayMethod.HuaweiPay onChange?:(method:PayMethod)voidbuild(){Column(){ForEach(this.getPayMethods(),(item:PayMethodItem){Row(){Image(item.icon).width(24).height(24)Text(item.name).fontSize(15).margin({left:10}).layoutWeight(1)Radio({value:item.name,group:payMethod}).checked(this.selecteditem.method).onChange((isChecked:boolean){if(isChecked){this.selecteditem.methodthis.onChange?.(item.method)}})}.width(100%).height(52).padding({left:16,right:16})})}.backgroundColor(#FFFFFF).borderRadius(12)}}价格明细底部价格明细要把每一项都列清楚用户最怕的就是价格对不上Componentstruct PriceDetail{PropgoodsTotal:number0// 商品总额Propfreight:number0// 运费PropcouponDiscount:number0// 优惠券PropshopDiscount:number0// 店铺优惠getfinalAmount():number{returnthis.goodsTotalthis.freight-this.couponDiscount-this.shopDiscount}build(){Column(){this.buildRow(商品金额,this.goodsTotal)this.buildRow(运费,this.freight)if(this.shopDiscount0){this.buildRow(店铺优惠,-this.shopDiscount,true)}if(this.couponDiscount0){this.buildRow(优惠券,-this.couponDiscount,true)}Divider().margin({top:8,bottom:8})Row(){Text(实付金额).fontSize(15).fontWeight(FontWeight.Bold)Blank()Text(¥${this.yuan(this.finalAmount)}).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#FF4D4F)}}.width(100%).padding(16).backgroundColor(#FFFFFF).borderRadius(12)}BuilderbuildRow(label:string,amount:number,isDiscount:booleanfalse){Row(){Text(label).fontSize(14).fontColor(#666)Blank()Text(isDiscount?-¥${this.yuan(Math.abs(amount))}:¥${this.yuan(amount)}).fontSize(14).fontColor(isDiscount?#FF4D4F:#333)}.width(100%).height(32)}yuan(fen:number):string{return(fen/100).toFixed(2)}}完整页面组装把所有模块组合到一起外面套个 ScrollComponentstruct OrderConfirmPage{Statedata:OrderConfirmDatanewOrderConfirmData([])StateshowAddressPicker:booleanfalsebuild(){Column(){NavBar({title:确认订单})Scroll(){Column({space:10}){// 收货地址AddressCard({address:this.data.address,onChoose:(){this.showAddressPickertrue}})// 商品列表Column(){ForEach(this.data.items,(item:OrderItem){OrderGoodsRow({item:item})})}.backgroundColor(#FFFFFF).borderRadius(12)// 优惠券CouponSelector({selectedCoupon:this.getSelectedCoupon(),available:this.getCouponGroups().available,onSelect:(coupon){this.data.selectedCouponIdcoupon.id}})// 支付方式PayMethodSelector({selected:this.data.payMethod,onChange:(m){this.data.payMethodm}})// 价格明细PriceDetail({goodsTotal:this.calcGoodsTotal(),freight:this.data.freight,couponDiscount:this.getCouponDiscountAmount(),shopDiscount:this.calcShopDiscount()})Blank().height(80)// 给底部按钮留空间}.padding({left:12,right:12,top:10})}.layoutWeight(1)// 底部提交按钮Row(){Column(){Text(实付金额).fontSize(12).fontColor(#999)Text(¥${this.yuan(this.calcFinalAmount())}).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#FF4D4F)}.alignItems(HorizontalAlign.Start)Button(提交订单).width(140).height(44).borderRadius(22).backgroundColor(#FF4D4F).fontColor(#FFFFFF).onClick(()this.submitOrder())}.width(100%).height(60).padding({left:16,right:12}).backgroundColor(#FFFFFF).justifyContent(FlexAlign.SpaceBetween)}.width(100%).height(100%).backgroundColor(#F5F5F5)}}一些建议价格一定要后端校验。前端算的价格只是给用户看的最终金额必须后端再算一遍。别问我怎么知道的——有人在前端改过价格字段提交过 0 元订单。优惠券叠加规则要提前定。满减券和折扣券能不能叠加店铺优惠和平台优惠能不能同时用这些规则不同项目差异很大写代码之前先跟产品确认清楚。地址变更要重新算运费。不同地区运费可能不同甚至有些地方不配送。地址变了要重新调接口算运费别用缓存的旧值。订单确认页做完下一步就是支付了。下一篇我们搞支付模块包括支付流程、结果页和订单状态管理。