首先说一说什么是SKU。。。。。。。本身百度去。。。javascript
相似京东上面,将来人类S5这个台笔记本(没错,我刚入手了)css
都是S5这个型号,可是由于CPU,显卡,内存,硬盘等不一样,价格也不同。CPU,显卡,内存,硬盘等属性组合成的一个惟一的商品,就能够用一个SKU来表示,像图上就有10个SKU。一系列的SKU能够归到一个SPU下进行管理。前端
那么一个SKU是怎么生成的呢?下面结合本身的一些经验,说说一些电商平台的大体产品结构以及SKU的生成方式。vue
1.阿里速卖通平台,阿里国际站java
这两个平台同一个爸爸,基本差很少。要建立一个商品须要先选一个类目,类目下面挂了一堆的属性,属性上又挂了一堆的属性值。属性分为销售属性和非销售属性(销售属性就是相似颜色,尺寸这些单个SKU独有的,非销售属性就是多个SKU共有的,好比同一个品牌型号“将来人类S5”)。非销售属性有必填和非必填,能够是单选,多选,文本等。销售属性就是构成SKU的关键。好比说有销售属性颜色和尺寸,颜色属性下有不少属性值(红,黄,蓝等等),尺寸(1,2,3,4等等)也是。当颜色选了红,黄,尺寸选了1,2,那么就应该生成2x2=4个SKU,每一个SKU有各自的价格,库存等。保存SKU的时候会与对应的销售属性相关联。
大体的数据模型以下算法
固然,实际比这更加复杂(好比产品的图片,单个SKU的图片,多个SKU共同的图片,非销售属性能够自定义添加分类不具备的。。。)数据库
2.eBay
跟上面两个平台相似,建立一个产品也要先选一个分类,分类下面也是有不少属性,属性有不少属性值。。。,不一样的地方是eBay没有区分销售属性和非销售属性(或者说所有是非销售属性),也容许添加自定义属性和属性值。eBay上SKU是手动添加的,SKU上的属性(SKU上的属性暂且都叫作销售属性)也是自定义的。好比说添加了一个SKU A,价格和数量这两个是必须的,还能够手动加个颜色属性,而后填上属性值红色。固然,若果增长一个SKU B,那么这个SKU也会有颜色这个属性,属性值能够自定义。最后校验这些SKU的属性值组合起来是否惟一。django
这样的优点就是能够随意控制SKU的数量,若是按照上面两个平台的规则,这里应该有4x4x1=16个SKU。这样自由度更高会不会使结构更加复杂呢?固然是NO,用上面的数据模型就能够搞定后端
3.Lazada,Linio平台
国际惯例,先选一个类目,类目下面一堆属性,有必填和非必填,属性下一堆属性值。最大的区别就是,一个产品只有一个SKU,属性不支持自定义。好比说要添加一个商品,有两种颜色,那么只能建立两个产品,这两个产品可能只有图片不同(颜色不同,可能没有颜色这个属性来选)数组
4.Wish平台
比较奇特,没有分类,产品有固定的属性,好比标题,描述,运费等。一个产品下能够有多个SKU,SKU的生成取决于固定的两大类属性,两个大类为:颜色和尺寸。好比颜色这个属性就是归类于颜色这个类,其余的属性(品牌,型号,容量等等归为尺寸这个大类)。尺寸类的属性值支持自定义,但只能选一个尺寸类属性(好比选了品牌就不能选容量,选了品牌后能够添加任意值)。两类属性不是必须同时存在,好比颜色选了红,能够不选尺寸类的属性,反之也同样。
忘记说了,一个SKU是靠一串惟一编码来标识的,好比1234A,1234B。通常来讲一个平台下不会存在两个相同的SKU,或这一个店铺下不会存在两个相同的SKU。
大体的逻辑和数据模型就这些,接下来讲说开发实现方面。
数据库大体就依靠上面的数据模型进行设计,编辑的时候,后端按照这些关联关系取出数据给到前端(我这边先后端未分离,页面仍是后端渲染,但我仍是把数据格式化为JSON再渲染到前端),保存的时候再进行相关的逻辑校验。由于后端的一些逻辑操做涉及后公司内部的业务,这里就不细说了。说说前端的具体细节,以速卖通的为例,用的是vue,前端拿到的数据以下
实现后的粗糙界面
data: { properties: properties, skus: skus }
遍历properties,获得材质,颜色,发货地,套餐这些属性对象,接着遍历这些对象里的values属性,获得属性值对象,根据属性对象的selectedValues判断属性值是否选上(由于我是后端渲染的js变量,因此初始化的时候selectedValues里的数据直接引用的属性值对象,若是是非后端渲染的话,要根据skus里的属性和属性值去初始化selectedValues的数据,而且存的是属性值对象的引用)
<tr v-for="(index,item) in properties"> <td><strong>{{item.Name}}:</strong></td> <td> <label v-for="value in item.values"><input type="checkbox" :value="value" v-model="item.selectedValues"/>{{value.Name}}</label> <table class="list_table" v-if="item.Name!='发货地'&&item.selectedValues.length>0"> <tbody> <tr> <th>{{item.Name}}</th> <th>自定义名称</th> <th v-if="item.Name=='颜色'">图片(无图片能够不填)</th> </tr> <tr v-for="selectedValue in item.selectedValues"> <td>{{selectedValue.Name}}</td> <td> <input type="text" v-model="selectedValue.DefinitionName" maxlength="20"/> </td> <td v-if="item.Name=='颜色'"> <div style="float: left"> <input type="file" style="width: 63px;"/> </div> <div style="float: right"> <a href="" rel="link" target="_blank"> <img :src="selectedValue.ImageUrl" width="30" height="35"/> </a> </div> </td> </tr> </tbody> </table> </td> </tr>
由于selectedValues经过v-model绑定,当选中或取消一个属性值的时候后,selectedValues也会随着改变,selectedValues里的数据是直接引用属性值而不是拷贝一份数据,因此修改selectedValues中的数据也会直接反映到属性值上,实现了属性值的自定义。
那么怎么根据选中的属性值生成SKU呢?
SKU表格处的表头是要根据选中的属性动态更新的,能够这样作
<tr> <th v-for="item in properties" v-if="item.selectedValues.length>0">{{item.Name}}</th> <th><span class="c_red">*</span>零售价</th> <th><span class="c_red">*</span>库存</th> <th>商品编码</th> </tr>
若是属性里的属性值都没有被选中(selectedValues.length==0),就不在表头显示这个属性。
SKU的初始显示
<tr v-for="sku in skus"> <td v-for="item in properties" v-if="item.selectedValues.length>0">{{getValueName(sku,item)}}</td> <td>US $<input type="text" v-model="sku.SkuPrice" class="w50" maxlength="9"/><span name="productUnitTips"></span></td> <td><input type="text" v-model="sku.StockQuantity" class="w50" maxlength="9"/></td> <td><input type="text" v-model="sku.SkuCode" class="w180" maxlength="20"/></td> </tr>
也是利用selectedValues.length让SKU的属性值列数与表头列数保持一致。由于SKU对象里的保存的是属性值Id和属性Id,须要一个方法去获取属性值的值
getValueName: function (sku, property) { var valueName = ""; $.each(sku.values, function () { var _this = this; if (this.propertyId == property.Id) { $.each(property.selectedValues, function () { if (_this.valueId == this.Id) { valueName = this.Name; return false; } }); } }); return valueName; }
你没有看错,这是JQ。。。
接下来就是SKU表格的更新了,个人作法是变动整块区域,就是给skus从新赋值。赋的新值从哪来呢?
将选中的属性值放到一个数组中
var ori = []; $.each(vm.properties, function (index, item) { var selectValues = this.selectedValues; if (selectValues.length > 0) { ori.push(selectValues); } });
获得这种结构的数组
[
[
{
'PropertyId': 10, 'Id': 477, 'Name': '铝', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 10, 'Id': 529, 'Name': '帆布', 'DefinitionName': '', 'ImageUrl': '' } ], [ { 'PropertyId': 200000828, 'Id': 201655809, 'Name': '壳+贴膜', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 200000828, 'Id': 201655810, 'Name': '壳+挂绳', 'DefinitionName': '', 'ImageUrl': '' } ] ]
求笛卡尔积后(后面有求笛卡尔积参考连接)
var ret = descartes(ori);
[
[
{
'PropertyId': 10, 'Id': 477, 'Name': '铝', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 200000828, 'Id': 201655809, 'Name': '壳+贴膜', 'DefinitionName': '', 'ImageUrl': '' } ], [ { 'PropertyId': 10, 'Id': 477, 'Name': '铝', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 200000828, 'Id': 201655810, 'Name': '壳+挂绳', 'DefinitionName': '', 'ImageUrl': '' } ], [ { 'PropertyId': 10, 'Id': 529, 'Name': '帆布', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 200000828, 'Id': 201655809, 'Name': '壳+贴膜', 'DefinitionName': '', 'ImageUrl': '' } ], [ { 'PropertyId': 10, 'Id': 529, 'Name': '帆布', 'DefinitionName': '', 'ImageUrl': '' }, { 'PropertyId': 200000828, 'Id': 201655810, 'Name': '壳+挂绳', 'DefinitionName': '', 'ImageUrl': '' } ] ]
大前端也用上了算法有木有,这里须要弄明白拿到的是什么数据,须要的是什么数据,而后就去想实现就OK了。
想要的数据已经拿到,从新构建skus
for (var i = 0; i < ret.length; i++) { var sku = {SkuCode: "", SkuPrice: "", StockQuantity: ""}; sku.values = []; $.each(ret[i], function () { sku.values.push({propertyId: this.PropertyId, valueId: this.Id}); }); vmSkus.push(sku); }
到此,更新SKU表格的代码已经实现,数据驱动视图更新,很清晰。可是何时去触发这个更新呢(什么时候去从新构建skus)? 很简单嘛,就是勾选或取消勾选属性值的时候去触发更新操做。勾选或取消勾选咱们能直接从selectedValues.length上获得反馈,而后使用vue 的watch就能够实现了。可是selectedValues是properties数组中元素的一个属性,vue的watch是没法用在数组元素的某一个字段上的(至少目前我发现是这样的),那么暴力一点,直接watch整个properties数组而且加上deep:true。这样是能够实现,可是当修改自定义属性的时候也会触发变动(业务会提刀来见的)。
最终解决方案
computed:{
allCheckedLength:function(){ var length=0; $.each(this.properties,function(){ length+=this.selectedValues.length; }); return length; } }
watch: { 'allCheckedLength': { handler: 'reBuild' } }
reBuild就是从新构建的方法。