摘要:最小庫存管理單元可以區分不同商品銷售的最小單元,是科學管理商品的采購銷售物流和財務管理以及和系統的數據統計的需求,通常對應一個管理信息系統的編碼。
問題描述
我們在選購一件商品的時候通常都是需要選擇相應的產品規格來計算價錢,不同規格的選擇出來的價格以及庫存數量都是不一樣的,比如衣服就有顏色,尺碼等屬性
下面引用sku的概念
最小庫存管理單元(Stock Keeping Unit, SKU)是一個會計學名詞,定義為庫存管理中的最小可用單元,例如紡織品中一個SKU通常表示規格、顏色、款式,而在連鎖零售門店中有時稱單品為一個SKU。最小庫存管理單元可以區分不同商品銷售的最小單元,是科學管理商品的采購、銷售、物流和財務管理以及POS和MIS系統的數據統計的需求,通常對應一個管理信息系統的編碼。 —— form wikipedia
那么我們在后臺管理系統當中該如何去對商品的規格進行添加編輯刪除,那么我們就需要設計一個sku規格生成組件來管理我們的產品規格設置
目標在設計一個組件的時候我們需要知道最終要完成的效果是如何,需求是不是能夠滿足我們
如圖中例子所示,我們需要設計類似這么一個可以無限級添加規格以及規格值,然后在表格里面設置產品價格和成本價庫存等信息,最終便完成了我們的需求
分析從大的方面說,規格和表格列表需要放在不同的組件里面,因為處理的邏輯不同
然后每一個規格都只能選擇其下的規格值,且已經選過的規格不能再被選擇,規格和規格值允許被刪除,每一次規格的增刪改都會影響著表格中的內容,但規格不受表格的影響,同時規格可以無限級添加....
盡可能的往多個方面考慮組件設計的合理性以及可能出現的情況
然后我們還需要知道后端那邊需要我們前端傳什么樣的數據類型和方式(很重要)
假設后端需要的添加規格數據為
{ spec_format: Array<{ spec_name: string; spec_id: number; value: Array<{ spec_value_name: string, spec_value_id: number }> }> }
然后設置每一個規格價格和庫存的數據為
{ skuArray: Array<{ attr_value_items: Array<{ spec_id: number, spec_value_id: number }>; price: number; renew_price: number; cost_renew_price?: number; cost_price?: number; stock: number; }> }
這里我把目錄分成如圖
g-specification用來管理規格和表格的組件,想當于他們的父級
spec-price是設置價格庫存等信息的表格組件
spec-item是規格
spec-value是其某個規格下的規格值列表選擇組件
我自己的個人習慣是喜歡把和視圖有關的數據比如組件的顯示與否,包括ngIf判斷的字段等等多帶帶放在一個ViewModel數據模型里,這樣好和其他的接口提交數據區分開來,而且也便于后期其他人的維護,在這里我就不詳細講解視圖上的邏輯交互了
首先創建一個SpecModel
class SpecModel { "spec_name": string = "" "spec_id": number = 0 "value": any[] = [] // 該規格對應的有的值 constructor() {} /* 賦值操作 */ public setData( data: any ): void { this["spec_name"] = data["spec_name"]!=undefined?data["spec_name"]:this["spec_name"] this["spec_name"] = data["spec_id"]!=undefined?data["spec_id"]:this["spec_id"] } /* 規格值賦值 */ public setValue( data: any[] ): void { this["value"] = Array.isArray( data ) == true ? [...data] : [] } }
這里我定義了一個和后端所需要的spec_format字段里的數組子集一樣的數據模型,每一個規格組件在創建的時候都會new一個這么一個對象,方便在g-specification組件里獲取到多個規格組件里的SpecModel組裝成一個spec_format數組
規格價格和庫存設置組件規格組件的設計因人而異,只是普通的數據傳入和傳出,組件之間的數據交互可能用Input or Output,也可以通過服務創建一個EventEmitter來交互,假設到了這里我們已經把規格組件和規格值列表組件處理完畢了并且通過g-specification.service這個文件來進行數據傳輸
在這個組件里我新創了一個SpecDataModel模型,作用是統一數據的來源,能夠在spec-price組件里面處理的數據類型和字段不缺失或多余等
export class SpecDataModel { "spec": any = {} "specValue": any[] = [] constructor( data: any = {} ){ this["spec"] = data["spec"] ? data["spec"]: this["spec"] this["specValue"] = data["specValue"] ? data["specValue"] : this["specValue"] this["specValue"].map(_e=>_e["spec"]=this["spec"]) } }
在這個服務里創建了一個EventEmitter來進行跨組件數據傳遞,主要傳遞的數據類型是SpecDataModel
@Injectable() export class GSpecificationService { public launchSpecData: EventEmitter= new EventEmitter () constructor() { } }
在規格組件里面每一次的增加和刪除都會next一次規格數據,圖例列舉了取消規格的操作
,每一次next的數據都會在spec-price組件里接收到
/* 點擊取消選中的規格值 */ public closeSpecValue( data: any, index: number ): void { this.viewModel["_selectSpecValueList"].splice( index,1 ) this.gSpecificationService.launchSpecData.next( this.launchSpecDataModel( this.viewModel["_selectSpecValueList"] ) ) } /* 操作完之后需要傳遞的值 */ public launchSpecDataModel( specValue: any[], spec: SpecModel = this.specModel ): SpecDataModel { return new SpecDataModel( {"spec":spec,"specValue":[...specValue] } ) }
然后在spec-price組件里就能接受其他地方傳遞進來的SpecDataModel數據
this.launchSpecRX$ = this.gSpecificationService.launchSpecData.subscribe(res=>{ // res === SpecDataModel })數據處理
現在spec-price組件已經能夠實時的獲取到規格組件傳遞進來的數據了,包括選擇的規格和規格值,那么
該如何處理這些數據使得滿足圖中的合并表格的樣式以及將價格、成本價、和庫存等信息數據綁定到所有規格里面,處理每一次的規格操作都能得到最新的SpecDataModel,顯然是需要將這些SpecDataModel統一歸并到一個數組里面,負責存放所有選擇過的規格
顯然還是需要在組件里面建立一個數據模型來處理接收過來的SpecDataModel,那么假定有一個_specAllData數組來存放所有規格
同時我們還觀察到,圖中的表格涉及到合并單元格,那么就需要用到tr標簽的rowspan屬性(還記得么?)
然后再次分析,發現不同數量的規格和規格值所出來的結果是一個全排列組合的情況
例:
版本: v1, v2, v3
容量: 10人,20人
那么出來的結果有3 X 2 = 6種情況,那么在表格當中呈現的結果就是六種,如果此時容量再多加一個規格值,那么結果就是3 X 3 = 9種情況
所以表格的呈現方式涉及到全排列算法和rowspan的計算方式
我們新建一個SpecPriceModel數據模型
class SpecPriceModel { "_title": string[] = ["新購價格(元)","成本價(元)","續費價格(元)","續費成本價(元)","庫存"] // 表格標題頭部 "_specAllData": any[] = [] // 所有規格傳遞過來的值 private "constTitle": string[] = [...this._title] // 初始的固定標題頭 }
因為表格的最后5列是固定的標題頭,而且每一次的規格添加都會增加一個標題頭,那么就需要把標題頭存放到一個變量里面
雖然_specAllData能接收到所有的規格但也有可能遇到重復數據的情況,而且當然所有的規格都被刪除了之后,_specAllData也應該會一個空數組,所以在SpecPriceModel里面就需要對_specAllData去重
public setAllSpecDataList( data: SpecDataModel ): void { if( data["specValue"].length > 0 ) { let _length = this._specAllData.length let bools: boolean = true for( let i: number = 0; i<_length; i++ ) { if( this._specAllData[i]["spec"]["id"] == data["spec"]["id"] ) { this._specAllData[i]["specValue"] = [...data["specValue"]] bools = false break } } if( bools == true ) { this._specAllData.push( data ) } }else { this._specAllData = this._specAllData.filter( _e=>_e["spec"]["name"] != data["spec"]["name"] ) } this.setTitle() }
假設這個時候我們得到的_specAllData數據為
[ { spec:{ name: "版本, id: 1 }, specValue:[ { spec_value_id: 11, spec_value_name: "v1.0" }, { spec_value_id: 111, spec_value_name: "v2.0" }, { spec_value_id: 1111, spec_value_name: "v3.0" } ] }, { spec:{ name: "容量, id: 2 }, specValue:[ { spec_value_id: 22, spec_value_name: "10人" }, { spec_value_id: 222, spec_value_name: "20人" } ] } ]
那么我們就剩下最后的合并單元格以及處理全排列組合的問題了,其實這個算法也有一個專業名詞叫笛卡爾積
笛卡爾乘積是指在數學中,兩個集合X和Y的笛卡尓積(Cartesian product),又稱直積,表示為X × Y,第一個對象是X的成員而第二個對象是Y的所有可能有序對的其中一個成員
這里我用了遞歸的方法處理所有存在的按順序的排列組合可能
// 笛卡爾積 let _recursion_spec_obj = ( data: any )=>{ let len: number = data.length if(len>=2){ let len1 = data[0].length let len2 = data[1].length let newlen = len1 * len2 let temp = new Array( newlen ) let index = 0 for(let i = 0; i那么就能得到所有出現的排列組合結果,為一個二維數組,暫時就叫_mergeRowspan好了
[ [ { spec:{ name: "版本", id: 1 }, spec_value_id: 11, spec_value_name: "v1.0" }, { spec:{ name: "容量", id: 1 }, spec_value_id: 22, spec_value_name: "10人" } ] // ....等等 ]出現的結果有3 X 2 = 6種
而tr標簽的rowspan屬性是規定單元格可橫跨的行數。
如圖例
v1.0 橫跨的行數為2,那么他的rowspan為2
10人和20人都是最小單元安么rowspan自然為1
可能圖中的例子的行數比較少并不能直接的看出規律,那么這次來個數據多點的
這次 v1.0的rowspan為4
10人和20人的rowspan為2
。。。那么我們就能得出,只要算出_mergeRowspan數組里面的每一個排列情況的rowspan值,然后在渲染表格的時候雙向綁定到tr標簽的rowspan就可以了
計算rowpsan舉上圖為例,總共有 3 X 2 X 2 = 12種情況,其中第一個規格的每一個規格值各占4行,第二個規格的每一個規格值各占2行,最后一個規格的規格值每個各占一行
this._tr_length = 1 // 全排列組合的總數 this._specAllData.forEach((_e,_index)=>{ this._tr_length *= _e["specValue"].length }) // 計算rowspan的值 let _rowspan_divide = 1 for( let i: number = 0; i最終得到的數據如圖
這里我們的每一條數據都能知道自己對應的rowspan的值是多少,這樣在渲染表格的時候我們就能通過*ngIf來判斷哪些該顯示哪些不該顯示。可能有的人會說,這個rowspan的拼接用原生DOM操作就可以了,那你知道操作這些rowspan需要多少行么。。
因為rowspan為4的占總數12的三分之一,所以只會在第一行和第五行以及第九行出現
rowspan為2的占總數12的六分之一,所以只會在第一、三、五、七、九、十一行出現
rospan為1的每一行都有那么我們得出*ngIf的判斷條件為 childen["rowspan"]==1||(i==0?true:i%childen["rowspan"]==0)
{{childen["spec_value_name"]}} 最后附完整的SpecPriceModel模型
class TableModel { "_title": string[] = ["新購價格(元)","成本價(元)","續費價格(元)","續費成本價(元)","庫存"] "_specAllData": any[] = [] // 所有規格傳遞過來的值 /* 合并所有的數據同時計算出最多存在的tr標簽的情況 需要用到二維數組 一層數組存放總tr條數 二層數組存放對象,該對象是所有規格按照排列組合的順序排序同時保存該規格的rowpan值 rowpan值的計算為,前一個規格 = 后面每個規格的規格值個數相乘 */ "_mergeRowspan": any[] = [] "_tr_length": number = 1 // tr標簽的總數 private "constTitle": string[] = [...this._title] // 初始的固定標題頭 /* 傳遞回來的規格數據處理 */ public setAllSpecDataList( data: SpecDataModel ): void { if( data["specValue"].length > 0 ) { let _length = this._specAllData.length let bools: boolean = true for( let i: number = 0; i<_length; i++ ) { if( this._specAllData[i]["spec"]["id"] == data["spec"]["id"] ) { this._specAllData[i]["specValue"] = [...data["specValue"]] bools = false break } } if( bools == true ) { this._specAllData.push( data ) } }else { this._specAllData = this._specAllData.filter( _e=>_e["spec"]["name"] != data["spec"]["name"] ) } this.setTitle() } /* 設置標題頭部 */ private setTitle(): void { let _title_arr = this._specAllData.map( _e=> _e["spec"]["name"] ) this._title = [..._title_arr,...this.constTitle] this.handleMergeRowspan() } /****計算規格 合并表格單元*****/ private handleMergeRowspan():void { this._tr_length = 1 // 全排列組合的總數 this._specAllData.forEach((_e,_index)=>{ this._tr_length *= _e["specValue"].length }) // 計算rowspan的值 let _rowspan_divide = 1 for( let i: number = 0; i{ let len: number = data.length if(len>=2){ let len1 = data[0].length let len2 = data[1].length let newlen = len1 * len2 let temp = new Array( newlen ) let index = 0 for(let i = 0; i _e["specValue"] ) this._mergeRowspan = _result_arr.length == 1? (()=>{ let result: any[] = [] _result_arr[0].forEach(_e=>{ result.push([_e]) }) return result || [] })() : _recursion_spec_obj( _result_arr ) // 重組處理完之后的數據,用于數據綁定 if( Array.isArray( this._mergeRowspan ) == true ) { this._mergeRowspan = this._mergeRowspan.map(_e=>{ return { items: _e, costData: { price: 0.01, renew_price: 0.01, cost_renew_price: 0.01, cost_price: 0.01, stock: 1 } } }) }else{ this._mergeRowspan = [] } } } 相比于傳統DOM操作rospan來動態合并表格的方式,這種通過計算規律和數據雙向綁定的方式來處理不僅顯得簡短也易于維護
本文只是提煉了設計sku組件當中比較困難的部分,當然也只是其中的一個處理方式,這種方法不僅在添加規格的時候顯得輕松,在編輯已有的規格也能輕松應對
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89856.html
摘要:我們現在正向的考慮,如何確定屬性是否可選。而且多維的情況下用戶可以跳著選。舉個例子如果有一個維的,那么最終生成的路徑表會有個最終可以查看這個多維屬性狀態判斷相關資料組合查詢算法探索 原文:https://keelii.github.io/2016/12/22/sku-multi-dimensional-attributes-state-algorithm/ 問題描述 這個問題來源于選擇...
摘要:優惠券選擇器優惠券選擇器提供了優惠券單元格和優惠券選擇功能。優惠券單元格只需傳入優惠券列表和當前使用的優惠券即可正確展示。使用參數可以控制優惠券單元格是否展示右側箭頭,這個可以用于提醒用戶能否切換優惠券。 Vant ( ?v?nt ) 是有贊前端團隊基于有贊統一的規范實現的 Vue 組件庫,提供了一整套 UI 基礎組件和業務組件。通過 Vant,可以快速搭建出風格統一的頁面,提升開發效...
摘要:如果設計不合理例如商品添加很簡單,但是修改商品就很復雜。在前期設計上我們要盡量避免這些坑謝謝你看到這里,希望我的文章能夠幫助到你。 showImg(https://segmentfault.com/img/bVbdtuc?w=1824&h=1028); 電商大伙每天都在用,類似某貓,某狗等。電商系統設計看似復雜又很簡單,看似簡單又很復雜本章適合初級工程師及中級工程師細看,大佬請隨意 前...
摘要:本篇我思考了很久到底要不要解析下商品接口開發的注意點。接口設計簡述電商系統設計之中,比較復雜的接口就論商品詳情的接口了,響應參數特別多,特別雜。 showImg(https://segmentfault.com/img/bVbeJkL?w=1162&h=712); 前言 我應該是少數在文章中直接展示接口文檔的人。本篇我思考了很久到底要不要解析下商品接口開發的注意點。 客戶端開發與服務端...
摘要:是有贊端規范的實現版本,提供了一整套基礎的組件以及常用的業務組件。目前我們有組件,其中包括以及等實用的業務組件。一套有贊設計師繪制的圖標庫。為了解決這些問題,提供了一套自己的時間選擇組件,包括日期選擇周選擇組件月選擇以及時間區間選擇。 Zent ( ?zent ) 是有贊 PC 端 Web UI 規范的 React 實現版本,提供了一整套基礎的 UI 組件以及常用的__業務組件__。通...
閱讀 1407·2021-11-24 10:20
閱讀 3649·2021-11-24 09:38
閱讀 2294·2021-09-27 13:37
閱讀 2196·2021-09-22 15:25
閱讀 2270·2021-09-01 18:33
閱讀 3488·2019-08-30 15:55
閱讀 1783·2019-08-30 15:54
閱讀 2081·2019-08-30 12:50