写在前面
在电商平台,sku属性选择是产品模块中的一个常见问题。其实,解决这个问题并不难,关键是要理清自己的思路,将这个大问题拆分成几个小问题,再逐一击破就好了。写这篇文章一来是对前段时间的小程序sku属性选择做个总结,二来是希望放在网上能帮助到大家。掘金上的内容都系本人原创,如需转载请注明出处,感谢!
需求分析及解决思路
其实我个人更倾向于将这个问题拆分为如下三个小问题:
需求1:sku页面的渲染
在商品列表页,点击不同的产品,会根据不同的产品id请求产品详情接口,跳转到对应的产品详情页面。在产品详情页面,点击加入购物车按钮,会弹出产品的sku页面。
某个产品的产品详情请求结果如下所示:
{data: {id: 246, name: "Sling Cosmetic Bags dsfsad测试",…}}
data: {id: 246, name: "Sling Cosmetic Bags dsfsad测试",…}
color_image: "https://assets.forucdn.com/basics/DD-W/j5tB2xyie4maeMWKboYDuOseOgz2Jar8kIZQE1u1.jpeg"
description: "<p>* EVA Flexibility soles with cross performance design for sneaker shoes </p><p>* Mesh -knit fabric upper lining construction with EVA padded insoles</p><p>* Complete with 4 eyelets and a lace up closure for a classic look</p><p>* Perfect for every season, wear them all year round</p>"
id: 246
images: ["https://appfiles.forucdn.com/samples/1/0/Z8-1-20210428091333-X41yFkML.jpg",…]
lowest_price: "9.99"
merchant: {id: 1, code: "HCFW", name: "迪摩信息有限公司", address: "拉萨西峰区",…}
address: "拉萨西峰区"
code: "HCFW"
id: 1
name: "迪摩信息有限公司"
thumb: "https://appfiles.forucdn.com/avatars/5/20210426151158-T6UCoTjYJ6.jpeg"
name: "Sling Cosmetic Bags dsfsad测试"
size_image: "https://appfiles.forucdn.com/testing/admin/basics/Z8/OyrsoiMFpUNYeP8eWhZdHfCsQqPsKYDHGtYi2KYb.jpg"
status: "active"
type: "public"
variants: [{id: 3795, price: "9.99", color: "白色", gender: "通用", size: "通用-均码"},…]
0: {id: 3795, price: "9.99", color: "白色", gender: "通用", size: "通用-均码"}
1: {id: 3796, price: "66.66", color: "富贵色", gender: "男士", size: "男士-38"}
2: {id: 3797, price: "34.56", color: "黄色", gender: "通用", size: "通用-41"}
3: {id: 3798, price: "34.56", color: "黄色", gender: "通用", size: "通用-42"}
4: {id: 3799, price: "34.56", color: "绿色", gender: "通用", size: "通用-41"}
5: {id: 3800, price: "34.56", color: "绿色", gender: "通用", size: "通用-42"}
6: {id: 3801, price: "12.34", color: "测试色", gender: "通用", size: "通用-测试均码"}
7: {id: 3802, price: "100.00", color: "黑色", gender: "男士", size: "男士-37"}
8: {id: 3803, price: "31.23", color: "dsad", gender: "男士", size: "男士-323"}
9: {id: 3804, price: "99.99", color: "测试", gender: "通用", size: "通用-41"}
10: {id: 3805, price: "99.99", color: "测试", gender: "通用", size: "通用-42"}
11: {id: 3806, price: "99.99", color: "测试", gender: "通用", size: "通用-43"}
下图是对应产品的详情页面:
下图是根据产品详情接口的请求结果,渲染出的对应产品的sku页面:
解决思路:
-
技术选型: 原生微信小程序MINA框架 + Vant Weapp
-
组件化的开发思想:
1. 产品详情页分为两个组件: 产品详情组件和底部导航组件。其中,底部导航组件又分为底部导航工具组件以及产品sku组件;
2. 在底部导航组件中,为加入购物车按钮添加点击事件,使用有赞提供的Popup弹出层组件编写产品sku组件,并使用一个变量用于控制弹出层的显示与隐藏。同时可以在弹出层组件的关闭回调中做一些初始化操作,在后续的需求中会具体提到需要处理的初始化操作。
- 产品sku组件的渲染:
1. 根据产品详情接口的请求结果,使用万能的flex和有赞提供的步进器组件布局。
2.需要特别注意颜色和尺码分类下的按钮渲染
。观察详情接口返回的variants
数据不难发现,返回的颜色和尺寸数据有一部分是重合的。所以,不能直接对返回的数据进行循环渲染。需要对返回的数据做去重处理,否则显示在颜色和尺寸分类下的部分数据会存在数据重叠的情况,这显然是不合理的。 比如,variants
的第10到第12条数据,测试这个颜色就重复了两次。variants
的第4、6、11条数据,通用-42这个尺码就重复了两次。
<view class="attribute-warp">
颜色
</view>
<view class="flex-button-warp stepper-warp">
<van-button
wx:for="{{colors}}"
wx:key="id"
custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
bindtap="handleColorClicked"
data-color="{{item}}"
disabled = "{{!m1.hasTag(availableColorArray, item.color)}}"
>
{{item.color}}
</van-button>
</view>
//监听传入的product数据,如果详情接口请求成功,且能够拿到详情数据,则使用集合过滤重复的颜色分类属性
properties: {
product: {
type: Object,
observer: function (data) {
if(Object.keys(data).length > 0) {
this.onClose()
const colors = Array.from(new Set(this.data.product.variants.map(item => item.color))).map((color, index) => ({
color,
id: index
}))
this.setData({
colors
})
}
}
}
}
需求2:显示已选产品的属性
这个小需求其实由三部分组成,都是在点击时触发:
1. 显示已选产品的分类属性(点击时触发):
如果没有选择产品的分类属性,提示语为请选择产品属性;如果选择了产品的分类属性,则将选择的产品分类属性在产品sku组件中显示出来。
解决思路:
既然是分不同条件显示已选产品的分类属性,最容易想到的自然是MINA框架里面的wx:if
和wx:else
。于是可以设定判断条件,只要两个分类属性都没有被选中,就提示请选择产品属性,否则显示已选产品的分类属性。那么如何获取按钮里面的值呢?其实只需要在产品分类属性的点击事件中,通过自定义属性传入当前点击的对象,再获取里面的值显示到产品sku上就可以了。
<view class="message">
<text wx:if="{{ selectedColor === '' && selectedSize === ''}}">请选择产品属性</text>
<text wx:else>已选属性: {{selectedColor}} {{selectedSize}}</text>
</view>
2. 显示已选产品的价格属性(点击时触发):
如果没有选择产品的分类属性或者选择的分类属性只有颜色属性或者尺码属性,显示的产品价格为返回的产品详情接口中的lowest_price
字段;如果选中的分类属性包含了颜色属性和尺码属性,则会在返回的产品详情接口中的variants
对象数组中去查找对应分类属性。如果能够找到对应分类属性的价格,则返回相应的价格,否则会报价格字段不存在
的错误。因此需要做点击分类属性的属性关联,这个会在需求3中解决。
解决思路:
逻辑是这样的:监听传入的product
数据,只要接收到了这个数据或者弹出层被关闭的时候,就立刻初始化currentPrice
的值,并将product.lowest_price
的值赋给currentPrice
。在产品分类属性的点击事件中,判断产品颜色分类属性和尺寸分类属性是否都有被选中。如果没有,则显示返回的产品详情接口中的lowest_price
字段;如果都有被选中,则在variants
中去查找选中分类属性对应的价格,如果找不到就会报价格字段不存在
的错误。因此需要做点击分类属性的属性关联,这个会在需求3中解决。
<view class="product-price-warp">
<view>¥</view>
<view class="product-price">{{ currentPrice }}</view>
</view>
3. 切换不同分类属性的逻辑(点击时触发):
拿点击颜色分类属性举栗子。点击颜色分类属性之前,其实可以分为三种情况(最后有两种情况结果一样,可以归并为一类):
- 还没有任何颜色分类属性被选中,此时点击会直接激活选中颜色分类属性的样式;
- 有颜色分类属性被选中且选中的颜色分类属性和要点击的颜色分类属性一致,此时点击会取消激活的颜色分类属性样式;
- 有颜色分类属性被选中且选中的颜色分类属性和要点击的颜色分类属性不一致,此时点击会取消之前激活的颜色分类属性样式,并激活当前选中颜色分类属性的样式。
解决思路: 举选择颜色分类属性的栗子来说,可以使用一个变量判断当前颜色分类属性是否被选中,以及当前选中的颜色分类属性对应的id。这样就可以和当前点击的对象的id做比较,从而处理不同的判断逻辑。这也是为什么在需求1中重构详情接口返回的数据时,除了取分类属性名外,还要为它们分配id的原因。当然,在最后关闭弹出层的回调中,需要重置所选颜色分类属性的文字和id、所选尺码分类属性的文字和id。
handleColorClicked(e) {
//此处如果不使用if条件判断,按钮依然可以点击
if (!this.data.availableColorArray.includes(e.currentTarget.dataset.color.color)) return
//比较当前选中对象的id和当前点击对象的id
//1.如果selectedSizeId === -1,则表明当前没有颜色分类属性被选中。此时点击需要将当前点击对象的id和值传过去,传id是为了进行下一次比较,传值是为了显示已选产品的分类属性。
//2.如果选中的颜色分类属性id和当前点击对象的id不一致。此时点击除了会取消选中的颜色分类属性,还会激活当前选中颜色分类的样式(第一种情况和第二种情况是一样的)。
//3.如果选中颜色分类属性的id和当前点击对象的id一致。此时点击会取消选中的颜色分类属性,相当于清空选中的颜色分类属性。
if(this.data.selectedColorId.length === 0 || this.data.selectedColorId !== e.currentTarget.dataset.color.id) {
const availableSizeArray = this.data.product.variants.filter(item => item.color === e.currentTarget.dataset.color.color).map(item => item.size)
this.setData({
selectedColorId: e.currentTarget.dataset.color.id,
selectedColor: e.currentTarget.dataset.color.color,
availableSizeArray
})
} else if(this.data.selectedColorId === e.currentTarget.dataset.color.id) {
const availableSizeArray = this.data.sizes.map(item => item.size)
this.setData({
selectedColorId: -1,
selectedColor: '',
availableSizeArray
})
}
this.setSelectedPrice()
}
需求3:处理点击不同分类按钮的属性关联问题
点击产品的颜色分类属性,会筛选产品的尺码分类属性,可用的显示,禁用的灰显且不可点击;取消选择对应的颜色分类属性后,产品的尺码分类属性会全部恢复为可用状态。点击产品的尺寸分类属性也是同样一个道理,所以需要为点击不同的分类按钮做属性关联。
解决思路:
以点击颜色分类属性举例,聊一聊产品尺码分类属性的筛选原则。点击颜色分类属性后, 需要在产品的variants
数据中,找到包含当前颜色分类属性的variant
的size
。这一个或多个size就是点击颜色分类属性后当前可用的size
属性集,取反就是禁用的size
属性集。因为在需求2中已经粘贴了点击事件的代码逻辑,此处就不再粘贴。
这里有两处地方需要特别注意:1.使用van-button
循环遍历分类数据后,disabled
属性的禁用原则是先筛选出可用的属性集然后取反。如果先对当前点击的color
属性取反,再通过可用的color
属性集选取禁用的size
属性集,可能会造成禁用的size
属性集中包含当前点击的color
属性对应的可用的size
属性集;2.由于微信小程序不支持在函数中传参,因此在禁用条件中需要使用wxs
语言来判断,不能使用!availableColorArray.includes(item.color)
判断。
// wxs不支持es6语法
//使用两个逗号的原因是在于可能会有名字包含测试和测试色(一个颜色包含另外一个颜色的名称)这类情况
//可以对数组循环遍历,判断名字是否一样,如果一样,就返回true
function hasTag(tags, name) {
var testStr = ',' + tags.join(',') + ','
return testStr.indexOf(',' + name + ',') != -1
}
// 导出
module.exports = {
hasTag: hasTag
}
<view class="attribute-warp">
颜色
</view>
<view class="flex-button-warp stepper-warp">
<van-button
wx:for="{{colors}}"
wx:key="id"
custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
bindtap="handleColorClicked"
data-color="{{item}}"
disabled = "{{!m1.hasTag(availableColorArray, item.color)}}"
>
{{item.color}}
</van-button>
</view>
代码整合
utils\helper.wxs:
// 不支持es6语法
function hasTag(tags, name) {
var testStr = ',' + tags.join(',') + ','
return testStr.indexOf(',' + name + ',') != -1
}
// 导出
module.exports = {
hasTag: hasTag
}
components\common\popup\index.wxml:
<wxs
src="../../../utils/helper.wxs"
module="m1"
/>
<van-popup
closeable
show="{{ visible }}"
position="bottom"
custom-class="popup"
bind:close="onClose"
>
<view class="flex-warp">
<view class="product-picture">
<base-image
width="182rpx"
height="182rpx"
class="product-image"
src="{{product.images[0]}}"
/>
</view>
<view class="product-explain">
<view class="product-price-warp">
<view>¥</view>
<view class="product-price">{{ currentPrice }}</view>
</view>
<view class="message">
<text wx:if="{{ selectedColor === '' && selectedSize === ''}}">请选择产品属性</text>
<text wx:else>已选属性: {{selectedColor}} {{selectedSize}}</text>
</view>
</view>
</view>
<view class="attribute-warp">
颜色
</view>
<view class="flex-button-warp stepper-warp">
<van-button
wx:for="{{colors}}"
wx:key="id"
custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
bindtap="handleColorClicked"
data-color="{{item}}"
disabled = "{{!m1.hasTag(availableColorArray, item.color)}}"
>
{{item.color}}
</van-button>
</view>
<view class="attribute-warp">
尺码
</view>
<view class="flex-button-warp stepper-warp">
<van-button
wx:for="{{sizes}}"
wx:key="id"
custom-class="color {{selectedSizeId === item.id ? 'selected' : '' }}"
bindtap="handleSizeClicked"
data-size="{{item}}"
disabled = "{{!m1.hasTag(availableSizeArray, item.size)}}"
>
{{item.size}}
</van-button>
</view>
<view class="attribute-warp">
数量
</view>
<view class="stepper-warp">
<van-stepper
max="10000"
value="{{ value }}"
async-change
bind:change="onChange"
input-class="stepper-input"
plus-class="stepper-operation"
minus-class="stepper-operation"
/>
</view>
<view bindtap="handleConfirmed" class="confirm-button">确定</view>
</van-popup>
components\common\popup\index.wxss:
.popup {
background: #FFFFFF;
box-shadow: 0px -4px 8px 0px rgba(0, 0, 0, 0.06);
border-radius: 20px 20px 0px 0px;
}
.flex-warp {
margin: 41rpx 17rpx 51rpx 32rpx;
display: flex;
}
.product-expalin {
display: flex;
flex-direction: column;
margin-left: 17rpx;
}
.product-price-warp {
display: flex;
margin-top: 39rpx;
margin-bottom: 36rpx;
font-size: 34rpx;
font-weight: bold;
color: #FA5151;
}
.message {
font-size: 30rpx;
color: #7A7A7A;
}
.attribute-warp {
font-size: 30rpx;
color: #181818;
margin: 47rpx 0 20rpx 33rpx;
}
.confirm-button {
margin-top: 62rpx;
height: 80rpx;
color: #fff;
line-height: 80rpx;
text-align: center;
font-size:30rpx;
background: #FA5151;
}
.stepper-input {
width: 166rpx;
height: 80rpx;
font-size: 30rpx;
color: #181818;
}
.stepper-operation {
width: 87rpx;
height: 80rpx;
background: #F2F2F2;
border-radius: 10rpx;
}
.stepper-warp {
margin-left: 30rpx;
}
.flex-button-warp {
display: flex;
flex-wrap: wrap;
}
.color {
min-width: 200rpx;
height: 80rpx;
background: #F2F2F2;
border-radius: 40rpx;
font-size: 30rpx;
color: #181818;
text-align: center;
line-height: 80rpx;
margin: 20rpx 10rpx 0 0;
}
.product-explain {
margin-left: 17rpx;
}
.selected {
background: #FFFFFF;
border: 2rpx solid #181818;
}
components\common\popup\index.js:
Component({
/**
* 组件的属性列表
*/
properties: {
product: {
type: Object,
observer: function (data) {
if(Object.keys(data).length > 0) {
this.onClose()
const colors = Array.from(new Set(this.data.product.variants.map(item => item.color))).map((color, index) => ({
color,
id: index
}))
const sizes = Array.from(new Set(this.data.product.variants.map(item => item.size))).map((size, index) => ({
size,
id: index
}))
this.setData({
colors,
sizes
})
}
}
},
visible: {
type: Boolean
}
},
/**
* 组件的初始数据
*/
data: {
value: 1,
selectedColor: '',
selectedSize: '',
currentPrice: 100,
colors: {},
sizes: {},
selectedColorId: -1,
selectedSizeId: -1,
availableSizeArray: [],
availableColorArray: []
},
/**
* 组件的方法列表
*/
methods: {
handleConfirmed() {
this.setData({
value: 1
})
if(this.data.selectedColor !== '' && this.data.selectedSize !== '') {
//全部选中且校验成功时调用
this.onClose()
wx.showToast({
title: '加入购物车成功',
icon: 'success',
duration: 2000
})
} else {
//未全部选中时调用
wx.showToast({
title: '请选择商品属性',
icon: 'none',
duration: 2000
});
}
},
onChange(event) {
this.setData({
value: event.detail
})
},
onClose() {
const availableSizeArray = this.data.product.variants.map(item => item.size)
const availableColorArray = this.data.product.variants.map(item => item.color)
this.setData({
currentPrice: this.data.product.lowest_price,
visible: false,
value: 1,
selectedColor: '',
selectedSize: '',
selectedColorId: -1,
selectedSizeId: -1,
availableSizeArray,
availableColorArray
})
},
setSelectedPrice() {
let currentPrice
if(this.data.selectedColor !== '' && this.data.selectedSize !== '') {
currentPrice = this.data.product.variants.find(item => item.size === this.data.selectedSize && item.color === this.data.selectedColor).price
} else {
currentPrice = this.data.product.lowest_price
}
this.setData({
currentPrice
})
},
handleSizeClicked(e) {
//此处如果不使用if条件判断,按钮依然可以点击
if (!this.data.availableSizeArray.includes(e.currentTarget.dataset.size.size)) return
if(this.data.selectedSizeId === -1 || this.data.selectedSizeId !== e.currentTarget.dataset.size.id) {
const availableColorArray = this.data.product.variants.filter(item => item.size === e.currentTarget.dataset.size.size).map(item => item.color)
this.setData({
selectedSizeId: e.currentTarget.dataset.size.id,
selectedSize: e.currentTarget.dataset.size.size,
availableColorArray
})
} else if(this.data.selectedSizeId === e.currentTarget.dataset.size.id) {
const availableColorArray = this.data.colors.map(item => item.color)
this.setData({
selectedSizeId: -1,
selectedSize: '',
availableColorArray
})
}
this.setSelectedPrice()
},
//禁用存在一对多的问题,不能直接使用该禁用的,只能使用未禁用的,然后再对未禁用的进行取反
handleColorClicked(e) {
//此处如果不使用if条件判断,按钮依然可以点击
if (!this.data.availableColorArray.includes(e.currentTarget.dataset.color.color)) return
if(this.data.selectedColorId.length === 0 || this.data.selectedColorId !== e.currentTarget.dataset.color.id) {
const availableSizeArray = this.data.product.variants.filter(item => item.color === e.currentTarget.dataset.color.color).map(item => item.size)
this.setData({
selectedColorId: e.currentTarget.dataset.color.id,
selectedColor: e.currentTarget.dataset.color.color,
availableSizeArray
})
} else if(this.data.selectedColorId === e.currentTarget.dataset.color.id) {
const availableSizeArray = this.data.sizes.map(item => item.size)
this.setData({
selectedColorId: -1,
selectedColor: '',
availableSizeArray
})
}
this.setSelectedPrice()
}
}
})
components\common\popup\index.json:
{
"component": true,
"usingComponents": {}
}
写在最后
由于代码写得比较仓促,本文的代码逻辑还是比较复杂,而且存在大量的重复代码,有时间和兴趣的小伙伴可以使用组件化的思想重新编写代码。年少轻狂,总以为天下事,无可不为,岁月蹉跎,终感到天下人力有尽头。单纯无知,总认为目光内,皆为好人,时间流转,终叹息社会人笑里藏刀。社会很大,人心很复杂,一路走来,背最黑的铁锅,闹最大的笑话。塞翁失马,焉知非福,惋惜之余也庆幸自己遇到了这么好的妈,希望自己能保持本心,不被社会改变。
学习&总结
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!