vue3项目中使用sku组件

一、认识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',{ } )

### 创建或使用 Vue3 中的 SKU 选择组件Vue3 开发环境中,SKU(库存单位)组件是一个常见的需求,尤其是在电商平台的应用场景下。以下是关于如何创建和使用的详细介绍。 #### 1. 封装一个可复用的 SKU 组件 为了提高代码的重用性和维护性,在 Vue3 中可以封装一个独立的 SKU 组件。该组件的主要功能包括显示商品规格、处理用户的选择以及返回最终选定的结果。 ##### 数据结构设计 首先定义好 SKU 的数据模型。通常情况下,SKU 数据会包含以下字段: - `specs`: 商品的所有规格选项及其对应的值列表。 - `stock`: 库存数量。 - `price`: 对应的价格信息。 示例数据如下: ```json { "specs": [ { "name": "颜色", "values": ["红色", "蓝色"] }, { "name": "容量", "values": ["64GB", "128GB"] } ], "skuList": [ { "color": "红色", "capacity": "64GB", "price": 999, "stock": 50 }, { "color": "蓝色", "capacity": "128GB", "price": 1199, "stock": 30 } ] } ``` ##### 实现逻辑 利用 Vue3 提供的 Composition API 和响应式特性来实现动态更新的功能[^1]。 ###### (1) 处理默认规格组合 当用户进入页面时,默认会选择某些规格项。可以通过计算属性或者方法获取当前选中的规格组合: ```javascript import { ref, computed } from &#39;vue&#39;; export default { setup(props) { const selectedSpecs = ref({}); // 默认初始化部分规格 props.specs.forEach(spec => { if (!selectedSpecs.value[spec.name]) { selectedSpecs.value[spec.name] = spec.values[0]; } }); // 计算当前选中的规格字符串形式 const currentSelectionString = computed(() => { return Object.entries(selectedSpecs.value).map(([key, value]) => `${key}:${value}`).join(&#39;,&#39;); }); return { selectedSpecs, currentSelectionString }; } }; ``` ###### (2) 动态过滤不可用选项 根据用户的实时选择,隐藏那些无法满足条件的其他规格选项。这一步骤需要用到幂集算法生成可能的子组合,并排除不符合库存约束的情况[^3]。 ```javascript function powerset(array) { return array.reduce( (subsets, value) => subsets.concat(subsets.map(set => [...set, value])), [[]] ); } // 过滤掉无库存的规格组合 const availableOptions = skuList.filter(skuItem => Object.keys(selectedSpecs.value).every(key => skuItem[key] === selectedSpecs.value[key]) ); ``` #### 2. 使用现有的开源解决方案 如果不想从零开始开发 SKU 组件,也可以借助成熟的第三方库。例如提到过的 **diygw-ui-admin** 是一款基于 Vue3 构建的后台管理系统模板,其中已经内置了一个支持多规格编辑的 SKU 组件[^2]。 安装方式: ```bash npm install diygw-ui-admin ``` 引入并注册组件: ```javascript import { SkuSelector } from &#39;diyw-ui-admin&#39;; import &#39;diyw-ui-admin/dist/style.css&#39;; // 加载样式文件 export default { components: { SkuSelector }, data() { return { productData: {} // 初始化商品数据对象 }; } }; ``` 调用实例: ```html <template> <div> <SkuSelector :data="productData" @change="handleSkuChange"></SkuSelector> </div> </template> <script> export default { methods: { handleSkuChange(selectedResult) { console.log(&#39;Selected SKU:&#39;, selectedResult); } } }; </script> ``` --- #### 总结 无论是自行封装还是采用现有工具链,Vue3 都提供了强大的技术支持去完成复杂的交互体验。通过合理规划数据流与视图层绑定关系,能够显著降低开发成本的同时增强系统的灵活性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值