SKU商品规格属性-笛卡尔乘积算法

黄粱一梦2024-05-3034

前言

只要是做电商类相关的产品,比如购物 APP、购物网站等等,都会遇到这么一个场景,每个商品对应着多个规格,用户可以根据不同的规格组合,选择出自己想要的产品。
我们自己在生活中也会经常用到这个功能,然而就是这样一个看似简单的商品多规格属性组合算法,在电商类业务中却是比较复杂的一块内容了。

商品选购图片

通过上图我们就能发现。商品多属性的集合通常是通过将各个规格的取值组合在一起,生成一系列 SKU。这些 SKU 可以唯一地标识一个商品,包括其多属性规格信息,例如颜色、尺码、款式等。

使用笛卡尔乘积算法

商品多属性SKU集合计算公式通常是:
SKU = 属性1选项值 + 属性2选项值 + …+ 属性n选项值
例如,假设一个T恤有三个属性:颜色、尺码和材质。颜色有红色、蓝色和绿色可选,尺码有S、M、L可选,材质有棉质和涤纶可选。

const combination = [
  { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
  { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
  { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
]

通过属性匹配就会存在以下几种SKU:

const skuList = [
  {'红色', 'S', '棉质'}, {'红色', 'S', '涤纶'}, {'红色', 'M', '棉质'}, 
  {'红色', 'M', '涤纶'}, {'红色', 'L', '棉质'}, {'红色', 'L', '涤纶'}, 
  {'蓝色', 'S', '棉质'}, {'蓝色', 'S', '涤纶'}, {'蓝色', 'M', '棉质'}, 
  {'蓝色', 'M', '涤纶'}, {'蓝色', 'L', '棉质'}, {'蓝色', 'L', '涤纶'}, 
  {'绿色', 'S', '棉质'}, {'绿色', 'S', '涤纶'}, {'绿色', 'M', '棉质'}, 
  {'绿色', 'M', '涤纶'}, {'绿色', 'L', '棉质'}, {'绿色', 'L', '涤纶'}
]

那么,我们应该怎么把 combination 通过计算成 skuList 呢?
其实很简单,我们可以使用 笛卡尔乘积算法
在数学中,笛卡尔积可以从多个集合中分别选取一个元素,进行组合的操作,生成一个新的集合。
设A,B为一个集合,将A中的元素作为第一个元素,B中的元素作为第二个元素,形成有序对。所有这些有序对都由一个称为a和B的笛卡尔积的集合组成,并被记录为AxB。

// 假设已经有的集中属性
const combination = reactive([
  { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
  { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
  { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
//   { name:'品牌',  id:'pinpai',list:['三星','华为','苹果','小米','XF'] }
])

let skuList = reactive([])

// 需要根据笛卡尔算法计算出各种排列组合
function cartesianProduct(){
    const array = combination.map(item => item.list.map(v => { return ({ name:item.name,val:v,id:item.id }) }))
    console.log('array',array);

    const data = []

    // 上述已经把当前的数据组合为[{name:"颜色",val:"红色"},{name:"颜色",val:"蓝色"}....]格式

    let func = function(...arrays){
        console.log('arrays',arrays);
        return arrays.reduce((acc, curr) => {
            const result = [];
            acc.forEach(x => {
            curr.forEach(y => {
                result.push([...x, y]);
            });
            });
            return result;
        }, [[]]);
    }
    console.log('....',...array);
    
    let newList = func(...array)

    return newList
}
skuList = cartesianProduct()
console.log('skuList',skuList);

判断规格是否有存货

在电商系统中,一般会有一个商品的 SKU 列表,每个 SKU 都代表着一种具体的产品规格和库存量。
当用户选择某一个规格时,需要遍历所有可能的 SKU,找到与该规格组合匹配的 SKU,并检查该 SKU 的库存情况。如果有库存,则该规格可以被勾选;否则,该规格应该被禁用或者显示为无法选择的状态。

如上 combination 表可知,当前产品有三个规格:颜色、尺码、材质。
其中,颜色和尺寸和材质之间存在依赖关系,即不同的颜色对应不同的尺寸和材质。
此外,每个 SKU 都会对应着特定的库存量。

当用户选择颜色为红色时,需要遍历所有SKU,找到颜色为红色的SKU,并获取该SKU对应的尺寸。
接下来,需要检查所有库存量大于0的SKU,看看它们中是否存在尺寸与当前选择的尺寸相同的SKU。
如果存在,则该尺寸可以被勾选;否则,该尺寸应该被禁用或者显示为无法选择的状态。
我们把 cartesianProduct 函数稍微改一下,加入库存

判断是否有存货,加入库存

import { reactive,ref } from 'vue'


// 假设已经有的集中属性
const combination = reactive([
  { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
  { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
  { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
//   { name:'品牌',  id:'pinpai',list:['三星','华为','苹果','小米','XF'] }
])

let skuList = reactive([])

// 需要根据笛卡尔算法计算出各种排列组合
function cartesianProduct(){
    const array = combination.map(item => item.list.map(v => { return ({ name:item.name,val:v,id:item.id }) }))
    console.log('array',array);

    const data = []

    // 上述已经把当前的数据组合为[{name:"颜色",val:"红色"},{name:"颜色",val:"蓝色"}....]格式

    let func = function(...arrays){
        console.log('arrays',arrays);
        return arrays.reduce((acc, curr) => {
            const result = [];
            acc.forEach(x => {
            curr.forEach(y => {
                result.push([...x, y]);
            });
            });
            return result;
        }, [[]]);
    }
    console.log('....',...array);
    
    let newList = func(...array)

    return newList.map(item => {
        const attributesValue = {}

        item.forEach(({id,val}) => {
            attributesValue[id] = val
        })

        return {
            attributes:item, // { id:"color",name:"颜色",val:"红色" }
            attributesValue,
            SKU:item.map(i => i.val).join(),
            stock: Math.floor(Math.random() * 6),
            price:1001
        }
    })
}
skuList = cartesianProduct()
console.log('skuList',skuList);

页面布局

<template>

    <h2>sku</h2>
    <div>
        <div v-for="item in combination">
            <div class="left">
                {{ item.name }}:
                <button :disabled="!checkIsNoStock(item.id,btnValue)" :class="[attributesValue[item.id] == btnValue ? 'is-checked' : '']"  @click="checkedHandler(item.id,btnValue)" style="margin: 5px;" v-for="(btnValue,index) in item.list" >{{ btnValue }}</button>
            </div>
        </div>
        <button style="padding: 10px 20px;border-radius: 0px;background-color: rgba(255, 38, 0, 0.644);color: #fff;width: 200px;margin-top: 50px;margin-left: calc(100% - 200px);cursor: pointer;" @click="submitSku">提交</button>
    </div>
</template>

<style lang="css" scoped>
button {
    background-color: transparent;
    border: 1px solid #ececec;
    border-radius: 20px;
    padding: 3px 15px;
}
.is-checked {
    color: blue;
    border-color: blue;
}
</style>

布局效果

image.png

加入函数校验当前按钮是否可以点击

如果需要实现每选中一个规格,其他依赖此规格的组合是否有存货,我们还需要添加一个函数计算是否可选
在js中添加函数 checkInventory

// 判断当前按钮是否可以点击 是否有库存
let checkIsNoStock = (id,value) => {
    let newAttributesValue = { ...attributesValue,[id]:value }
    for(const sku of skuList){
        let match = true
        for(const [key,v] of Object.entries(newAttributesValue)) {
            if(v && sku.attributesValue[key] !==v) {
                match = false
                break
            }
        }
        if(match&&sku.stock >0){
            return true
        }
    }
    return false
}

完整代码

<script setup>
import { reactive,ref } from 'vue'


// 假设已经有的集中属性
const combination = reactive([
  { name: '颜色', id:'color', list: ['红色', '蓝色', '绿色'] },
  { name: '尺码', id:'size', list: ['S', 'M', 'L'] },
  { name: '材质', id:'texture', list: ['棉质', '涤纶'] },
//   { name:'品牌',  id:'pinpai',list:['三星','华为','苹果','小米','XF'] }
])

let skuList = reactive([])

// 需要根据笛卡尔算法计算出各种排列组合
function cartesianProduct(){
    const array = combination.map(item => item.list.map(v => { return ({ name:item.name,val:v,id:item.id }) }))
    console.log('array',array);

    const data = []

    // 上述已经把当前的数据组合为[{name:"颜色",val:"红色"},{name:"颜色",val:"蓝色"}....]格式

    let func = function(...arrays){
        console.log('arrays',arrays);
        return arrays.reduce((acc, curr) => {
            const result = [];
            acc.forEach(x => {
            curr.forEach(y => {
                result.push([...x, y]);
            });
            });
            return result;
        }, [[]]);
    }
    console.log('....',...array);
    
    let newList = func(...array)

    return newList.map(item => {
        const attributesValue = {}

        item.forEach(({id,val}) => {
            attributesValue[id] = val
        })

        return {
            attributes:item, // { id:"color",name:"颜色",val:"红色" }
            attributesValue,
            SKU:item.map(i => i.val).join(),
            stock: Math.floor(Math.random() * 6),
            price:1001
        }
    })
}
skuList = cartesianProduct()
console.log('skuList',skuList);


const attributesValue = reactive({
  color:'',
  size:'',
  texture:'',
})

let checkedHandler = (id,value) => {
    attributesValue[id] = value
    console.log('id',id,value);
}
// 判断当前按钮是否可以点击 是否有库存
let checkIsNoStock = (id,value) => {
    let newAttributesValue = { ...attributesValue,[id]:value }
    for(const sku of skuList){
        let match = true
        for(const [key,v] of Object.entries(newAttributesValue)) {
            if(v && sku.attributesValue[key] !==v) {
                match = false
                break
            }
        }
        if(match&&sku.stock >0){
            return true
        }
    }
    return false
}

let submitSku = () => {
    console.log('提交的商品sku',attributesValue);
}

</script>


<template>

    <h2>sku</h2>
    <div>
        <div v-for="item in combination">
            <div class="left">
                {{ item.name }}:
                <button :disabled="!checkIsNoStock(item.id,btnValue)" :class="[attributesValue[item.id] == btnValue ? 'is-checked' : '']"  @click="checkedHandler(item.id,btnValue)" style="margin: 5px;" v-for="(btnValue,index) in item.list" >{{ btnValue }}</button>
            </div>
        </div>
        <button style="padding: 10px 20px;border-radius: 0px;background-color: rgba(255, 38, 0, 0.644);color: #fff;width: 200px;margin-top: 50px;margin-left: calc(100% - 200px);cursor: pointer;" @click="submitSku">提交</button>
    </div>
</template>

<style lang="css" scoped>
button {
    background-color: transparent;
    border: 1px solid #ececec;
    border-radius: 20px;
    padding: 3px 15px;
}
.is-checked {
    color: blue;
    border-color: blue;
}
</style>

项目演示地址

Cloud Studio Template

分类:前端Vue

标签:前端vue随笔

上一篇MongoDB 设置用户/密码以访问数据库下一篇Vue3图片上传剪裁vue-picture-cropper

版权声明

本文系作者 @黄粱一梦 转载请注明出处,文中若有转载的以及参考文章地址也需注明。\(^o^)/~

Preview