纠结哈,还能再简单点嘛?...SKU算法
当时某团购项目需要匹配sku,看了同事写的sku,看着看着懵圈了。后面自行找了哈。发现sku算法大多基本归于笛卡尔积,邻接矩阵等。
参考文献:sku 多维属性状态判断算法
商品多种规格属性的选择(sku 算法)
https://github.com/zaxlct/vue-sku
SKU
作用:让用户选择商品的规格,选择的时候,组件的选中状态要进行更新,没有库存的要禁用。
- 步骤一:初始化规格渲染
- 步骤二:点击规格更新选中状态。(seleted)
激活当前规格,取消同排其他规格。
- 步骤三:点击规格更新禁用状态。(重点)
核心原理:当前的规格SKU,或者组合起来的规格Sku,**库存为0时禁用**。
生成**有效路径字典**是为了协助和简化这个过程。
实现思路: 1.根据库存字段得到有效的SKU .
2.根据有效的SKU数组使用幂等算法powerSet算法得到所有子集。
3.根据子集生成路径字典对象pathMap,匹配不上则禁用。
点击规格更新禁用状态: 使用name字段作为key去路径字典pathMap做匹配
已经选择的规格放在对应索引的数组里,当无undefined,那么用户匹配了所有有效规格。此时可以产出sku数 据。
- 步骤四:产出选择的sku数据
把产出的sku信息,拼接为路径字典的key,去路径字典pathMap找。
<template>
<h1>SKUccA</h1>
<ul class="goods-sku">
<li class="group" v-for="(item, index) in goods.specs">
<span class="lab">{{ item.name }}</span>
<div class="guige">
<template v-for="(val, sindex) in item.values">
<span :class="['guige__items', { 'selected': val.selected ,'disabled':val.disabled}]"
@click="changeSku(item, val)">{{ val.name }}</span>
</template>
</div>
</li>
</ul>
</template>
<script setup>
/*
* sku步骤:
* 1.展示规格列表
* 2.点击更新选中状态 。 点击则选中(添加Class的selected),并同排取消选中状态
* 3.点击规格更新禁用状态 - 生成有效路径字典
* 3.1 使用name字段作为key去路径字典pathMap做匹配
4.产出sku数据
*/
// http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074
import { ref, reactive, onMounted } from 'vue'
// 商品数据
import { goodsData } from './data.js'
import powerSet from './power-set.js'
let goods = ref(goodsData);
console.log('goods.value---',goods.value);
let pathMap = {}
onMounted(() => {
pathMap = getPathMap(goods.value.skus);
console.log('pathMap---', pathMap);
initDisabledStatus(goods.value.specs,pathMap);
})
// 切换选中状态
/**
* @param {同一排的对象} item
* @param {当前点击项} val
*/
const changeSku = (item, val) => {
if(val.disabled) return
console.log('切换选中状态');
// 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中。 点击的已选中则直接取消
if (val.selected) {
val.selected = false
} else {
item.values.forEach(val => val.selected = false)
val.selected = true;
}
// console.log(item);
// const arr = getSelectedValus(goods.value.specs);
// console.log('选中的规格',arr);
// 点击按钮时更新
updateDisabledStatus(goods.value.specs,pathMap);
// 产出SKU对象数据
const index = getSelectedValues(goods.value.specs).findIndex(item=>item===undefined)
if(index>-1){
console.log('找到了,信息不完整--555');
}else{
console.log('没有找到,信息完整,可以产出');
}
// 获取sku对象
const key = getSelectedValues(goods.value.specs).join('-');
console.log('key11111----',key);
const skuIds = pathMap[key];
console.log('skuIds-------',skuIds);
// 以skuIds作为匹配项去goods.value.skus数组中查找
let skuObj = goods.value.skus.find(item=>item.id === skuIds[0]);
console.log('sku对象为',skuObj);
}
/**
* 根据skus生成有效路径字典对象 (根据库存)
* @param {*} skus
*/
const getPathMap = (skus) => {
console.log(1222, skus);
let pathMap = {};
// 1.根据skus字段生成有效的sku数组
const effectiveSkus = skus.filter(sku => sku.inventory > 0);
console.log('有效的sku', effectiveSkus);
//2.根据有效的sku数组,使用算法 [1,2]=>[[1],[2],[1,2]]
effectiveSkus.forEach(sku => {
// 2.1 获取匹配的valueName组成的数组
const selectedValArr = sku.specs.map(val => val.valueName);
// console.log('selectedValArr--', selectedValArr);
// 2.2 使用算法获取子集
const valueArrPowerSet = powerSet(selectedValArr);
// console.log('算法获得子集--', valueArrPowerSet);
//3. 把得到的子集生成最终的路径字典对象
valueArrPowerSet.forEach(arr => {
//初始化key ,数组join
const key = arr.join('-')
//如果已经存在当前key了,就往数组中直接添加skuId,如果不存在key,直接赋值
// 如果没有就先初始化一个空数组
// console.log('pathMap[key]----',pathMap[key]);
if (pathMap[key]) {
pathMap[key].push(sku.id)
} else {
pathMap[key] = [sku.id]
}
})
});
return pathMap
}
/**
* 初始化规格时禁用状态
* @param {*} specs
* @param {*} pathMap
*/
const initDisabledStatus =(specs,pathMap)=>{
specs.forEach(spec => {
spec.values.forEach(val=>{
// console.log('规格',pathMap[val.name]);
if(pathMap[val.name]){
val.disabled = false
}else{
val.disabled = true
}
})
});
}
// 获取当前选中项的匹配数组(规格集合)
const getSelectedValues = (specs) =>{
// 选中规格的数组
const selectedArr = []
specs.forEach((spec, index) => {
// 目标:找到values中selected为true的项,然后把它的name字段添加到数组对应的位置
const selectedVal = spec.values.find(val => val.selected)
selectedArr.push(selectedVal ? selectedVal.name:undefined);
// if (selectedVal) {
// selectedArr[index] = selectedVal.name
// } else {
// selectedArr[index] = undefined
// }
})
return selectedArr
}
/**
* 切换时更新按钮禁用状态
* @param {*} specs
* @param {*} pathMap
*/
const updateDisabledStatus =(specs,pathMap)=>{
// 遍历每一种规格
specs.forEach((item, i) => {
// 拿到当前选择的项目
const selectedArr = getSelectedValues(specs)
// 遍历每一个按钮
item.values.forEach(val => {
if (!val.selected) {
selectedArr[i] = val.name
// 去掉undefined之后组合成key
const key = selectedArr.filter(value => value).join('-')
// val.disabled = !pathMap[key]
if(pathMap[key]){
val.disabled = false;
}else{
val.disabled = true;
}
}
})
})
}
</script>
<style scoped>
.goods-sku {
padding: 50px;
}
.lab {
color: blue;
}
.group {
display: flex;
margin-bottom: 20px;
}
.lab {
margin: 10px 30px;
}
.guige {
padding: 0;
margin: 0;
display: flex;
flex: 1;
}
.guige__items {
padding: 10px 20px;
font-size: 20px;
margin:0 20px;
}
.guige__items.selected {
color: lightseagreen;
border: 1px solid lightseagreen;
}
.guige__items.disabled{
background: gray;
opacity: .7;
cursor: not-allowed;
}
</style>
幂等算法power-set.js , 获取某集合所有子集
export default function bwPowerSet (originalSet) {
const subSets = []
//我们将有 2^n 种可能的组合(其中 n 是原始集合的长度)。
//这是因为对于原始集合的每个元素我们都会决定是否包含
//是否(每个集合元素有 2 个选项)。
const numberOfCombinations = 2 ** originalSet.length
//0 到 2^n 范围内的每个二进制表示的数字都完全符合我们的需要:
//它通过其位(0 或 1)显示是否包含集合中的相关元素。
//例如,对于集合 {1, 2, 3},二进制数 0b010 意味着我们需要
//仅包含“2”到当前集合中。
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
const subSet = []
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
//决定是否需要将当前元素包含到子集中。
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex])
}
}
//将当前子集添加到所有子集列表中。
subSets.push(subSet)
}
return subSets
}
数据提供:data.js
//data.js
export const goodsData = {
// 规格
"specs": [
{
"name": "颜色",
"id": "1369139574067957762",
"values": [
{
"name": "黑色",
},
{
"name": "蓝色",
}
]
},
{
"name": "尺寸",
"id": "1369141324204216321",
"values": [
{
"name": "30cm",
},
{
"name": "20cm",
},
{
"name": "10cm",
}
]
},
{
"name": "产地",
"id": "1369141916305723393",
"values": [
{
"name": "日本",
},
{
"name": "中国",
}
]
}
],
// 所有存在的skus
"skus": [{
"id": "1369155862131642369",
"skuCode": "goods-sku-001",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "20cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155863389933570",
"skuCode": "goods-sku-007",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "20cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155864430120962",
"skuCode": "goods-sku-003",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 20,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "10cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155865461919746",
"skuCode": "goods-sku-009",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "10cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155866451775490",
"skuCode": "goods-sku-005",
"price": "128.00",
"oldPrice": "210.00",
"inventory": 99918,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "30cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155867420659714",
"skuCode": "goods-sku-011",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 999,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "30cm"
}, {
"name": "产地",
"valueName": "中国"
}]
}, {
"id": "1369155868389543937",
"skuCode": "goods-sku-002",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 99814,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "20cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}, {
"id": "1369155869354233857",
"skuCode": "goods-sku-008",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "20cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}, {
"id": "1369155870306340866",
"skuCode": "goods-sku-004",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "10cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}, {
"id": "1369155871241670658",
"skuCode": "goods-sku-010",
"price": "128.00",
"oldPrice": "200.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "10cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}, {
"id": "1369155872197971970",
"skuCode": "goods-sku-006",
"price": "148.00",
"oldPrice": "190.00",
"inventory": 99952,
"specs": [{
"name": "颜色",
"valueName": "蓝色"
}, {
"name": "尺寸",
"valueName": "30cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}, {
"id": "1369155873162661889",
"skuCode": "goods-sku-012",
"price": "148.00",
"oldPrice": "210.00",
"inventory": 0,
"specs": [{
"name": "颜色",
"valueName": "黑色"
}, {
"name": "尺寸",
"valueName": "30cm"
}, {
"name": "产地",
"valueName": "日本"
}]
}]
}
一步一叩首,今天的自己比昨天好一点就行,明天的自己需追寻