@react-vant-next/campaign
Version:
React Mobile UI Components based on Vant UI - Next Generation
382 lines (379 loc) • 16.8 kB
JavaScript
import { __rest, __awaiter } from 'tslib';
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { useSetState } from '@react-vant-next/hooks';
import { Popup, Image, BORDER_BOTTOM, ActionBar, ImagePreview, Toast } from '@react-vant-next/ui';
import { createNamespace, mergeProps } from '@react-vant-next/utils';
import cls from 'clsx';
import { useRef, useState, useMemo, useCallback, useEffect, useImperativeHandle } from 'react';
import SkuRow from './components/SkuRow.js';
import SkuRowItem from './components/SkuRowItem.js';
import SkuRowPropItem from './components/SkuRowPropItem.js';
import SkuStepper from './components/SkuStepper.js';
import { UNSELECTED_SKU_VALUE_ID, LIMIT_TYPE } from './constants.js';
import { isAllSelected, getSelectedSkuValues, getSelectedPropValues, getSkuComb, getSelectedProperties, getSkuImgValue } from './utils.js';
const { QUOTA_LIMIT } = LIMIT_TYPE;
const [bem] = createNamespace("sku");
function Sku(_a) {
var _b, _c;
var { ref } = _a, p = __rest(_a, ["ref"]);
const props = mergeProps(p, {
stepperTitle: "购买数量",
properties: [],
showAddCartBtn: true,
disableSoldoutSku: true,
showHeaderImage: true,
previewOnClickImage: true,
showSoldoutSku: true,
resetOnHide: true,
safeAreaInsetBottom: true,
quota: 0,
quotaUsed: 0,
startSaleNum: 1,
stockThreshold: 50,
bodyOffsetTop: 200,
customStepperConfig: {},
});
const stepperError = useRef(false);
const [visible, setVisible] = useState(false);
const [state, updateState] = useSetState({
selectedSku: {},
selectedProp: {},
selectedNum: props.startSaleNum,
});
const { sku, properties = [] } = props;
const { tree = [] } = sku;
const bodyStyle = useMemo(() => {
const maxHeight = window.innerHeight - props.bodyOffsetTop;
return {
maxHeight: `${maxHeight}px`,
};
}, [props.bodyOffsetTop]);
const imageList = useMemo(() => {
const { goods } = props;
const rs = [goods === null || goods === void 0 ? void 0 : goods.picture];
if (sku.tree.length > 0) {
sku.tree.forEach((treeItem) => {
if (!treeItem.v)
return;
treeItem.v.forEach((vItem) => {
const img = vItem.previewImgUrl || vItem.imgUrl || vItem.img_url;
if (img && !rs.includes(img)) {
rs.push(img);
}
});
});
}
return rs;
}, [(_b = props.goods) === null || _b === void 0 ? void 0 : _b.picture, sku.tree]);
const hasSku = useMemo(() => !sku.none_sku, [sku.none_sku]);
const hasSkuOrAttr = useMemo(() => hasSku || properties.length > 0, [hasSku, properties]);
const isSkuCombSelected = useMemo(() => {
// SKU 未选完
if (hasSku && !isAllSelected(tree, state.selectedSku)) {
return false;
}
// 属性未全选
return !properties
.filter(i => i.is_necessary !== false)
.some(i => (state.selectedProp[i.k_id] || []).length === 0);
}, [hasSku, state]);
const selectedSkuValues = useMemo(() => {
return getSelectedSkuValues(tree, state.selectedSku);
}, [tree, state.selectedSku]);
const selectedPropValues = useMemo(() => {
return getSelectedPropValues(properties, state.selectedProp);
}, [properties, state.selectedProp]);
const selectedSkuComb = useMemo(() => {
let skuComb = null;
if (isSkuCombSelected) {
if (hasSku) {
skuComb = getSkuComb(sku.list, state.selectedSku);
}
else {
skuComb = {
id: sku.collection_id,
price: Math.round(+sku.price * 100),
stock_num: sku.stock_num,
};
}
if (skuComb) {
skuComb.properties = getSelectedProperties(properties, state.selectedProp);
skuComb.property_price = selectedPropValues.reduce((acc, cur) => acc + (cur.price || 0), 0);
}
}
return skuComb;
}, [
isSkuCombSelected,
hasSku,
JSON.stringify(sku),
JSON.stringify(state),
properties,
selectedPropValues,
]);
const unselectedSku = useMemo(() => {
return tree.filter(item => !state.selectedSku[item.k_s]).map(item => item.k);
}, [tree, state.selectedSku]);
const getUnselectedProp = useCallback((isNecessary) => {
return properties
.filter(item => (isNecessary ? item.is_necessary !== false : true))
.filter(item => (state.selectedProp[item.k_id] || []).length < 1)
.map(item => item.k);
}, [properties, state.selectedProp]);
const selectedText = useMemo(() => {
if (selectedSkuComb) {
const values = selectedSkuValues.concat(selectedPropValues);
return `已选 ${values.map(item => item.name).join(" ")}`;
}
return `请选择 ${unselectedSku.concat(getUnselectedProp()).join(" ")}`;
}, [
unselectedSku,
getUnselectedProp,
selectedSkuComb,
selectedSkuValues,
selectedPropValues,
]);
const price = useMemo(() => {
if (selectedSkuComb) {
return ((selectedSkuComb.price + selectedSkuComb.property_price)
/ 100).toFixed(2);
}
// sku.price是一个格式化好的价格区间
return sku.price;
}, [JSON.stringify(selectedSkuComb), sku.price]);
const stock = useMemo(() => {
const { stockNum } = props.customStepperConfig;
if (stockNum !== undefined) {
return stockNum;
}
if (selectedSkuComb) {
return selectedSkuComb.stock_num;
}
return sku.stock_num;
}, [sku.stock_num, JSON.stringify(selectedSkuComb)]);
const stockContent = useMemo(() => {
if (props.stockRender) {
return props.stockRender(stock);
}
return (jsxs(Fragment, { children: ["\u5269\u4F59", jsx("span", { className: cls(bem("stock-num", {
highlight: stock < props.stockThreshold,
})), children: stock }), "\u4EF6"] }));
}, [stock]);
const onSelect = (skuValue) => {
// 点击已选中的sku时则取消选中
const selectedSku = state.selectedSku[skuValue.skuKeyStr] === skuValue.id
? Object.assign(Object.assign({}, state.selectedSku), { [skuValue.skuKeyStr]: UNSELECTED_SKU_VALUE_ID }) : Object.assign(Object.assign({}, state.selectedSku), { [skuValue.skuKeyStr]: skuValue.id });
updateState({ selectedSku });
if (props.onSkuSelected) {
props.onSkuSelected({
skuValue,
selectedSku: state.selectedSku,
selectedSkuComb,
});
}
};
const onPropSelect = (propValue) => {
const arr = state.selectedProp[propValue.skuKeyStr] || [];
const pos = arr.indexOf(propValue.id);
if (pos > -1) {
arr.splice(pos, 1);
}
else if (propValue.multiple) {
arr.push(propValue.id);
}
else {
arr.splice(0, 1, propValue.id);
}
const selectedProp = Object.assign(Object.assign({}, state.selectedProp), { [propValue.skuKeyStr]: arr });
updateState({ selectedProp });
if (props.onSkuPropSelected) {
props.onSkuPropSelected({
propValue,
selectedProp: state.selectedProp,
selectedSkuComb,
});
}
};
const onOverLimit = (data) => {
const { action, limitType, quota, quotaUsed } = data;
const { handleOverLimit } = props.customStepperConfig;
if (handleOverLimit) {
handleOverLimit(data);
return;
}
if (action === "minus") {
if (props.startSaleNum > 1) {
Toast(`${props.startSaleNum}件起售`);
}
else {
Toast("至少选择一件");
}
}
else if (action === "plus") {
if (limitType === QUOTA_LIMIT) {
if (quotaUsed > 0) {
Toast(`每人限购${quota}件,你已购买${quotaUsed}件`);
}
else {
Toast(`每人限购${quota}件`);
}
}
else {
Toast("库存不足");
}
}
};
const onStepperState = (data) => {
stepperError.current = data.valid
? null
: Object.assign(Object.assign({}, data), { action: "plus" });
};
const validateSku = () => {
if (state.selectedNum === 0) {
return "商品已经无法购买啦";
}
if (isSkuCombSelected) {
return "";
}
return `请选择 ${unselectedSku.concat(getUnselectedProp(true)).join(" ")}`;
};
const getSkuData = () => {
return {
goodsId: props.goodsId,
selectedNum: state.selectedNum,
selectedSkuComb,
};
};
const onAddCart = (data) => {
var _a;
(_a = props.onAddCart) === null || _a === void 0 ? void 0 : _a.call(props, data);
};
const onBuyClicked = (data) => {
var _a;
(_a = props.onBuyClicked) === null || _a === void 0 ? void 0 : _a.call(props, data);
};
const onBuyOrAddCart = (type) => __awaiter(this, void 0, void 0, function* () {
// sku 不符合购买条件
if (stepperError.current) {
onOverLimit(stepperError.current);
return;
}
if (props.customSkuValidator) {
if (!(yield props.customSkuValidator(type, Object.assign(Object.assign({}, state.selectedSku), state.selectedProp)))) {
return;
}
}
else {
const error = validateSku();
if (error) {
Toast(error);
return;
}
}
const data = getSkuData();
if (type === "add-cart") {
onAddCart(data);
}
else {
onBuyClicked(data);
}
});
const show = (initialValue) => {
setVisible(true);
if (initialValue) {
updateState(initialValue);
}
};
const reset = () => {
updateState({
selectedSku: {},
selectedProp: {},
selectedNum: props.startSaleNum,
});
};
const onPopupClose = () => {
setVisible(false);
if (props.popupProps && props.popupProps.onClose)
props.popupProps.onClose();
};
const onPopupClosed = () => {
if (props.resetOnHide) {
reset();
}
if (props.popupProps && props.popupProps.onClosed)
props.popupProps.onClosed();
};
const onPreviewImage = (selectedValue) => {
let index = 0;
let indexImage = imageList[0];
if (selectedValue && selectedValue.imgUrl) {
imageList.some((image, pos) => {
if (image === selectedValue.imgUrl) {
index = pos;
return true;
}
return false;
});
indexImage = selectedValue.imgUrl;
}
const params = Object.assign(Object.assign({}, selectedValue), { index,
imageList,
indexImage });
if (!props.previewOnClickImage)
return;
ImagePreview.open({
images: imageList,
startPosition: index,
onClose: () => {
if (props.onClosePreview)
props.onClosePreview(params);
},
});
if (props.onOpenPreview) {
props.onOpenPreview(params);
}
};
const renderHeaderInfo = () => {
var _a;
return (jsxs(Fragment, { children: [((_a = props.skuHeaderPriceRender) === null || _a === void 0 ? void 0 : _a.call(props, price)) || (jsxs("div", { className: cls(bem("goods-price")), children: [jsx("span", { className: cls(bem("price-symbol")), children: "\uFFE5" }), jsx("span", { className: cls(bem("price-num")), children: price }), props.priceTag && (jsx("span", { className: cls(bem("price-tag")), children: props.priceTag }))] })), props.skuHeaderOriginPrice && (jsx("div", { className: cls(bem("header-item")), children: props.skuHeaderOriginPrice })), !props.hideStock && (jsx("div", { className: cls(bem("header-item")), children: jsx("span", { className: cls(bem("stock")), children: stockContent }) })), !props.hideSelectedText && (jsx("div", { className: cls(bem("header-item")), children: selectedText }))] }));
};
const renderHeader = () => {
if (props.skuHeader)
return props.skuHeader;
const selectedValue = getSkuImgValue(sku, state.selectedSku);
const imgUrl = selectedValue
? selectedValue.imgUrl
: props.goods.picture;
return (jsxs("div", { className: cls(bem("header"), BORDER_BOTTOM), children: [props.showHeaderImage && (jsx(Image, { fit: "cover", src: imgUrl, className: cls(bem("header__img-wrap")), onClick: () => onPreviewImage(selectedValue), children: props.skuHeaderImageExtra })), jsxs("div", { className: cls(bem("header__goods-info")), children: [renderHeaderInfo(), props.skuHeaderExtra] })] }));
};
const renderGroup = () => {
return (props.skuGroup
|| (hasSkuOrAttr && (jsxs("div", { className: cls(bem("group-container", {
"hide-soldout": !props.showSoldoutSku,
})), children: [tree.map((skuTreeItem, i) => (jsx(SkuRow, { skuRow: skuTreeItem, children: skuTreeItem.v.map((skuValue, idx) => (jsx(SkuRowItem, { skuList: sku.list, skuValue: skuValue, skuKeyStr: `${skuTreeItem.k_s}`, selectedSku: state.selectedSku, disableSoldoutSku: props.disableSoldoutSku, largeImageMode: skuTreeItem.largeImageMode, previewIcon: props.previewIcon, onSkuSelected: onSelect, onSkuPreviewImage: selectedValue => onPreviewImage(selectedValue) }, idx))) }, i))), properties.map((skuTreeItem, i) => (jsx(SkuRow, { skuRow: skuTreeItem, children: skuTreeItem.v.map((skuValue, idx) => (jsx(SkuRowPropItem, { skuValue: skuValue, skuKeyStr: `${skuTreeItem.k_id}`, selectedProp: state.selectedProp, multiple: skuTreeItem.is_multiple, onSkuPropSelected: onPropSelect }, idx))) }, i)))] }))));
};
const renderStepper = () => props.skuStepper || (jsx(SkuStepper, { currentNum: state.selectedNum, onChange: (currentValue) => {
updateState({ selectedNum: Number.parseInt(`${currentValue}`, 10) });
if (props.onStepperChange)
props.onStepperChange(currentValue);
}, stock: stock, quota: props.quota, quotaUsed: props.quotaUsed, startSaleNum: props.startSaleNum, disableStepperInput: props.disableStepperInput, customStepperConfig: props.customStepperConfig, stepperTitle: props.stepperTitle, hideQuotaText: props.hideQuotaText, onSkuStepperState: onStepperState, onSkuOverLimit: onOverLimit }));
const renderBody = () => {
return (jsxs("div", { className: cls(bem("body")), style: bodyStyle, children: [props.skuBodyTop, renderGroup(), props.skuGroupExtra, renderStepper()] }));
};
const renderActions = () => {
return (props.skuActions || (jsx("div", { className: cls(bem("actions")), children: jsxs(ActionBar, { children: [props.showAddCartBtn && (jsx(ActionBar.Button, { type: "warning", text: props.addCartText || "加入购物车", onClick: () => onBuyOrAddCart("add-cart") })), jsx(ActionBar.Button, { type: "danger", text: props.buyText || "立即购买", onClick: () => onBuyOrAddCart("buy-clicked") })] }) })));
};
useEffect(() => {
if (props.initialSku) {
updateState(props.initialSku);
}
}, [JSON.stringify(props.initialSku)]);
useImperativeHandle(ref, () => ({
reset,
getSkuData,
show,
update: updateState,
}));
return (jsxs(Popup, Object.assign({ round: true, closeable: true, position: "bottom" }, props.popupProps, { visible: visible, onClose: onPopupClose, onClosed: onPopupClosed, className: cls((_c = props.popupProps) === null || _c === void 0 ? void 0 : _c.className, bem("container")), children: [renderHeader(), renderBody(), props.skuActionsTop, renderActions(), props.skuActionsBottom] })));
}
export { Sku as default };
//# sourceMappingURL=Sku.js.map