本文將提供一種前端SKU算法的實現思路,這可能並非最佳的實現方式,但能夠爲沒有思路的小夥伴提供一種解決方案。javascript
對於SPU與SKU概念還不瞭解的小夥伴,請先移步認識SKU與SPU瞭解一下大體概念,本文便再也不詳述。html
對於前端來講,若是沒真的作過SKU,可能並不會瞭解到它的複雜之處,仔細觀察,小小的規格選擇,卻包含着不少實現細節難點,這裏提供一種實現思路供你們參考。前端
這裏以7七月老師提供的商品數據爲例,因爲數據過長,這裏只貼出sku_list
中的一條數據,這條數據格式也是整個實現過程當中最重要的數據格式。vue
{
...
sku_list: [
{
"id":2,
"price":77.76,
"discount_price":null,
"online":true,
"img":"",
"title":"金屬灰·七龍珠",
"spu_id":2,
"category_id":17,
"root_category_id":3,
"specs":[
{
"key_id":1,
"key":"顏色",
"value_id":45,
"value":"金屬灰"
},
{
"key_id":3,
"key":"圖案",
"value_id":9,
"value":"七龍珠"
},
{
"key_id":4,
"key":"尺碼",
"value_id":14,
"value":"小號 S"
}
],
"code":"2$1-45#3-9#4-14",
"stock":5
},
...
],
...
}
複製代碼
新建一個類Spu模擬從後端獲取數據java
class Spu {
data = []
constructor() {
this.data = this.getData();
}
getData() {
const data = sku_list; //這裏的sku_list就是七月老師提供的數據中對應字段的數據,咱們目前只須要這些就能夠了
return data
}
}
複製代碼
後端給咱們的數據中只有單品---不一樣規格組合好的商品列表,咱們須要從單品列表中獲取到全部的規格。這裏咱們能夠從數據中看到每一個單品的specs
屬性就是這個單品所包含的不一樣規格,咱們須要將其提取出來。react
currentSkuList = []
_getCurrentSkuList() {
this.currentSkuList = this.data.map(item => item.specs);
}
複製代碼
提取結果以下git
咱們提取到的數據格式爲github
顏色 圖案 尺碼
金屬灰 七龍珠 小號 S
青芒色 灌籃高手 中號 M
青芒色 聖鬥士 大號 L
橘黃色 七龍珠 小號 S
複製代碼
但根據效果圖,咱們須要以下這種格式的數據,所以咱們就須要將上面這個矩陣
轉置,它可能不是一個標準的矩陣,由於每種規格的數目不必定相同,這裏相同能夠更好的理解。算法
_transMatrix() {
this._getCurrentSkuList();
let transResult = {};
this.currentSkuList.forEach(specs => {
specs.forEach(item => {
if(!transResult[item['key_id']]) {
transResult[item['key_id']] = {
key_id: item['key_id'],
key: item['key'],
value_list: {
[item['value_id']]: {
value_id: item['value_id'],
value: item['value'],
selected: false,
disabled: false
}
}
}
} else if (!transResult[item['key_id']].value_list[item['value_id']]) {
transResult[item['key_id']].value_list[item['value_id']] = {
value_id: item['value_id'],
value: item['value'],
selected: false,
disabled: false
}
}
})
})
return transResult;
}
複製代碼
轉換結果後端
這裏利用了JavaScript對象的靈活性,設置了一種數據結構
{
key_id: { //規格id
key: string, // 規格名稱
key_id: number, // 規格id
[value_list: object]: { // 規格值列表 object
value_id: { // 規格值id
value_id: number, // 規格值id
value: string, // 規格值
selected: boolean, // 是否選中
disabled: boolean, // 是否不可選
}
}
},
key_id: {...},
...
}
複製代碼
經過對currentSkuList
當前單品列表的遍歷和篩選,將其提取爲上述數據格式的數據,這裏使用對象而不是數組的方式是爲了更方便的進行提取
將提取結果變爲數組格式
getAllSpecsList() {
const transResult = this._transMatrix();
this.allSpecsList = Object.keys(transResult).map(key => {
let obj = JSON.parse(JSON.stringify(transResult[key]));
obj.value_list = Object.keys(obj.value_list).map(vk => obj.value_list[vk]);
return obj;
})
return this.allSpecsList;
}
複製代碼
將轉置的矩陣變爲數組,方便咱們在頁面中使用wx:for
循環,若是使用的react
或vue
來作循環,使用數組也是很方便的,若是直接使用上面的對象格式,還要再作一次轉換數組,因此爲了方便直接使用,咱們再作一遍轉換。
上面使用了JSON.parse(JSON.stringify(obj))
的方式對轉置後的矩陣對象作了一個深拷貝,經過Object.keys()
與Array.map
API很方便就能夠將以前的對象轉換爲數組
轉換結果
Tips:
注意這裏不是二維數組,而是以前咱們設計的數據結構的對象數組,使用這種數據結構能夠是咱們後續輕易獲取咱們想要的數據。
這一步的標題實在不知道該叫什麼,但這是咱們整個SKU算法實現的最重要的一步。
選擇邏輯的設計 對於選擇一個規格,其餘規格中只有對應可選的部分能選擇,其他部分不可選,直接的視覺效果就是置灰。可是如何進行聯動,咱們選擇一個規格後,後面可能還有不少中規格,咱們案例中選擇一種以後還有兩種,但事實上可能並不止如此,剩餘的可能有七八中規格,每種規格甚至可能會有10多種值,每次選取的多是這些規格中的任意一個,若是暴力循環全部的可能性,效果可能會不好。
這裏提供一種思路,就是根據後端給的SKU列表來肯定一種規格對應其餘規格中可選的部分,描述不是很清楚,下面舉個例子來講明: 知道了顏色規格爲青芒色
,那麼其餘規格中圖案只有兩種能夠選:灌籃高手
和聖鬥士
,尺碼的規格只有中號 M
和大號 L
可選。
那麼咱們就能夠肯定這樣一張表:
那麼是否是能夠這樣來想,咱們每次點擊選中一種規格以後,就去遍歷其餘規格,在遍歷其餘規格的時候,若是這種規格中的規格值不在咱們的列表中,那麼就給他置灰不可選(disabled),這樣咱們須要關心的事情就很簡單了,再來捋一捋
咱們每次點擊一個規格值以後,咱們不用關心這種規格了,好比選了青芒色,就不用關心其餘顏色了,只用關心其餘種類的規格,其餘種類規格里面也不須要所有都關心,咱們只須要關心這種規格中有哪一個能夠跟我當前點的這個匹配。
舉例: 如上表中,肯定了顏色是灰色,就去遍歷其餘種類的規格(圖案
和尺碼
),遍歷圖案的時候,只用知道七龍珠
跟我匹配,其他不匹配的所有置灰,遍歷尺碼的時候,只要知道小號 S
跟我匹配,其餘一概置灰。
再選圖案,就只能選七龍珠,選了七龍珠以後,再去遍歷其餘種類的規格,顏色中只有青芒色
跟金屬灰
能夠跟我匹配,其餘所有置灰。
再選尺碼,只能選S了,根據上述規則,再去置灰其餘兩種規格中不匹配的。
這樣剩下的就是可選的了。
將上述思路轉換爲代碼:
getSelectable() {
if(this.allSpecsList.length === 0) {
this.allSpecsList()
}
if(this.currentSkuList.length === 0) {
this.currentSkuList = this.data.map(item => item.specs);
}
const rowLength = this.allSpecsList.length;
for(let row = 0; row < rowLength; row++) {
let { key_id, key } = this.allSpecsList[row];
let columnList = this.allSpecsList[row].value_list;
this.selectable[key_id] = {
key_id,
key,
selectableList: {}
}
for (let column = 0; column < columnList.length; column++) {
let { value_id, value } = columnList[column];
this.selectable[key_id].selectableList[value_id] = {
value_id,
value,
matchItems: null
}
}
this.currentSkuList.forEach(specificSpecs => {
let matchItems = {};
let currentVlaueId = '';
specificSpecs.forEach(specsItem => {
if(specsItem.key_id !== key_id) {
matchItems[specsItem.key_id] = [specsItem]
} else {
currentVlaueId = specsItem.value_id;
}
})
if (!this.selectable[key_id].selectableList[currentVlaueId].matchItems) {
this.selectable[key_id].selectableList[currentVlaueId].matchItems = matchItems;
} else {
Object.keys(this.selectable[key_id].selectableList[currentVlaueId].matchItems).forEach(k => {
this.selectable[key_id].selectableList[currentVlaueId].matchItems[k].push(...matchItems[k])
})
}
})
}
return this.selectable;
}
複製代碼
轉換結果
這裏又是藉助JavaScript對象的靈活性設計了一種數據結構來表達咱們上述的思路
{
key_id: { // key_id 肯定當前選了哪一種類型的規格
key: string, // 規格名
key_id: number, // 規格id
selectableList: { // 選擇列表,用來描述咱們上面畫的表
value_id: { // 當前選擇了哪一個規格,上面key_id肯定了規格種類,這裏value_id肯定了這種規格中的具體規格,
matchItems: { // 當前規格對應其餘規格的匹配項
key_id: [ // 當前規格匹配的種類規格的種類id,
{
key_id: numebr, // 種類id,這裏爲了方便數組中的直接取了後端返回的數據
key: string, // 規格種類名稱
value_id: number, // 匹配項中的規格id
value: string, //匹配項中的規格名稱
},
...
]
}
}
}
},
key_id: {...},
...
}
複製代碼
設計好數據結構以後,想辦法經過後端數據給出的單品列表,肯定每個規格項能夠匹配的其餘規格,最終將數據轉換爲咱們設計出來的數據結構。
上一步可謂是整個解決方案中最核心也是最難的地方了,只要獲取了咱們想要的數據結構,接下來就能夠經過簡單的業務邏輯來實現最後的效果。
頁面的data配置
data: {
sku: null,
allSpecsList: [],
selectable: null,
selected: [],
selectedItem: null,
selectTips: '請選擇:',
}
複製代碼
初始化數據
initData() {
const sku = new Sku();
const allSpecsList = sku.getAllSpecsList();
const selectable = sku.getSelectable();
this.setData({
allSpecsList,
selectable,
sku
});
},
複製代碼
初始化數據時候,咱們建立Sku實例對象,將其保存在data中,再拿到allSpecsList
(轉置後的矩陣數組),來循環實現頁面的規格展現。 拿到selectable
方便後續處理點擊某個規格時候對其餘規格置灰的處理。
實現頁面展現
<wxs src="../../wxs/specs.wxs" module="s"></wxs>
<view class="container">
<view class="spu-info">
<image class="selected-img" src="{{selectedItem && selectedItem.img ? selectedItem.img : 'http://i1.sleeve.7yue.pro/assets/5605cd6c-f869-46db-afe6-755b61a0122a.png'}}"></image>
<view class="spu-container">
<text class="spu-title">雙色可選</text>
<view class="spu-content">
<view class="price">$1000</view>
<view class="tips">
{{selectTips}}
</view>
</view>
</view>
</view>
<view class="select-options">
<block wx:for="{{allSpecsList}}"
wx:for-item="specs"
wx:for-index="x"
wx:key="specs.key_id"
>
<view class="specs-item">
<text class="specs-title">{{specs.key}}</text>
<view class="specs-value-list">
<block wx:for="{{specs.value_list}}"
wx:for-item="value"
wx:for-index="y"
wx:key="{{value.value_id}}"
>
<view class="value-container {{s.getButtonExtraClass(value.selected, value.disabled)}}"
data-key_id="{{specs.key_id}}"
data-value_id="{{value.value_id}}"
data-select="{{s.getButtonStatus(value.selected, value.disabled)}}"
data-x="{{x}}"
data-y="{{y}}"
bindtap="handleClickSpecs"
>
<text class="value">{{value.value}}</text>
</view>
</block>
</view>
</view>
</block>
</view>
</view>
複製代碼
頁面的實現主要就是咱們定義的數據結構的循環遍歷等操做,將數據展現過來。其實頁面沒什麼好講的,這裏要說的主要是幾個自定義data屬性的設計:這裏咱們將一個按鈕成爲一個cell
data-key_id
: 標識當前cell的規格id,惟一肯定一種規格data-value_id
: 標識當前cell的具體規格id,惟一表示一種規格data-select
: 經過wxs實現一個函數,傳入selected與disabled,來肯定當前cell是可選仍是已選仍是禁用狀態,data-x
: 用更簡單的方式肯定當前點擊的cell是哪一種類型的,只能肯定相同x的是同一種規格,不能知道具體是哪一種 5: data-y
: 簡單肯定當前選中的cell是某種規格的一種,肯定位置,x與y的設計能夠更方便查找cell位置再說明一下cell樣式的肯定也是經過wxs中的一個函數,傳入seleted與disabled肯定當前cell的樣式。
點擊某中規格時候的處理
handleClickSpecs(event) {
const { key_id, value_id, select, x, y } = event.currentTarget.dataset
if (select === 'disabled') {
return;
}
if (select === 'selectable') {
this.data.selected.forEach((item, index) => {
if (item.x === x) {
this.data.selected.splice(index, 1)
}
})
this.data.selected.push({x, y, key_id, value_id});
this.handleSelectOneOption(x, y, key_id, value_id);
}
if (select === 'active') {
this.clearAllSelectedAndDisabled();
this.data.selected.forEach((item, index) => {
if (item.x === x && item.y === y) {
this.data.selected.splice(index, 1);
}
})
this.data.selected.forEach(item => {
this.handleSelectOneOption(item.x, item.y, item.key_id, item.value_id);
})
}
this.setData({
allSpecsList: this.data.allSpecsList,
selected: this.data.selected
})
}
複製代碼
在點擊具體的規格時候,咱們會面臨三種狀態,默認的狀態是可選的狀態selectable
,因此咱們只用額外增長兩種狀態active
已選狀態和disabled
禁用狀態。經過event.currentTarget獲取到點擊的這個cell,而後經過其dataset
屬性獲取到咱們以前設計的一些輔助更簡單獲取當前cell屬性的數據。
disabled
的狀態,直接返回,由於不可點。selectable
狀態,當前cell能夠點擊,點擊以後要對其餘種類單元格作個遍歷,將不匹配的置灰。並將當前選中的數據放入selected
中保存起來方便後續使用,這裏要強調的一點是在選擇的時候要經過以前設置的x變量,遍歷當前種類的規格,若是以前有選過,則刪除它,由於一種規格只能選一個值。active
狀態,點擊則須要將其置爲可選狀態,這時候若是想要將某些置灰屬性恢復可選,是不太好操做的,咱們這裏選擇一種簡單的方式,就是將當前點擊的元素從selected
中刪除,而後遍歷selected
中的元素,將其從新「點擊」一遍(固然這裏使用代碼來點擊)。這樣就實現了基本的功能其中handleSelectOneOption
的代碼以下,它實現了點擊一個cell,去置灰其餘種類的規格
handleSelectOneOption(x, y, key_id, value_id) {
this.data.allSpecsList[x].value_list[y].selected = true;
this.data.allSpecsList[x].value_list.forEach((specs, index) => {
if (index === y) {
specs.selected = true;
} else {
specs.selected = false;
}
})
const selectableMatchItems = this.data.selectable[key_id].selectableList[value_id].matchItems;
this.data.allSpecsList.forEach((specsRow, index) => {
if (index === x) {
return;
}
specsRow.value_list.forEach(specs => {
specs.disabled = false;
if (specs.selected) {
return;
}
const result = selectableMatchItems[specsRow.key_id].find(item => item.value_id === specs.value_id);
if (!result) {
specs.disabled = true;
}
})
})
}
複製代碼
經過x選項咱們能夠知道咱們不須要遍歷當前x這一行(也就是這一種屬性),若是不是當前種類的屬性,就拉過來遍歷,若是不跟當前規格匹配,那就置灰。 有個細節要注意一下,在置灰過程當中首先恢復了這種屬性的置灰狀態,這是爲了防止以前置灰的效果影響當前置灰的結果。
在點擊active
狀態的cell時候,咱們也有一個細節操做,就是先將全部的置灰與選中所有恢復默認,再從新操做,也是爲了防止以前狀態影響咱們如今的結果。
清空全部置灰與選中的代碼
clearAllSelectedAndDisabled() {
this.data.allSpecsList.forEach(row => {
row.value_list.forEach(specs => {
specs.selected = false;
specs.disabled = false;
})
})
}
複製代碼
咱們看到實現的效果,在咱們點擊的時候,上方提示文字會有請選擇xxx,當選完以後,會有已選xxx的文字效果,下面將代碼展現出來。
getSelectedInfo() {
let selectTips = '';
if(this.data.selected.length === this.data.allSpecsList.length) {
let selectedText = []
this.data.allSpecsList.forEach(rowSpecs => {
rowSpecs.value_list.forEach(specs => {
if(specs.selected) {
selectedText.push(specs.value)
}
})
})
selectTips = '已選:' + selectedText.join(',');
const selectedItem = this.data.sku.getSelectable(selectedText.join('·'));
this.setData({
selectTips,
selectedItem
})
return
}
const selectedRow = this.data.selected.map(item => item.x);
const unSelected = []
this.data.allSpecsList.forEach((item, index) => {
if(!selectedRow.includes(index)) {
unSelected.push(item.key)
}
})
selectTips = '請選擇:' + unSelected.join(',');
this.setData({
selectTips
})
}
複製代碼
主要思路就是查看selected已選列表中的數量跟規格列表的種類數量是否是相同,若是相同那就所有選中了,而後經過這些選中的規格能夠肯定一個SKU,咱們就能拿到這個SKU的庫存,價格,圖像等信息,這裏咱們把這個SKU存起來了,添加庫顯示等能夠直接在頁面添加相關數據。
若是已選屬性數量跟規格種類數量不匹配,那就找有哪一個規格的數據沒選,將其提示在上方。
當前SKU算法的實現可能不是最好的,但本身感受不是很難理解,最好的效果是看思路而後親自動手實現一次,有可能看着簡單,可是動手時候會有不少意想不到的困難。
最後說明一下這篇文章主要是對慕課網學習中7七月
老師佈置做業的獨立完成和思考,全部思路與代碼也都是本身一成天思考和動手完成的,若是有小夥伴想一塊兒加入成長,能夠慕課來報7七月
老師的從java後端到全棧課程的學習。