朋友问我像淘宝那样的选择商品的规格,如商品的颜色,尺寸,款式,并且还要根据后端传来的库存,哪些还有,哪些没了。咋一看不复杂,然后后面被一步一步的套牢,出不来了。最终借助 Google 的力量,完成了对这个类型的需求,有个初步的了解,并输出 blog 以记录。
什么是 sku
- Stock Keeping Unit(库存量单位)
- 定义有很多,在电商上来看,我的理解是,一件商品的基础属性组成的单品,如一件衣服的基础属性有颜色,款式,尺寸三个属性,那么这三个属性合一就组成了一个单品 sku,不同的颜色、款式、尺寸,就构成了不同的单品。
OK,对于第一次接触的同学来说,让我们从开始了解商品规则和其选择吧~
我是从数据存储到前端展示这样一个流程,开始分享的。(如果对后端感兴趣的话可以看前两段
在数据库中的存储
在搜索中看到不少设计,太过复杂的方式我还没看懂,不过还是看到一个分了几个表建立的,感觉容易理解些。
建表:
- 商品表:主键是商品 ID、自增,还有商品的 name 字段。
- 属性表:一件商品都有哪些属性,主键是属性 ID、自增,还要有属性的 name 字段。同时可以和商品表关联
- 属性值表:一个属性都有哪些取值,主键是属性值 ID、自增,还要有属性值的 name 字段。同时和属性表关联
- sku 表:由上面 3 个表可以构成 sku 表,主键是 sku_id、自增,可以有 sku_value,sku_sku_code,stock,price 字段。
后端返回数据
返回数据的话,比如针对某个商品返回它的 sku,那么理想的是返回三个字段,一个是属性对象,一个是属性值对象,一个这个商品的 sku 对象。
不过我示例中的返回数据,不是这样,所以要进一步处理成前端需要的格式。
思路部分
- 首先,将商品的属性和属性值,展示出来,比如颜色下有蓝色、红色,款式下有经典款和流行款,尺寸有 M 和 L。
- 当选择一个属性后,需要判断和其他属性构成的 sku,库存是否为空,为空的话是要置灰或者提示没有库存了。
- 选择用对象存储不同的 sku 的库存,性能是否好点。
- 对已选择的属性,进行其他属性循环填充,如果没库存,就可以置灰其他属性。
处理数据+代码规划
朋友给的数据对象,有三个字段,分别是该商品的 shopSkuVo(sku 对象),shopMainSpecVo(属性对象),shopChildVoList(属性值对象)
- 那么,首先我要将属性对象和属性值对象,取出来,得到一个规格对象,如:
let mainSpec = [
{
id: 1,
name: '颜色',
attr: [
{
id: 1,
name: '蓝色',
},
{
id: 2,
name: '白色',
},
],
},
{
id: 2,
name: '尺寸',
attr: [
{
id: 3,
name: 'M',
},
{
id: 4,
name: 'L',
},
],
},
];
- 得到这样的规格字段后,就可以借助 Vue 中的 v-for,进行 dom 渲染了。
- 考虑到用对象来存储 sku 的数据,这里我根据数据的特色,使用 sku_value 来做对象的 key,对应的值就是这条 sku 数据。如:
let skuObj = {
'红色/M/经典款': {
id: 1, // sku_id
sku_value: '红色/M/经典款',
stock: 20,
price: '33.0',
},
'红色/M/流行款': {
id: 4, // sku_id
sku_value: '红色/M/经典款',
stock: 330,
price: '53.0',
},
};
- 核心思路,每次用户点击一个属性后,将该属性和其他点击过的属性,以及还没点击的属性,组合起来,检查库存数量,进而判断还没点击的属性,是否置灰显示。
代码实现
后端数据格式展示:(基于篇幅,我只展示了每种的前两条数据)
let data = {
shopSkuVo: [
{
skuId: '38',
goodsId: null,
specNames: 'S/红/乞丐款',
specIds: null,
skuNo: null,
skuImg: 'C30000/1/img/20190322/37ff02207d0349489a90a3f7d4ff578d',
skuPrice: null,
stock: 15,
enabled: null,
goodSkuSpecDetails: null,
classifyList: null,
},
{
skuId: '39',
goodsId: null,
specNames: 'S/红/爆炸款',
specIds: null,
skuNo: null,
skuImg: 'C30000/1/img/20190322/37ff02207d0349489a90a3f7d4ff578d',
skuPrice: null,
stock: 14,
enabled: null,
goodSkuSpecDetails: null,
classifyList: null,
},
],
shopMainSpecVo: [
{
skuId: '38',
mainSpecId: '1',
mainSpecMerchantId: null,
mainSpecName: '尺寸',
mainSpecSort: null,
mainSpecPid: '0',
goodSkuSpecDetailsID: '6742da9e002b496e8058c8cebaf292f2',
},
{
skuId: '38',
mainSpecId: '12',
mainSpecMerchantId: null,
mainSpecName: '颜色',
mainSpecSort: null,
mainSpecPid: '0',
goodSkuSpecDetailsID: 'df0f08465d9f43cf8f951ed79743cea5',
},
],
shopChildVoList: [
{
skuId: '38',
ChildId: '52',
ChildMerchantId: null,
ChildSpecName: 'S',
ChildSpecPid: '1',
goodSkuSpecDetailsID: '6742da9e002b496e8058c8cebaf292f2',
},
{
skuId: '38',
ChildId: '13',
ChildMerchantId: null,
ChildSpecName: '红',
ChildSpecPid: '12',
goodSkuSpecDetailsID: 'df0f08465d9f43cf8f951ed79743cea5',
},
],
};
Vue 实例绑定的 template 模板:
<div v-for="(shopMain, rowIndex) in mainSpec" :key="rowIndex">
<span>: </span>
<ul class="item clearfix">
<li
v-for="(cItem, colIndex) in shopMain.item"
@click="clickButton($event, cItem.name, rowIndex, colIndex)"
:class="[cItem.enabled?'':'disabled',subIndex[rowIndex] == colIndex?'checked':'']"
>
</li>
</ul>
</div>
Vue 实例中定义的 data 字段:
data() {
return {
shopSkuVo: data.shopSkuVo, // 本地我将后端返回的数据放到一个js中了,用import引入。
shopMainSpecVo: data.shopMainSpecVo,
shopChildVoList: data.shopChildVoList,
// sku 对象,
skuObj: {},
// 规则属性 数组对象
mainSpec: [],
// 选择的 属性 数组和其下标,用来进行stock判断和前端样式控制。
selectItemArr: [],
subIndex: [],
}
},
```
处理 sku 数据
- 对属性对象和属性值对象进行数据处理,得到一个规则对象。对 sku 对象进行数据处理,得到 sku,商品 value 为 key 的 sku 对象。
created() {
this.shopMainSpecVo = this.deduplication(this.shopMainSpecVo, 'mainSpecId')
this.shopMainSpecVo.forEach(s => {
let obj = {}
obj.name = s.mainSpecName
obj.item = []
this.filterShopByKey(s.mainSpecId).forEach(n => {
obj.item.push({name: n})
})
this.mainSpec.push(obj)
})
this.skuObj = this.getObjByKey(this.shopSkuVo, 'specNames')
this.checkItem()
},
methods: {
deduplication(arr, key) {
let obj = {}, result = []
arr.forEach(a => {
if (!obj.hasOwnProperty(a[key])) {
obj[a[key]] = a
}
})
for (let k in obj) {
result.push(obj[k])
}
return result
},
filterShopByKey(key) {
return Array.from(new Set(this.shopChildVoList.filter(f => f.ChildSpecPid === key).map(m => m.ChildSpecName))).sort()
},
getObjByKey(arr, key) {
let obj = {}
arr.forEach(a => {
obj[a[key]] = a
})
return obj
},
// 属性值按钮点击,控制点击元素高亮、同属性的其他属性值去高亮,并进行检查。
clickButton(e, item, rowIndex, colIndex) {
if (this.selectItemArr[rowIndex] !== item) { // 如果点击当前属性中 未勾选的按钮,则取消该属性别的按钮勾选,且记录下来。
this.selectItemArr[rowIndex] = item
this.subIndex[rowIndex] = colIndex
} else { // 如果是点击当前属性中 已勾选的按钮,则取消勾选
this.selectItemArr[rowIndex] = ''
this.subIndex[rowIndex] = -1
}
this.checkItem()
},
checkItem() {
// 临时 存放 选择属性值的数组。
let tmpSelectArr = []
// 将已经勾选的属性,存入临时数组里。
this.mainSpec.forEach((m, index) => {
tmpSelectArr[index] = this.selectItemArr[index] ? this.selectItemArr[index] : ''
})
// 对已勾选属性进行补充,即如果已经选了颜色和尺寸了,那么模拟用户选择款式,目前是暴力循环。在选择完后,调用isEnabled方法,判断补充的属性值是否可以点击。
this.mainSpec.forEach((m, mIndex) => {
let last = tmpSelectArr[mIndex]
m.item.forEach((s, sIndex) => {
tmpSelectArr[mIndex] = s.name
s.enabled = this.isEnabled(tmpSelectArr)
})
tmpSelectArr[mIndex] = last
})
this.$forceUpdate() //重绘
},
// 判断属性组合之后,添加的属性是否可以点击
isEnabled(arr) {
for (let i in arr) {
if (arr[i] === '') return true // 如果添加的属性中,有空的,直接返回true。
}
return this.skuObj[arr.join('/')].stock === 0 ? false : true
},
}
样式适配状态
- template 的样式,其中一个 css 属性非常棒,是 pointer-events: none;,这个属性可以阻止当前元素的事件触发,再搭配 cursor 和 background-color,就可以实现不可点击的交互。
.item {
display: inline-block;
}
.item li {
float: left;
padding: 5px 10px;
margin-right: 5px;
background-color: #c9e9ed;
cursor: pointer;
}
.item li.checked {
background-color: orange;
}
.item li.disabled {
background-color: #cccccc;
cursor: not-allowed;
pointer-events: none;
}
文档信息
- 本文作者:Bran.nie
- 本文链接:https://bran-nie.cn/2019/05/07/sku/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
Be the first person to leave a comment!