Detailed explanation of sku algorithm and demo continued

Detailed explanation of sku algorithm and demo continued

Preface

Those who have done the front-end sales of e-commerce projects should have encountered the problem of calculating the inventory of products of different specifications. The industry term is sku(stock Keeping Unit)that the inventory unit corresponds to the specific specifications we sell, such as the specific model and specifications of a mobile phone, one of which iphone6s 4G is one sku. Here we distinguish spu(Standard Product Unit), standardized product units, such as a mobile phone model iphone6sis one spu.

sku algorithm

When displaying products on the front-end sku, we need to calculate different inventory levels and dynamically display them to users according to different user choices . Here is an skualgorithm derived .

data structure

Let's first take a look at the general data structure of storing inventory on the back-end server:

// 
const skuList = [
  {
    skuId: "0",
    skuGroup: [" ", " "],
    remainStock: 7,
    price: 2,
    picUrl: "https://dummyimage.com/100x100/ff00b4/ffffff&text= ",
  },
  {
    skuId: "1",
    skuGroup: [" ", " "],
    remainStock: 3,
    price: 4,
    picUrl: "https://dummyimage.com/100x100/ff00b4/ffffff&text= ",
  },
  {
    skuId: "2",
    skuGroup: [" ", " "],
    remainStock: 0,
    price: 0.01,
    picUrl: "https://dummyimage.com/100x100/0084ff/ffffff&text= ",
  },
  {
    skuId: "3",
    skuGroup: [" ", " "],
    remainStock: 1,
    price: 1,
    picUrl: "https://dummyimage.com/100x100/0084ff/ffffff&text= ",
  },
];

// 
const skuNameList = [
  {
    skuName: " ",
    skuValues: [" ", " "],
  },
  {
    skuName: " ",
    skuValues: [" ", " "],
  },
];
 

Algorithm demo

After the front-end user selects a single specification or multiple specifications, we need to dynamically calculate whether other buttons can be clicked at this time (combination has inventory), and the total inventory corresponding to the current state, the cover image and the price range.

Take the above data as an example

Nothing was selected at the beginning, the default picture is displayed, the picture corresponding to the first combination (['Red-Big']) in the specification list, the inventory is the total inventory of the product, and the price is the price range of the product. Then when the user selects a certain attribute or several attributes, the corresponding picture, inventory, and price range are calculated in real time.

At the same time, according to the currently selected attributes, the unselectable attributes are grayed out. In this example, the corresponding inventory of the product is 0, so when we choose one of them to be blue or large, we need to gray out the other attribute option.

Implementation ideas-the second algorithm

Ideas

skuList skuNameList skuNameList-skuValues skuNameList

  • First define the variable skuStock(inventory object), skuPartNameStock(used to cache non-full-name inventory, such as {'small': 4})

  • Take the selected attribute set under the specification list as the input parameter selected. If the relevant attribute is not selected in the current specification, an empty string is passed in, that is, at the beginningselected === ['', '']

  • Determine the currently selected attribute selectedAre there buffer stock, there is a direct return buffer stock

  • Judge whether all are selected currently, if all are selected, the inventory read from skuStock will be returned, and the inventory will be cached in time before then

  • Define the inventory variable remainStock, and the attribute array willSelected will be selected

  • Traverse inventory specifications to determine whether the current specification attribute has been selected, and if selected, the current attribute will be pushed into willSelected

  • If not selected, traverse the attribute array, combine the attribute array and the selected array selected, recursively obtain the current combined inventory, and accumulate the inventory

  • Finally, the accumulated inventory is returned as the corresponding inventory when the selected attribute is selected, and cached in the skuPartNameStock object in time

//sku 
const skuStock = skuList.forEach(sku => {
  this.skuStock[sku.skuGroup && sku.skuGroup.join("-")] = sku.remainStock;
});
// 
const skuPartNameStock = {};

/**
 *  
 * @param {Array} selected  
 * @return {Object} skuInfo
 *
 */
