UNPKG

@react-vant-next/campaign

Version:

React Mobile UI Components based on Vant UI - Next Generation

386 lines (381 loc) 17.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var tslib = require('tslib'); var jsxRuntime = require('react/jsx-runtime'); var hooks = require('@react-vant-next/hooks'); var ui = require('@react-vant-next/ui'); var utils = require('@react-vant-next/utils'); var cls = require('clsx'); var React = require('react'); var SkuRow = require('./components/SkuRow.js'); var SkuRowItem = require('./components/SkuRowItem.js'); var SkuRowPropItem = require('./components/SkuRowPropItem.js'); var SkuStepper = require('./components/SkuStepper.js'); var constants = require('./constants.js'); var utils$1 = require('./utils.js'); const { QUOTA_LIMIT } = constants.LIMIT_TYPE; const [bem] = utils.createNamespace("sku"); function Sku(_a) { var _b, _c; var { ref } = _a, p = tslib.__rest(_a, ["ref"]); const props = utils.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 = React.useRef(false); const [visible, setVisible] = React.useState(false); const [state, updateState] = hooks.useSetState({ selectedSku: {}, selectedProp: {}, selectedNum: props.startSaleNum, }); const { sku, properties = [] } = props; const { tree = [] } = sku; const bodyStyle = React.useMemo(() => { const maxHeight = window.innerHeight - props.bodyOffsetTop; return { maxHeight: `${maxHeight}px`, }; }, [props.bodyOffsetTop]); const imageList = React.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 = React.useMemo(() => !sku.none_sku, [sku.none_sku]); const hasSkuOrAttr = React.useMemo(() => hasSku || properties.length > 0, [hasSku, properties]); const isSkuCombSelected = React.useMemo(() => { // SKU 未选完 if (hasSku && !utils$1.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 = React.useMemo(() => { return utils$1.getSelectedSkuValues(tree, state.selectedSku); }, [tree, state.selectedSku]); const selectedPropValues = React.useMemo(() => { return utils$1.getSelectedPropValues(properties, state.selectedProp); }, [properties, state.selectedProp]); const selectedSkuComb = React.useMemo(() => { let skuComb = null; if (isSkuCombSelected) { if (hasSku) { skuComb = utils$1.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 = utils$1.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 = React.useMemo(() => { return tree.filter(item => !state.selectedSku[item.k_s]).map(item => item.k); }, [tree, state.selectedSku]); const getUnselectedProp = React.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 = React.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 = React.useMemo(() => { if (selectedSkuComb) { return ((selectedSkuComb.price + selectedSkuComb.property_price) / 100).toFixed(2); } // sku.price是一个格式化好的价格区间 return sku.price; }, [JSON.stringify(selectedSkuComb), sku.price]); const stock = React.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 = React.useMemo(() => { if (props.stockRender) { return props.stockRender(stock); } return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: ["\u5269\u4F59", jsxRuntime.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]: constants.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) { ui.Toast(`${props.startSaleNum}件起售`); } else { ui.Toast("至少选择一件"); } } else if (action === "plus") { if (limitType === QUOTA_LIMIT) { if (quotaUsed > 0) { ui.Toast(`每人限购${quota}件,你已购买${quotaUsed}件`); } else { ui.Toast(`每人限购${quota}件`); } } else { ui.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) => tslib.__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) { ui.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; ui.ImagePreview.open({ images: imageList, startPosition: index, onClose: () => { if (props.onClosePreview) props.onClosePreview(params); }, }); if (props.onOpenPreview) { props.onOpenPreview(params); } }; const renderHeaderInfo = () => { var _a; return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [((_a = props.skuHeaderPriceRender) === null || _a === void 0 ? void 0 : _a.call(props, price)) || (jsxRuntime.jsxs("div", { className: cls(bem("goods-price")), children: [jsxRuntime.jsx("span", { className: cls(bem("price-symbol")), children: "\uFFE5" }), jsxRuntime.jsx("span", { className: cls(bem("price-num")), children: price }), props.priceTag && (jsxRuntime.jsx("span", { className: cls(bem("price-tag")), children: props.priceTag }))] })), props.skuHeaderOriginPrice && (jsxRuntime.jsx("div", { className: cls(bem("header-item")), children: props.skuHeaderOriginPrice })), !props.hideStock && (jsxRuntime.jsx("div", { className: cls(bem("header-item")), children: jsxRuntime.jsx("span", { className: cls(bem("stock")), children: stockContent }) })), !props.hideSelectedText && (jsxRuntime.jsx("div", { className: cls(bem("header-item")), children: selectedText }))] })); }; const renderHeader = () => { if (props.skuHeader) return props.skuHeader; const selectedValue = utils$1.getSkuImgValue(sku, state.selectedSku); const imgUrl = selectedValue ? selectedValue.imgUrl : props.goods.picture; return (jsxRuntime.jsxs("div", { className: cls(bem("header"), ui.BORDER_BOTTOM), children: [props.showHeaderImage && (jsxRuntime.jsx(ui.Image, { fit: "cover", src: imgUrl, className: cls(bem("header__img-wrap")), onClick: () => onPreviewImage(selectedValue), children: props.skuHeaderImageExtra })), jsxRuntime.jsxs("div", { className: cls(bem("header__goods-info")), children: [renderHeaderInfo(), props.skuHeaderExtra] })] })); }; const renderGroup = () => { return (props.skuGroup || (hasSkuOrAttr && (jsxRuntime.jsxs("div", { className: cls(bem("group-container", { "hide-soldout": !props.showSoldoutSku, })), children: [tree.map((skuTreeItem, i) => (jsxRuntime.jsx(SkuRow.default, { skuRow: skuTreeItem, children: skuTreeItem.v.map((skuValue, idx) => (jsxRuntime.jsx(SkuRowItem.default, { 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) => (jsxRuntime.jsx(SkuRow.default, { skuRow: skuTreeItem, children: skuTreeItem.v.map((skuValue, idx) => (jsxRuntime.jsx(SkuRowPropItem.default, { skuValue: skuValue, skuKeyStr: `${skuTreeItem.k_id}`, selectedProp: state.selectedProp, multiple: skuTreeItem.is_multiple, onSkuPropSelected: onPropSelect }, idx))) }, i)))] })))); }; const renderStepper = () => props.skuStepper || (jsxRuntime.jsx(SkuStepper.default, { 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 (jsxRuntime.jsxs("div", { className: cls(bem("body")), style: bodyStyle, children: [props.skuBodyTop, renderGroup(), props.skuGroupExtra, renderStepper()] })); }; const renderActions = () => { return (props.skuActions || (jsxRuntime.jsx("div", { className: cls(bem("actions")), children: jsxRuntime.jsxs(ui.ActionBar, { children: [props.showAddCartBtn && (jsxRuntime.jsx(ui.ActionBar.Button, { type: "warning", text: props.addCartText || "加入购物车", onClick: () => onBuyOrAddCart("add-cart") })), jsxRuntime.jsx(ui.ActionBar.Button, { type: "danger", text: props.buyText || "立即购买", onClick: () => onBuyOrAddCart("buy-clicked") })] }) }))); }; React.useEffect(() => { if (props.initialSku) { updateState(props.initialSku); } }, [JSON.stringify(props.initialSku)]); React.useImperativeHandle(ref, () => ({ reset, getSkuData, show, update: updateState, })); return (jsxRuntime.jsxs(ui.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] }))); } exports.default = Sku; //# sourceMappingURL=Sku.js.map