本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。
前言
上一篇我们讲到了 sku 选择组件利用矩阵算法解决动态变化选项的需求,依旧遗留了一个大问题:
就是当规格大于 2 个的时候,你会发现这个算法存在 bug
举个例子
"skuList": [
{
"showPrice": "6.00",//优惠展示价格
"favType": 1,// 优惠类型
"skuId": "1234",
"originalSalePrice": "12.01", // 商品原价
//"originalSaleCurrency": "", // 商品原价货币
"salePrice": "11.01", // 商品售卖价
"discount": "-40%", // 商品折扣文案
"saleCurrency": "USD", // 商品售卖价货币
"skuMinVolume": 1, // SKU 商品最小起售数量
"skuIncreaseVolume": 1, // SKU 商品最小增加数量
"maxBuyVolume": 99, // 最大购买数量
"skuStatus": 1, // SKU状态 0:已下架;1:在售
"skuImg":"https://s4.forcloudcdn.com/item/images/dmc/7cdb5ab0-88b5-45a4-a5af-d683ccbd9336-750x1000.jpeg_750x0c.jpeg",//sku图片
"skuAttrList": [ // SKU属性信息
{
"attrNameId": "1", // 属性名ID
"attrName": "颜色",//属性名
"attrValue": "红"//属性值
},
{
"attrNameId": "2", // 属性名ID
"attrName": "码数",//属性名
"attrValue": "XL"//属性值
},
{
"attrNameId": "3", // 属性名ID
"attrName": "套餐",//属性名
"attrValue": "套餐2"//属性值
}
],
}, {
"showPrice": "6.00",//优惠展示价格
"favType": 1,// 优惠类型
"skuId": "2345",
"originalSalePrice": "12.01", // 商品原价
//"originalSaleCurrency": "", // 商品原价货币符号 废弃
"salePrice": "11.01", // 商品售卖价
"saleCurrency": "USD", // 商品售卖价货币符号
"discount": "-40%", // 商品折扣文案
"skuMinVolume": 1, // SKU 商品最小起售数量
"skuIncreaseVolume": 1, // SKU 商品最小增加数量
"maxBuyVolume": 99, // 最大购买数量
"skuStatus": 1, // SKU状态
"skuImg":"https://s4.forcloudcdn.com/item/images/dmc/4a085065-0f92-4406-a55e-ae4150b91def-779x974.jpeg_750x0c.jpeg",//sku图片
"skuAttrList": [ // SKU属性信息
{
"attrNameId": "1", // 属性名ID
"attrName": "颜色",//属性名
"attrValue": "红"//属性值
},
{
"attrNameId": "2", // 属性名ID
"attrName": "码数",//属性名
"attrValue": "XXL"//属性值
},
{
"attrNameId": "3", // 属性名ID
"attrName": "套餐",//属性名
"attrValue": "套餐1"//属性值
}
],
}, {
"skuId": "3456",
"originalSalePrice": "12.01", // 商品原价
//"originalSaleCurrency": "", // 商品原价货币符号
"salePrice": "11.01", // 商品售卖价
"saleCurrency": "USD", // 商品售卖价货币符号
"discount": "-40%", // 商品折扣文案
"skuMinVolume": 1, // SKU 商品最小起售数量
"skuIncreaseVolume": 1, // SKU 商品最小增加数量
"maxBuyVolume": 99, // 最大购买数量
"skuStatus": 1, // SKU状态
"skuImg":"https://s4.forcloudcdn.com/item/images/dmc/4a085065-0f92-4406-a55e-ae4150b91def-779x974.jpeg_750x0c.jpeg",//sku图片
"skuAttrList": [ // SKU属性信息
{
"attrNameId": "1", // 属性名ID
"attrName": "颜色",//属性名
"attrValue": "红"//属性值
},
{
"attrNameId": "2", // 属性名ID
"attrName": "码数",//属性名
"attrValue": "XXL"//属性值
},
{
"attrNameId": "3", // 属性名ID
"attrName": "套餐",//属性名
"attrValue": "套餐2"//属性值
}
],
},
{
"skuId": "4567",
"originalSalePrice": "12.01", // 商品原价
//"originalSaleCurrency": "", // 商品原价货币符号
"salePrice": "11.01", // 商品售卖价
"saleCurrency": "USD", // 商品售卖价货币符号
"discount": "-40%", // 商品折扣文案
"skuMinVolume": 1, // SKU 商品最小起售数量
"skuIncreaseVolume": 1, // SKU 商品最小增加数量
"maxBuyVolume": 99, // 最大购买数量
"skuStatus": 1, // SKU状态
"skuImg":"https://s4.forcloudcdn.com/item/images/dmc/4a085065-0f92-4406-a55e-ae4150b91def-779x974.jpeg_750x0c.jpeg",//sku图片
"skuAttrList": [ // SKU属性信息
{
"attrNameId": "1", // 属性名ID
"attrName": "颜色",//属性名
"attrValue": "白"//属性值
},
{
"attrNameId": "2", // 属性名ID
"attrName": "码数",//属性名
"attrValue": "XL"//属性值
},
{
"attrNameId": "3", // 属性名ID
"attrName": "套餐",//属性名
"attrValue": "套餐1"//属性值
}
],
}
],
"skuAttrSortedList":[ // sku属性展示顺序
{
"attrName": "颜色",
"attrNameId": "1",
"attrValues": ["红", "白"]
},{
"attrName": "码数",
"attrNameId": "2",
"attrValues": ["XL", "XXL"]
},
{
"attrName": "套餐",
"attrNameId": "3",
"attrValues": ["套餐1", "套餐2"]
}
]
当 skulist 和 skuAttrSortedList 为上面数组时,我们尝试填充数组,会有以下矩阵:
当我们选择红色、XL后,交集求出来,6 个属性值居然都是剩余可选的,但是我们看 skulist,并没有 红色、XL、套餐 1 这个组合,这就存在问题了,我们选出了一个不存在的 sku,这是为什么呢?
因为我们填充 1 的依据只是单单两个点之间是否连通,但实际上 A 和 B 连通、B 和 C 连通 并不代表 A 和 C 也是连通的,那问题来了,我们怎么保证 A 和 B 和 C 是在同个 sku 里连通的呢?
这时候我们需要使用加权标记!
我们需要定义不连通则填充 0,同级连通填充 1,sku 连通填充 2、3、4···同个 sku 下连通填充同个数字。
我们来修改一下原来填充 1 的方法 setAdjoinVertexs:
setAdjoinVertexs(side, sides, weight) {
let pIndex = '';
for (let i = 0; i < this.vertex.length; i += 1) {
if (side === this.vertex[i]) {
pIndex = i;
break;
}
}
sides.forEach((item) => {
let index = '';
for (let i = 0; i < this.vertex.length; i += 1) {
if (item === this.vertex[i]) {
index = i;
break;
}
}
// this.adjoinArray[pIndex * this.len + index] = 1;
const cur = this.adjoinArray[pIndex * this.len + index];
if (typeof cur !== 'number') { // specList.length > 3时,存在单边多权的情况
this.adjoinArray[pIndex * this.len + index].push(weight);
} else if (cur > 1) {
this.adjoinArray[pIndex * this.len + index] = [cur, weight];
} else {
this.adjoinArray[pIndex * this.len + index] = weight;
}
});
}
这个方法变成了未填充的填充权值,已经填充了的则是变成一个数组存放每次连通填充的权值。
然后修改下初始化的几个方法:
initSpec() {
this.specCombinationList.forEach((item, index) => this.fillInSpec(item.skuAttrValueList, index + 2));
},
initSameLevel() {
this.product.skuAttrSortedList.forEach((item) => {
const params = [];
// 获取同级别顶点
item.attrValues.forEach((val) => {
if (this.optionSpecs.includes(val)) params.push(val);
});
// 同级点位创建
this.fillInSpec(params, 1);
});
},
fillInSpec(params, weight) {
params.forEach((param) => {
this.setAdjoinVertexs(param, params, weight);
});
}
initSpec 方法则变成每个 sku 点与点连通都填充 sku 在 skulist 组合列表对应索引 +2,意思是第一个 sku 权值 2,第二个 sku 权值 3;initSameLevel 则是写死权值 1。
那最终初始化后的矩阵是:
关键是我们要如何求所有列的并集和选择列的交集呢?
现在因为采用权值叠加的方式,那原来格子求和的方法就不能用了。
我们先看求并集:
求并集比较简单,也是看每一列里是否有不为 0 的,只要这一列中有一个不为 0 的,则这一列对应的属性在初始时都是可选的。
getUnion(params) {
const paramsVertex = params.map((id) => this.getVertexCol(id));
const union = [];
paramsVertex.forEach((col, index) => {
if (col.some((item) => item !== 0)) union.push(params[index]);
});
return union;
}
所以一开始所有属性都是可选的。
接下来我们看如何选交集:
我们先选中红色这一列,可以看到每一行的数组或者数字都是没有 0 的,则所有属性都是剩余可选的。
既然都可选,那我们再选中 XL 这列的时候,要知道剩余可选有哪些的话我们可以聚焦到每一行,比如我们看套餐 1 这行:
看出来,红色和套餐 1 只在第 2 个 sku 里组合了,因为他们两点间权值为 3,而 XL 和套餐 1 则是在第 4 个 sku 里组合了,所以可见红色、XL、套餐 1是不可能在一个 sku 组合里的,所以不存在红色、XL、套餐 1这个 sku,选中红色、XL 后,套餐 1 不是剩余可选的。
我们再看套餐 2 这行,红色和套餐 2 在第 1 个 sku 和第 3 个 sku 里组合了,XL 和套餐 2 在第 1 个 sku 里组合了,所以求交集得出红色、XL、套餐 2是在第 1 个 sku 里组合了,选中红色、XL 后,套餐 2是可选的。
所以我们看这个属性是否可选,需要知道这一行的已选属性列间是否存在同个权值。
我们修改原来求交集的方法 getIntersection:
getIntersection(params) {
const paramsVertex = params.map((id) => this.getVertexCol(id));
const intersection = [];
this.vertex.forEach((type, index) => {
const row = paramsVertex.map((col) => col[index]).filter((t) => t !== 1);
if (this.isItemEqual(row)) {
intersection.push(type);
}
});
return intersection;
},
/*
* @param params
* 传入一个交集行,判断内部是否互相相等
*/
isItemEqual(params) {
if (params && params.includes(0)) return false;
let weight = -1;
// 找出权值
if (params.length) {
params.some((t) => {
if (typeof t === 'number') weight = t;
return typeof t === 'number';
});
if (weight === -1) {
return this.isArrayUnions(params);
}
}
return params.every((t) => {
if (typeof t === 'number') {
return t === weight;
}
return t && t.includes(weight);
});
},
/*
* @param params
* 传入多个数组,判断是否有交集
*/
isArrayUnions(params) {
if (!params.length || !params[0]) return false;
return params[0].some((t) => params.every((_t) => (_t) && (_t).includes(t)));
}
这样我们就可以兼容规格下的 sku 选择了!
欢迎大家留言讨论,祝工作顺利、生活愉快!
我是bigo前端,下期见。