function getRemainByKey(selected) {
  const selectedJoin = selected.join("-");

  // 
  if (typeof skuPartNameStock[selectedJoin] !== "undefined") {
    return skuPartNameStock[selectedJoin];
  }

  // skuStock 
  if (selected.length === skuNameList.length) {
    skuPartNameStock[selectedJoin] = skuStock[selectedJoin]
      ? skuStock[selectedJoin]
      : 0;
    return skuPartNameStock[selectedJoin];
  }

  let remainStock = 0;
  const willSelected = [];

  for (let i = 0; i < skuNameList.length; i += 1) {
    // sku 
    const exist = skuNameList[i].skuValues.find(
      name => name === selected[0]
    );
    if (exist && selected.length > 0) {
      willSelected.push(selected.shift());
    } else {
      // sku sku
      for (let j = 0; j < skuNameList[i].skuValues.length; j += 1) {
        remainStock += this.getRemainByKey(
          willSelected.concat(skuNameList[i].skuValues[j], selected)
        );
      }
      break;
    }
  }
  // 
  skuPartNameStock[selectedJoin] = remainStock;
  return skuPartNameStock[selectedJoin];
}
 

demo

Using this algorithm wrote skuModalof vue demo, stick the next code, we can see the effect cited as a component to facilitate understanding

