SKU組件(React版)
這里的一些邏輯還是需要自己再優(yōu)化一下的
起因
今天看掘金的時(shí)候看到前端SKU算法實(shí)現(xiàn),因?yàn)楣疽灿猩婕暗絊KU的業(yè)務(wù),記錄一下自己寫(xiě)SKU的一個(gè)例子吧,剛好他有提供后端的API接口數(shù)據(jù),mock一下干起來(lái)淮摔,但是在做的時(shí)候還是有很多問(wèn)題的,這里做一下記錄
實(shí)現(xiàn)效果
mock數(shù)據(jù)
export const simulatedSku = {
id: 2,
title: "林間有風(fēng)自營(yíng)針織衫",
subtitle: "瓜瓜設(shè)計(jì)忆家,3件包郵",
category_id: 12,
root_category_id: 2,
price: "77.00",
img: "",
for_theme_img: "",
description: null,
discount_price: "62.00",
tags: "包郵$熱門(mén)",
is_test: true,
online: true,
sku_list: [
{
id: 2,
price: 77.76,
discount_price: null,
online: true,
img: "",
title: "金屬灰·七龍珠",
spu_id: 2,
category_id: 17,
root_category_id: 3,
specs: [
{
key_id: 1,
key: "顏色",
value_id: 45,
value: "金屬灰"
},
{
key_id: 3,
key: "圖案",
value_id: 9,
value: "七龍珠"
},
{
key_id: 4,
key: "尺碼",
value_id: 14,
value: "小號(hào) S"
}
],
code: "2$1-45#3-9#4-14",
stock: 5
},
{
id: 3,
price: 66,
discount_price: 59,
online: true,
img: "",
title: "青芒色·灌籃高手",
spu_id: 2,
category_id: 17,
root_category_id: 3,
specs: [
{
key_id: 1,
key: "顏色",
value_id: 42,
value: "青芒色"
},
{
key_id: 3,
key: "圖案",
value_id: 10,
value: "灌籃高手"
},
{
key_id: 4,
key: "尺碼",
value_id: 15,
value: "中號(hào) M"
}
],
code: "2$1-42#3-10#4-15",
stock: 999
},
{
id: 3,
price: 66,
discount_price: 59,
online: true,
img: "",
title: "橘黃色·灌籃高手",
spu_id: 2,
category_id: 17,
root_category_id: 3,
specs: [
{
key_id: 1,
key: "顏色",
value_id: 44,
value: "橘黃色"
},
{
key_id: 3,
key: "圖案",
value_id: 10,
value: "灌籃高手"
},
{
key_id: 4,
key: "尺碼",
value_id: 15,
value: "中號(hào) M"
}
],
code: "2$1-42#3-10#4-15",
stock: 999
},
{
id: 4,
price: 88,
discount_price: null,
online: true,
img: "",
title: "青芒色·圣斗士",
spu_id: 2,
category_id: 17,
root_category_id: 3,
specs: [
{
key_id: 1,
key: "顏色",
value_id: 42,
value: "青芒色"
},
{
key_id: 3,
key: "圖案",
value_id: 11,
value: "圣斗士"
},
{
key_id: 4,
key: "尺碼",
value_id: 16,
value: "大號(hào) L"
}
],
code: "2$1-42#3-11#4-16",
stock: 8
},
{
id: 5,
price: 77,
discount_price: 59,
online: true,
img:
"http://i1.sleeve.7yue.pro/assets/09f32ac8-1af4-4424-b221-44b10bd0986e.png",
title: "橘黃色·七龍珠",
spu_id: 2,
category_id: 17,
root_category_id: 3,
specs: [
{
key_id: 1,
key: "顏色",
value_id: 44,
value: "橘黃色"
},
{
key_id: 3,
key: "圖案",
value_id: 9,
value: "七龍珠"
},
{
key_id: 4,
key: "尺碼",
value_id: 14,
value: "小號(hào) S"
}
],
code: "2$1-44#3-9#4-14",
stock: 7
}
],
spu_img_list: [
{
id: 165,
img:
"http://i1.sleeve.7yue.pro/assets/5605cd6c-f869-46db-afe6-755b61a0122a.png",
spu_id: 2
}
],
spu_detail_img_list: [
{
id: 24,
img: "http://i2.sleeve.7yue.pro/n4.png",
spu_id: 2,
index: 1
}
],
sketch_spec_id: 1,
default_sku_id: 2
};
簡(jiǎn)單的封裝一個(gè)SKUCard和SKUGroup
類(lèi)似于RadioGroup和Radio,我們先封裝一個(gè)簡(jiǎn)單的SKU Group和SKU組件抑钟,便于狀態(tài)的統(tǒng)一管理
- SKU Card的實(shí)現(xiàn),其實(shí)很簡(jiǎn)單,就是在激活的時(shí)候和非激活的時(shí)候通過(guò)狀態(tài)位,修改css屬性,另外onChange的時(shí)候?qū)⒒貞?yīng)的SKU的value進(jìn)行傳遞
- value: sku對(duì)應(yīng)的sku_id
- label: 顯示的sku名稱(chēng)
- onChange: sku發(fā)生變化的時(shí)候的回調(diào)函數(shù)
- disabled: 禁用標(biāo)志位
- activate: 是否為激活模式
export const SkuCard = props => {
const { value, label, onChange, disabled, activate, style } = props;
const [innerActive, setInnerActive] = useState(activate ?? false);
const handleChange = value => () => {
if (!disabled) {
onChange?.(value, !innerActive);
setInnerActive(!innerActive);
}
};
return (
<div
className={
disabled ?? false
? "disabled"
: activate ?? innerActive
? "activate"
: "normal"
}
onClick={handleChange(value)}
style={{ ...(style ?? {}) }}
>
{label}
</div>
);
};
- SKU Group: 集中管理SKU的狀態(tài),類(lèi)似于RadioGroup, CheckboxGroup其實(shí)都可以模仿這種封裝的思路
- 利用props.children獲取各個(gè)子元素的ReactElement對(duì)象雀监,之后通過(guò)cloneElement將父組件內(nèi)管理狀態(tài)的onChange方法進(jìn)行注入(類(lèi)似于HOC那種感覺(jué))双吆,將子組件的activate和onChange方法通過(guò)父組件進(jìn)行管理
- 封裝一些其他自己要用的屬性
- 大功告成
// 定義了Empty,這個(gè)Empty對(duì)空的時(shí)候進(jìn)行設(shè)置
export const Empty = Symbol("empty");
export const SkuGroup = props => {
const { value, onChange, skuName } = props;
const [selected, setSelected] = useState(value);
const { children } = props;
const _onChange = (value, activate) => {
const _value = !activate && selected === value ? Empty : value;
setSelected(_value);
onChange?.(_value);
};
const renderGroupChild = (child, index) => {
const { props: childProps } = child;
return React.cloneElement(child, {
...childProps,
onChange: _onChange,
activate: childProps.value === selected,
key: `create-${index}`,
style: {
...(childProps?.style ?? {}),
marginLeft: index === 0 ? 0 : "20px"
}
});
};
return (
<div className="skuGroup">
{skuName && <div className="labelName">{skuName}</div>}
{children.map((child, index) => {
return child?.type === SkuCard ? renderGroupChild(child, index) : child;
})}
</div>
);
};
SKU組件實(shí)現(xiàn)的思路分析
- 從數(shù)據(jù)來(lái)看,每個(gè)商品(SPU)中包含多個(gè)SKU,所以要將多個(gè)SKU分別提出來(lái)整理成這個(gè)樣子,就是想sku進(jìn)行歸類(lèi)
- 點(diǎn)擊選中某個(gè)SKU之后,將選中的SKU的id作為篩選列表中的值,我們需要遍歷整個(gè)商品列表,篩選出在商品列表中所有滿(mǎn)足篩選條件的商品
- 通過(guò)在滿(mǎn)足條件的商品列表中進(jìn)行遍歷,得到剩下可選的sku,其余的將sku中的disabled設(shè)為true即不能被選擇
// 代碼中的幾個(gè)關(guān)鍵變量
// skuList: 商品擁有的所有sku組合的型號(hào)(SPU中的所有商品類(lèi)型)
// sku: 需要顯示的sku card
// selectSku: radio顯示選中值的[1, 2, 3]
// 初始化的時(shí)候aviableSku就是所有的商品類(lèi)目
const _getSku = (aviableSku = []) => {
const _sku = {};
const _aviableSku = {};
// 得到目前可以選擇的所有商品的sku
aviableSku.forEach(item => {
item.forEach(x => {
const key = JSON.stringify({ key_id: x.key_id, key: x.key });
const value = {
value_id: x.value_id,
value: x.value,
disabled: false
};
_aviableSku[key]
? _aviableSku[key].some(z => z.value_id === x.value_id)
? null
: _aviableSku[key].push(value)
: (_aviableSku[key] = [value]);
});
});
// 將SKU中所有不滿(mǎn)足aviableSku的東西diabled掉
skuList.forEach(item => {
// 每個(gè)商品
item.forEach((x, i) => {
// 商品下的每個(gè)sku
const key = JSON.stringify({ key_id: x.key_id, key: x.key });
const value = {
value_id: x.value_id,
value: x.value,
disabled: !_aviableSku[key].some(item => item.value_id === x.value_id)
};
_sku[key]
? _sku[key].some(z => z.value_id === x.value_id)
? null
: _sku[key].push(value)
: (_sku[key] = [value]);
});
});
setMySku(_sku);
};
- 在選擇sku的時(shí)候,我們需要確定這個(gè)sku是如何改變的,并且調(diào)整對(duì)應(yīng)的aviableSku
useEffect(() => {
// 利用useRef記錄上一次選擇sku的狀態(tài)
if (prevSku.current) {
// 找到哪一個(gè)SKU的值發(fā)生了變化
const cIndex = findChangeIndex(prevSku.current, selectSku);
if (cIndex !== -1) {
const changeValue = selectSku[cIndex];
let otherCondition = {};
const keys = Object.keys(sku);
selectSku.forEach((item, index) => {
if (
changeValue === Empty
// 改變值為Empty,說(shuō)明原來(lái)選中,現(xiàn)在取消選中場(chǎng)景
? index !== cIndex && item !== Empty
// 說(shuō)明Item是有限定值的
: item !== Empty
) {
// 將限定值保存在otherCondition中
// 記錄現(xiàn)在的限定狀態(tài)
const key_id = JSON.parse(keys[index])["key_id"];
otherCondition[key_id]?.push(item) ??
(otherCondition[key_id] = [item]);
}
});
// 通過(guò)限定矩陣的值挑選出滿(mǎn)足條件的商品類(lèi)別
const aviableSku = skuList.filter(good => {
const aviableGood = good.map(sku => {
const isInOther = otherCondition[sku.key_id];
return isInOther !== undefined
? isInOther.includes(sku.value_id)
: true;
});
return aviableGood.every(item => item);
});
_getSku(aviableSku);
}
} else {
_getSku(skuList);
}
prevSku.current = selectSku;
}, [selectSku]);