前端SKU算法的實現

本文將提供一種前端SKU算法的實現思路,這可能並非最佳的實現方式,但能夠爲沒有思路的小夥伴提供一種解決方案。javascript

對於SPU與SKU概念還不瞭解的小夥伴,請先移步認識SKU與SPU瞭解一下大體概念,本文便再也不詳述。html

實現效果預覽

實現思路

對於前端來講,若是沒真的作過SKU,可能並不會瞭解到它的複雜之處,仔細觀察,小小的規格選擇,卻包含着不少實現細節難點,這裏提供一種實現思路供你們參考。前端

1. 獲取後端數據

這裏以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
  }
}
複製代碼

2.提取規格數據

後端給咱們的數據中只有單品---不一樣規格組合好的商品列表,咱們須要從單品列表中獲取到全部的規格。這裏咱們能夠從數據中看到每一個單品的specs屬性就是這個單品所包含的不一樣規格,咱們須要將其提取出來。react

currentSkuList = []

_getCurrentSkuList() {
  this.currentSkuList = this.data.map(item => item.specs);
}
複製代碼

提取結果以下git

3.矩陣轉置

咱們提取到的數據格式爲github

顏色     圖案     尺碼
金屬灰   七龍珠   小號 S
青芒色   灌籃高手 中號 M
青芒色   聖鬥士   大號 L
橘黃色   七龍珠   小號 S
複製代碼

但根據效果圖,咱們須要以下這種格式的數據,所以咱們就須要將上面這個矩陣轉置,它可能不是一個標準的矩陣,由於每種規格的數目不必定相同,這裏相同能夠更好的理解。算法

enter description here

_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循環,若是使用的reactvue來作循環,使用數組也是很方便的,若是直接使用上面的對象格式,還要再作一次轉換數組,因此爲了方便直接使用,咱們再作一遍轉換。

上面使用了JSON.parse(JSON.stringify(obj))的方式對轉置後的矩陣對象作了一個深拷貝,經過Object.keys()Array.mapAPI很方便就能夠將以前的對象轉換爲數組

轉換結果

enter description here

Tips: 注意這裏不是二維數組,而是以前咱們設計的數據結構的對象數組,使用這種數據結構能夠是咱們後續輕易獲取咱們想要的數據。

4.最重要的一步:遍歷

這一步的標題實在不知道該叫什麼,但這是咱們整個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: {...},
    ...
}
複製代碼

設計好數據結構以後,想辦法經過後端數據給出的單品列表,肯定每個規格項能夠匹配的其餘規格,最終將數據轉換爲咱們設計出來的數據結構。

5.書寫簡單的業務邏輯

上一步可謂是整個解決方案中最核心也是最難的地方了,只要獲取了咱們想要的數據結構,接下來就能夠經過簡單的業務邏輯來實現最後的效果。

頁面的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

  1. data-key_id: 標識當前cell的規格id,惟一肯定一種規格
  2. data-value_id: 標識當前cell的具體規格id,惟一表示一種規格
  3. data-select: 經過wxs實現一個函數,傳入selected與disabled,來肯定當前cell是可選仍是已選仍是禁用狀態,
  4. 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屬性的數據。

  1. 若是是disabled的狀態,直接返回,由於不可點。
  2. 若是是selectable狀態,當前cell能夠點擊,點擊以後要對其餘種類單元格作個遍歷,將不匹配的置灰。並將當前選中的數據放入selected中保存起來方便後續使用,這裏要強調的一點是在選擇的時候要經過以前設置的x變量,遍歷當前種類的規格,若是以前有選過,則刪除它,由於一種規格只能選一個值。
  3. 若是是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;
      })
    })
  }
複製代碼

6.完善細節:聯動上面的提示文字

咱們看到實現的效果,在咱們點擊的時候,上方提示文字會有請選擇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後端到全棧課程的學習。

相關文章
相關標籤/搜索