HarmonyOS7餐厅点餐页面实战:分类切换与购物车汇总

HarmonyOS7餐厅点餐页面实战:分类切换与购物车汇总
文章目录前言菜品数据同时服务列表和购物车分类切换只是换视图不改菜单数据购物车汇总全靠派生计算数量加减要守住下限底部购物车栏由 totalCount 控制显示真实点餐页还差哪些东西完整代码最后总结前言点餐页的状态流很典型用户切分类列表要变用户加减菜品底部购物车要变购物车为空时底部栏还要消失。这个案例没有做复杂动画也没有接接口反而方便看清楚 ArkUI 页面里“主数据 派生数据 交互更新”的关系。当前实现是一个简化的餐厅点餐页。它支持分类切换、菜品数量加减、总价和总件数汇总。需要注意的是showCart虽然会在点击购物车图标时切换但源码里没有根据它渲染购物车明细弹层去结算按钮也没有提交订单逻辑。菜品数据同时服务列表和购物车每个菜品用MenuItem_n29l表示。这个模型里最关键的字段不是名字或价格而是count。interfaceMenuItem_n29l{id:numbername:stringprice:numbercategory:stringemoji:stringdesc:stringcount:number}category决定菜品出现在哪个分类下count决定它是否进入购物车。也就是说页面没有单独维护一个购物车数组而是把“购物车状态”压回到每个菜品自己的count字段里。对这种小型点餐页来说这样写很直观。分类切换只是换视图不改菜单数据页面顶部的分类保存在categories当前选中的分类保存在selectedCat。Statecategories:string[][推荐,招牌,主食,小食,饮品,甜品]StateselectedCat:string推荐点击某个分类时只是修改selectedCat.onClick(()this.selectedCatcat)真正展示在列表里的数据来自filteredgettergetfiltered():MenuItem_n29l[]{returnthis.menuItems.filter(mm.categorythis.selectedCat)}这个设计挺干净。menuItems仍然保存完整菜单分类切换只是改变观察角度不会把数组删来删去。用户在“推荐”里加了红烧肉再切到“饮品”红烧肉的count仍然留在原数据里底部总价也不会丢。购物车汇总全靠派生计算源码里有三个和购物车有关的 gettergetcartItems():MenuItem_n29l[]{returnthis.menuItems.filter(mm.count0)}gettotalPrice():number{returnthis.menuItems.reduce((s,m)sm.price*m.count,0)}gettotalCount():number{returnthis.menuItems.reduce((s,m)sm.count,0)}cartItems当前没有被 UI 使用但它已经表达了购物车明细该怎么取只要count 0就算入购物车。底部栏用的是totalPrice和totalCount所以任何菜品数量变化都会立刻反映到总价和角标数量上。这里的好处是没有重复状态。页面不需要同时维护menuItems、cartItems、totalPrice三份数据只要维护菜品数量其他结果实时算出来就行。小数据量下这种写法够用如果菜单有几百上千项再考虑缓存或拆分计算也不迟。数量加减要守住下限changeCount()是整页唯一真正修改菜品数量的函数。它接收菜品id和变化量delta然后用map()更新目标菜品。changeCount(id:number,delta:number){this.menuItemsthis.menuItems.map(m{if(m.idid){m.countMath.max(0,m.countdelta)}returnm})}Math.max(0, m.count delta)防止数量减成负数。列表里也做了一个 UI 约束只有item.count 0时才显示减号和数量默认只露出加号。if(item.count0){Text(−).onClick(()this.changeCount(item.id,-1))Text(${item.count})}Text().onClick(()this.changeCount(item.id,1))这段逻辑适合教学但从工程习惯上看还可以更稳一点。当前map()里直接修改了m.count再返回原对象。更彻底的不可变写法是命中目标时返回{ ...m, count: Math.max(0, m.count delta) }。页面现在能正常表达状态刷新但复杂项目里我会避免原地改对象。底部购物车栏由totalCount控制显示底部栏外面包了一层条件渲染if(this.totalCount0){Row(){Text(${this.totalCount})Text(¥${this.totalPrice})Button(去结算)}}这个细节很符合真实点餐体验没选菜时不显示购物车栏选了菜以后才出现。购物车角标显示总件数总价用所有菜品的price * count累加得到。不过源码里点击购物车图标只是执行this.showCart !this.showCart没有任何地方根据showCart显示明细。也就是说当前页面底部栏只是一个入口雏形。如果要补完整可以用Stack再叠一个购物车面板数据直接使用已经写好的cartItemsgetter。真实点餐页还差哪些东西如果把这个案例继续做成可上线的点餐功能我会先处理三件事购物车明细、规格选择、结算校验。很多菜品不只是数量还有规格、口味、加料这时count放在菜品上就不够了需要引入真正的购物车项模型。结算前也要重新校验价格、库存和配送状态不能完全相信前端当前显示的总价。这个示例的价值在于把最小闭环写清楚了分类只控制列表视图数量才是购物车状态底部汇总全部从菜单数据推导出来。理解这条线再去加规格弹窗或订单页代码不容易散。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。import{router}fromkit.ArkUIinterfaceMenuItem_n29l{id:numbername:stringprice:numbercategory:stringemoji:stringdesc:stringcount:number}EntryComponentstruct RestaurantOrderingPage{Statecategories:string[][推荐,招牌,主食,小食,饮品,甜品]StateselectedCat:string推荐StatemenuItems:MenuItem_n29l[][{id:1,name:红烧肉套餐,price:38,category:推荐,emoji:,desc:秘制红烧肉配米饭肥而不腻,count:0},{id:2,name:夫妻肺片,price:26,category:推荐,emoji:,desc:传统川味麻辣鲜香,count:0},{id:3,name:招牌炸鸡,price:32,category:招牌,emoji:,desc:外酥内嫩秘制腌料,count:0},{id:4,name:番茄鸡蛋面,price:18,category:主食,emoji:,desc:手工拉面汤头浓郁,count:0},{id:5,name:炸薯条,price:12,category:小食,emoji:,desc:现炸现卖外脆里软,count:0},{id:6,name:柠檬水,price:10,category:饮品,emoji:,desc:新鲜柠檬微甜爽口,count:0},{id:7,name:芒果冰淇淋,price:16,category:甜品,emoji:,desc:进口芒果浓郁香甜,count:0},{id:8,name:特色宫保鸡丁,price:34,category:招牌,emoji:,desc:经典川菜花生鸡丁,count:0},]StateshowCart:booleanfalsegetfiltered():MenuItem_n29l[]{returnthis.menuItems.filter(mm.categorythis.selectedCat)}getcartItems():MenuItem_n29l[]{returnthis.menuItems.filter(mm.count0)}gettotalPrice():number{returnthis.menuItems.reduce((s,m)sm.price*m.count,0)}gettotalCount():number{returnthis.menuItems.reduce((s,m)sm.count,0)}changeCount(id:number,delta:number){this.menuItemsthis.menuItems.map(m{if(m.idid){m.countMath.max(0,m.countdelta)}returnm})}build(){Stack({alignContent:Alignment.Bottom}){Column(){Row(){Image($r(app.media.ic_back)).width(24).height(24).onClick(()router.back())Column(){Text(饭点小厨).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#333)Text( 预计配送 30 分钟).fontSize(11).fontColor(#888)}.layoutWeight(1).alignItems(HorizontalAlign.Center)Text().fontSize(20)}.width(100%).padding({left:16,right:16,top:12,bottom:12}).backgroundColor(#FFF)// Category tabsScroll(){Row(){ForEach(this.categories,(cat:string){Text(cat).fontSize(13).fontColor(this.selectedCatcat?#FF6000:#666).borderWidth({bottom:this.selectedCatcat?2:0}).borderColor(#FF6000).padding({left:16,right:16,bottom:8}).onClick(()this.selectedCatcat)})}.padding({left:8})}.scrollable(ScrollDirection.Horizontal).backgroundColor(#FFF).margin({bottom:8})// Menu listList({space:1}){ForEach(this.filtered,(item:MenuItem_n29l){ListItem(){Row(){Text(item.emoji).fontSize(50).width(70).textAlign(TextAlign.Center)Column(){Text(item.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(#333)Text(item.desc).fontSize(12).fontColor(#888).margin({top:4}).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})Row(){Text(\¥\).fontSize(12).fontColor(#FF6000)Text(\\${item.price}\).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FF6000)}.margin({top:6})}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({left:8})// Qty controlRow(){if(item.count0){Text(−).fontSize(18).width(28).height(28).borderRadius(14).backgroundColor(#FF6000).fontColor(#FFF).textAlign(TextAlign.Center).lineHeight(28).onClick(()this.changeCount(item.id,-1))Text(\\${item.count}\).fontSize(14).fontWeight(FontWeight.Bold).width(28).textAlign(TextAlign.Center)}Text().fontSize(18).width(28).height(28).borderRadius(14).backgroundColor(#FF6000).fontColor(#FFF).textAlign(TextAlign.Center).lineHeight(28).onClick(()this.changeCount(item.id,1))}.alignItems(VerticalAlign.Center).padding({right:4})}.width(100%).backgroundColor(#FFF).padding({top:12,bottom:12}).border({width:{bottom:1},color:#F5F5F5})}})}.layoutWeight(1)}.width(100%).height(100%).backgroundColor(#F8F8F8)// Cart barif(this.totalCount0){Row(){Row(){Text(\\${this.totalCount}\).fontSize(13).fontColor(#FFF).width(22).height(22).borderRadius(11).backgroundColor(#FF4757).textAlign(TextAlign.Center).lineHeight(22)Text().fontSize(24).margin({left:8})}.onClick(()this.showCart!this.showCart)Text(\¥\${this.totalPrice}\).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFF).layoutWeight(1).margin({left:12})Button(去结算).backgroundColor(#FFF).fontColor(#FF6000).borderRadius(24).padding({left:20,right:20,top:8,bottom:8})}.width(calc(100% - 32vp)).backgroundColor(#FF6000).borderRadius(32).padding({left:16,right:8,top:8,bottom:8}).margin({bottom:20}).shadow({radius:12,color:#FF600040,offsetY:4})}}}}最后总结餐厅点餐页面 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。