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