电商经典的sku选择v2优化版

迭代

最近公司又要做新的业务需求,所以sku选择重新写一次,比起之前的做法,更加简洁、逻辑更清晰。(以前的sku选择旧文章直接从博客中删除了,当前文章即为最优版本)

业务说明

电商业务中,商品的规格选择是最常见的操作,如果规格比较少,完全可以使用扁平化的一维数组完全展示,例如红色+128G+6G内存

但我们平台的部分商品,规格非常多,可能交叉握手后达到几十上百种,这时候就不能展示组合后的选择,因为太多了,用户直接看花眼;

这种情况,只能是下图中类似于淘宝的规格选择,每次交互操作,能选择的进行点亮,没有库存的(比如任何包含丝绸材质的)直接置灰禁用;

部分规格,比如红色 大 纯棉是没有库存的,但是红色 小 纯棉红色 中 纯棉又有库存,这种情况,在选择部分规格后,最终还未选择的规格,也要置灰处理,如图操作;

交互由前端全部完成;
操作演示

以下为了能清晰解释和说明,我将数据和页面单独提出来,做了一个简单的DEMO来演示,DEMO源码也放在了github上,具体的实现过程可以直接看源码:github源码地址
DEMO演示

方法思路

数据结构

先看一下服务端返回的原始数据结构:

properties:规格的属性,比如颜色(红黄蓝绿)尺寸(小中大)型号(abcd),只包含了基础的规格类目信息,前端的布局也使用properties数组进行渲染;
其中attr_id是规格的属性id,即颜色这个类目的id,attribute_id是规格的具体属性值id,即红色这个属性值的id;
服务端返回的数据结构

skuData:服务端将所有子规格交叉组合给出的枚举,比如:红色,小,a,并且每种组合都明确给出了库存数量;
可以看到,skuData中的properties,是properties中的attr_idattribute_id的组合,比如颜色attr_id2000532红色attribute_id2006533,那么properties就是2000532:2006533

stock是库存数量,如果为0的话,肯定就是无库存,要进行置灰禁用;

重点:什么叫有效规格?什么时候该置灰禁用?

重点部分来了,这部分是整个sku选择的核心,也是最难的部分,如果理解了这个,那么整个sku选择就很简单了;

先来理解清楚概念,后续再讲代码逻辑;
初始化置灰

先看初始化的情况,为什么丝绸这一项,一开始就置灰了?可以看skuData的数据,凡是包含丝绸这一项的规格,库存都是0,所以一开始,认为丝绸就是无效的,要被禁用;
反之,某一个规格项,只要在skuData至少有一项包含它,并且库存大于0,那么就是有效的规格;

例如:点击选了了部分规格,例如红色;小,那么只要红色;小skuDataproperties子集,并且库存大于0,就是有效规格;

规格项点击时的交互

当用户点击某些规格项时,某些规格也会被禁用,例如:我选择了红色;中时,绿色丝绸3个规格项是被禁用的,为什么操作过程中,某些项就被禁用了?
部分被禁用

因为每操作一步,都会有已被选择项,我们需要将已被选择项套用到每一个规格上,组成一个预检查规格值,在skuData中,校验该值是否有效,例如:
部分选择

左边的图,就是组合后的预检查规格值红色 -> 红色;红色;中黄色 -> 黄色;红色;中… …但这样组合,肯定存在一些问题,某些同一类的规格重复了

过滤后,正确的检查项应该是右图:红色 -> 红色;中黄色 -> 黄色;中

也就说,绿色;中红色;大红色;中;丝绸,这3种情况,在skuData中,只要是包含它们的,都没有库存了,所以要被置灰

思路总结

每次计算时,先取得已被选择项,然后遍历properties,将已被选择项套用到每一个规格上,组成预检查规格值,在skuData中,校验该值是否有效,如果无效,就将该规格项置灰禁用;

代码具体实现

properties初始化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
properties.forEach(item => {
//以 key-value的形式,初始化默认值, 如 颜色:'xxx',尺码:'xxx' ... 实际业务中推荐使用attr_id作为key,避免汉字可能出现重复的问题
selectedSku[item.attr_name] = ''

item.attr_id = +item?.attr_id || 0 //格式化为Number类型

item.values.forEach(it => {
it.attr_id = +item?.attr_id || 0 //父级的id值
it.attr_name = item.attr_name //父级的规格类名称
it.attribute_id = +it?.attribute_id || 0//格式化为Number类型
it.com_id = `${it?.attr_id}:${it?.attribute_id}` //规格id组合值,`父级attr_id 拼接 自身的attribute_id`
})
})

重点在于selectedSku,这个对象,是用来存储已被选择项的,每次操作时,都会更新这个对象;
初始化默认状态
选择了部分规格
selectedSku内的value值,就是每个小规格的com_id (即 父级attr_id 拼接 自身的attribute_id)

skuData初始化处理

1
2
3
skuData.forEach(it => {
it.properties_arr = it.properties.split(',')
})

仅仅将properties处理为数组形式,方便后续的处理;
skuData处理

规格计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @description: 规格计算,遍历所有规格,将无效的规格设置为disabled=1
*/
const calcSku = () => {
let properties = CloneDeep(state.properties)

properties.forEach(item => {
item.values.forEach(it => {
const is_effect = isEffectSku(it.attr_name, it.com_id)
it.disabled = is_effect ? 0 : 1
})
})

console.log(`经过计算,新的properties ->`, properties)
state.properties = properties
}

以上的代码都非常好理解,遍历properties,如果为无效,那么就将disabled置为1,重点在于isEffectSku这个方法

isEffectSku方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @description: 验证某一个规格,是否为有效的规格(此规格存在并且库存>0)
*/
const isEffectSku = (key, value) => {
const skuData = CloneDeep(state.skuData)
let selectedSku = CloneDeep(state.selectedSku) //当前已被选择的规格
selectedSku[key] = value //覆盖后,已被选择的规格
let selectedSkuArr = Object.values(selectedSku).filter(it => it.length > 0) //将对象处理为数组形式,并过滤其中的空项

//判断数组A是否为数组B的子集
const _isChildArr = (listA, listB) => {
if (listA.length === 0 || listB.length === 0) return false
return listA.every(it => listB.includes(it))
}

//是skuData的子集 && 库存>0,就认为是有效规格
const has_stock = skuData.some(it => _isChildArr(selectedSkuArr, it.properties_arr) && +it?.stock > 0)
return has_stock
}

每一次规格点击时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @description: 规格项点击
*/
const skuItemClick = (item) => {
const { disabled, attr_name, com_id } = item
let selectedSku = CloneDeep(state.selectedSku)
if (+disabled === 1) {
console.log('该项已被禁用,return')
return
}

console.log(`点击项为 ->`, { ...item })

//如果当前项已被选中,那么置为空取消选中;如果当前项未被选中,那么添加该项的com_id进行选中
selectedSku[attr_name] = selectedSku[attr_name] === com_id ? '' : com_id

state.selectedSku = selectedSku
console.log(`新的 selectedSku ->`, selectedSku)

calcSku()//重新执行计算
}

每次规格点击时,如果点击的规格无效,直接return不作任何处理;如果有效,仅做点亮和取消点亮的操作,最后重新执行计算;

最后

规格选择看似很复杂,但只要理解清楚原理,理清思路,整体实现还是非常简单的,希望这篇文章能帮助到你;

DEMO源码也放在了github上,具体的实现过程可以下载下来直接看代码:github源码地址


电商经典的sku选择v2优化版
https://liujiaweb.cn/posts/39305.html
作者
Liu Jia
发布于
2023年10月24日
许可协议