HarmonyOS7 SKU 选择器为什么总写崩?规格组合和库存联动这次讲清
文章目录前言什么是 SKU规格矩阵的核心算法选择联动半模态 SKU 选择器使用方式踩过的坑前言SKU 选择器应该是电商 App 里算法含量最高的组件之一了。规格排列组合、库存联动、无库存自动置灰——这几个需求放一块儿第一次写的时候我折腾了两天。今天把思路彻底捋清楚保证你看完能自己写出来。什么是 SKU先对齐概念。SPU 是标准化产品单元比如iPhone 16。SKU 是最小库存单元比如iPhone 16 / 黑色 / 256GB。一个 SPU 下面可能有几十种 SKU 组合每种 SKU 的价格和库存都不一样。数据结构我这样设计// 规格值interfaceSpecValue{id:stringname:string// 黑色、白色、XL}// 规格组interfaceSpecGroup{id:stringname:string// 颜色、尺码values:SpecValue[]}// SKU 条目interfaceSkuItem{id:stringspecIds:string[]// 对应每个规格组的选中值 id如 [color_black, size_xl]price:numberstock:numberimageUrl:string}// 完整的 SKU 数据interfaceSkuData{specGroups:SpecGroup[]skuList:SkuItem[]}关键是specIds这个数组它定义了每个 SKU 对应的规格组合。后面判断某个规格组合有没有库存就靠它。规格矩阵的核心算法这是整个 SKU 选择器最难的部分。用户在颜色里选了黑色我需要判断尺码里哪些选项还有货。比如黑色XL有货但黑色XXL没货那 XXL 就得置灰。核心思路对于每个未选的规格组遍历它的每个规格值检查当前已选规格 这个规格值能否组成一个有效 SKU库存0。// 判断某个规格组合是否可选isSpecValueAvailable(currentSelected:Mapstring,string,// groupId - valueIdgroupId:string,valueId:string):boolean{// 构建一个临时的选择状态把当前要判断的规格值也加进去consttestSelectednewMap(currentSelected)testSelected.set(groupId,valueId)// 遍历所有 SKU看有没有匹配的且有库存的returnthis.skuData.skuList.some(sku{if(sku.stock0)returnfalse// 检查这个 SKU 是否包含所有已选的规格值for(const[gid,vid]oftestSelected.entries()){constgroupthis.skuData.specGroups.find(gg.idgid)if(!group)continueconstidxthis.skuData.specGroups.indexOf(group)if(sku.specIds[idx]!vid)returnfalse}returntrue})}这段代码跑通了你会发现一个问题规格组很多的时候性能有点慢。实际上三到四个规格组、每组十来个规格值的情况完全够用因为总计算量也就几百次遍历。但如果你的商品规格特别多比如定制类商品可以考虑提前构建一个规格组合-SKU的映射表来加速。选择联动用户选完所有规格后要联动展示价格、库存和商品图。逻辑很简单——找到完全匹配的那个 SKU// 找到匹配的 SKUfindMatchedSku():SkuItem|null{constallSelectedthis.selectedMap.sizethis.skuData.specGroups.lengthif(!allSelected)returnnullreturnthis.skuData.skuList.find(sku{returnthis.skuData.specGroups.every((group,idx){returnsku.specIds[idx]this.selectedMap.get(group.id)})})??null}// 选中后联动onSpecSelect(groupId:string,valueId:string){// 切换选中if(this.selectedMap.get(groupId)valueId){this.selectedMap.delete(groupId)// 取消选择}else{this.selectedMap.set(groupId,valueId)}// 联动更新constmatchedthis.findMatchedSku()if(matched){this.currentPricematched.pricethis.currentStockmatched.stockthis.currentImagematched.imageUrl}}半模态 SKU 选择器UI 层面用bindSheet做半模态弹出体验跟淘宝、京东一样Componentstruct SkuSelector{PropskuData:SkuDataStateselectedMap:Mapstring,stringnewMap()StatecurrentImage:stringStatecurrentPrice:number0Statequantity:number1onConfirm?:(skuId:string,qty:number)voidbuild(){Column(){// 顶部商品信息区Row(){Image(this.currentImage||this.skuData.skuList[0]?.imageUrl).width(100).height(100).borderRadius(8)Column(){Text(¥${(this.currentPrice/100).toFixed(2)}).fontSize(20).fontColor(#FF4D4F).fontWeight(FontWeight.Bold)Text(库存:${this.currentStock}).fontSize(12).fontColor(#999).margin({top:4})Text(已选:${this.getSelectedText()}).fontSize(12).fontColor(#666).margin({top:4})}.alignItems(HorizontalAlign.Start).margin({left:12})}.width(100%).padding(16)Divider()// 规格选择区Scroll(){Column(){ForEach(this.skuData.specGroups,(group:SpecGroup){Column(){Text(group.name).fontSize(14).fontWeight(FontWeight.Medium).margin({bottom:8})Flex({wrap:FlexWrap.Wrap}){ForEach(group.values,(val:SpecValue){Text(val.name).fontSize(13).padding({left:14,right:14,top:6,bottom:6}).borderRadius(16).backgroundColor(this.getSpecBg(group.id,val.id)).fontColor(this.getSpecColor(group.id,val.id)).margin({right:8,bottom:8}).onClick(()this.onSpecSelect(group.id,val.id))})}}.width(100%).padding({left:16,right:16,top:12})})// 数量选择Row(){Text(数量).fontSize(14)Blank()QuantityStepper({value:this.quantity,max:this.currentStock})}.width(100%).padding(16)}}.layoutWeight(1)// 确认按钮Button(确定).width(92%).height(44).borderRadius(22).backgroundColor(#FF4D4F).fontColor(#FFFFFF).margin({bottom:20}).onClick(()this.handleConfirm())}.width(100%)}}规格按钮的样式根据选中状态和无库存状态变化这部分抽成方法读起来更清晰getSpecBg(groupId:string,valueId:string):ResourceColor{if(this.selectedMap.get(groupId)valueId)return#FFE8E8if(!this.isSpecValueAvailable(this.selectedMap,groupId,valueId))return#F5F5F5return#FFFFFF}getSpecColor(groupId:string,valueId:string):ResourceColor{if(this.selectedMap.get(groupId)valueId)return#FF4D4Fif(!this.isSpecValueAvailable(this.selectedMap,groupId,valueId))return#CCCCCCreturn#333333}使用方式商品详情页弹出 SKU 选择器用bindSheet一行搞定Image($r(app.media.btn_buy)).onClick((){this.showSkuSheettrue}).bindSheet($$this.showSkuSheet,SkuSelector({skuData:this.skuData,onConfirm:(skuId,qty){this.addToCart(skuId,qty)}}),{height:SheetSize.FIT_CONTENT,dragBar:true})踩过的坑取消选中导致已选规格无效的问题。用户在颜色里从黑色切换到白色这时候之前选的XL可能没库存了。切换规格后要重新校验其他规格组里的选中值如果失效就自动清空。只有一组规格的时候。别写死两层嵌套的逻辑用ForEach动态渲染规格组代码通用性好很多。SKU 选择器写好了是个很通用的组件稍微改改能用在各种需要多维规格选择的场景。下一篇我们进入订单确认页那个页面的难点在优惠券计算逻辑比 SKU 好搞多了。