跳到主要内容

SKU选择的实现

· 阅读需 6 分钟

最近在做电商相关的项目,电商中很常见的一个功能就是关于SKU选择,一开始没当回事,做了之后发现涉及到的逻辑还挺多的,特别是对按钮的禁选状态上的计算。

渲染选项

先了解一下数据结构,一个商品SKU是包含多个属性的,这里以一台手机为例,由运营商、容量、颜色组成一个完整的SKU。

实时编辑器
// 属性列表
function SkuChoose () {
  const [currentChoose, setCurrentChoose] = useState({})
  
  const attrList = [
    {
      attr_id: 'xxx',
      attr_name: '运营商',
      attr_values: [
        { value_id: '11', value_name: '移动' },
        { value_id: '22', value_name: '电信' },
        { value_id: '33', value_name: '联通' },
      ]
    },
    {
      attr_id: 'yyy',
      attr_name: '容量',
      attr_values: [
        { value_id: '64', value_name: '64GB' },
        { value_id: '128', value_name: '128GB' },
        { value_id: '256', value_name: '256GB' },
      ]
    },
    {
      attr_id: 'zzz',
      attr_name: '颜色',
      attr_values: [
        { value_id: 'black', value_name: '黑色' },
        { value_id: 'white', value_name: '白色' },
      ]
    }
  ]
  
  const handleChoose = (attr_id, value_id) => {
    if (currentChoose[attr_id] === value_id) {
      const temp = { ...currentChoose }
      delete temp[attr_id]
      setCurrentChoose(temp)
      console.log(`取消了属性 ${attr_index}${attr_id}@${value_id}`)
      return
    }
    setCurrentChoose({ ...currentChoose, [attr_id]: value_id })
    console.log(`选中了属性 ${attr_index}${attr_id}@${value_id}`)
  }
  
  return (
    <div>
      {
        attrList.map(attr => (
          <div key={attr.attr_id}>
            <label>{attr.attr_name}</label>
            {
              attr.attr_values.map(v => (
                <button
                  key={v.value_id} 
                  onClick={() => handleChoose(attr.attr_id, v.value_id)}
                  style={{ marginRight: 10, fontWeight: currentChoose[attr.attr_id] === v.value_id ? '900' : '300' }}
                >{v.value_name}</button>
              ))
            }
          </div>
        ))
      }
      
      当前选中:{JSON.stringify(currentChoose)}
    </div>
  )
}
结果
Loading...

上面列出了属性列表,我们可以选择其中的属性组合成SKU,选中的属性会加粗显示,上面共形成了 3*3*2=18 款SKU,但实际情况是并不是每种组合都是有效的,只有有库存的SKU才是有效的属性组合。那么下面我们来看一个例子看一下真实的情况。

从SKU视角看问题

// 有效的SKU列表
const skuList = [
{
id: 1,
stock: 5,
sku_attr: [
{ attr_id: 'xxx', value_id: '11' },
{ attr_id: 'yyy', value_id: '128' },
{ attr_id: 'zzz', value_id: 'black' }
]
},
{
id: 2,
stock: 10,
sku_attr: [
{ attr_id: 'xxx', value_id: '22' },
{ attr_id: 'yyy', value_id: '128' },
{ attr_id: 'zzz', value_id: 'white' }
]
},
{
id: 3,
stock: 10,
sku_attr: [
{ attr_id: 'xxx', value_id: '22' },
{ attr_id: 'yyy', value_id: '64' },
{ attr_id: 'zzz', value_id: 'white' }
]
}
]

这里共列出了3款SKU,从SKU视角来考虑,在选择属性的时候我们需要验证当前属性是否可以被选择,比如在运营商选择了 电信 的时候,只有第一个SKU符合要求,则容量属性只有 64GB128GB 是可选的,其他按钮应该设置为禁选,同理颜色也只有 白色 是允许选择的;如果一开始选择的是 黑色 ,那其他两个属性就分别只有 移动128GB 是可选的,所以我们可以得出对每个按钮来说,和已选择的属性一起合并起来,是否可能在列表中找到符合要求的SKU即可。

生成组合选项

比如对于第一个SKU,属性构成为:

const sku_attr = [
{ attr_id: 'xxx', value_id: '11' },
{ attr_id: 'yyy', value_id: '128' },
{ attr_id: 'zzz', value_id: 'black' }
]

允许产生的选项集合有:

const option1 = { xxx: '11' }
const option2 = { yyy: '128' }
const option3 = { zzz: 'black' }
const option4 = { xxx: '11', yyy: '128' }
const option5 = { xxx: '11', zzz: 'black' }
const option6 = { yyy: '128', zzz: 'black' }
const option7 = { xxx: '11', yyy: '128', zzz: 'black' }

考虑使用以下统计方式:

const psArr = []

sku_attr.forEach(attr => {
// 对已有的选项做遍历,在队尾添加当前选项叠加当前属性形成的新选项
for (let i = 0, len = psArr.length; i < len; i++) {
const option = { ...psArr[i], [attr.attr_id]: attr.value_id }
psArr.push(option)
}
// 添加一个当前属性独立组成的选项
psArr.push({ [attr.attr_id]: attr.value_id })
})

这是一个SKU形成的选项,我们要对所有的SKU都走一遍这个流程,最后加起来就是所有的选项了,当然这里面可能会有重复选项,可以进一步作去重处理(不去重对后面的计算也没有影响),去重的方法有很多,我这里采用 Set

function uniqueArray(array) {
const unique = Array.from(new Set(array.map(item => JSON.stringify(item))));
return unique.map(item => JSON.parse(item));
}

let allPsArr = []
skuList.forEach(sku => {
const sku_attr = sku.sku_attr
const psArr = []
// ...
allPsArr = allPsArr.concat(psArr)
})

uniqueArray(allPsArr)

设置按钮状态

到这里,我们拿到了所有的选项集合,那么很容易结合按钮本身的数据来设置禁选状态:

function isDisabled (currentChoose, attr_id, value_id) {
const option = { ...currentChoose, [attr_id]: value_id }
const keys = Object.keys(option)
const isExist = allPsArr.some(item => {
if (Object.keys(item).length !== keys.length) return false
for (let i = 0; i < keys.length; i++) {
if (item[keys[i]] !== option[keys[i]]) return false
}
return true
})
return !isExist
}