通过发送请求获取商品详情数据,包括商品规格(specs)和库存信息(skus)。
选中状态更新:根据当前状态进行激活或取消激活的逻辑,通过为每个规格项添加的“selected”字段来标识是否激活,同时利用样式处理,通过动态类属性来直观地显示选中状态。
禁用状态更新:基于库存情况来确定规格是否禁用,通过生成有效路径字典来简化匹配过程。具体步骤包括获取有效 Sku 数组、生成子集以及构建路径字典对象。
初始化规格禁用:通过遍历规格对象,利用“name”字段与路径字典进行匹配来确定是否禁用,并通过增加“disabled”字段和动态类名来实现显示上的禁用效果。
点击时组合禁用更新:当用户点击规格时,通过特定步骤获取选中项数组并进行匹配来更新禁用状态。
产出有效 SKU 信息:以已选择项数组中不存在“undefined”来判断用户已选择所有有效规格,然后通过拼接已选数组为路径字典的键来获取相应的 SKU 信息对象。
vue
<script setup>
import { onMounted, ref } from "vue";
import axios from "axios";
// 导入幂级算法
import bwPowerSet from "./bwPowerSet";
// 存储用于在页面中展示的规格数据
const specs = ref([]);
// 创建 UI 状态 (选中、禁用)
const UIState = ref([]);
// 声明规格查询字典
let pathMap = {};
//获取商品详情数据
function requestGoodsApi(id) {
return axios.get("https://apipc-xiaotuxian-front.itheima.net/goods", {
params: { id },
});
}
// 根据规格数据创建其对应的界面状态
function createUIStatus(specs) {
// UI 状态数组
const UIStatus = [];
// 遍历源规格分组
specs.forEach((spec) => {
// 创建规格分组
const group = [];
// 遍历具体的规格选项
spec.values.forEach(() => {
// 设置每一个规格选项的选中状态和禁用状态(初始值)
group.push({ selected: false, disabled: false });
});
// 将规格组对象添加到拷贝结果数组中
UIStatus.push(group);
});
// 返回 UI 状态数组
return UIStatus;
}
// 设置规格的选中状态
function setSelected(index, i) {
// 获取当前用户点击的规格
const current = UIState.value[index][i];
// 获取当前用户点击的规格对应的规格组
const group = UIState.value[index];
// 如果当前规格已经是禁用状态, 不能被选择, 所以阻止代码继续执行
if (current.disabled) return;
// 如果用户点击的规格已经是选中的
if (current.selected) {
// 让其取消选中
current.selected = false;
} else {
// 先将该规格中的所有规格取消选中
group.forEach((item) => (item.selected = false));
// 将当前用户点击的规格设置为选中
current.selected = true;
}
// 用户选择规格后更新规格的禁用状态
setDisabled();
}
// 设置规格的禁用状态
function setDisabled() {
// 遍历每一组规格数据
specs.value.forEach((spec, index) => {
// 获取用户选择的规格名称数组
const selected = getUserSelected();
// 遍历这一组规格数据中的具体规格
spec.values.forEach((value, i) => {
// 如果当前规格已经被选中了, 说明它可以选, 不需要被禁用
if (UIState.value[index][i].selected) return;
// 将当前规格名称放入用户选择的规格数组名称中, 待匹配
selected[index] = value.name;
// 检测当前规格是否可以选择
const key = selected.filter((name) => name).join("_");
// 如不能选择, 设置当前规格的 disabled 为 true
UIState.value[index][i].disabled = !(key in pathMap);
});
});
}
// 获取用户选择的规格名称数组
function getUserSelected() {
// 声明用于存储用户选择的规格名称数组
const names = [];
// 遍历规格组
specs.value.forEach((spec, index) => {
// 查找当前规格组中被选中的规格的索引
const selectedIndex = UIState.value[index].findIndex(
(item) => item.selected
);
// 如果找到了
if (selectedIndex !== -1) {
// 将该规格放在它自己的位置上
names[index] = spec.values[selectedIndex].name;
} else {
// 如果没有找到, 当前规格使用 undefined 进行占位
names[index] = undefined;
}
//获取到选中的规格
console.log(names);
});
// 返回用户选择的规格名称数组
return names;
}
// 创建规格查询字典
function createPathMap(skus) {
// 过滤出有库存的商品规格组合
skus
.filter((sku) => sku.inventory > 0)
// 遍历有库存的商品规格组合
.forEach((sku) => {
// 将当前遍历的规格组合中的规格名称临时存到一个数组中
// ['蓝色', '20cm', '中国']
const valueNames = sku.specs.map((spec) => spec.valueName);
// 获取用户可以选择的所有可能的规格及规格组合
// ['蓝色', '20cm']
// [['蓝色'], ['20cm'], ['蓝色', '20cm']]
const sets = bwPowerSet(valueNames).filter((set) => set.length > 0);
// 获取当前商品的规格数量, 将用于判断某个规格是否是完整的
const max = valueNames.length;
// 遍历用户可以选择的所有可能的规格及规格组合
sets.forEach((set) => {
// 将规格名称以 _ 进行拼接
const key = set.join("_");
// 用于判断当前规格是否是完整的
const isCompleted = set.length === max;
// 判断规格查询对象中是否已经存储了当前规格
if (!(key in pathMap)) {
// 如果当前规格是完整的
if (isCompleted) {
// 将当前规格或规格组合添加到规格查询对象中并赋值 sku id
pathMap[key] = sku.id;
} else {
// 将当前规格或规格组合添加到规格查询对象中
pathMap[key] = null;
}
}
});
});
return pathMap;
}
// 组件挂载完成之后
onMounted(async () => {
// 获取商品详情数据
const response = await requestGoodsApi("1369155859933827074");
// 保存规格数据
specs.value = response.data.result.specs;
// 为规则数据附加 UI 状态
UIState.value = createUIStatus(specs.value);
// 创建规格查询对象
pathMap = createPathMap(response.data.result.skus);
// 设置规格的初始禁用效果
setDisabled();
});
</script>
<template>
<div class="goods-sku">
<dl v-for="(spec, index) in specs" :key="spec.id">
<dt>{{ spec.name }}</dt>
<dd>
<template v-for="(item, i) in spec.values">
<img
v-if="item.picture"
:class="{
selected: UIState[index][i].selected,
disabled: UIState[index][i].disabled,
}"
@click="setSelected(index, i)"
:src="item.picture"
:alt="item.name"
/>
<span
v-else
:class="{
selected: UIState[index][i].selected,
disabled: UIState[index][i].disabled,
}"
@click="setSelected(index, i)"
>{{ item.name }}</span
>
</template>
</dd>
</dl>
</div>
</template>
<style scoped>
.goods-sku {
padding-left: 10px;
padding-top: 20px;
}
.goods-sku dl {
display: flex;
align-items: center;
}
.goods-sku dl dt {
width: 50px;
color: #999;
}
.goods-sku dl dd {
flex: 1;
color: #666;
}
.goods-sku dl dd > img {
width: 50px;
height: 50px;
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
display: block;
float: left;
}
.goods-sku dl dd > img.selected {
border-color: #27ba9b;
}
.goods-sku dl dd > img.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
.goods-sku dl dd > span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
}
.goods-sku dl dd > span.selected {
border-color: #27ba9b;
}
.goods-sku dl dd > span.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
</style>
bowPowerSet.js
export default function bwPowerSet(originalSet) {
// 初始化一个空数组用于存储子集
const subSets = [];
// 计算原始集合的元素个数
const numberOfCombinations = 2 ** originalSet.length;
// 循环生成所有可能的组合
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;
}