注:本功能使用 vue 框架实现
功能点说明
-
未选择规格时,须要将不能选择的规格禁用
-
选择规格后须要重新计算能选的规格与不能选的规格,将不能选的规格禁用
-
尽量不要进行太多的循环与 if 判断,让语义更清晰
思路分析
skuList 与 stockList 的数据格式
skuList 的数据格式如下:
"skuList": [
{
"name": "颜色",
"sort": 1,
"children": [
{
"name": "黑色",
"id": 1,
"sort": 1
},
{
"name": "红色",
"id": 3,
"sort": 2
},
{
"name": "粉色",
"id": 4,
"sort": 3
}
]
},
{
"name": "尺码",
"sort": 2,
"children": [
{
"name": "35",
"id": 5,
"sort": 1
},
{
"name": "36",
"id": 6,
"sort": 2
},
{
"name": "34",
"id": 7,
"sort": 3
}
]
},
{
"name": "材质",
"sort": 4,
"children": [
{
"name": "帆布鞋",
"id": 11,
"sort": 1
},
{
"name": "凉鞋",
"id": 12,
"sort": 2
},
{
"name": "运动鞋",
"id": 13,
"sort": 3
}
]
}
]
stockList 的数据格式如下:
"stockList": [
{
"skuId": "1_7_12",
"stock": 888,
"price": 15535400,
"combination": "黑色;34;凉鞋"
},
{
"skuId": "1_5_12",
"stock": 99,
"price": 100,
"combination": "黑色;35;凉鞋"
},
{
"skuId": "4_6_13",
"stock": 150,
"price": 10011,
"combination": "粉色;36;运动鞋"
},
{
"skuId": "3_5_12",
"stock": 9999,
"price": 100,
"combination": "红色;35;凉鞋"
},
{
"skuId": "4_5_13",
"stock": 100000,
"price": 10088,
"combination": "粉色;35;运动鞋"
}
]
stockList 的处理
生成 idMap
遍历现有的可选规格列表 stockList,根据可选的规格组合生成 idMap,也就是将每个 id 可选的其余节点筛选出来
// 规格组合的id字符串
handleCombination() {
const idMap = {
// id1: [id4, id2, id3],
};
this.stockList.forEach((item) => {
const skuIdList = item.skuId.split("_");
// 生成idMap
skuIdList.forEach((skuId) => {
if (idMap[skuId]) {
idMap[skuId].push(...skuIdList);
} else if (idMap[skuId] === undefined) {
idMap[skuId] = [...skuIdList];
}
});
});
// value去重
for (const id in idMap) {
idMap[id] = [...new Set(idMap[id])];
}
this.idMap = idMap;
},
生成 matrix
遍历 sideList 两次,为 matrix 二维数组中的元素进行赋值,第一次遍历得到 rowIndex、rowItem,第二次遍历得到 colindex、colItem
// 根据combineAllId生成matrix二维数组
handleMatrix() {
this.sideList.forEach((rowItem, rowIndex) => {
this.sideList.forEach((colItem, colindex) => {
this.matrix[rowIndex] = this.matrix[rowIndex] || [];
if (rowItem.id === colItem.id) {
// 相同的规格是不连通的
this.matrix[rowIndex][colindex] = 0;
return;
}
const rowIdList = this.idMap[rowItem.id];
if (!(this.idMap[rowItem.id] && this.idMap[colItem.id])) {
// 某个规格与其他规格没有交集
this.matrix[rowIndex][colindex] = 0;
} else if (this.broIdMap[rowItem.id].includes("" + colItem.id)) {
// 同级属性下的规格 彼此之间是相互可选的
this.matrix[rowIndex][colindex] = 1;
} else if (
this.idMap[rowItem.id]?.includes("" + colItem.id) ||
this.idMap[colItem.id]?.includes("" + rowItem.id)
) {
// 规格之间是连通的
this.matrix[rowIndex][colindex] = 1;
} else {
// 规格之间是不连通的
this.matrix[rowIndex][colindex] = 0;
}
});
});
console.log("this.matrix", this.matrix);
}
使用 matrix 二维数组存放规格与规格之间的关系,0 表示两规格之间是不连通的,不可选择,1 表示可两规格之间是连接的,可选择
const matrix = [
[0, 1, 1, 1, 0, 1, 0, 1, 0],
[1, 0, 1, 1, 0, 0, 0, 1, 0],
[1, 1, 0, 1, 1, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 1, 0, 1, 1],
[0, 0, 1, 1, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 1, 0, 1, 0, 0, 1],
[0, 0, 1, 1, 1, 0, 0, 1, 0],
];
skuList 的处理
填充 selectedId 数组
用一个数组保存用户勾选的规格的 id
// 填充selectedId数组
handleSelectedId() {
this.selectedId = Array(this.skuList.length).fill("");
console.log("this.selectedId", this.selectedId);
}
生成 sideList
使用 sideList 数组存放所有的规格,最终格式如下:
const sideList = [
{ name: '黑色', id: 1, sort: 1 },
{ name: '红色', id: 3, sort: 2 },
{ name: '粉色', id: 4, sort: 3 },
{ name: '35', id: 5, sort: 1 },
{ name: '36', id: 6, sort: 2 },
{ name: '34', id: 7, sort: 3 },
{ name: '帆布鞋', id: 11, sort: 1 },
{ name: '凉鞋', id: 12, sort: 2 },
{ name: '运动鞋', id: 13, sort: 3 },
];
// 矩阵的每个边 数组
handleSideList() {
// broIdMap = { id2:id1, id3 }
const broIdMap = {};
let broIdList = [];
this.skuList.forEach((itemx) => {
broIdList = [];
itemx.children.forEach((itemy) => {
this.sideList.push(itemy);
// 生成broIdList
broIdList.push("" + itemy.id);
});
// 生成broIdMap
broIdList.forEach((id) => {
broIdMap[id] = broIdList;
});
});
this.broIdMap = broIdMap;
console.log("this.sideList", this.sideList);
console.log("this.broIdMap", this.broIdMap);
}
使用 broIdMap 保存同级属性的 id
const broIdMap = {
1: ['1', '3', '4'],
3: ['1', '3', '4'],
4: ['1', '3', '4'],
5: ['5', '6', '7'],
6: ['5', '6', '7'],
7: ['5', '6', '7'],
11: ['11', '12', '13'],
12: ['11', '12', '13'],
13: ['11', '12', '13'],
};
遍历 sideList 数组,生成 specMap 对象,key 为 id,value 为索引,该对象用于存放每个规格在 sideList 数组中的索引 用户选择规格时可以直接通过 id 拿到索引值,通过索引拿到对应的数组
const specMap = { 1: 0, 3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 11: 6, 12: 7, 13: 8 };
// 根据sideList生成specMap, key 为 id,value 为索引
handleSpecMap() {
this.sideList.forEach((item, index) => {
this.specMap[item.id] = index;
// this.$set(this.specMap, "" + item.id, index);
});
console.log("this.specMap", this.specMap);
}
selectedId 用于保存用户勾选的 id,selectedName 用于保存 name,显示在页面让用户知道选择了哪些规格
this.selectedId = ['', 6, 13];
处理用户选择规格后的情况
selectedId 处理
将用户勾选的规格 id 放在 selectedId 数组中
// 选择规格
selectSpec(itemy, index, indey) {
if (this.selectedId[index] != itemy.id) {
//不包含当前点击规格则添加
this.selectedId[index] = itemy.id; //如果没有则把当前规格添加
this.subIndex[index] = indey; //添加选中样式
this.selectedName[index] = itemy.name;
} else {
// 包含当前点击规格则取消选中
this.selectedId[index] = "";
this.subIndex[index] = -1; //去除样式
this.selectedName[index] = "";
}
// this.selectedId = this.handleSelectArr();
console.log("this.selectedId", this.selectedId);
// 拼接规格字符串
this.handleName();
this.handleSelectedArr();
},
selectedArr 处理
获取勾选的规格在 matrix 对应的数组,selectedArr 也是一个二维数组
matrix[specMap[id]相当于矩阵中的每一行
const selectedArr = [];
this.selectedId.forEach((id, index) => {
// 若不是""
if (id) {
selectedArr.push(matrix[specMap[id]]);
}
});
- selectedArr 长度等于 0,则没有勾选
- selectedArr 长度等于 1,则 interSection 为 selectedArr 第 0 个数组
- selectedArr 长度大于 1,说明用户至少勾选了 2 个规格,若选择的是多个规格则求两个数组的交集 interSection
选择规格时,通过 specMap,可直接用 id 拿到索引,通过索引拿到对应的数组,这个数组对应着当前可选的其它规格
// 处理勾选的规格对应的数组
handleSelectedArr() {
// 获取勾选的规格在matrix对应的数组 去除""的情况
const selectedArr = [];
const interSection = [];
this.selectedId.forEach((id, index) => {
// 若不是""
if (id) {
selectedArr.push(this.matrix[this.specMap[id]]);
}
});
// 取消勾选的所有规格后为[]
this.interSection = selectedArr[0] || [];
console.log("this.interSection", this.interSection);
// 选择多规格时 求交集
if (selectedArr.length <= 1) return;
console.log("selectedArr", selectedArr);
let prev = [];
selectedArr.forEach((itemx, index) => {
// 第一次循环
if (index === 0) {
prev = itemx;
return;
}
itemx.forEach((itemy, indey) => {
// console.log(prev[indey], itemy);
if (itemy === prev[indey] && itemy === 1) {
interSection[indey] = 1;
} else {
interSection[indey] = 0;
}
});
// 每次循环后
prev = itemx;
});
this.interSection = interSection;
console.log("this.interSection", this.interSection);
},
页面格式处理
在页面渲染时通过 isSelectable 函数判断该按钮是否禁用
<span
v-for="(itemy, indey) in itemx.children"
:key="indey"
:class="`${isSelectable(itemy) ? '' : 'noProduct'} ${
subIndex[index] === indey ? 'act' : ''
}`"
@click.stop="itemy.disabled ? $message({msg: '不可点击',
type: 'warning',
time: 1500,
}) : selectSpec(itemy, index, indey)"
>{{ itemy.name }}</span
>
// 页面初始化时 处理不可选择的规格
isSelectable(itemy) {
const colItem = this.specMap[itemy.id];
// 页面初始化时或用户清空选择了的规格后 interSection为[] 因此需要先判断matrix
if (!this.matrix[colItem].includes(1)) {
return false;
}
if (this.interSection.length > 0) {
return this.interSection[colItem];
}
return true;
},