前言
今天在写项目中的一个小组件,感觉挺不错的,然后分享给大家,如果以后需要使用这个就,看一下这个博客就行
先说一下SKU和SPU的概念:
- SPU(Standard Product Unit)标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
- SKU(Stock Keeping Unit)库存量单位,即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU
是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
SPU:代表一种商品,拥有很多相同的属性。
SKU:代表的是改商品可选择的规格的任意的组合,他是库存单位的唯一标识
比如在这个下图中出现的使用场景
思路
- 从后端获取到所有带有SKU的组合的列表数据,找到有库存的SKU的列表,下面是有库存的SKU
[蓝色,中国,10cm]
[绿色,中国,20cm]
[蓝色,日本,30cm]
[黑色,日本,30cm]
然后计算SKU集合的笛卡尔集,得到后面的这个值,如下面的代码:
[蓝色,中国,10cm]--->[蓝色,中国,10cm,蓝色#中国,蓝色#10cm,中国#10cm,蓝色#中国#10cm]
[绿色,中国,20cm]--->[绿色,中国,20cm,绿色#中国,绿色#20cm,中国#20cm,绿色#中国#20cm]
[蓝色,日本,30cm]--->[蓝色,日本,30cm,蓝色#日本,蓝色#30cm,日本#30cm,蓝色#日本#30cm]
[黑色,日本,30cm]--->[黑色,日本,30cm,黑色#日本,黑色#30cm,日本#30cm,黑色#日本#30cm]
然后生成一个专用的路径的字典,如下代码
const dic = {
//如果出现了两次的话,就写两个skuId
'蓝色':[skuId,skuId],
'中国':[skuId],
蓝色#中国#10cm:[skuId]
蓝色#中国:[skuId]
}
开始啦
在goods/components
文件夹中新建一个goods-sku.vue
的文件
基础布局代码,如下
<template>
<div class="goods-sku">
<dl>
<dt>颜色</dt>
<dd>
<img class="selected" src="https://yanxuan-item.nosdn.127.net/d77c1f9347d06565a05e606bd4f949e0.png" alt="">
<img class="disabled" src="https://yanxuan-item.nosdn.127.net/d77c1f9347d06565a05e606bd4f949e0.png" alt="">
</dd>
</dl>
<dl>
<dt>尺寸</dt>
<dd>
<span class="disabled">10英寸</span>
<span class="selected">20英寸</span>
<span>30英寸</span>
</dd>
</dl>
<dl>
<dt>版本</dt>
<dd>
<span>美版</span>
<span>港版</span>
</dd>
</dl>
</div>
</template>
<script>
export default {
name: 'GoodsSku'
}
</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>
然后再组件中使用
<!-- 图片的信息 -->
<div class="spec">
<GoodsName :goods="detail" />
<!-- 规格组件 -->
<GoodsSku />
</div>
<script>
import GoodsSku from './components/goods-sku'
components:{GoodsSku}
</script>
开始渲染组件
在商品规格组件把:specs="detail.specs"
注入进去
代码如下
<div class="spec">
<GoodsName :goods="detail" />
<!-- 规格组件 -->
<GoodsSku :specs="detail.specs" />
</div>
在子组件中接一下数据
props:{
specs:{
type:Array,
default:()=>[]
}
}
然后渲染页面,代码如下
<template>
<div class="goods-sku">
<dl v-for='(item, index) in specs' :key='index'>
<dt>{{item.name}}</dt>
<dd>
<template v-for='(tag, n) in item.values' :key='n'>
<img :class='{selected: tag.selected}' v-if='tag.picture' :src="tag.picture" alt="" >
<span :class='{selected: tag.selected}' v-else >{{tag.name}}</span>
</template>
</dd>
</dl>
</div>
</template>
绑定按钮点击事件,完成选中和取消选中
- 当前点击的是选中,取消即可
- 当前点击的是未选中,先当前规格按钮全部取消,当前按钮选中
代码如下
<template>
<div class="goods-sku">
<dl v-for='(item, i) in specs' :key='i'>
<dt>{{item.name}}</dt>
<dd>
<template v-for='(tag, n) in item.values' :key='n'>
<img :class='{selected: tag.selected}' v-if='tag.picture' :src="tag.picture" alt="" @click='toggle(tag, item.values)'>
<span :class='{selected: tag.selected}' v-else @click='toggle(tag, item.values)'>{{tag.name}}</span>
</template>
</dd>
</dl>
</div>
</template>
<script>
export default {
name: 'GoodsSku',
props: {
// 商品的规格参数
specs: {
type: Array,
default: () => []
}
},
setup () {
// 控制标签的选中和反选(保证仅仅可以选中一个标签)
const toggle = (tag, list) => {
// 处理当前点击的标签是否选中
tag.selected = !tag.selected
// 处理点击当前标签之外的其他标签的情况
list.forEach(item => {
if (item.name !== tag.name) {
// 其他标签,都编程不选中的状态
item.selected = false
}
})
}
return { toggle }
}
}
</script>
禁用效果的思路分析
大致步骤:
- 根据后台返回的skus数据得到有效(有库存)sku组合在字典中新建一个js文件
/**
* 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;
}
- 根据有效的sku组合得到所有的子集集合(笛卡尔集)
- 根据子集集合组合成一个路径字典,也就是对象。
import powerSet from '@/vendor/power-set.js'
// 生成路径字典
const usePathMap = (skus) => {
// 路径字典结果
const result = {}
// 规格分隔符
const spliter = '※'
skus.forEach(sku => {
// 过滤掉无效sku数据
console.log(sku.inventory)
if (sku.inventory === 0) return
// 获取规格的集合数据:[蓝色,中国,10cm]
const spec = sku.specs.map(item => item.valueName)
// 计算当个sku规格的笛卡尔集
const specSet = powerSet(spec)
specSet.forEach(item => {
// 排除空数组
if (item.length === 0) return
// 生成字典的key
const key = item.join(spliter)
// 把key添加到字典中
if (result[key]) {
// 字典中已经存在当前的key
result[key].push(sku.id)
} else {
// 字典中不存在当前的key
result[key] = [sku.id]
}
})
})
return result
}
参照示例
禁用效果-设置状态
目的:在组件初始化的时候,点击规格的时候,去更新其他按钮的禁用状态。
代码如下
// 获取选中的所有规格的值
const getSelectedValues = (specs) => {
// 选中的所有的规格数据
const result = []
specs.forEach((item, index) => {
// 获取规格的选中的信息
const spec = item.values.find(tag => tag.selected)
if (spec) {
// 该规格被选中了
result[index] = spec.name
} else {
// 该规格没有选中
result[index] = undefined
}
})
return result
}
// 控制规格标签是否被禁用
const updateDisabledStatus = (specs, pathMap) => {
// seletedValues = [undefined, undefined, undefined]
specs.forEach((spec, i) => {
// 每次规格的遍历,选中的值需要重新初始化
const seletedValues = getSelectedValue(specs)
spec.values.forEach(tag => {
if (tag.selected) {
// 标签本身就是选中状态,不需要处理
return
} else {
// 没有选中(初始化时,需要判断单个规格的禁用状态)
seletedValues[i] = tag.name
}
// 此时,需要判断当前的按钮是否应该被禁用
// 基于当前选中的值,组合一个路径
// 过滤掉undefined值,基于剩余的值组合一个路径
let currentPath = seletedValues.filter(item => item)
if (currentPath.length > 0) {
// 拼接路径字符串 currentPath = 黑色★10cm
currentPath = currentPath.join(spliter)
// 判断当前的路径是否在路径字典中(如果在字典中没有找到该路径,证明当前的标签应该禁用)
tag.disabled = !pathMap[currentPath]
}
// 单独判断单个按钮是否应该禁用
// tag.disabled = !pathMap[tag.name]
})
})
}
setup (props) {
const pathMap = getPathMap(props.goods.skus)
// 组件初始化的时候更新禁用状态
+ updateDisabledStatus(props.specs, pathMap)
const clickSpecs = (item, val) => {
// 如果是禁用状态不作为
+ if (val.disabled) return
// 1. 选中与取消选中逻辑
if (val.selected) {
val.selected = false
} else {
item.values.find(bv => { bv.selected = false })
val.selected = true
}
// 点击的时候更新禁用状态
+ updateDisabledStatus(props.specs, pathMap)
}
return { clickSpecs }
}
总结
这个知识也是前端工程师必会的,加油
- spu代表一种商品,拥有很多相同的属性。
- sku代表该商品可选规格的任意组合,他是库存单位的唯一标识。
下面是我的个人理解,好了,今天就到这里了,明天见