当前位置: 首页 > news >正文

HarmonyOS7 购物车计数器页实战:Counter/步进器 不只是会用,还要用得顺手

文章目录

      • 先把数量当成业务状态,而不是按钮效果
      • 关键点在边界控制
      • 自定义步长适合做配置面板
      • 页面结构怎么看
      • 完整代码
      • 写在最后

购物车里的“加一减一”看起来太普通了,普通到很多人会直接在点击事件里写count++。但真做过电商页面就知道,这里最容易出细碎问题:数量能不能减到 0、最大值怎么限制、不同商品的数量怎么单独更新、步长是不是固定的。

这个 HarmonyOS7 页面把购物车数量和自定义步进器放在一起讲,正好能把 Counter 类场景里最实用的部分拆清楚。

先把数量当成业务状态,而不是按钮效果

页面里有两类数据:CartItem_tnna表示购物车商品,里面有价格、数量、单位和图标;StepConfig表示自定义步长配置,适合拿来做评分、预算、库存这类非购物车场景。

我比较喜欢这个拆法。购物车数量不是一个孤立数字,它和商品 id、价格、单位绑定在一起。后面要算总价、清空商品、同步后端库存,都能顺着这个数据结构往下扩。

关键点在边界控制

updateItemCount()是这篇最值得看的地方。它没有直接改原数组里的对象,而是用map()返回一份新的数组;命中对应商品后,再用Math.max()Math.min()把数量卡在099之间。

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 元一跳。

所以这篇不只是一个购物车案例,它其实是在教你写“可配置的数字调节器”。只要把minmaxstep抽成数据,UI 就不用跟业务规则绑死。

页面结构怎么看

ShoppingCounterPageScroll包住两个主要模块:购物车商品数量、自定义步进器。前者偏业务,后者偏组件能力展示。读代码时可以先看cartItemsstepConfigs的初始数据,再看两个更新方法,最后看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 更靠谱。

http://www.cnnetsun.cn/news/3522975.html

相关文章:

  • VLC for Android:打破格式限制,你的移动娱乐中心
  • 如何使用node-jsonc-parser实现JSON Schema验证与智能提示
  • rtsp-stream性能对比:与传统RTSP代理服务器的优劣分析
  • 鸿蒙原生开发手记:徒步迹 - 扫码功能:Scan Kit 集成
  • 把代码库探索交给 Claude Code Subagent,主会话才不会被文件读取拖垮
  • Spy Extension vs 普通扩展:核心差异与安全风险对比
  • 如何高效处理文本数据:Python字符串匹配实战指南
  • TI C2000 ERAD模块实战:硬件级调试与性能分析全解析
  • Docker 容器化部署的实用技巧:镜像优化、多阶段构建与日志
  • 文件读写全套易错笔记
  • Web Application Basics
  • SQL 数据库创建学生表+SQL 完整创建数据库代码(主文件+次文件+日志文件)
  • Ansible for Kubernetes多环境管理:从开发到生产的完整CI/CD流水线设计
  • 三类“核”底层差异拆解|全网独家复现传统滤波核、CNN可学习卷积核、SVM核函数、助力视觉预处理/特征提取/小样本分类精准涨点
  • HarmonyOS7 长按上下文菜单:用 bindContextMenu 做好快捷操作
  • Single主题社区贡献指南:如何参与开源项目开发
  • 自动设置 JAVA_HOME 和将 JDK 的 bin 目录添加到 PATH
  • Day5 哈希表part01
  • 3个关键功能让老旧Mac也能轻松制作Windows启动盘:WinDiskWriter深度解析
  • dynamic-struct版本演进:从v1.0到v1.4.0的重要变化指南
  • Jellium Desktop未来路线图:即将到来的5大令人兴奋的功能
  • 医疗机构 Hoxhunt 游戏化模拟钓鱼培训落地与人因风险治理研究
  • 有关线程池知识
  • 如何利用cpu_rec快速识别物联网设备固件中的CPU架构:10个实用技巧
  • 多维聚合实战:从GROUP BY到空间导航的数据分析跃迁
  • 90%开发者踩坑:MCP Resource注册≠模型能读取资源
  • 边缘推理引擎内建性能计数器设计:逐层延迟、内存峰值与功耗度量的采集和 Prometheus 导出方案
  • GimpPs终极指南:如何3步将GIMP界面秒变Photoshop专业风格
  • Windows Defender Remover终极指南:如何彻底移除系统自带杀毒软件
  • 3种方式解锁Windows终极权限:NSudo深度解析与实践指南