SKU组件(React版)
SKU组件(React版)
这里的一些逻辑还是需要自己再优化一下的
起因
今天看掘金的时候看到前端SKU算法实现,因为公司也有涉及到SKU的业务,记录一下自己写SKU的一个例子吧,刚好他有提供后端的API接口数据,mock一下干起来,但是在做的时候还是有很多问题的,这里做一下记录
实现效果
Peek 2020-03-16 12-08.gif
mock数据
export const simulatedSku = {
id: 2,
title: "林间有风自营针织衫",
subtitle: "瓜瓜设计,3件包邮",
category_id: 12,
root_category_id: 2,
price: "77.00",
img: "",
for_theme_img: "",
description: null,
discount_price: "62.00",
tags: "包邮$热门",
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: "小号 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: "中号 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: "中号 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: "大号 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: "小号 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
};
简单的封装一个SKUCard和SKUGroup
类似于RadioGroup和Radio,我们先封装一个简单的SKU Group和SKU组件,便于状态的统一管理
SKU Card的实现,其实很简单,就是在激活的时候和非激活的时候通过状态位,修改css属性,另外onChange的时候将回应的SKU的value进行传递
value: sku对应的sku_id
label: 显示的sku名称
onChange: sku发生变化的时候的回调函数
disabled: 禁用标志位
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 (
{label}
);
};
SKU Group: 集中管理SKU的状态,类似于RadioGroup, CheckboxGroup其实都可以模仿这种封装的思路
利用props.children获取各个子元素的ReactElement对象,之后通过cloneElement将父组件内管理状态的onChange方法进行注入(类似于HOC那种感觉),将子组件的activate和onChange方法通过父组件进行管理
封装一些其他自己要用的属性
大功告成
// 定义了Empty,这个Empty对空的时候进行设置
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 (
{skuName &&
);
};
SKU组件实现的思路分析
从数据来看,每个商品(SPU)中包含多个SKU,所以要将多个SKU分别提出来整理成这个样子,就是想sku进行归类
选区_059.png
点击选中某个SKU之后,将选中的SKU的id作为筛选列表中的值,我们需要遍历整个商品列表,筛选出在商品列表中所有满足筛选条件的商品
通过在满足条件的商品列表中进行遍历,得到剩下可选的sku,其余的将sku中的disabled设为true即不能被选择
// 代码中的几个关键变量
// skuList: 商品拥有的所有sku组合的型号(SPU中的所有商品类型)
// sku: 需要显示的sku card
// selectSku: radio显示选中值的[1, 2, 3]
// 初始化的时候aviableSku就是所有的商品类目
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中所有不满足aviableSku的东西diabled掉
skuList.forEach(item => {
// 每个商品
item.forEach((x, i) => {
// 商品下的每个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的时候,我们需要确定这个sku是如何改变的,并且调整对应的aviableSku
useEffect(() => {
// 利用useRef记录上一次选择sku的状态
if (prevSku.current) {
// 找到哪一个SKU的值发生了变化
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,说明原来选中,现在取消选中场景
? index !== cIndex && item !== Empty
// 说明Item是有限定值的
: item !== Empty
) {
// 将限定值保存在otherCondition中
// 记录现在的限定状态
const key_id = JSON.parse(keys[index])["key_id"];
otherCondition[key_id]?.push(item) ??
(otherCondition[key_id] = [item]);
}
});
// 通过限定矩阵的值挑选出满足条件的商品类别
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]);
{skuName}
}
{children.map((child, index) => {
return child?.type === SkuCard ? renderGroupChild(child, index) : child;
})}
发表评论 (审核通过后显示评论):