淺談微信小程序之sku屬性選擇思路
發表時間:2021-5-11
發布人:葵宇科技
瀏覽次數:33
寫在前面
??在電商平臺,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": {}
}
復制代碼
寫在最后
??由于代碼寫得比較倉促,本文的代碼邏輯還是比較復雜,而且存在大量的重復代碼,有時間和興趣的小伙伴可以使用組件化的思想重新編寫代碼。年少輕狂,總以為天下事,無可不為,歲月蹉跎,終感到天下人力有盡頭。年輕無知,總認為目光內,皆為好人,時間流轉,終嘆息社會人笑里藏刀。社會很大,人心很復雜,一路走來,背最黑的鐵鍋,鬧最大的笑話。塞翁失馬,焉知非福,惋惜之余也慶幸自己遇到了這么好的媽,希望自己能保持本心,不被社會改變。