<template>
  <div v-if="visible" class="modal">
    <div class="content">
      <div class="title">
        {{ skuInfo.specName }}
        <span class="close" @click="close">
          <svg
            t="1590840102842"
            class="icon"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="1264"
            width="32"
            height="32"
          >
            <path
              d="M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z"
              p-id="1265"
              fill="#666666"
            ></path>
          </svg>
        </span>
      </div>
      <div class="info">
        <img :src="skuInfo.pic" class="pic"/>
        <div class="sku-info">
          <span class="price">
             {{
              skuInfo.minPrice === skuInfo.maxPrice
                ? skuInfo.minPrice
                : skuInfo.minPrice + "-" + skuInfo.maxPrice
            }}
          </span>
          <span class="selected">{{ skuInfo.selectedTip }}</span>
          <span class="stock"> {{ skuInfo.remainStock }} </span>
        </div>
      </div>

      <div v-for="(sku, index) in skuStatusGroup" :key="index" class="spec">
        <span class="name">{{ sku.name }}</span>
        <div class="group">
          <span
            v-for="(keyInfo, idx) in sku.list"
            :key="idx"
            class="spec-name"
            :class="{
              active: keyInfo.status === 1,
              disabled: keyInfo.status === -1
            }"
            @click="selectSku(index, idx)"
            >{{ keyInfo.key }}</span
          >
        </div>
      </div>
      <div class="footer">
        <button
          class="btn"
          :class="skuInfo.isSelectedAll ? 'active' : ''"
          type="button"
          @click="confirm"
        >
           
        </button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: Boolean
  },
  data() {
    return {
      skuInfo: {
        // sku 
        minPrice: 0,
        maxPrice: 0,
        pic: "",
        selected: [], // sku   ''  
        realSelectd: [],
        selectedTip: "",
        specName: "",
        stock: 0,
        isSelectedAll: false
      },
      skuStatusGroup: [], // sku 
      skuStock: {}, //sku   - 
      skuPartNameStock: {}, //sku ( )  
      skuList: [], // sku 
      skuInfoCache: {} // sku skuInfo
    };
  },
  methods: {
    initSku(data) {
      const { skuList, skuNameList } = data;

      // sku 
      this.clearOldSku();

      skuNameList.forEach(({ skuName, skuValues }) => {
        this.skuStatusGroup.push({
          name: skuName,
          list: skuValues.map(value => ({
            key: value,
            status: 0 //0   -1   1  
          }))
        });
      });

      this.skuNameList = skuNameList;

      // 
      this.skuInfo.specName = skuNameList.map(item => item.skuName).join(" | ");

      //sku  
      skuList.forEach(sku => {
        this.skuStock[sku.skuGroup && sku.skuGroup.join("-")] = sku.remainStock;
      });

      //sku 
      this.skuList = skuList || [];

      // sku 
      this.filterSkuKey();
    },

    // sku 
    clearOldSku() {
      this.skuStatusGroup = [];
      this.skuStock = {};
      this.skuPartNameStock = {};
      this.skuList = [];
      this.skuInfoCache = {};
    },

    close() {
      this.$emit("update:visible", false);
    },

    // skuInfo
    updateSkuInfo(selected) {
      const { skuStatusGroup } = this;
      const realSelectd = selected.filter(item => item);

      const priceInfo = this.getskuInfoByKey(selected);
      const stock = this.getRemainByKey(realSelectd);
      const isSelectedAll = realSelectd.length === selected.length;
      const selectedTip = isSelectedAll
        ? `  ${realSelectd.join(" ")}`
        : `  ${selected
            .map((item, idx) => {
              if (!item) {
                return skuStatusGroup[idx].name;
              }
              return null;
            })
            .filter(item => item)
            .join(" ")}`;

      this.skuInfo = Object.assign({}, this.skuInfo, priceInfo, {
        selected,
        stock,
        realSelectd,
        isSelectedAll,
        selectedTip
      });
    },

    // sku sku 
    filterSkuKey() {
      const { skuStatusGroup } = this;
      const selected = [];

      // sku 
      skuStatusGroup.forEach(sku => {
        let pos = 0;
        const isInSelected = sku.list.some((skuInfo, idx) => {
          pos = idx;
          return skuInfo.status === 1;
        });

        selected.push(isInSelected ? sku.list[pos].key : "");
      });

      // skuInfo
      this.updateSkuInfo(selected);

      // sku 
      skuStatusGroup.forEach((sku, skuIdx) => {
        const curSelected = selected.slice();

        // 
        sku.list.forEach(skuInfo => {
          if (skuInfo.status === 1) {
            return;
          }

          // sku 
          const cacheKey = curSelected[skuIdx];
          curSelected[skuIdx] = skuInfo.key;
          const stock = this.getRemainByKey(curSelected.filter(item => item));
          curSelected[skuIdx] = cacheKey;

          // sku 
          if (stock <= 0) {
            //eslint-disable-next-line no-param-reassign
            skuInfo.status = -1;
          } else {
            //eslint-disable-next-line no-param-reassign
            skuInfo.status = 0;
          }
        });
      });
    },

    //sku   sku
    selectSku(listIdx, keyIdx) {
      const { list } = this.skuStatusGroup[listIdx];
      const { status } = list[keyIdx];

      //status -1   0   1  
      if (status === -1) {
        return;
      }

      // sku 
      list.forEach((keyInfo, idx) => {
        if (keyInfo.status !== -1) {
          if (idx === keyIdx) {
            //eslint-disable-next-line no-param-reassign
            keyInfo.status = 1 - status;
          } else {
            //eslint-disable-next-line no-param-reassign
            keyInfo.status = 0;
          }
        }
      });

      // sku
      this.filterSkuKey();
    },

    /**
     *  sku 
     * @param {Array} selected  sku 
     */
    getskuInfoByKey(selected = []) {
      const { skuList } = this;
      const cacheInfo = this.skuInfoCache[
        selected.filter(item => item).join("-")
      ];

      // 
      if (cacheInfo) {
        return cacheInfo;
      }

      const info = {
        minPrice: -1,
        maxPrice: -1,
        pic: ""
      };

      skuList.forEach(sku => {
        const group = sku.skuGroup;

        //  key => key  
        const isInclude = selected.every(
          (name, index) => name === "" || name === group[index]
        );

        if (isInclude) {
          const { minPrice, maxPrice } = info;
          //  -1
          info.minPrice =
            minPrice === -1 ? sku.price : Math.min(minPrice, sku.price);
          info.maxPrice =
            maxPrice === -1 ? sku.price : Math.max(maxPrice, sku.price);
          info.pic = sku.picUrl;
        }
      });

      // sku 
      if (selected[0] === "") info.pic = skuList[0].picUrl;

      this.skuInfoCache[selected.filter(item => item).join("-")] = info;

      return info;
    },

    /**
     * sku   sku 
     * @param {Array} selected  sku 
     */
    getRemainByKey(selected = []) {
      const { skuStock, skuPartNameStock, skuNameList } = this;
      const selectedJoin = selected.join("-");

      // 
      if (typeof skuPartNameStock[selectedJoin] !== "undefined") {
        return skuPartNameStock[selectedJoin];
      }

      // sku   
      if (selected.length === skuNameList.length) {
        skuPartNameStock[selectedJoin] = skuStock[selectedJoin]
          ? skuStock[selectedJoin]
          : 0;
        return skuPartNameStock[selectedJoin];
      }

      let remainStock = 0;
      const willSelected = [];

      for (let i = 0; i < skuNameList.length; i += 1) {
        // sku 
        const exist = skuNameList[i].skuValues.find(
          _item => _item === selected[0]
        );
        if (exist && selected.length > 0) {
          willSelected.push(selected.shift());
        } else {
          // sku sku
          for (let j = 0; j < skuNameList[i].skuValues.length; j += 1) {
            remainStock += this.getRemainByKey(
              willSelected.concat(skuNameList[i].skuValues[j], selected)
            );
          }
          break;
        }
      }
      // 
      skuPartNameStock[selectedJoin] = remainStock;
      return skuPartNameStock[selectedJoin];
    },

    // 
    confirm() {
      const { skuList } = this;

      if (skuList.length > 1 && !this.skuInfo.isSelectedAll) {
        return;
      }

      const { skuId } = this.skuList.filter(item => {
        if (item.skuGroup.join("-") === this.skuInfo.realSelectd.join("-")) {
          return true;
        }
        return false;
      })[0];

      this.$emit("confirm", skuId);
    }
  }
};
</script>

