話不多說(shuō),先上圖,就知道要做什么了
在掘金上或者各種網(wǎng)址上有很多關(guān)于“商品多規(guī)格選擇”的問(wèn)題,例如有全排列組合的,我寫該需求的時(shí)候,也在網(wǎng)上找了很多解答,思路都非常棒,但是并沒(méi)有緊貼業(yè)務(wù)場(chǎng)景。(有些緊貼業(yè)務(wù)場(chǎng)景,但是不符合我所需要的業(yè)務(wù)場(chǎng)景)。
真正的業(yè)務(wù)場(chǎng)景是,首次渲染,就需要把所有支持的規(guī)格都呈現(xiàn)出來(lái),不能選擇的規(guī)格置灰,我們要根據(jù)用戶每一次選擇的規(guī)格,找出剩下可選的規(guī)格和不可選的規(guī)格
,就是如下效果:
什么是sku
經(jīng)常會(huì)聽(tīng)到一個(gè)詞“SKU”,“SKU”是什么呢?通俗來(lái)說(shuō)就是身份證,產(chǎn)品的身份證。我們每一個(gè)人,都有一個(gè)身份證號(hào)碼,同樣的,每一個(gè)產(chǎn)品在電商系統(tǒng)中也有一個(gè)身份證號(hào)碼,那就是SKU。 英文全稱為 stock keeping unit, 簡(jiǎn)稱SKU,定義為保存庫(kù)存控制的最小可用單位,例如紡織品中一個(gè)SKU通常表示規(guī)格,顏色,款式)。 STOCK KEEP UNIT.這是客戶拿到商品放到倉(cāng)庫(kù)后給商品編號(hào),歸類的一種方法
SKU中包含的信息
1. 品項(xiàng)
可以結(jié)合上面關(guān)于單品、SKU和品種的解釋來(lái)理解。也就是只要屬性有不同,那么就是不同的品項(xiàng)(SKU)??梢哉f(shuō)這是SKU看作是一種產(chǎn)品的角度來(lái)分析理解的。屬性有很多種,也就是說(shuō)同樣的產(chǎn)品只要在人們對(duì)其進(jìn)行保存、管理、銷售、服務(wù)上有不同的方式,那么它(SKU)就不再是相同的了。
2. 編碼
這個(gè)概念是基于信息系統(tǒng)和貨物編碼管理來(lái)說(shuō)的,像“品項(xiàng)”中介紹的那樣,不同的品項(xiàng)(SKU)就有不同的編碼。這樣子,我們才可以依照不同的SKU數(shù)據(jù)來(lái)分析庫(kù)存、銷售狀況。但是這里的產(chǎn)品如“品項(xiàng)”所說(shuō),并非是一個(gè)泛泛的產(chǎn)品的概念,而是很精確的產(chǎn)品概念。
3.單位單位
基本上就是基于管理來(lái)說(shuō)的吧,這個(gè)名字上是數(shù)字化管理方式的產(chǎn)物。但是這里的單位和我們平時(shí)的“單位”有什么區(qū)別呢?看看產(chǎn)品的包裝單位的不同,SKU就不同——你就知道了。
產(chǎn)品SKU在電商倉(cāng)庫(kù)中的作用
1.從貨品角度看
SKU是指單獨(dú)一種商品,其貨品屬性已經(jīng)被確定。也就是說(shuō)同樣的貨品只要在人們對(duì)其進(jìn)行保存、管理、銷售、服務(wù)上有不同的方式,那么就需要被定義為不同的SKU
2.從業(yè)務(wù)管理的角度看
SKU還含有貨品包裝單位的信息。例如:SKU#123是指330ml瓶裝黑啤(以瓶為單位);SKU#456 是指330ml瓶裝黑啤(以提為單位,6瓶為1提);SKU#789 是指330ml瓶裝黑?。ㄒ韵錇閱挝?,24瓶為1箱)。由于計(jì)量單位(包裝單位)不同,為業(yè)務(wù)管理需要,應(yīng)劃歸于不同的SKU,當(dāng)然可以有單位轉(zhuǎn)換的算法協(xié)助轉(zhuǎn)換SKU。
3.從信息系統(tǒng)和貨物編碼角度看
SKU只是一個(gè)編碼。不同的一種商品(商品名稱)就有不同的編碼(SKU#)。而這個(gè)編碼與被定義的商品做了一一對(duì)應(yīng)的關(guān)聯(lián),這樣我們才可以依照不同SKU的數(shù)據(jù)來(lái)記錄和分析庫(kù)存和銷售情況。
一般講SKU是在某一體系(例如:公司或工廠)內(nèi)部自定義和使用的??珞w系需要重新定義或做SKU轉(zhuǎn)換。
業(yè)務(wù)場(chǎng)景
筆者沒(méi)接觸過(guò)商品多規(guī)格業(yè)務(wù),以及自己對(duì)于小程序這方面,接觸的也不多,代碼質(zhì)量不足,不要介意。
使用技術(shù)棧
主要是針對(duì) taro+react+hook
版本的小程序,由于以前的開(kāi)發(fā)都是用js+class
,所以hook用的比較少(雖然自己也很想用hook)
下面重點(diǎn)解析sku選擇器
主要代碼
1.sku的格式
spu的所有sku規(guī)格形式 (spu_spec_all_values )
表明一共有兩個(gè)規(guī)格,name為該規(guī)格的名字,values是該規(guī)格下的種類
全部目前已上架的sku商品規(guī)格 (sku_setting_specs )
進(jìn)入商品詳情頁(yè)默認(rèn)選中的sku規(guī)格 (specification )
2.代碼
我這里主要呈現(xiàn)payModal 的核心代碼,商品詳情頁(yè)的代碼就不呈現(xiàn)了
主要思路: 進(jìn)來(lái)商品詳情頁(yè)面默認(rèn)選中規(guī)格(如果沒(méi)有該sku規(guī)格,或者sku的庫(kù)存為0,則置灰) -> 切換可點(diǎn)擊的不同規(guī)格 -> 獲取到選擇完的sku信息,加入購(gòu)物車或者立即購(gòu)買,或者查看該sku的詳情
因?yàn)樾〕绦蛴昧薽obx,class添加了observer()包裹,所以獲取的接口數(shù)據(jù)為object或者array 都需要用mobx內(nèi)置toJS方法解析出來(lái)
- 核心代碼
const [skuInfo, setSkuInfo] = useState({});// 存儲(chǔ)sku規(guī)格的信息
const [skuHold, setSkuHold] = useState(0); // sku的庫(kù)存
useEffect(() => {
if (toJS(specification) && toJS(specification).length) {
setActiveSizeValue(formatSpecification(toJS(specification)));
setDrawOptions();
}
setSkuHold(stock);
setSkuInfo({
skuStock: stock, // 庫(kù)存
skuImage: sku_img, // sku圖片
skuOriPrice: ori_price, // 舊價(jià)格
skuIsShowLinePrice: is_show_line_price, // 是否展示劃線
skuShowPrice: show_price // 現(xiàn)在sku的價(jià)格
});
}, [spu_spec_all_values, specification, sku_setting_specs, ori_price, is_show_line_price])
/**
* 核心代碼
* @param selectedSpec 已選中的數(shù)組
* @param currentSpecName 當(dāng)前點(diǎn)擊的規(guī)格的名稱
* @param value 默認(rèn)已選的規(guī)格(只會(huì)剛進(jìn)來(lái)的時(shí)候有值)
* @param typeClick 是否點(diǎn)擊選擇了
*/
const skuCore = (selectedSpec, currentSpecName, value, typeClick) => {
const spec1 = typeClick !== 'click' && value ? value : spec;
const skus = toJS(sku_setting_specs);
Object.keys(spec1).forEach((sk) => {
if (sk !== currentSpecName) {
// 找出該規(guī)格中選中的值
const currentSpecSelectedValue = http://www.wxapp-union.com/spec1[Object.keys(spec1).find((_sk) => sk === _sk) ||''].find((sv) => sv.select)
spec1[sk].forEach((sv) => {
// 判斷當(dāng)前的規(guī)格的值是否是選中的,如果是選中的 就不要判斷是否可以點(diǎn)擊直接跳過(guò)循環(huán)
if (!sv.select) {
const _ssTemp = [...selectedSpec]
// 如果當(dāng)前規(guī)格有選中的值
if (!!currentSpecSelectedValue) {
const sIndex = _ssTemp.findIndex((_sv) => _sv === `${sk}:${currentSpecSelectedValue.value}`)
_ssTemp.splice(sIndex, 1)
}
_ssTemp.push(`${sk}:${sv.value}`)
const _tmpPath = []
// 找到包含該路徑的全部sku
skus.forEach((sku) => {
// 找出skus里面包含目前所選中的規(guī)格的路徑的數(shù)組的數(shù)量
const querSkus = _ssTemp.filter((_sst) => {
const querySpec = objTransformArr(sku.specs).some(p => {
return p === _sst
})
return querySpec
})
const i = querSkus.length
if (i === _ssTemp.length) {
_tmpPath.push(sku) // 把包含該路徑的sku全部放到一個(gè)數(shù)組里
}
})
const hasHoldPath = _tmpPath.find((p) => p.stock) // 判斷里面是要有個(gè)sku不為0 則可點(diǎn)擊
let isNotEmpty = hasHoldPath ? hasHoldPath.stock : 0
sv.disable = !isNotEmpty
}
})
}
})
// 判斷是否可以添加進(jìn)購(gòu)物車,比如屬性是否有選,庫(kù)存情況等
if (judgeCanAdd(skus)) {
const sku_info = getSkuInfoByKey(spec1) || {}; // 獲取sku信息
const hold = sku_info.stock || 0; // 獲取sku的庫(kù)存
setSkuHold(hold);
setSkuInfo({
skuStock: sku_info.stock,
skuImage: sku_info.image,
skuOriPrice: sku_info.ori_price,
skuIsShowLinePrice: sku_info.is_show_line_price,
skuShowPrice: sku_info.show_price
});
}
}
復(fù)制代碼
- 通過(guò)skus初始化 各個(gè)規(guī)格
// 初始化已選的規(guī)格 格式 ["尺寸:S", "毫升:100ml"]
const formatSpecification = (specification) => {
if (!Array.isArray(specification)) {
return []
}
return specification.map(i =>
`${i.key}:${i.val}`
)
}
// activeSizeValue 格式 ["尺寸:S", "毫升:100ml"]
const [activeSizeValue, setActiveSizeValue] = useState([]);
// spec 格式 {尺寸:{value: "S", disable: false, select: true}}
const [spec, setSpec] = useState({});
useEffect(() => {
// 如果存在多規(guī)格 則執(zhí)行skus初始化 各個(gè)規(guī)格
if (toJS(specification) && toJS(specification).length) {
// 把默認(rèn)已選的規(guī)格初始化給 activeSizeValue
setActiveSizeValue(formatSpecification(toJS(specification)));
setDrawOptions();
}
}, [specification])
/**
* 通過(guò)skus初始化 各個(gè)規(guī)格
*/
const setDrawOptions = () => {
// spu的所有sku規(guī)格形式
const skus = toJS(spu_spec_all_values) || [];
let _tags = {};//臨時(shí)存儲(chǔ)
const _tempTagsStrArray = {}; //臨時(shí)存儲(chǔ)
const defaultValue = http://www.wxapp-union.com/toJS(specification) || []; // 默認(rèn)規(guī)格
// 所有的規(guī)格name
const nameKeys = defaultValue.map(item => item.key);
skus.forEach(s => {
s.values.forEach(p => {
// 不存在該key時(shí),初始化對(duì)象
if (!_tags[s.name]) {
_tags[s.name] = []
_tempTagsStrArray[s.name] = [];
}
//
if (!_tempTagsStrArray[s.name].includes(p)) {
_tempTagsStrArray[s.name].push(p);
// 為了判斷規(guī)格 是否已選
const result = defaultValue.some(function (item) {
if (item.val === p && item.key === s.name) {
return true;
}
})
_tags[s.name].push({
value: p,
disable: true,
select: result ? true : false
})
}
})
});
setSpecDisable(_tags)
}
復(fù)制代碼
- 設(shè)置規(guī)格是否可選
// 為了每次第一次渲染都渲染spu下的默認(rèn)sku規(guī)格
const [num, setNum] = useState(0)
/**
* 用于初始化規(guī)格和規(guī)格都沒(méi)選中的時(shí)候 設(shè)置 規(guī)格是否可以點(diǎn)擊,
* 該路徑上如果跟該屬性的組合沒(méi)有則該屬性不能點(diǎn)擊
*/
const setSpecDisable = (tags, isReset) => {
const skus = toJS(sku_setting_specs) || [];
const defaultValue = http://www.wxapp-union.com/toJS(specification) || [];
Object.keys(tags).forEach((sk) => {
tags[sk].forEach((sv) => {
const currentSpec = `${sk}:${sv.value}`;
// 找到含有該規(guī)格的路徑下 庫(kù)存不為0的 sku
const querySku = skus.find((sku) => {
for (let key in sku.specs) {
const queryProperty = `${key}:${sku.specs[key]}` === currentSpec;
if (queryProperty) {
return queryProperty && sku.stock
}
}
})
// 如果找到 對(duì)應(yīng)該屬性的路徑 sku有不為0 的則可選
sv.disable = !querySku
})
})
setSpec({ ...tags })
// activeSizeValue不為空
if (!isReset) {
// 這是因?yàn)閙obx的問(wèn)題,導(dǎo)致activeSizeValue初始化有問(wèn)題,臨時(shí)存儲(chǔ),后期優(yōu)化
const a = formatSpecification(defaultValue);
// 第一次進(jìn)來(lái)渲染接口的數(shù)據(jù),以后都是點(diǎn)擊的數(shù)據(jù)
// num > (defaultValue.length - 1) 默認(rèn)規(guī)格的長(zhǎng)度
const b = activeSizeValue.length || num > (defaultValue.length - 1) ? activeSizeValue : a;
defaultValue.forEach(item => {
if (b.length) {
skuCore(b, item.key, tags);
setNum(num + 1)
}
})
}
}
復(fù)制代碼
- 規(guī)格點(diǎn)擊事件
/**
* k - 規(guī)格名稱,如:尺寸
* currentSpectValue - 規(guī)格value 如:'s'
* 規(guī)格選項(xiàng)點(diǎn)擊事件
*/
const onPressSpecOption = (k, currentSpectValue) => {
let isCancel = false;
// 找到在全部屬性spec中對(duì)應(yīng)的屬性
const currentSpects = spec[Object.keys(spec).find((sk) => sk === k) || ''] || [];
// 上一個(gè)被選中的的屬性
const prevSelectedSpectValue = http://www.wxapp-union.com/currentSpects.find((cspec) => cspec.select) || {}
// 設(shè)置前一個(gè)被選中的值為未選中
prevSelectedSpectValue.select = false;
// 只有當(dāng)當(dāng)前點(diǎn)擊的屬性值不等于上一個(gè)點(diǎn)擊的屬性值時(shí)候設(shè)置為選中狀態(tài)
if (prevSelectedSpectValue === currentSpectValue) {
isCancel = true
} else {
// 設(shè)置當(dāng)前點(diǎn)擊的狀態(tài)為選中
currentSpectValue.select = true
}
// 全部有選中的規(guī)格數(shù)組 ##可優(yōu)化
const selectedSpec = Object.keys(spec)
.filter((sk) => spec[sk].find((sv) => sv.select))
.reduce((prev, currentSpecKey) => {
return [...prev, `${currentSpecKey}:${spec[currentSpecKey].find((__v) => __v.select).value}`]
}, [])
if (isCancel) {
// 如果是取消且全部沒(méi)選中
if (!selectedSpec.length) {
// 初始化是否可點(diǎn)
setSpecDisable(spec,'reset')
}
}
// 如果規(guī)格中有選中的 則對(duì)整個(gè)規(guī)格就行 庫(kù)存判斷 是否可點(diǎn)
if (selectedSpec.length) {
skuCore(selectedSpec, k, '', 'click');
}
// 把選中的規(guī)格賦值給activeSizeValue
setActiveSizeValue(selectedSpec);
setSpec({ ...spec });
}
復(fù)制代碼
- 判斷是否可以購(gòu)買或加入購(gòu)物車
// 判斷是否可以加入購(gòu)物車
const [canFlag, setCanFlag] = useState(false);
/** 判斷是否可以購(gòu)買或加入購(gòu)物車,比如屬性是否有選,庫(kù)存情況等 */
const judgeCanAdd = (skus = []) => {
const sks = Object.keys(spec);
// 已經(jīng)選擇的規(guī)格個(gè)數(shù)
let s = sks.filter((sk) => spec[sk].some((sv) => sv.select)).length
// 比較已選的長(zhǎng)度是否和需要選擇的長(zhǎng)度一致
let _cf = s === sks.length
if (!skus || !skus.length) {
_cf = false
}
if (skus && skus.length === 1 && !Object.keys(skus[0].specs).length && skus[0].stock <= 0) {
_cf = false
}
setCanFlag(_cf)
return _cf
}
復(fù)制代碼
- 返回規(guī)格信息
/**
* @param _spec 規(guī)格屬性
* 返回所有信息
*/
const getSkuInfoByKey = (_spec) => {
// 已選的規(guī)格:[{ name:規(guī)格名稱, value:已選規(guī)格內(nèi)容 }]
const selectedSpec = {};
Object.keys(_spec).forEach((k) => {
const selectedValue = http://www.wxapp-union.com/_spec[k].find((sv) => sv.select);
if (selectedValue) {
// 這塊部分也可以在選擇的時(shí)候直接處理
selectedSpec[k] = selectedValue.value
}
})
const skus = toJS(sku_setting_specs) || [];
const querySku = skus.find((sku) => {
// 對(duì)比兩個(gè)數(shù)組找到 兩個(gè)都不存在的sku 如果為0 則說(shuō)明完全匹配就是該sku
const diffSkus = isObjShallowEqual(selectedSpec, sku.specs)
return diffSkus
}) || {};
return querySku;
}
復(fù)制代碼
總結(jié)
因?yàn)榇a一直修修改改,所有有很多冗余和質(zhì)量不好的代碼,后期會(huì)優(yōu)化,敬請(qǐng)諒解(因?yàn)闃I(yè)務(wù)需求,時(shí)間太趕了,先湊合著把功能實(shí)現(xiàn),后期再進(jìn)去優(yōu)化)。
如果你覺(jué)得這篇文章對(duì)你有用的話,請(qǐng)給個(gè)贊吧??!
也可以掃碼進(jìn)入小程序查看,掃掃下面的二維碼