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 iphone6s
is 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 sku
algorithm 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
selected
Are 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 skuModal
of 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