<style lang="less" scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;

  &:before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: rgba(0, 0, 0, 0.2);
  }

  .content {
    position: absolute;
    top: 50%;
    left: 50%;
    max-height: 900px;
    padding: 0 20px 20px;
    overflow: auto;
    background: #fff;
    border-radius: 12px;
    transform: translate(-50%, -50%);
    z-index: 1;

    .title {
      display: flex;
      justify-content: space-between;
      color: #666;
      font-size: 32px;
      line-height: 60px;
      text-align: left;
      border-bottom: 1px solid #eee;

      .close {
        display: flex;
        align-items: center;
      }
    }

    .info {
      display: flex;
      margin-top: 10px;

      .pic {
        width: 180px;
        height: 180px;
        border-radius: 4px;
      }

      .sku-info {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        margin-left: 30px;
        color: #999;
        font-size: 26px;

        span {
          margin-bottom: 20px;
        }

        .price {
          color: #333;
        }
      }
    }

    .spec {
      display: flex;
      padding: 20px;

      .name {
        color: #999;
        font-size: 24px;
        line-height: 54px;
      }

      .group {
        margin-left: 20px;

        .spec-name {
          display: inline-block;
          height: 54px;
          margin: 0 30px 10px 0;
          padding: 0 40px;
          line-height: 54px;
          color: #333;
          font-size: 28px;
          background: rgba(245, 245, 245, 1);
          border-radius: 28px;
          border: 1px solid rgba(204, 204, 204, 1);

          &.active {
            color: #ff981a;
            background: #ffeeeb;
            border: 1px solid #ff981a;
          }

          &.disabled {
            color: #cccccc;
            background: #f5f5f5;
            border: 1px solid transparent;
          }
        }
      }
    }

    .btn {
      width: 690px;
      height: 80px;
      color: rgba(255, 255, 255, 1);
      font-size: 32px;
      background: rgba(204, 204, 204, 1);
      border-radius: 44px;
      outline: none;

      &.active {
        color: #fff;
        background: #ff981a;
      }
    }
  }
}
</style>
 

How to use

<!--   -->
<skuModal ref="sku" :visible.sync="visible" @confirm="confirm"></skuModal>
 
// sku
this.$refs.sku.initSku({
  skuNameList, // 
  skuList // 
});
 

summary

Those who have done e-commerce projects should have dealt with or heard of sku. Learning related concepts and a true understanding of how to calculate sku can help us become more familiar with the business and improve our ability to handle related businesses. In the future, the interviewer's questions will be more stable. The first sku algorithm can refer to the previous blog.

reference


Welcome to the front-end learning group to study together~ 516913974