前端商品多规格选择问题 SKU 算法实现
业务场景
需要实现的业务场景是,要根据用户每一次选择的规格,确定剩下可选和不可选的规格,表现在前端页面上,即将不可选的规格置灰。这是一个十分常见的业务场景,在淘宝、京东、拼多多等电商平台上均需要用户选择规格属性。
实现思路
-
创建顶点集和空邻接矩阵
此时的邻接矩阵:
1L 4L 红色 黑色 1L 0 0 0 0 4L 0 0 0 0 红色 0 0 0 0 黑色 0 0 0 0
- 根据后端配置数据,更新邻接矩阵
但这步工作还没有完成,若最终的无向图如上图所示,会出现什么情况呢?在选择了
1L
之后,4L
就置灰不可选了,这显然不合常理。所以需要根据每列规格属性将顶点连接起来。(如:将1L
和4L
连接起来)初始化后的邻接矩阵:
1L 4L 红色 黑色 1L 0 1 1 1 4L 1 0 1 0 红色 1 1 0 1 黑色 1 0 1 0
-
每次选中状态时,根据邻接矩阵处理禁用项
根据选中值查询矩阵值,来判断是否禁用。
假设选中
4L
,则颜色属性就可以根据交点值判断哪些选项可选,哪些不可选。
具体操作
-
创建顶点集和空邻接矩阵
// 异步获取到某商品的相关数据 properties = [ // 商品的规格属性 { id: "1", name: "容量", attributes: [ { value: "1L", isActive: false, isDisabled: false }, { value: "4L", isActive: false, isDisabled: false }, ], }, { id: "2", name: "颜色", attributes: [ { value: "红色", isActive: false, isDisabled: false }, { value: "黑色", isActive: false, isDisabled: false }, ], }, ]; skuList = [ // 库存单元Array,即每一个单规格选项Array { id: "10", attributes: ["1L", "红色"] }, { id: "20", attributes: ["1L", "黑色"] }, { id: "30", attributes: ["4L", "红色"] }, ];
根据该数据结构,创建顶点集
// 构造初始空邻接矩阵存储无向图 initMatrix() { let matrix = [] // 矩阵 let vertexList = [] // 顶点集 this.properties.forEach((prop) => { prop.attributes.forEach((attr) => { vertexList.push(attr.value); }); }); const len = this.vertexList.length for (let i = 0; i < len; i++) { matrix[i] = new Array(len).fill(0); } },
-
根据后端配置数据,更新邻接矩阵
// 根据 skuList 和 properties 设置邻接矩阵的值 setAdjMatrixValue() { // 处理后端配置数据 this.skuList.forEach((sku) => { this.associateAttributes(sku.attributes); }); // 处理同级节点的情况 (【红,黑】) this.properties.forEach((prop) => { this.associateAttributes(prop.attributes); }); }, // 将 attributes 属性组中的属性在无向图中联系起来 associateAttributes(attributes) { attributes.forEach((attr1) => { attributes.forEach((attr2) => { // 因 properties 与 skuList 数据结构不一致,需作处理 if (attr1 !== attr2 || attr1.value !== attr2.value) { if (attr1.value && attr2.value) { attr1 = attr1.value; attr2 = attr2.value; } const index1 = this.vertexList.indexOf(attr1); const index2 = this.vertexList.indexOf(attr2); if (index1 > -1 && index2 > -1) { // 更新邻接矩阵 this.matrix[index1][index2] = 1; } } }); }); },
-
每次选中状态时,根据邻接矩阵处理禁用项
根据选中项的不同,如选中
4L
,遍历顶点集与4L
的交点值// 重置每个 attribute 的 isDisabled 状态 this.properties.forEach((prop) => { prop.attributes.forEach((attr) => { attr.isDisabled = !this.canAttributeSelect(attr); }); }); }, // 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰 canAttributeSelect(attribute) { if (!this.selected || !this.selected.length || attribute.isActive) { return true; } let res = []; this.selected.forEach((value) => { const index1 = this.vertexList.indexOf(value); const index2 = this.vertexList.indexOf(attribute.value); res.push(this.matrix[index1][index2]); }); return res.every((item) => item === 1); },
vue源码
<template>
<div class="root">
<p>商品多规格选择示例</p>
<div v-for="(property, propertyIndex) in properties" :key="propertyIndex">
<p>{{ property.name }}</p>
<div class="sku-box-area">
<template v-for="(attribute, attributeIndex) in property.attributes">
<div
:key="attributeIndex"
:class="[
'sku-box',
'sku-text',
attribute.isActive ? 'active' : '',
attribute.isDisabled ? 'disabled' : '',
]"
@click="handleClickAttribute(propertyIndex, attributeIndex)"
>
{{ attribute.value }}
</div>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
name: "SkuSelector",
components: {},
computed: {},
data() {
return {
properties: [], // property 列表
skuList: [], // sku 列表
matrix: [], // 邻接矩阵存储无向图
vertexList: [], // 顶点数组
selected: [], // 当前已选的 attribute 列表
};
},
mounted() {
this.properties = [
{
id: "1",
name: "容量",
attributes: [
{ value: "1L", isActive: false, isDisabled: false },
{ value: "4L", isActive: false, isDisabled: false },
],
},
{
id: "2",
name: "颜色",
attributes: [
{ value: "红色", isActive: false, isDisabled: false },
{ value: "黑色", isActive: false, isDisabled: false },
],
},
];
this.skuList = [
{ id: "10", attributes: ["1L", "红色"] },
{ id: "20", attributes: ["1L", "黑色"] },
{ id: "30", attributes: ["4L", "红色"] },
// { id: "40", attributes: ["4L", "黑色"] },
];
this.initEmptyAdjMatrix();
this.setAdjMatrixValue();
},
methods: {
// 当点击某个 attribute 时,如:黑色
handleClickAttribute(propertyIndex, attributeIndex) {
const attr = this.properties[propertyIndex].attributes[attributeIndex];
// 若选项置灰,直接返回,表现为点击无响应
if (attr.isDisabled) {
return;
}
// 重置每个 attribute 的 isActive 状态
const isActive = !attr.isActive;
this.properties[propertyIndex].attributes[attributeIndex].isActive =
isActive;
if (isActive) {
this.properties[propertyIndex].attributes.forEach((attr, index) => {
if (index !== attributeIndex) {
attr.isActive = false;
}
});
}
// 维护当前已选的 attribute 列表
this.selected = [];
this.properties.forEach((prop) => {
prop.attributes.forEach((attr) => {
if (attr.isActive) {
this.selected.push(attr.value);
}
});
});
// 重置每个 attribute 的 isDisabled 状态
this.properties.forEach((prop) => {
prop.attributes.forEach((attr) => {
attr.isDisabled = !this.canAttributeSelect(attr);
});
});
},
// 构造初始空邻接矩阵存储无向图
initEmptyAdjMatrix() {
this.properties.forEach((prop) => {
prop.attributes.forEach((attr) => {
this.vertexList.push(attr.value);
});
});
for (let i = 0; i < this.vertexList.length; i++) {
this.matrix[i] = new Array(this.vertexList.length).fill(0);
}
},
// 根据 skuList 和 properties 设置邻接矩阵的值
setAdjMatrixValue() {
this.skuList.forEach((sku) => {
this.associateAttributes(sku.attributes);
});
this.properties.forEach((prop) => {
this.associateAttributes(prop.attributes);
});
},
// 将 attributes 属性组中的属性在无向图中联系起来
associateAttributes(attributes) {
attributes.forEach((attr1) => {
attributes.forEach((attr2) => {
// 因 properties 与 skuList 数据结构不一致,需作处理
if (attr1 !== attr2 || attr1.value !== attr2.value) {
if (attr1.value && attr2.value) {
attr1 = attr1.value;
attr2 = attr2.value;
}
const index1 = this.vertexList.indexOf(attr1);
const index2 = this.vertexList.indexOf(attr2);
if (index1 > -1 && index2 > -1) {
this.matrix[index1][index2] = 1;
}
}
});
});
},
// 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
canAttributeSelect(attribute) {
if (!this.selected || !this.selected.length || attribute.isActive) {
return true;
}
let res = [];
this.selected.forEach((value) => {
const index1 = this.vertexList.indexOf(value);
const index2 = this.vertexList.indexOf(attribute.value);
res.push(this.matrix[index1][index2]);
});
return res.every((item) => item === 1);
},
},
};
</script>
<style>
.root {
width: 350px;
padding: 24px;
}
.sku-box-area {
display: flex;
flex: 1;
flex-direction: row;
flex-wrap: wrap;
}
.sku-box {
border: 1px solid #cccccc;
border-radius: 6px;
margin-right: 12px;
padding: 8px 10px;
margin-bottom: 10px;
}
.sku-text {
font-size: 16px;
line-height: 16px;
color: #666666;
}
.active {
border-color: #ff6600;
color: #ff6600;
}
.disabled {
opacity: 0.5;
border-color: #e0e0e0;
color: #999999;
}
</style>
最后一句
学习心得!若有不正,还望斧正。希望掘友们不要吝啬对我的建议。