★规格组件-SKU&SPU概念
官方话术:
- SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
- SKU(Stock Keeping Unit)库存量单位,即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
总结一下:
- spu代表一种商品,拥有很多相同的属性。
- sku代表该商品可选规格的任意组合,他是库存单位的唯一标识。
—
- 如何判断组合选择的规格参数是否可以选中?
- 从后端可以得到所有的SKU数据
- 我们需要过滤出有库存的SKU数据
- 为了方便进行组合判断,需要计算每个SKU规格的集合数据的【笛卡尔集】
- 为了方便判断是否可以选择规格,可以基于笛卡尔集生成规格的【l路径字典】
- 此时当点击规格标签时,把选中的规格进行组合,然后去字典中判断,只要有一个存在,就证明这种组合是有效的(点击组合点击)
<template>
<div class="goods-sku">
<dl v-for='(item, index) in specs' :key='index'>
<!-- 规格类型 -->
<dt>{{item.name}}</dt>
<dd>
<template v-for='(tag, i) in item.values' :key='i'>
<!-- 规格下的选项:图片或者文字 -->
<img @click='toggle(tag, item.values)' :class='{selected: tag.selected, disabled: tag.disabled}' v-if='tag.picture' :src="tag.picture" alt="">
<span @click='toggle(tag, item.values)' :class='{selected: tag.selected, disabled: tag.disabled}' v-else>{{tag.name}}</span>
</template>
</dd>
</dl>
</div>
</template>
<script>
import powerSet from '@/verdor/power-set.js'
const spliter = '#'
// 生成路径字典
const usePathMap = skus => {
// 最终的字典
const pathMap = {}
skus.forEach(sku => {
// 过滤掉库存为0的sku
if (sku.inventory === 0) return
// 把sku中的规格数据转换为普通数组
// attrs = [蓝色,中国,10cm]
const attrs = sku.specs.map(item => item.valueName)
// 计算集合的笛卡尔集
const subsets = powerSet(attrs)
// 把集合转换为字典
subsets.forEach(subset => {
// 排除空集
if (subset.length === 0) return
// 把规格的组合拼接为路径字典的key
const pathKey = subset.join(spliter)
// 把结果添加到路径字典中即可
if (pathMap[pathKey]) {
// 已经存在该key了
pathMap[pathKey].push(sku.id)
} else {
// 尚未存在,添加一个新的
pathMap[pathKey] = [sku.id]
}
})
})
return pathMap
}
// 获取选中的所有的规格的值
const getSelectedValues = specs => {
// result = [蓝色, undefinde, 10cm]
const result = []
specs.forEach((item, index) => {
// 获取当前规格选中的标签
const tag = item.values.find(tag => tag.selected)
if (tag) {
// 有选中的
result[index] = tag.name
} else {
// 没有选中
result[index] = undefined
}
})
return result
}
// 更新每一个规格选项的禁用状态
const updateDisabledStatus = (specs, pathMap) => {
// 把每一种规格的每一个选项遍历一屏
specs.forEach((item, index) => {
// 获取选中的值 result = [蓝色, 中国, 10cm]
const result = getSelectedValues(specs)
item.values.forEach(tag => {
if (tag.selected) {
// 如果标签本身就是选中的,不需要处理
return
} else {
// 没有选中,就把当前标签的名称添加到选中的值中单独判断即可
result[index] = tag.name
}
// 判断此时选中的值是否在字典中
// 把选中的值拼接为字符串(字典的key): 过滤掉undefined
// const pathKey = result.filter(item => item).join(spliter)
// 获取选中的值
console.log(result)
const seletedValues = result.filter(item => item)
if (seletedValues.length > 0) {
// 把选中的值拼接为字符串(字典的key): 过滤掉undefined
const pathKey = seletedValues.join(spliter)
// 根据路径字典控制标签的禁用状态
tag.disabled = !pathMap[pathKey]
}
// 根据pathKey去路径字典中判断是否存在
// if (!pathMap[pathKey]) {
// // 此时的pathKey不在路径字典中,应该被禁用
// tag.disabled = true
// }
})
})
}
// 初始化规格的选中状态: 根据skuId控制规格的选中
const initSkuSeletedStatus = (skuId, specs, skus) => {
// 1、获取需要选中的规格
const selectedSpecs = skus.find(item => item.id === skuId).specs
if (selectedSpecs) {
selectedSpecs.forEach(spec => {
// 2、找到需要选中的规格列表(所有的规格选项)
const allSpecs = specs.find(item => item.name === spec.name).values
// 3、根据需要选中的规格获取对应的数据选项
const ret = allSpecs.find(item => item.name === spec.valueName)
// 4、控制规格选中
if (ret) ret.selected = true
})
}
}
export default {
name: 'GoodsSku',
props: {
specs: {
type: Array,
default: () => []
},
skus: {
type: Array,
default: () => []
},
skuId: {
type: String,
default: ''
}
},
setup(props, { emit }) {
if (props.skuId) {
initSkuSeletedStatus(props.skuId, props.specs, props.skus)
}
const pathMap = usePathMap(props.skus)
// console.log(pathMap)
// const ret = getSelectedValues(props.specs)
// console.log(ret)
// 把每一个单独的选项判断一遍,并处理禁用状态
updateDisabledStatus(props.specs, pathMap)
// 控制选中和反选
const toggle = (tag, list) => {
// 判断标签是否为禁用状态
if (tag.disabled) return
// 当前的选中,其他的取消选中
tag.selected = !tag.selected
list.forEach(item => {
if (item !== tag) {
// 其他标签设置为不选中状态
item.selected = false
}
})
// 触发更新所有规格选项禁用状态的操作
updateDisabledStatus(props.specs, pathMap)
// 获取选中规格的相关商品信息(必须选中所有的规格)排除undefined
const spec = getSelectedValues(props.specs).filter(item => item)
if (spec.length === props.specs.length) {
// 所有的规格都选择了,获取当前规格的详细商品信息
// 根据选中的规格生成路径字典的key
const pathKey = spec.join(spliter)
// 根据路径查找路径字典中的对应的skuId
const currentSkuId = pathMap[pathKey][0]
// 根据当前的skuId获取sku中记录的商品信息
const currentSku = props.skus.find(item => item.id === currentSkuId)
// 准备传递给父组件的数据
// specsText -> 颜色:黑色 产地:日本 尺寸:30cm
// let specsText = ''
// currentSku.specs.forEach(item => {
// specsText += item.name + ':' + item.valueName + ' '
// })
const specsText = currentSku.specs.reduce((result, item) => result + item.name + ':' + item.valueName + ' ', '')
const skuInfo = {
skuId: currentSku.id,
price: currentSku.price,
oldPrice: currentSku.oldPrice,
inventory: currentSku.inventory,
specsText: specsText
}
// 把获取的规格相关数据传递给父组件
emit('sku-info', skuInfo)
}
}
return { toggle }
}
}
</script>
<style scoped lang="less">
.sku-state-mixin () {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: @xtxColor;
}
&.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;
.sku-state-mixin ();
}
> span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
.sku-state-mixin ();
}
}
}
}
</style>
将集合转化为笛卡尔集的组件
/**
* Find power-set of a set using BITWISE approach.
*
* @param {*[]} originalSet
* @return {*[][]}
*/
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
}