HarmonyOS7 购物车计数器页实战:Counter/步进器 不只是会用,还要用得顺手
文章目录
- 先把数量当成业务状态,而不是按钮效果
- 关键点在边界控制
- 自定义步长适合做配置面板
- 页面结构怎么看
- 完整代码
- 写在最后
购物车里的“加一减一”看起来太普通了,普通到很多人会直接在点击事件里写count++。但真做过电商页面就知道,这里最容易出细碎问题:数量能不能减到 0、最大值怎么限制、不同商品的数量怎么单独更新、步长是不是固定的。
这个 HarmonyOS7 页面把购物车数量和自定义步进器放在一起讲,正好能把 Counter 类场景里最实用的部分拆清楚。
先把数量当成业务状态,而不是按钮效果
页面里有两类数据:CartItem_tnna表示购物车商品,里面有价格、数量、单位和图标;StepConfig表示自定义步长配置,适合拿来做评分、预算、库存这类非购物车场景。
我比较喜欢这个拆法。购物车数量不是一个孤立数字,它和商品 id、价格、单位绑定在一起。后面要算总价、清空商品、同步后端库存,都能顺着这个数据结构往下扩。
关键点在边界控制
updateItemCount()是这篇最值得看的地方。它没有直接改原数组里的对象,而是用map()返回一份新的数组;命中对应商品后,再用Math.max()和Math.min()把数量卡在0到99之间。
updateItemCount(itemId:number,delta:number){this.cartItems=this.cartItems.map((item:CartItem_tnna):CartItem_tnna=>{if(item.id===itemId){constnewCount=Math.max(0,Math.min(99,item.count+delta))return{id:item.id,name:item.name,price:item.price,count:newCount,icon:item.icon,unit:item.unit}}returnitem})}这段代码解决了两个实际问题:只更新当前商品;不让数量跑出合法范围。很多购物车 bug 都出在这里,比如减成负数、快速点击导致状态乱跳。
自定义步长适合做配置面板
第二块的updateStepValue()思路类似,只是它不再固定加一减一,而是根据StepConfig.step变化。这个场景很常见:购买数量可能是 1 件一跳,积分兑换可能是 10 分一跳,预算金额可能是 100 元一跳。
所以这篇不只是一个购物车案例,它其实是在教你写“可配置的数字调节器”。只要把min、max、step抽成数据,UI 就不用跟业务规则绑死。
页面结构怎么看
ShoppingCounterPage用Scroll包住两个主要模块:购物车商品数量、自定义步进器。前者偏业务,后者偏组件能力展示。读代码时可以先看cartItems和stepConfigs的初始数据,再看两个更新方法,最后看ForEach里怎么把每一项渲染出来。
如果你要改成真实项目,我建议下一步加总价计算、商品删除和库存提示。基础数量控制已经有了,继续扩并不难。
完整代码
下面是完整代码。入口组件名是ShoppingCounterPage,可以直接放到自己的 HarmonyOS7 页面里按需改业务字段。
// Counter 计数器组件interfaceCartItem_tnna{id:numbername:stringprice:numbercount:numbericon:stringunit:string}interfaceStepConfig{label:stringmin:numbermax:numberstep:numbervalue:numbercolor:string}@Entry@Componentstruct ShoppingCounterPage{@StatecartItems:CartItem_tnna[]=[{id:1,name:'有机牛奶',price:12.8,count:1,icon:'🥛',unit:'盒'},{id:2,name:'全麦面包',price:18.5,count:2,icon:'🍞',unit:'袋'},{id:3,name:'鸡蛋',price:3.5,count:1,icon:'🥚',unit:'个'},{id:4,name:'苹果汁',price:9.9,count:0,icon:'🧃',unit:'瓶'},]@StatestepConfigs:StepConfig[]=[{label:'加热时长(分钟)',min:1,max:60,step:1,value:5,color:'#EF4444'},{label:'购买数量(件)',min:0,max:100,step:5,value:10,color:'#2563EB'},{label:'字体大小(px)',min:12,max:36,step:2,value:16,color:'#7C3AED'},]gettotalPrice():number{returnthis.cartItems.reduce((sum:number,item:CartItem_tnna):number=>sum+item.price*item.count,0)}gettotalCount():number{returnthis.cartItems.reduce((sum:number,item:CartItem_tnna):number=>sum+item.count,0)}updateItemCount(itemId:number,delta:number){this.cartItems=this.cartItems.map((item:CartItem_tnna):CartItem_tnna=>{if(item.id===itemId){constnewCount=Math.max(0,Math.min(99,item.count+delta))return{id:item.id,name:item.name,price:item.price,count:newCount,icon:item.icon,unit:item.unit}}returnitem})}updateStepValue(index:number,delta:number){this.stepConfigs=this.stepConfigs.map((cfg:StepConfig,i:number):StepConfig=>{if(i===index){constnewVal=Math.max(cfg.min,Math.min(cfg.max,cfg.value+delta*cfg.step))return{label:cfg.label,min:cfg.min,max:cfg.max,step:cfg.step,value:newVal,color:cfg.color}}returncfg})}@BuilderCounterButton(icon:string,action:()=>void,disabled:boolean){Text(icon).fontSize(18).width(32).height(32).textAlign(TextAlign.Center).fontColor(disabled?'#D1D5DB':'#374151').backgroundColor(disabled?'#F9FAFB':'#F3F4F6').borderRadius(8).onClick(():void=>{if(!disabled)action()})}build(){Column({space:0}){// 标题Text('Counter 计数器').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#111827').width('100%').padding({left:20,right:20,top:20,bottom:4})Text('购物车数量、步进器等常用场景').fontSize(13).fontColor('#6B7280').width('100%').padding({left:20,right:20,bottom:16})// 一、购物车计数Column({space:12}){Text('一、购物车商品数量').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:8}){ForEach(this.cartItems,(item:CartItem_tnna):void=>{Row(){Text(item.icon).fontSize(28).width(48).height(48).textAlign(TextAlign.Center).backgroundColor('#F9FAFB').borderRadius(10)Column({space:4}){Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#111827')Text('¥'+item.price.toFixed(1)+' / '+item.unit).fontSize(12).fontColor('#9CA3AF')}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({left:12})// 计数器Row({space:6}){this.CounterButton('−',():void=>this.updateItemCount(item.id,-1),item.count<=0)Text(item.count.toString()).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#111827').width(36).textAlign(TextAlign.Center)this.CounterButton('+',():void=>this.updateItemCount(item.id,1),item.count>=99)}.alignItems(VerticalAlign.Center)}.width('100%').padding({left:16,right:16,top:12,bottom:12}).backgroundColor(item.count>0?'#FFFFFF':'#FAFAFA').borderRadius(10).alignItems(VerticalAlign.Center)},(item:CartItem_tnna):string=>item.id.toString())}// 合计Row(){Text(`共${this.totalCount}件商品`).fontSize(13).fontColor('#6B7280')Blank()Text('合计:').fontSize(14).fontColor('#374151')Text('¥'+this.totalPrice.toFixed(1)).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#EF4444')}.width('100%').padding({left:16,right:16,top:12,bottom:12}).backgroundColor('#FFF7ED').borderRadius(10).alignItems(VerticalAlign.Center)}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start).margin({bottom:16})// 二、步进器Column({space:12}){Text('二、步进器(自定义步长)').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')Column({space:10}){ForEach(this.stepConfigs,(cfg:StepConfig,index:number):void=>{Column({space:8}){Row(){Text(cfg.label).fontSize(13).fontColor('#374151').layoutWeight(1)Text(`步长${cfg.step}`).fontSize(11).fontColor('#9CA3AF')}.width('100%')Row({space:10}){// 减号按钮Button('−').fontSize(18).width(40).height(40).fontColor(cfg.value<=cfg.min?'#D1D5DB':cfg.color).backgroundColor(cfg.value<=cfg.min?'#F9FAFB':'#F3F4F6').borderRadius(8).onClick(():void=>{if(cfg.value>cfg.min)this.updateStepValue(index,-1)})// 进度条 + 数值Column({space:4}){Text(cfg.value.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor(cfg.color).width('100%').textAlign(TextAlign.Center)Progress({value:cfg.value,total:cfg.max,type:ProgressType.Linear}).color(cfg.color).backgroundColor('#E5E7EB').style({strokeWidth:4}).width('100%')}.layoutWeight(1).alignItems(HorizontalAlign.Center)// 加号按钮Button('+').fontSize(18).width(40).height(40).fontColor(cfg.value>=cfg.max?'#D1D5DB':cfg.color).backgroundColor(cfg.value>=cfg.max?'#F9FAFB':'#F3F4F6').borderRadius(8).onClick(():void=>{if(cfg.value<cfg.max)this.updateStepValue(index,1)})}.width('100%').alignItems(VerticalAlign.Center)}.width('100%').padding(12).backgroundColor('#FAFAFA').borderRadius(10)},(cfg:StepConfig):string=>cfg.label)}}.width('100%').padding({left:20,right:20}).alignItems(HorizontalAlign.Start)Blank()}.width('100%').height('100%').backgroundColor('#FFFFFF')}}写在最后
购物车计数器页 这个案例的价值,不在于用了多少组件,而在于它把Counter/步进器放进了一个完整页面里。照着这个思路改自己的业务,比单独背 API 更靠谱。
