首先說一說什麼是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就是從新構建的方法。