在工作中遇到的计算sku的需求, 记录下来思路和核心代码以防不时之需。
使用的技术有 react框架 和 lodash库
sku.gif
确认后端返回的数据
"itemSkus": {
"listData": [
{
"id": 598,
"pictureUrl": "", //
"sellPrice": 11.00, // 售价
"totalInventory": 12, // 库存
"attributesIdArr": [200, 445] // 属性组合
},
{
// ...
"sellPrice": 13, // 售价
"attributesIdArr": [200, 447] // 属性组合
},
{
// ...
"sellPrice": 31, // 售价
"attributesIdArr": [302, 445] // 属性组合
},
{
// ...
"sellPrice": 33, // 售价
"attributesIdArr": [302, 447] // 属性组合
},
{
// ...
"sellPrice": 20, // 售价
"attributesIdArr": [201, 445] // 属性组合
},
{
// ...
"sellPrice": 22, // 售价
"attributesIdArr": [201, 446] // 属性组合
},
]
},
"attrs": {
"listData": [
{
"id": 23,
"name": "套餐",
"attrValues": {
"listData": [
{
"id": 200,
"name": "一年签证"
},
{
"id": 201,
"name": "两年签证"
},
{
"id": 302,
"name": "三年签证"
}
]
}
},
{
"id": 67,
"name": "时效",
"attrValues": {
"listData": [
{
"id": 445,
"name": "一年"
},
{
"id": 446,
"name": "两年"
},
{
"id": 447,
"name": "三年"
},
{
"id": 448,
"name": "四年"
}
]
}
}
// ...
]
},
现在我们有三组属性和六种组合
<-- 商品规格 -->
套餐 : 一年签(200) 两年签(201) 三年签(302)
时效 : 一年 (445) 两年(446) 三年(447) 四年(448)
服务 : 普通(500) 专享(501)
<-- sku -->
[200, 445] - 一年签 / 一年
[200, 447] - 一年签 / 三年
[302, 445] - 三年签 / 一年
[302, 447] - 三年签 / 三年
[201, 445] - 两年签 / 一年
[201, 446] - 两年签 / 两年
接下来我们将从以下三步进行分析, 实现上图的效果
1. 转换商品规格的数据
这一步我们做两件事情
-
过滤没用的属性
从sku的组合中可知, "时效"的第四个属性(四年) 和 "服务"整条属性并未被用到, 所以最开始需要先过滤一遍商品规格
(如果你的业务中每个属性都被用到的话, 那么这一步不是必须的)
【实现思路】-
循环 sku 的组合, 将每个组合合并到 allAttributesIdArr 数组中, 即使数据重复也么关系 image.png
循环商品属性, 判断每一个规格的 id 是否存在 allAttributesIdArr 的数组中, 如果没在的话则删除这个规格的子属性, 若每个子属性都没被用到则删除整条规格
-
循环 sku 的组合, 将每个组合合并到 allAttributesIdArr 数组中, 即使数据重复也么关系
-
给每个属性初始状态
我们需要有一个status字段控制标签的 选中, 未选中 和 无效状态
初始化的时候每个标签都是未选中状态
实现中用的
cloneDeep 和 pullAt 方法来自于 lodash;
isInArray 则是自己封装的工具类, 用于判断某个值是否存在数组中
// 显示状态 status 的常量
const STATUS_UNSELECTED = 0; // 未选中状态
const STATUS_SELECTED = 1; // 选中状态
const STATUS_DISABLED = 2; // 无效状态
// 转换商品规格的数据
convertAttrs = (record) => {
const { attrs, itemSkus, itemCategoryId } = record;
// itemSkus 中用到的所有属性的 id 数组
let allAttributesIdArr = [];
for (let elem of Array.values(itemSkus.listData)) {
allAttributesIdArr = attributesIdArr.concat(attributesIdArr);
}
// 遍历 attrs 添加默认显示状态(未选中状态)同时删除没用的属性或整条属性
let _attrs = cloneDeep(attrs);
let deleteAttrIndex = [];
for (let [attrIndex, elem] of Array.entries(attrs.listData)) {
let deleteIndex = [];
const _attrsElem = _attrs.listData[attrIndex].attrValues.listData;
const attrsElemLen = _attrsElem.length;
for (let [index, attrElem] of Array.entries(elem.attrValues.listData)) {
// 添加初始状态
_attrsElem[index].status = STATUS_UNSELECTED;
if (!isInArray(allAttributesIdArr, attrElem.id)) deleteIndex.push(index);
}
// 如果删除数据的长度等于这条属性的长度 那么删除整条属性 反之则删除某些属性
attrsElemLen === deleteIndex.length
? deleteAttrIndex.push(attrIndex)
: pullAt(_attrsElem, deleteIndex);
}
pullAt(_attrs.listData, deleteAttrIndex);
return { attrs: _attrs, itemSkus };
};
2. 初始化数据
这一步我们同样需要做两件事情
-
计算出用户可能选择的所有组合(在数学上也称为: 幂集)
【实现思路】
-
获取单个组合可能产生的所有组合
例如 [200, 445] 这个组合可能产生 [ [200], [445], [200, 445] ] 这三种组合
具体算法参考这段代码
image.png
上面代码的计算过程是这样
attributesIdArrItem powerSet
[[]] // 循环前的值
200 [[], [200]] // 将 200 和 powerSet 的元素合并, 并将得到的结果push到powerSet中得到新的powerSet = [[], [200]]
445 [[], [200], [445], [200, 445]] //将 400 和上一次的 powerSet 的每个元素合并, 将得到的结果push到powerSet中得到新的powerSet = [[], [200], [445], [200, 445]]
-
获得这个组合的产生的所有可能组合后将他转换成json格式, 并且将这个组合对应的数据放入完整的选择中, 方便后续的取值image.png
-
循环 sku 计算出全部的可能image.png
-
未选中时页面显示的默认数据, 如价格区间、库存、图片等 (本文只计算价格区间)
循环 sku 获得所有价格的数组, 从中取出最大最小值就是我们想要的价格区间了
实现中用的
min 和 max 方法来自于 lodash;
// 获取组合的幂集 && 计算价格区间
getSkuInit = (record) => {
const { attrs, itemSkus } = this.convertAttrs(record);
let skuAvailableSet = {}; // 所有可能的属性组合
let sellPriceArr = []; // 保存所有的价格
let attrsLen = attrs.listData.length;
// 获取组合的幂集
for (let skusItem of Array.values(itemSkus.listData)) {
sellPriceArr.push(skusItem.sellPrice);
// 储存幂集
let powerSet = [[]];
for (let attributesIdArrItem of Array.values(skusItem.attributesIdArr)) {
let len = powerSet.length;
for (let i = 0; i < len; i++) {
powerSet.push(powerSet[i].concat(attributesIdArrItem));
}
}
for (let powerSetItem of Array.values(powerSet)) {
const tmpSet = powerSetItem.join(',');
// skuAvailableSet存成 [可能组合的id字符串]: [商品属性], 后续取值的时候方便
if (attrsLen === powerSetItem.length) {
skuAvailableSet[tmpSet] = skusItem;
} else if (tmpSet) {
skuAvailableSet[tmpSet] = {};
}
}
}
// 计算价格区间
const minsellPrice = min(sellPriceArr);
const maxsellPrice = max(sellPriceArr);
this.sellPriceRange =
minsellPrice === maxsellPrice ? minsellPrice : `${minsellPrice}-${maxsellPrice}`;
return { skuAvailableSet, sellPrice: this.sellPriceRange, attrs, itemSkus };
};
3. 点击属性
这一步我们需要判断用户点击时, 其他属性的显示状态(不可选还是未选中) 以及价格(或者图片, 库存等) 的变化
【实现思路】
!!!! 不行了 写不动了, 明年回来再写吧 !!!!
/*
attrValue: 点击的属性
attrIndex: 被点击的属性组的坐标(纵坐标)
attrValuesIndex: 被点击的属性组下value的坐标(横坐标)
*/
doSkuSet = (attrValue, attrIndex, attrValuesIndex) => {
let { record, selectedValIdArr, skuAvailableSet, sellPrice } = this.state;
let { attrs = { listData: [] }, ...other } = record;
// 设置当前属性的显示状态
const status = attrValue.status;
switch (status) {
case STATUS_UNSELECTED:
selectedValIdArr[attrIndex] = attrValue.id;
attrs.listData[attrIndex].attrValues.listData[attrValuesIndex].status = STATUS_SELECTED;
break;
case STATUS_SELECTED:
selectedValIdArr[attrIndex] = '';
attrs.listData[attrIndex].attrValues.listData[attrValuesIndex].status = STATUS_UNSELECTED;
break;
case STATUS_DISABLED:
return;
default:
break;
}
// 设置标签的状态 & 商品的价格
const selectedValIdTrueArr = compact(selectedValIdArr);
if (selectedValIdTrueArr.length) {
// 设置标签显示状态
for (let [attrsIndex, attrsItem] of Array.entries(attrs.listData)) {
for (let attrValueElem of Array.values(attrsItem.attrValues.listData)) {
const attrValueId = attrValueElem.id;
// 当前属性是否在已选中的 selectedValIdArr 中, 如果是的话不用重新设置状态
if (attrValueId === selectedValIdArr[attrsIndex]) continue;
// 构造当前属性与其他attr已选中的属性的组合
let tmpSet = cloneDeep(selectedValIdArr);
tmpSet[attrsIndex] = attrValueId;
// 判断tmpSet的组合是否可选
if (skuAvailableSet[compact(tmpSet).join(',')]) {
attrValueElem.status = STATUS_UNSELECTED;
} else {
attrValueElem.status = STATUS_DISABLED;
}
}
}
// 设置显示价格
const selecteSellPrice = skuAvailableSet[selectedValIdTrueArr.join(',')].sellPrice;
sellPrice = selecteSellPrice ? selecteSellPrice : this.sellPriceRange;
} else {
// 全不选的时候将所有属性设置为初始值
for (let attrsItem of Array.values(attrs.listData)) {
for (let attrValueElem of Array.values(attrsItem.attrValues.listData)) {
attrValueElem.status = STATUS_UNSELECTED;
}
}
sellPrice = this.sellPriceRange;
}
this.setState({ selectedValIdArr, sellPrice, record: { attrs, ...other } });
this.forceUpdate();
};