一、认识sku组件
sku组件的作用是为了让用户能够选择商品的规格,从而提交购物车,在选择的过程中,组件的选状态要进行更新,组件还要提示用户当前的规格是否禁用,每次选择都要产出对应的sku数据
二、功能拆解
2.1.初始化规格渲染
<template>
<div class="goods-sku" v-if="goods">
<dl v-for="item in goods.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img
v-if="val.picture"
:src="val.picture"
:title="val.name"
/>
<span
v-else
>{{ val.name }}</span
>
</template>
</dd>
</dl>
</div>
</template>
2.2.点击规格更新选中状态
点击规格更新选中状态思路:
1.如果当前激活了,就取消激活
2.如果当前未激活,就把和自己同排的其他规格取消激活,再把自己激活
数据设计:每一个规格项都添加一个selected字段来决定是否激活,true为激活,false为未激活
<img
v-if="val.picture"
:class="{ selected: val.selected }"
@click="changeSelectedStatus(item, val)"
:src="val.picture"
:title="val.name"
/>
<span
v-else
:class="{ selected: val.selected }"
@click="changeSelectedStatus(item, val)"
>{{ val.name }}</span>
//切换选中状态
const changeSelectedStatus = (item, val) => {
//item:同一排对象,val:当前点击项
if (val.selected) {
val.selected = false;
} else {
item.values.forEach((val) => (val.selected = false));
val.selected = true;
}
};
2.3.点击规格更新禁用状态
2.3.1点击规格更新禁用状态- 生成有效路径字典
核心原理:
当前的规格sku或者组合起来的规格sku,在sku数组中对应项的库存为零时,当前规格会被禁用,生成路径字典是为了协助和简化这个匹配过程
实现步骤:
1.根据库存字段得到有效的sku数组
2.根据有效的sku数组使用powerSet算法得到所有子集
3.根据子集生成路径字典对象
//生成有效路径字典对象
const getPathMap = (goods) => {
const pathMap = {};
// 1.根据库存字段得到有效的sku数组
const effectiveSkus = goods.skus.filter((sku) => sku.inventory > 0);
// 2.根据有效的sku数组使用powerSet算法得到所有子集
effectiveSkus.forEach((sku) => {
//2.1获取匹配的valueName组成的数组
const selectedValArr = sku.specs.map((val) => val.valueName);
//2.2使用算法获取子集
const valueNamePowerSet = powerSet(selectedValArr);
// 3.根据子集生成路径字典对象
valueNamePowerSet.forEach((arr) => {
//初始化key
const key = arr.join("-");
//如果已经存在当前key就往数组中直接添加skuId,如果不存在直接做赋值
if (pathMap[key]) {
pathMap[key].push(sku.id);
} else {
pathMap[key] = [sku.id];
}
});
});
return pathMap;
};
2.3.2点击规格更新禁用状态- 初始化规格禁用
思路:遍历每一个规格对象,使用name字段作为key去路径字典pathMap中做匹配,匹配不上禁用
实现步骤:
1.通过增加disabled字段,匹配上路径字段,disabled为false匹配不上路径字段
2.disabled为true配合动态类名控制禁用类名
//初始化禁用状态
const initDosabledStatus = (specs, pathMap) => {
specs.forEach((spe) => {
spe.values.forEach((val) => {
if (pathMap[val.name]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
initDosabledStatus(goods.value.specs, pathMap);
2.3.3点击规格更新禁用状态- 点击时组合禁用更新禁用
思路(点击规格时):
1.按照顺序得到规格选中项的数组['蓝色','20cm',undefined]
2.遍历每一个规格
2.1把name字段的值填充到对应的位置
2.2过滤掉undefined项使用join方法形成一个有效的key
2.3使用key去pathMap中进行匹配,匹配不上,则当前项禁用
//获取相中项的数组
const getSelectedValues = (specs) => {
const arr = [];
specs.forEach((spe) => {
//找到values中selected为true的项,然后把它的name字段添加到对应的位置
const selectedVal = spe.values.find((item) => item.selected);
arr.push(selectedVal ? selectedVal.name : undefined);
});
return arr;
};
//切换时更新禁用状态
const updateDisabledStatus = (specs, pathMap) => {
specs.forEach((spe, index) => {
const selectedValues = getSelectedValues(specs);
spe.values.forEach((val) => {
selectedValues[index] = val.name;
const key = selectedValues.filter((value) => value).join("-");
if (pathMap[key]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
2.4.产出对应的sku数据
产出有效的sku信息
按照顺序得到规格选中项的数组['蓝色','20cm',undefined],如果找不到undefined,那么用户已经选择了所有有效规格,此时可以产出数据
如果获取当前sku信息对象?
把已选择项数组拼接为路径字典的key,去路径字典pathMAp中找即可
//产出sku对象
const index = getSelectedValues(goods.value.specs).findIndex(
(item) => item === undefined
);
if (index > -1) {
} else {
const key = getSelectedValues(goods.value.specs).join("-");
const skuIds = pathMap[key];
const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
console.log("skuObj", skuObj);
}
三、sku组件完整代码
sku.vue
<template>
<div class="goods-sku" v-if="goods">
<dl v-for="item in goods.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img
v-if="val.picture"
:class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)"
:src="val.picture"
:title="val.name"
/>
<span
v-else
:class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)"
>{{ val.name }}</span
>
</template>
</dd>
</dl>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import powerSet from "./power-set.js";
const goods = ref();
let pathMap = {};
const getGoods = async () => {
//1135076无库存规格
//1369155859933827074
const res = await axios.get(
"http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074"
);
goods.value = res.data.result;
pathMap = getPathMap(goods.value);
initDosabledStatus(goods.value.specs, pathMap);
};
onMounted(() => {
getGoods();
});
//切换选中状态
const changeSelectedStatus = (item, val) => {
if (val.disabled) {
return;
}
//item:同一排对象,val:当前点击项
if (val.selected) {
val.selected = false;
} else {
item.values.forEach((val) => (val.selected = false));
val.selected = true;
}
updateDisabledStatus(goods.value.specs, pathMap);
//产出sku对象
const index = getSelectedValues(goods.value.specs).findIndex(
(item) => item === undefined
);
if (index > -1) {
} else {
const key = getSelectedValues(goods.value.specs).join("-");
const skuIds = pathMap[key];
const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
console.log("skuObj", skuObj);
}
};
//生成有效路径字典对象
const getPathMap = (goods) => {
const pathMap = {};
// 1.根据库存字段得到有效的sku数组
const effectiveSkus = goods.skus.filter((sku) => sku.inventory > 0);
// 2.根据有效的sku数组使用powerSet算法得到所有子集
effectiveSkus.forEach((sku) => {
//2.1获取匹配的valueName组成的数组
const selectedValArr = sku.specs.map((val) => val.valueName);
//2.2使用算法获取子集
const valueNamePowerSet = powerSet(selectedValArr);
// 3.根据子集生成路径字典对象
valueNamePowerSet.forEach((arr) => {
//初始化key
const key = arr.join("-");
//如果已经存在当前key就往数组中直接添加skuId,如果不存在直接做赋值
if (pathMap[key]) {
pathMap[key].push(sku.id);
} else {
pathMap[key] = [sku.id];
}
});
});
return pathMap;
};
//初始化禁用状态
const initDosabledStatus = (specs, pathMap) => {
specs.forEach((spe) => {
spe.values.forEach((val) => {
if (pathMap[val.name]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
//获取相中项的数组
const getSelectedValues = (specs) => {
const arr = [];
specs.forEach((spe) => {
//找到values中selected为true的项,然后把它的name字段添加到对应的位置
const selectedVal = spe.values.find((item) => item.selected);
arr.push(selectedVal ? selectedVal.name : undefined);
});
return arr;
};
//切换时更新禁用状态
const updateDisabledStatus = (specs, pathMap) => {
specs.forEach((spe, index) => {
const selectedValues = getSelectedValues(specs);
spe.values.forEach((val) => {
selectedValues[index] = val.name;
const key = selectedValues.filter((value) => value).join("-");
console.log("key", key);
if (pathMap[key]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
</script>
<style scoped lang="scss">
@mixin sku-state-mixin {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: #27ba9b;
}
&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}
.goods-sku {
padding-left: 10px;
padding-top: 20px;
dl {
display: flex;
padding-bottom: 20px;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
flex: 1;
color: #666;
> img {
width: 50px;
height: 50px;
margin-bottom: 4px;
@include sku-state-mixin;
}
> span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
margin-bottom: 4px;
@include sku-state-mixin;
}
}
}
}
</style>
powerSet.js
export default function bwPowerSet(originalSet) {
const subSets = []
// We will have 2^n possible combinations (where n is a length of original set).
// It is because for every element of original set we will decide whether to include
// it or not (2 options for each set element).
const numberOfCombinations = 2 ** originalSet.length
// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
// it shows by its bits (0 or 1) whether to include related element from the set or not.
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
// include only "2" to the current set.
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
const subSet = []
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
// Decide whether we need to include current element into the subset or not.
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex])
}
}
// Add current subset to the list of all subsets.
subSets.push(subSet)
}
return subSets
}
四、组件通信
1.把异步数据内容由父组件获取并且通过组件通信方式传给sku组件
2.当用户选择有效规格后,把sku对象信息通过组件通信的方式抛给父组件
defineProps({
goods: {
type: Object,
default: () => ({ specs: [], skus: [] }),
},
})
const emits=defineProps(['change'])
emit('change',skuObj)
emit('change',{ } )