[improvement] rename packages dir to src (#3659)
This commit is contained in:
+480
@@ -0,0 +1,480 @@
|
||||
/* eslint-disable camelcase */
|
||||
import Vue from 'vue';
|
||||
import Popup from '../popup';
|
||||
import Toast from '../toast';
|
||||
import ImagePreview from '../image-preview';
|
||||
import SkuHeader from './components/SkuHeader';
|
||||
import SkuRow from './components/SkuRow';
|
||||
import SkuRowItem from './components/SkuRowItem';
|
||||
import SkuStepper from './components/SkuStepper';
|
||||
import SkuMessages from './components/SkuMessages';
|
||||
import SkuActions from './components/SkuActions';
|
||||
import { createNamespace, isDef } from '../utils';
|
||||
import { isAllSelected, isSkuChoosable, getSkuComb, getSelectedSkuValues } from './utils/skuHelper';
|
||||
import { LIMIT_TYPE, UNSELECTED_SKU_VALUE_ID } from './constants';
|
||||
|
||||
const [createComponent] = createNamespace('sku');
|
||||
const { QUOTA_LIMIT } = LIMIT_TYPE;
|
||||
|
||||
export default createComponent({
|
||||
props: {
|
||||
sku: Object,
|
||||
goods: Object,
|
||||
quota: Number,
|
||||
value: Boolean,
|
||||
buyText: String,
|
||||
quotaUsed: Number,
|
||||
goodsId: [Number, String],
|
||||
hideStock: Boolean,
|
||||
hideQuotaText: Boolean,
|
||||
stepperTitle: String,
|
||||
getContainer: Function,
|
||||
customSkuValidator: Function,
|
||||
closeOnClickOverlay: Boolean,
|
||||
disableStepperInput: Boolean,
|
||||
resetStepperOnHide: Boolean,
|
||||
resetSelectedSkuOnHide: Boolean,
|
||||
initialSku: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
showSoldoutSku: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showAddCartBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
bodyOffsetTop: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
messageConfig: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
placeholderMap: {},
|
||||
uploadImg: () => Promise.resolve(),
|
||||
uploadMaxSize: 5
|
||||
})
|
||||
},
|
||||
customStepperConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedSku: {},
|
||||
selectedNum: 1,
|
||||
show: this.value
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
show(val) {
|
||||
this.$emit('input', val);
|
||||
if (!val) {
|
||||
const selectedSkuValues = getSelectedSkuValues(this.sku.tree, this.selectedSku);
|
||||
|
||||
this.$emit('sku-close', {
|
||||
selectedSkuValues,
|
||||
selectedNum: this.selectedNum,
|
||||
selectedSkuComb: this.selectedSkuComb
|
||||
});
|
||||
|
||||
if (this.resetStepperOnHide) {
|
||||
this.resetStepper();
|
||||
}
|
||||
|
||||
if (this.resetSelectedSkuOnHide) {
|
||||
this.resetSelectedSku(this.skuTree);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
value(val) {
|
||||
this.show = val;
|
||||
},
|
||||
|
||||
skuTree(val) {
|
||||
this.resetSelectedSku(val);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
skuGroupClass() {
|
||||
return [
|
||||
'van-sku-group-container',
|
||||
'van-hairline--bottom',
|
||||
{
|
||||
'van-sku-group-container--hide-soldout': !this.showSoldoutSku
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
bodyStyle() {
|
||||
if (this.$isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// header高度82px, sku actions高度50px,如果改动了样式自己传下bodyOffsetTop调整下
|
||||
const maxHeight = window.innerHeight - this.bodyOffsetTop;
|
||||
|
||||
return {
|
||||
maxHeight: maxHeight + 'px'
|
||||
};
|
||||
},
|
||||
|
||||
isSkuCombSelected() {
|
||||
return isAllSelected(this.sku.tree, this.selectedSku);
|
||||
},
|
||||
|
||||
isSkuEmpty() {
|
||||
return Object.keys(this.sku).length === 0;
|
||||
},
|
||||
|
||||
hasSku() {
|
||||
return !this.sku.none_sku;
|
||||
},
|
||||
|
||||
selectedSkuComb() {
|
||||
if (!this.hasSku) {
|
||||
return {
|
||||
id: this.sku.collection_id,
|
||||
price: Math.round(this.sku.price * 100),
|
||||
stock_num: this.sku.stock_num
|
||||
};
|
||||
}
|
||||
if (this.isSkuCombSelected) {
|
||||
return getSkuComb(this.sku.list, this.selectedSku);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
price() {
|
||||
if (this.selectedSkuComb) {
|
||||
return (this.selectedSkuComb.price / 100).toFixed(2);
|
||||
}
|
||||
// sku.price是一个格式化好的价格区间
|
||||
return this.sku.price;
|
||||
},
|
||||
|
||||
skuTree() {
|
||||
return this.sku.tree || [];
|
||||
},
|
||||
|
||||
imageList() {
|
||||
const imageList = [this.goods.picture];
|
||||
|
||||
if (this.skuTree.length > 0) {
|
||||
const treeItem = this.skuTree.filter(item => item.k_s === 's1')[0] || {};
|
||||
|
||||
if (!treeItem.v) {
|
||||
return imageList;
|
||||
}
|
||||
|
||||
treeItem.v.forEach(vItem => {
|
||||
const img = vItem.imgUrl || vItem.img_url;
|
||||
if (img) {
|
||||
imageList.push(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return imageList;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
const skuEventBus = new Vue();
|
||||
this.skuEventBus = skuEventBus;
|
||||
|
||||
skuEventBus.$on('sku:close', this.onClose);
|
||||
skuEventBus.$on('sku:select', this.onSelect);
|
||||
skuEventBus.$on('sku:numChange', this.onNumChange);
|
||||
skuEventBus.$on('sku:previewImage', this.onPreviewImage);
|
||||
skuEventBus.$on('sku:overLimit', this.onOverLimit);
|
||||
skuEventBus.$on('sku:addCart', this.onAddCart);
|
||||
skuEventBus.$on('sku:buy', this.onBuy);
|
||||
|
||||
this.resetStepper();
|
||||
this.resetSelectedSku(this.skuTree);
|
||||
|
||||
// 组件初始化后的钩子,抛出skuEventBus
|
||||
this.$emit('after-sku-create', skuEventBus);
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetStepper() {
|
||||
const { skuStepper } = this.$refs;
|
||||
const { selectedNum } = this.initialSku;
|
||||
const num = isDef(selectedNum) ? selectedNum : 1;
|
||||
|
||||
if (skuStepper) {
|
||||
skuStepper.setCurrentNum(num);
|
||||
} else {
|
||||
this.selectedNum = num;
|
||||
}
|
||||
},
|
||||
|
||||
resetSelectedSku(skuTree) {
|
||||
this.selectedSku = {};
|
||||
|
||||
// 重置 selectedSku
|
||||
skuTree.forEach(item => {
|
||||
this.selectedSku[item.k_s] = this.initialSku[item.k_s] || UNSELECTED_SKU_VALUE_ID;
|
||||
});
|
||||
|
||||
// 只有一个 sku 规格值时默认选中
|
||||
skuTree.forEach(item => {
|
||||
const key = item.k_s;
|
||||
const valueId = item.v[0].id;
|
||||
if (
|
||||
item.v.length === 1 &&
|
||||
isSkuChoosable(this.sku.list, this.selectedSku, { key, valueId })
|
||||
) {
|
||||
this.selectedSku[key] = valueId;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getSkuMessages() {
|
||||
return this.$refs.skuMessages ? this.$refs.skuMessages.getMessages() : {};
|
||||
},
|
||||
|
||||
getSkuCartMessages() {
|
||||
return this.$refs.skuMessages ? this.$refs.skuMessages.getCartMessages() : {};
|
||||
},
|
||||
|
||||
validateSkuMessages() {
|
||||
return this.$refs.skuMessages ? this.$refs.skuMessages.validateMessages() : '';
|
||||
},
|
||||
|
||||
validateSku() {
|
||||
if (this.selectedNum === 0) {
|
||||
return '商品已经无法购买啦';
|
||||
}
|
||||
|
||||
if (this.isSkuCombSelected) {
|
||||
return this.validateSkuMessages();
|
||||
}
|
||||
|
||||
// 自定义sku校验
|
||||
if (this.customSkuValidator) {
|
||||
const err = this.customSkuValidator(this);
|
||||
if (err) return err;
|
||||
}
|
||||
|
||||
return '请先选择商品规格';
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
|
||||
onSelect(skuValue) {
|
||||
// 点击已选中的sku时则取消选中
|
||||
this.selectedSku =
|
||||
this.selectedSku[skuValue.skuKeyStr] === skuValue.id
|
||||
? { ...this.selectedSku, [skuValue.skuKeyStr]: UNSELECTED_SKU_VALUE_ID }
|
||||
: { ...this.selectedSku, [skuValue.skuKeyStr]: skuValue.id };
|
||||
|
||||
this.$emit('sku-selected', {
|
||||
skuValue,
|
||||
selectedSku: this.selectedSku,
|
||||
selectedSkuComb: this.selectedSkuComb
|
||||
});
|
||||
},
|
||||
|
||||
onNumChange(num) {
|
||||
this.selectedNum = num;
|
||||
},
|
||||
|
||||
onPreviewImage(indexImage) {
|
||||
const index = this.imageList.findIndex(image => image === indexImage);
|
||||
|
||||
const params = {
|
||||
index,
|
||||
imageList: this.imageList,
|
||||
indexImage
|
||||
};
|
||||
|
||||
this.$emit('open-preview', params);
|
||||
|
||||
ImagePreview({
|
||||
images: this.imageList,
|
||||
startPosition: index,
|
||||
onClose: () => {
|
||||
this.$emit('close-preview', params);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onOverLimit(data) {
|
||||
const { action, limitType, quota, quotaUsed } = data;
|
||||
const { handleOverLimit } = this.customStepperConfig;
|
||||
|
||||
if (handleOverLimit) {
|
||||
handleOverLimit(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'minus') {
|
||||
Toast('至少选择一件');
|
||||
} else if (action === 'plus') {
|
||||
if (limitType === QUOTA_LIMIT) {
|
||||
let msg = `限购${quota}件`;
|
||||
if (quotaUsed > 0) msg += `,${`你已购买${quotaUsed}件`}`;
|
||||
Toast(msg);
|
||||
} else {
|
||||
Toast('库存不足');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onAddCart() {
|
||||
this.onBuyOrAddCart('add-cart');
|
||||
},
|
||||
|
||||
onBuy() {
|
||||
this.onBuyOrAddCart('buy-clicked');
|
||||
},
|
||||
|
||||
onBuyOrAddCart(type) {
|
||||
const error = this.validateSku();
|
||||
if (error) {
|
||||
Toast(error);
|
||||
} else {
|
||||
this.$emit(type, this.getSkuData());
|
||||
}
|
||||
},
|
||||
|
||||
getSkuData() {
|
||||
return {
|
||||
goodsId: this.goodsId,
|
||||
selectedNum: this.selectedNum,
|
||||
selectedSkuComb: this.selectedSkuComb,
|
||||
messages: this.getSkuMessages(),
|
||||
cartMessages: this.getSkuCartMessages()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
if (this.isSkuEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
sku,
|
||||
goods,
|
||||
price,
|
||||
skuEventBus,
|
||||
selectedSku,
|
||||
selectedNum,
|
||||
stepperTitle,
|
||||
hideQuotaText,
|
||||
selectedSkuComb
|
||||
} = this;
|
||||
|
||||
const slotsProps = {
|
||||
price,
|
||||
selectedNum,
|
||||
skuEventBus,
|
||||
selectedSku,
|
||||
selectedSkuComb
|
||||
};
|
||||
const slots = name => this.slots(name, slotsProps);
|
||||
|
||||
const Header = slots('sku-header') || (
|
||||
<SkuHeader sku={sku} goods={goods} skuEventBus={skuEventBus} selectedSku={selectedSku}>
|
||||
{slots('sku-header-price') || (
|
||||
<div class="van-sku__goods-price">
|
||||
<span class="van-sku__price-symbol">¥</span>
|
||||
<span class="van-sku__price-num">{price}</span>
|
||||
</div>
|
||||
)}
|
||||
</SkuHeader>
|
||||
);
|
||||
|
||||
const Group =
|
||||
slots('sku-group') ||
|
||||
(this.hasSku && (
|
||||
<div class={this.skuGroupClass}>
|
||||
{this.skuTree.map(skuTreeItem => (
|
||||
<SkuRow skuRow={skuTreeItem}>
|
||||
{skuTreeItem.v.map(skuValue => (
|
||||
<SkuRowItem
|
||||
skuList={sku.list}
|
||||
skuValue={skuValue}
|
||||
selectedSku={selectedSku}
|
||||
skuEventBus={skuEventBus}
|
||||
skuKeyStr={skuTreeItem.k_s}
|
||||
/>
|
||||
))}
|
||||
</SkuRow>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
|
||||
const Stepper = slots('sku-stepper') || (
|
||||
<SkuStepper
|
||||
ref="skuStepper"
|
||||
quota={this.quota}
|
||||
hideStock={this.hideStock}
|
||||
quotaUsed={this.quotaUsed}
|
||||
skuEventBus={skuEventBus}
|
||||
selectedNum={selectedNum}
|
||||
selectedSku={selectedSku}
|
||||
stepperTitle={stepperTitle}
|
||||
skuStockNum={sku.stock_num}
|
||||
hideQuotaText={hideQuotaText}
|
||||
selectedSkuComb={selectedSkuComb}
|
||||
disableStepperInput={this.disableStepperInput}
|
||||
customStepperConfig={this.customStepperConfig}
|
||||
onChange={event => {
|
||||
this.$emit('stepper-change', event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Messages = slots('sku-messages') || (
|
||||
<SkuMessages
|
||||
ref="skuMessages"
|
||||
goodsId={this.goodsId}
|
||||
messageConfig={this.messageConfig}
|
||||
messages={sku.messages}
|
||||
/>
|
||||
);
|
||||
|
||||
const Actions = slots('sku-actions') || (
|
||||
<SkuActions
|
||||
buyText={this.buyText}
|
||||
skuEventBus={skuEventBus}
|
||||
showAddCartBtn={this.showAddCartBtn}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
vModel={this.show}
|
||||
position="bottom"
|
||||
class="van-sku-container"
|
||||
getContainer={this.getContainer}
|
||||
closeOnClickOverlay={this.closeOnClickOverlay}
|
||||
>
|
||||
{Header}
|
||||
<div class="van-sku-body" style={this.bodyStyle}>
|
||||
{slots('sku-body-top')}
|
||||
{Group}
|
||||
{slots('extra-sku-group')}
|
||||
{Stepper}
|
||||
{Messages}
|
||||
</div>
|
||||
{Actions}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import { inherit } from '../../utils/functional';
|
||||
import Button from '../../button';
|
||||
|
||||
// Types
|
||||
import Vue, { CreateElement, RenderContext } from 'vue/types';
|
||||
import { DefaultSlots } from '../../utils/types';
|
||||
|
||||
export type SkuActionsProps = {
|
||||
buyText?: string;
|
||||
skuEventBus: Vue;
|
||||
showAddCartBtn?: boolean;
|
||||
};
|
||||
|
||||
const [createComponent, bem] = createNamespace('sku-actions');
|
||||
|
||||
function SkuActions(
|
||||
h: CreateElement,
|
||||
props: SkuActionsProps,
|
||||
slots: DefaultSlots,
|
||||
ctx: RenderContext<SkuActionsProps>
|
||||
) {
|
||||
const createEmitter = (name: string) => () => {
|
||||
props.skuEventBus.$emit(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={bem()} {...inherit(ctx)}>
|
||||
{props.showAddCartBtn && (
|
||||
<Button
|
||||
square
|
||||
size="large"
|
||||
type="warning"
|
||||
text="加入购物车"
|
||||
onClick={createEmitter('sku:addCart')}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
square
|
||||
size="large"
|
||||
type="danger"
|
||||
text={props.buyText || '立即购买'}
|
||||
onClick={createEmitter('sku:buy')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkuActions.props = {
|
||||
buyText: String,
|
||||
skuEventBus: Object,
|
||||
showAddCartBtn: Boolean
|
||||
};
|
||||
|
||||
export default createComponent<SkuActionsProps>(SkuActions);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import { inherit } from '../../utils/functional';
|
||||
import Icon from '../../icon';
|
||||
|
||||
// Types
|
||||
import Vue, { CreateElement, RenderContext } from 'vue/types';
|
||||
import { DefaultSlots } from '../../utils/types';
|
||||
import { SkuData, SkuGoodsData, SelectedSkuData } from '../type';
|
||||
|
||||
export type SkuHeaderProps = {
|
||||
sku: SkuData;
|
||||
goods: SkuGoodsData;
|
||||
skuEventBus: Vue;
|
||||
selectedSku: SelectedSkuData;
|
||||
};
|
||||
|
||||
const [createComponent, bem] = createNamespace('sku-header');
|
||||
|
||||
function getSkuImg(sku: SkuData, selectedSku: SelectedSkuData) {
|
||||
const id = selectedSku.s1;
|
||||
|
||||
if (id) {
|
||||
// skuImg 挂载在 skuTree 中 s1 上
|
||||
const treeItem = sku.tree.filter(item => item.k_s === 's1')[0] || {};
|
||||
if (treeItem.v) {
|
||||
const matchedSku = treeItem.v.filter(skuValue => skuValue.id === id)[0];
|
||||
if (matchedSku) {
|
||||
return matchedSku.imgUrl || matchedSku.img_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function SkuHeader(
|
||||
h: CreateElement,
|
||||
props: SkuHeaderProps,
|
||||
slots: DefaultSlots,
|
||||
ctx: RenderContext<SkuHeaderProps>
|
||||
) {
|
||||
const { sku, goods, skuEventBus, selectedSku } = props;
|
||||
const goodsImg = getSkuImg(sku, selectedSku) || goods.picture;
|
||||
|
||||
const previewImage = () => {
|
||||
skuEventBus.$emit('sku:previewImage', goodsImg);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={[bem(), 'van-hairline--bottom']} {...inherit(ctx)}>
|
||||
<div class={bem('img-wrap')} onClick={previewImage}>
|
||||
<img src={goodsImg} />
|
||||
</div>
|
||||
<div class={bem('goods-info')}>
|
||||
<div class="van-sku__goods-name van-ellipsis">{goods.title}</div>
|
||||
{slots.default && slots.default()}
|
||||
<Icon
|
||||
name="close"
|
||||
class="van-sku__close-icon"
|
||||
onClick={() => {
|
||||
skuEventBus.$emit('sku:close');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkuHeader.props = {
|
||||
sku: Object,
|
||||
goods: Object,
|
||||
skuEventBus: Object,
|
||||
selectedSku: Object
|
||||
};
|
||||
|
||||
export default createComponent<SkuHeaderProps>(SkuHeader);
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import Icon from '../../icon';
|
||||
import Loading from '../../loading';
|
||||
import Uploader from '../../uploader';
|
||||
|
||||
const [createComponent, bem] = createNamespace('sku-img-uploader');
|
||||
|
||||
export default createComponent({
|
||||
props: {
|
||||
value: String,
|
||||
uploadImg: Function,
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 6
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 正在上传的图片 base64
|
||||
paddingImg: ''
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
imgList() {
|
||||
return this.value && !this.paddingImg ? [this.value] : [];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
afterReadFile(file) {
|
||||
// 上传文件
|
||||
this.paddingImg = file.content;
|
||||
this.uploadImg(file.file, file.content)
|
||||
.then(img => {
|
||||
this.$emit('input', img);
|
||||
this.$nextTick(() => {
|
||||
this.paddingImg = '';
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.paddingImg = '';
|
||||
});
|
||||
},
|
||||
|
||||
onOversize() {
|
||||
this.$toast(`最大可上传图片为${this.maxSize}MB,请尝试压缩图片尺寸`);
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const { imgList, paddingImg } = this;
|
||||
|
||||
const ImageList = (paddingImg || imgList.length > 0) && (
|
||||
<div class="van-clearfix">
|
||||
{imgList.map(img => (
|
||||
<div class={bem('img')}>
|
||||
<img src={img} />
|
||||
<Icon
|
||||
name="clear"
|
||||
class={bem('delete')}
|
||||
onClick={() => {
|
||||
this.$emit('input', '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{paddingImg && (
|
||||
<div class={bem('img')}>
|
||||
<img src={paddingImg} />
|
||||
<Loading type="spinner" class={bem('uploading')} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={bem()}>
|
||||
<Uploader
|
||||
disabled={!!paddingImg}
|
||||
afterRead={this.afterReadFile}
|
||||
maxSize={this.maxSize * 1024 * 1024}
|
||||
onOversize={this.onOversize}
|
||||
>
|
||||
<div class={bem('header')}>
|
||||
{paddingImg ? (
|
||||
<div>正在上传...</div>
|
||||
) : (
|
||||
[
|
||||
<Icon name="photograph" />,
|
||||
<span class="label">{this.value ? '重拍' : '拍照'} 或 </span>,
|
||||
<Icon name="photo" />,
|
||||
<span class="label">{this.value ? '重新选择照片' : '选择照片'}</span>
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
</Uploader>
|
||||
{ImageList}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import Cell from '../../cell';
|
||||
import CellGroup from '../../cell-group';
|
||||
import Field from '../../field';
|
||||
import { isEmail } from '../../utils/validate/email';
|
||||
import { isNumber } from '../../utils/validate/number';
|
||||
import SkuImgUploader from './SkuImgUploader';
|
||||
|
||||
const [createComponent, bem] = createNamespace('sku-messages');
|
||||
|
||||
const PLACEHOLDER = {
|
||||
id_no: '输入身份证号码',
|
||||
text: '输入文本',
|
||||
tel: '输入数字',
|
||||
email: '输入邮箱',
|
||||
date: '点击选择日期',
|
||||
time: '点击选择时间',
|
||||
textarea: '点击填写段落文本',
|
||||
mobile: '输入手机号码'
|
||||
};
|
||||
|
||||
export default createComponent({
|
||||
props: {
|
||||
messages: Array,
|
||||
messageConfig: Object,
|
||||
goodsId: [Number, String]
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
messageValues: this.resetMessageValues(this.messages)
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
messages(val) {
|
||||
this.messageValues = this.resetMessageValues(val);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetMessageValues(messages) {
|
||||
return (messages || []).map(() => ({ value: '' }));
|
||||
},
|
||||
|
||||
getType(message) {
|
||||
if (+message.multiple === 1) {
|
||||
return 'textarea';
|
||||
}
|
||||
if (message.type === 'id_no') {
|
||||
return 'text';
|
||||
}
|
||||
return message.datetime > 0 ? 'datetime-local' : message.type;
|
||||
},
|
||||
|
||||
getMessages() {
|
||||
const messages = {};
|
||||
|
||||
this.messageValues.forEach((item, index) => {
|
||||
let { value } = item;
|
||||
if (this.messages[index].datetime > 0) {
|
||||
value = value.replace(/T/g, ' ');
|
||||
}
|
||||
messages[`message_${index}`] = value;
|
||||
});
|
||||
|
||||
return messages;
|
||||
},
|
||||
|
||||
getCartMessages() {
|
||||
const messages = {};
|
||||
|
||||
this.messageValues.forEach((item, index) => {
|
||||
let { value } = item;
|
||||
const message = this.messages[index];
|
||||
if (message.datetime > 0) {
|
||||
value = value.replace(/T/g, ' ');
|
||||
}
|
||||
messages[message.name] = value;
|
||||
});
|
||||
|
||||
return messages;
|
||||
},
|
||||
|
||||
getPlaceholder(message) {
|
||||
const type = +message.multiple === 1 ? 'textarea' : message.type;
|
||||
const map = this.messageConfig.placeholderMap || {};
|
||||
return map[type] || PLACEHOLDER[type];
|
||||
},
|
||||
|
||||
validateMessages() {
|
||||
const values = this.messageValues;
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const { value } = values[i];
|
||||
const message = this.messages[i];
|
||||
|
||||
if (value === '') {
|
||||
// 必填字段的校验
|
||||
if (String(message.required) === '1') { // eslint-disable-line
|
||||
const textType = message.type === 'image'
|
||||
? '请上传'
|
||||
: '请填写';
|
||||
return textType + message.name;
|
||||
}
|
||||
} else {
|
||||
if (message.type === 'tel' && !isNumber(value)) {
|
||||
return '请填写正确的数字格式留言';
|
||||
}
|
||||
if (message.type === 'mobile' && !/^\d{6,20}$/.test(value)) {
|
||||
return '手机号长度为6-20位数字';
|
||||
}
|
||||
if (message.type === 'email' && !isEmail(value)) {
|
||||
return '请填写正确的邮箱';
|
||||
}
|
||||
if (message.type === 'id_no' && (value.length < 15 || value.length > 18)) {
|
||||
return '请填写正确的身份证号码';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
return (
|
||||
<CellGroup class={bem()}>
|
||||
{this.messages.map((message, index) => (message.type === 'image' ? (
|
||||
<Cell
|
||||
class={bem('image-cell')}
|
||||
label="仅限一张"
|
||||
title={message.name}
|
||||
key={`${this.goodsId}-${index}`}
|
||||
required={String(message.required) === '1'}
|
||||
>
|
||||
<SkuImgUploader
|
||||
vModel={this.messageValues[index].value}
|
||||
uploadImg={this.messageConfig.uploadImg}
|
||||
maxSize={this.messageConfig.uploadMaxSize}
|
||||
/>
|
||||
</Cell>
|
||||
) : (
|
||||
<Field
|
||||
vModel={this.messageValues[index].value}
|
||||
maxlength="200"
|
||||
label={message.name}
|
||||
key={`${this.goodsId}-${index}`}
|
||||
required={String(message.required) === '1'}
|
||||
placeholder={this.getPlaceholder(message)}
|
||||
type={this.getType(message)}
|
||||
/>
|
||||
)))}
|
||||
</CellGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import { inherit } from '../../utils/functional';
|
||||
|
||||
// Types
|
||||
import { CreateElement, RenderContext } from 'vue/types';
|
||||
import { DefaultSlots } from '../../utils/types';
|
||||
import { SkuTreeItemData } from '../type';
|
||||
|
||||
export type SkuRowProps = {
|
||||
skuRow: SkuTreeItemData;
|
||||
};
|
||||
|
||||
const [createComponent, bem] = createNamespace('sku-row');
|
||||
|
||||
function SkuRow(
|
||||
h: CreateElement,
|
||||
props: SkuRowProps,
|
||||
slots: DefaultSlots,
|
||||
ctx: RenderContext<SkuRowProps>
|
||||
) {
|
||||
return (
|
||||
<div class={bem()} {...inherit(ctx)}>
|
||||
<div class={bem('title')}>{props.skuRow.k}:</div>
|
||||
{slots.default && slots.default()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkuRow.props = {
|
||||
skuRow: Object
|
||||
};
|
||||
|
||||
export default createComponent<SkuRowProps>(SkuRow);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import { isSkuChoosable } from '../utils/skuHelper';
|
||||
|
||||
const [createComponent] = createNamespace('sku-row-item');
|
||||
|
||||
export default createComponent({
|
||||
props: {
|
||||
skuList: Array,
|
||||
skuValue: Object,
|
||||
skuKeyStr: String,
|
||||
skuEventBus: Object,
|
||||
selectedSku: Object
|
||||
},
|
||||
|
||||
computed: {
|
||||
choosable() {
|
||||
return isSkuChoosable(this.skuList, this.selectedSku, {
|
||||
key: this.skuKeyStr,
|
||||
valueId: this.skuValue.id
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelect() {
|
||||
if (this.choosable) {
|
||||
this.skuEventBus.$emit('sku:select', {
|
||||
...this.skuValue,
|
||||
skuKeyStr: this.skuKeyStr
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const choosed = this.skuValue.id === this.selectedSku[this.skuKeyStr];
|
||||
|
||||
return (
|
||||
<span
|
||||
class={[
|
||||
'van-sku-row__item',
|
||||
{
|
||||
'van-sku-row__item--active': choosed,
|
||||
'van-sku-row__item--disabled': !this.choosable
|
||||
}
|
||||
]}
|
||||
onClick={this.onSelect}
|
||||
>
|
||||
{this.skuValue.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { createNamespace } from '../../utils';
|
||||
import Stepper from '../../stepper';
|
||||
import { LIMIT_TYPE } from '../constants';
|
||||
|
||||
const [createComponent] = createNamespace('sku-stepper');
|
||||
const { QUOTA_LIMIT, STOCK_LIMIT } = LIMIT_TYPE;
|
||||
|
||||
export default createComponent({
|
||||
props: {
|
||||
quota: Number,
|
||||
quotaUsed: Number,
|
||||
hideStock: Boolean,
|
||||
skuEventBus: Object,
|
||||
skuStockNum: Number,
|
||||
selectedSku: Object,
|
||||
selectedNum: Number,
|
||||
stepperTitle: String,
|
||||
hideQuotaText: Boolean,
|
||||
selectedSkuComb: Object,
|
||||
disableStepperInput: Boolean,
|
||||
customStepperConfig: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentNum: this.selectedNum,
|
||||
// 购买限制类型: 限购/库存
|
||||
limitType: STOCK_LIMIT
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentNum(num) {
|
||||
this.skuEventBus.$emit('sku:numChange', num);
|
||||
},
|
||||
|
||||
stepperLimit(limit) {
|
||||
if (limit < this.currentNum) {
|
||||
this.currentNum = limit;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
stock() {
|
||||
const { stockNum } = this.customStepperConfig;
|
||||
if (stockNum !== undefined) {
|
||||
return stockNum;
|
||||
}
|
||||
if (this.selectedSkuComb) {
|
||||
return this.selectedSkuComb.stock_num;
|
||||
}
|
||||
return this.skuStockNum;
|
||||
},
|
||||
|
||||
stockText() {
|
||||
const { stockFormatter } = this.customStepperConfig;
|
||||
if (stockFormatter) return stockFormatter(this.stock);
|
||||
|
||||
return `剩余${this.stock}件`;
|
||||
},
|
||||
|
||||
quotaText() {
|
||||
const { quotaText, hideQuotaText } = this.customStepperConfig;
|
||||
|
||||
if (hideQuotaText) return '';
|
||||
|
||||
let text = '';
|
||||
|
||||
if (quotaText) {
|
||||
text = quotaText;
|
||||
} else if (this.quota > 0) {
|
||||
text = `每人限购${this.quota}件`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
|
||||
stepperLimit() {
|
||||
const quotaLimit = this.quota - this.quotaUsed;
|
||||
let limit;
|
||||
|
||||
// 无限购时直接取库存,有限购时取限购数和库存数中小的那个
|
||||
if (this.quota > 0 && quotaLimit <= this.stock) {
|
||||
// 修正负的limit
|
||||
limit = quotaLimit < 0 ? 0 : quotaLimit;
|
||||
this.limitType = QUOTA_LIMIT;
|
||||
} else {
|
||||
limit = this.stock;
|
||||
this.limitType = STOCK_LIMIT;
|
||||
}
|
||||
|
||||
return limit;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setCurrentNum(num) {
|
||||
this.currentNum = num;
|
||||
},
|
||||
|
||||
onOverLimit(action) {
|
||||
this.skuEventBus.$emit('sku:overLimit', {
|
||||
action,
|
||||
limitType: this.limitType,
|
||||
quota: this.quota,
|
||||
quotaUsed: this.quotaUsed
|
||||
});
|
||||
},
|
||||
|
||||
onChange(currentValue) {
|
||||
const { handleStepperChange } = this.customStepperConfig;
|
||||
handleStepperChange && handleStepperChange(currentValue);
|
||||
this.$emit('change', currentValue);
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
return (
|
||||
<div class="van-sku-stepper-stock">
|
||||
<div class="van-sku-stepper-container">
|
||||
<div class="van-sku__stepper-title">{this.stepperTitle || '购买数量'}:</div>
|
||||
<Stepper
|
||||
vModel={this.currentNum}
|
||||
class="van-sku__stepper"
|
||||
max={this.stepperLimit}
|
||||
disableInput={this.disableStepperInput}
|
||||
onOverlimit={this.onOverLimit}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
{!this.hideStock && <div class="van-sku__stock">{this.stockText}</div>}
|
||||
{!this.hideQuotaText && this.quotaText && (
|
||||
<div class="van-sku__quota">{this.quotaText}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export const LIMIT_TYPE = {
|
||||
QUOTA_LIMIT: 0,
|
||||
STOCK_LIMIT: 1
|
||||
};
|
||||
|
||||
export const UNSELECTED_SKU_VALUE_ID = '';
|
||||
|
||||
export default {
|
||||
LIMIT_TYPE,
|
||||
UNSELECTED_SKU_VALUE_ID
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
goods_id: '946755',
|
||||
quota: 15,
|
||||
quota_used: 0,
|
||||
goods_info: {
|
||||
title: '测试商品',
|
||||
picture:
|
||||
'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg',
|
||||
price: 1
|
||||
},
|
||||
sku: {
|
||||
price: '1.00',
|
||||
stock_num: 227,
|
||||
none_sku: false,
|
||||
hide_stock: false,
|
||||
collection_id: 2261,
|
||||
tree: [
|
||||
{
|
||||
k: '颜色',
|
||||
k_id: '1',
|
||||
v: [
|
||||
{
|
||||
id: '30349',
|
||||
name: '天蓝色',
|
||||
imgUrl:
|
||||
'https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'
|
||||
},
|
||||
{
|
||||
id: '1215',
|
||||
name: '白色'
|
||||
}
|
||||
],
|
||||
k_s: 's1',
|
||||
count: 2
|
||||
},
|
||||
{
|
||||
k: '尺寸',
|
||||
k_id: '2',
|
||||
v: [
|
||||
{
|
||||
id: '1193',
|
||||
name: '1'
|
||||
},
|
||||
{
|
||||
id: '1194',
|
||||
name: '2'
|
||||
}
|
||||
],
|
||||
k_s: 's2',
|
||||
count: 2
|
||||
}
|
||||
],
|
||||
list: [
|
||||
{
|
||||
id: 2259,
|
||||
price: 100,
|
||||
discount: 100,
|
||||
code: '',
|
||||
s1: '1215',
|
||||
s2: '1193',
|
||||
s3: '0',
|
||||
s4: '0',
|
||||
s5: '0',
|
||||
stock_num: 110,
|
||||
goods_id: 946755
|
||||
},
|
||||
{
|
||||
id: 2260,
|
||||
price: 100,
|
||||
discount: 100,
|
||||
code: '',
|
||||
s1: '1215',
|
||||
s2: '1194',
|
||||
s3: '0',
|
||||
s4: '0',
|
||||
s5: '0',
|
||||
stock_num: 0,
|
||||
goods_id: 946755
|
||||
},
|
||||
{
|
||||
id: 2257,
|
||||
price: 100,
|
||||
discount: 100,
|
||||
code: '',
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
s3: '0',
|
||||
s4: '0',
|
||||
s5: '0',
|
||||
stock_num: 111,
|
||||
goods_id: 946755
|
||||
},
|
||||
{
|
||||
id: 2258,
|
||||
price: 100,
|
||||
discount: 100,
|
||||
code: '',
|
||||
s1: '30349',
|
||||
s2: '1194',
|
||||
s3: '0',
|
||||
s4: '0',
|
||||
s5: '0',
|
||||
stock_num: 6,
|
||||
goods_id: 946755
|
||||
}
|
||||
],
|
||||
messages: [
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
multiple: '0',
|
||||
name: '留言1',
|
||||
type: 'text',
|
||||
required: '1'
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
multiple: 0,
|
||||
name: '留言2',
|
||||
type: 'id_no',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
multiple: 0,
|
||||
name: '留言3',
|
||||
type: 'image',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
multiple: 1,
|
||||
name: '留言4',
|
||||
type: 'text',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
name: '数字',
|
||||
multiple: 0,
|
||||
type: 'tel',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
name: '邮件',
|
||||
multiple: 0,
|
||||
type: 'email',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
name: '日期',
|
||||
multiple: 0,
|
||||
type: 'date',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
name: '时间含日期',
|
||||
multiple: 0,
|
||||
type: 'time',
|
||||
required: 0
|
||||
},
|
||||
{
|
||||
datetime: '0',
|
||||
disable: false,
|
||||
name: '时间',
|
||||
multiple: 0,
|
||||
type: 'time',
|
||||
required: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<demo-section>
|
||||
<!-- 基础用法 -->
|
||||
<demo-block :title="$t('basicUsage')">
|
||||
<div class="sku-container">
|
||||
<van-sku
|
||||
v-model="showBase"
|
||||
:sku="skuData.sku"
|
||||
:goods="skuData.goods_info"
|
||||
:goods-id="skuData.goods_id"
|
||||
:hide-stock="skuData.sku.hide_stock"
|
||||
:quota="skuData.quota"
|
||||
:quota-used="skuData.quota_used"
|
||||
reset-stepper-on-hide
|
||||
reset-selected-sku-on-hide
|
||||
disable-stepper-input
|
||||
:close-on-click-overlay="closeOnClickOverlay"
|
||||
:message-config="messageConfig"
|
||||
:custom-sku-validator="customSkuValidator"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
<van-button
|
||||
block
|
||||
type="primary"
|
||||
@click="showBase = true"
|
||||
>
|
||||
{{ $t('basicUsage') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</demo-block>
|
||||
|
||||
<!-- 自定义步进器 -->
|
||||
<demo-block :title="$t('title2')">
|
||||
<div class="sku-container">
|
||||
<van-sku
|
||||
hide-quota-text
|
||||
v-model="showStepper"
|
||||
:sku="skuData.sku"
|
||||
:goods="skuData.goods_info"
|
||||
:goods-id="skuData.goods_id"
|
||||
:hide-stock="skuData.sku.hide_stock"
|
||||
:quota="skuData.quota"
|
||||
:quota-used="skuData.quota_used"
|
||||
:custom-stepper-config="customStepperConfig"
|
||||
:message-config="messageConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
<van-button
|
||||
block
|
||||
type="primary"
|
||||
@click="showStepper = true"
|
||||
>
|
||||
{{ $t('title2') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</demo-block>
|
||||
|
||||
<!-- 隐藏售罄sku -->
|
||||
<demo-block :title="$t('hideSoldoutSku')">
|
||||
<div class="sku-container">
|
||||
<van-sku
|
||||
hide-quota-text
|
||||
v-model="showSoldout"
|
||||
:sku="skuData.sku"
|
||||
:goods="skuData.goods_info"
|
||||
:goods-id="skuData.goods_id"
|
||||
:hide-stock="skuData.sku.hide_stock"
|
||||
:quota="skuData.quota"
|
||||
:quota-used="skuData.quota_used"
|
||||
:custom-stepper-config="customStepperConfig"
|
||||
:message-config="messageConfig"
|
||||
:show-soldout-sku="false"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
<van-button
|
||||
block
|
||||
type="primary"
|
||||
@click="showSoldout = true"
|
||||
>
|
||||
{{ $t('hideSoldoutSku') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="$t('advancedUsage')">
|
||||
<div class="sku-container">
|
||||
<van-sku
|
||||
v-model="showCustom"
|
||||
:stepper-title="$t('stepperTitle')"
|
||||
:sku="skuData.sku"
|
||||
:goods="skuData.goods_info"
|
||||
:goods-id="skuData.goods_id"
|
||||
:hide-stock="skuData.sku.hide_stock"
|
||||
:quota="skuData.quota"
|
||||
:quota-used="skuData.quota_used"
|
||||
show-add-cart-btn
|
||||
reset-stepper-on-hide
|
||||
:initial-sku="initialSku"
|
||||
:message-config="messageConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
>
|
||||
<template #sku-header-price="{ price }">
|
||||
<div class="van-sku__goods-price">
|
||||
<span class="van-sku__price-symbol">¥</span><span class="van-sku__price-num">{{ price }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #sku-actions="{ skuEventBus }">
|
||||
<div class="van-sku-actions">
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="warning"
|
||||
@click="onPointClicked"
|
||||
>
|
||||
{{ $t('button1') }}
|
||||
</van-button>
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="danger"
|
||||
@click="props.skuEventBus.$emit('sku:buy')"
|
||||
>
|
||||
{{ $t('button2') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-sku>
|
||||
<van-button
|
||||
block
|
||||
type="primary"
|
||||
@click="showCustom = true"
|
||||
>
|
||||
{{ $t('advancedUsage') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</demo-block>
|
||||
</demo-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import skuData from './data';
|
||||
import { LIMIT_TYPE } from '../constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
'zh-CN': {
|
||||
title2: '自定义步进器',
|
||||
hideSoldoutSku: '隐藏售罄规格',
|
||||
stepperTitle: '我要买',
|
||||
button1: '积分兑换',
|
||||
button2: '买买买'
|
||||
},
|
||||
'en-US': {
|
||||
title2: 'Custom Stepper Related Config',
|
||||
hideSoldoutSku: 'Hide Soldout Sku',
|
||||
stepperTitle: 'Stepper title',
|
||||
button1: 'Button',
|
||||
button2: 'Button'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
this.skuData = skuData;
|
||||
return {
|
||||
showBase: false,
|
||||
showCustom: false,
|
||||
showStepper: false,
|
||||
showSoldout: false,
|
||||
closeOnClickOverlay: true,
|
||||
initialSku: {
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
selectedNum: 3
|
||||
},
|
||||
customSkuValidator: () => '请选择xxx',
|
||||
customStepperConfig: {
|
||||
quotaText: '单次限购100件',
|
||||
stockFormatter: (stock) => `剩余${stock}件`,
|
||||
handleOverLimit: (data) => {
|
||||
const { action, limitType, quota } = data;
|
||||
|
||||
if (action === 'minus') {
|
||||
this.$toast('至少选择一件商品');
|
||||
} else if (action === 'plus') {
|
||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
||||
this.$toast(`限购${quota}件`);
|
||||
} else {
|
||||
this.$toast('库存不够了');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
messageConfig: {
|
||||
uploadImg: (file, img) => new Promise(resolve => {
|
||||
setTimeout(() => resolve(img), 1000);
|
||||
}),
|
||||
uploadMaxSize: 3
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onBuyClicked(data) {
|
||||
this.$toast('buy:' + JSON.stringify(data));
|
||||
},
|
||||
|
||||
onAddCartClicked(data) {
|
||||
this.$toast('add cart:' + JSON.stringify(data));
|
||||
},
|
||||
|
||||
onPointClicked() {
|
||||
this.$toast('积分兑换');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import "../../style/var";
|
||||
|
||||
.demo-sku {
|
||||
background-color: @white;
|
||||
|
||||
.sku-container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,297 @@
|
||||
# Sku
|
||||
|
||||
### Install
|
||||
|
||||
```javascript
|
||||
import { Sku } from 'vant';
|
||||
|
||||
Vue.use(Sku);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showBase"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
:reset-stepper-on-hide="resetStepperOnHide"
|
||||
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
|
||||
:close-on-click-overlay="closeOnClickOverlay"
|
||||
:disable-stepper-input="disableStepperInput"
|
||||
:message-config="messageConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Stepper Config
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showBase"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
:custom-stepper-config="customStepperConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showCustomAction"
|
||||
stepper-title="Stepper title"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
show-add-cart-btn
|
||||
reset-stepper-on-hide
|
||||
:initial-sku="initialSku"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
>
|
||||
<!-- custom sku-header-price -->
|
||||
<template slot="sku-header-price" slot-scope="props">
|
||||
<div class="van-sku__goods-price">
|
||||
<span class="van-sku__price-symbol">¥</span><span class="van-sku__price-num">{{ props.price }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- custom sku actions -->
|
||||
<template slot="sku-actions" slot-scope="props">
|
||||
<div class="van-sku-actions">
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="warning"
|
||||
@click="onPointClicked"
|
||||
>
|
||||
Button
|
||||
</van-button>
|
||||
<!-- trigger sku inner event -->
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="danger"
|
||||
@click="props.skuEventBus.$emit('sku:buy')"
|
||||
>
|
||||
Button
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-sku>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| Attribute | Description | Type | Default |
|
||||
|------|------|------|------|
|
||||
| v-model | Whether to show sku | `Boolean` | `false` |
|
||||
| sku | Sku data | `Object` | - |
|
||||
| goods | Goods info | `Object` | - |
|
||||
| goods-id | Goods id | `String | Number` | - |
|
||||
| hide-stock | Whether to hide stock | `Boolean` | `false` |
|
||||
| hide-quota-text | Whether to hide quota text | `Boolean` | `false` |
|
||||
| show-add-cart-btn | Whether to show cart button | `Boolean` | `true` |
|
||||
| quota | Quota (0 as no limit) | `Number` | `0` |
|
||||
| quota-used | Used quota | `Number` | `0` |
|
||||
| reset-stepper-on-hide | Whether to reset stepper when hide | `Boolean` | `false` |
|
||||
| reset-selected-sku-on-hide | Whether to reset selected sku when hide | `Boolean` | `false` |
|
||||
| disable-stepper-input | Whether to disable stepper input | `Boolean` | `false` |
|
||||
| close-on-click-overlay | Whether to close sku popup when click overlay | `Boolean` | `false` |
|
||||
| stepper-title | Quantity title | `String` | `Quantity` |
|
||||
| custom-stepper-config | Custom stepper related config | `Object` | `{}` |
|
||||
| message-config | Message related config | `Object` | `{}` |
|
||||
| get-container | Return the mount node for sku | `String | () => HTMLElement` | - |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Description | Arguments |
|
||||
|------|------|------|
|
||||
| add-cart | Triggered when click cart button | data: Object |
|
||||
| buy-clicked | Triggered when click buy button | data: Object |
|
||||
| stepper-change | Triggered when stepper value changed | value: number |
|
||||
| sku-selected | Triggered when select sku | { skuValue, selectedSku, selectedSkuComb } |
|
||||
| open-preview | Triggered when open image preview | data: Object |
|
||||
| close-preview | Triggered when close image preview | data: Object |
|
||||
|
||||
### Methods
|
||||
|
||||
Use ref to get sku instance and call instance methods
|
||||
|
||||
| Name | Attribute | Return value | Description |
|
||||
|------|------|------|------|
|
||||
| getSkuData | - | skuData | Get current skuData |
|
||||
|
||||
### Slots
|
||||
|
||||
| Name | Description |
|
||||
|------|------|
|
||||
| sku-header | Custom header |
|
||||
| sku-header-price | Custom header price area |
|
||||
| sku-body-top | Custom content before sku-group |
|
||||
| sku-group | Custom sku |
|
||||
| extra-sku-group | Extra custom content |
|
||||
| sku-stepper | Custom stepper |
|
||||
| sku-messages | Custom messages |
|
||||
| sku-actions | Custom button actions |
|
||||
|
||||
### Sku Data Structure
|
||||
|
||||
```javascript
|
||||
sku: {
|
||||
tree: [
|
||||
{
|
||||
k: 'Color',
|
||||
v: [
|
||||
{
|
||||
id: '30349',
|
||||
name: 'Red',
|
||||
imgUrl: 'https://img.yzcdn.cn/1.jpg'
|
||||
},
|
||||
{
|
||||
id: '1215',
|
||||
name: 'Blue',
|
||||
imgUrl: 'https://img.yzcdn.cn/2.jpg'
|
||||
}
|
||||
],
|
||||
k_s: 's1'
|
||||
}
|
||||
],
|
||||
list: [
|
||||
{
|
||||
id: 2259,
|
||||
price: 100,
|
||||
s1: '1215',
|
||||
s2: '1193',
|
||||
s3: '0',
|
||||
stock_num: 110
|
||||
}
|
||||
],
|
||||
price: '1.00',
|
||||
stock_num: 227,
|
||||
collection_id: 2261,
|
||||
none_sku: false,
|
||||
messages: [
|
||||
{
|
||||
datetime: '0',
|
||||
multiple: '0',
|
||||
name: 'Message',
|
||||
type: 'text',
|
||||
required: '1'
|
||||
}
|
||||
],
|
||||
hide_stock: false
|
||||
}
|
||||
```
|
||||
|
||||
### initialSku Data Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
// Key:skuKeyStr
|
||||
// Value:skuValueId
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
selectedNum: 3
|
||||
}
|
||||
```
|
||||
|
||||
### Goods Data Structure
|
||||
|
||||
```javascript
|
||||
goods: {
|
||||
title: 'Title',
|
||||
picture: 'https://img.yzcdn.cn/1.jpg'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### customStepperConfig Data Structure
|
||||
|
||||
```javascript
|
||||
customStepperConfig: {
|
||||
// custom quota text
|
||||
quotaText: 'only 5 can buy',
|
||||
// custom callback when over limit
|
||||
handleOverLimit: (data) => {
|
||||
const { action, limitType, quota, quotaUsed } = data;
|
||||
|
||||
if (action === 'minus') {
|
||||
Toast('at least select one');
|
||||
} else if (action === 'plus') {
|
||||
// const { LIMIT_TYPE } = Sku.skuConstants;
|
||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
||||
let msg = `Buy up to ${quota}`;
|
||||
if (quotaUsed > 0) msg += `,you already buy ${quotaUsed}`;
|
||||
Toast(msg);
|
||||
} else {
|
||||
Toast('not enough stock');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### messageConfig Data Structure
|
||||
|
||||
```javascript
|
||||
messageConfig: {
|
||||
// the upload image callback
|
||||
uploadImg: () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
|
||||
});
|
||||
},
|
||||
// max file size (MB)
|
||||
uploadMaxSize: 3,
|
||||
// placehold config
|
||||
placeholderMap: {
|
||||
text: 'xxx',
|
||||
tel: 'xxx',
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Events Params Data Structure
|
||||
|
||||
```javascript
|
||||
skuData: {
|
||||
goodsId: '946755',
|
||||
messages: {
|
||||
message_0: '12',
|
||||
message_1: ''
|
||||
},
|
||||
cartMessages: {
|
||||
'Message 1': 'xxxx'
|
||||
},
|
||||
selectedNum: 1,
|
||||
selectedSkuComb: {
|
||||
id: 2257,
|
||||
price: 100,
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
s3: '0',
|
||||
stock_num: 111
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
import Sku from './Sku';
|
||||
import SkuActions from './components/SkuActions';
|
||||
import SkuHeader from './components/SkuHeader';
|
||||
import SkuMessages from './components/SkuMessages';
|
||||
import SkuStepper from './components/SkuStepper';
|
||||
import SkuRow from './components/SkuRow';
|
||||
import SkuRowItem from './components/SkuRowItem';
|
||||
import skuHelper from './utils/skuHelper';
|
||||
import constants from './constants';
|
||||
|
||||
Sku.SkuActions = SkuActions;
|
||||
Sku.SkuHeader = SkuHeader;
|
||||
Sku.SkuMessages = SkuMessages;
|
||||
Sku.SkuStepper = SkuStepper;
|
||||
Sku.SkuRow = SkuRow;
|
||||
Sku.SkuRowItem = SkuRowItem;
|
||||
Sku.skuHelper = skuHelper;
|
||||
Sku.skuConstants = constants;
|
||||
|
||||
export default Sku;
|
||||
@@ -0,0 +1,249 @@
|
||||
@import '../style/var';
|
||||
@import '../style/mixins/clearfix';
|
||||
|
||||
.van-sku {
|
||||
&-container {
|
||||
max-height: max-content; /* avoid androiod keyboard cover fields */
|
||||
overflow-y: visible;
|
||||
font-size: 14px;
|
||||
background: @white;
|
||||
}
|
||||
|
||||
&-body {
|
||||
max-height: 350px;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* sku header */
|
||||
&-header {
|
||||
margin-left: 15px;
|
||||
|
||||
&__img-wrap {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: -10px;
|
||||
background: @background-color;
|
||||
border-radius: 2px;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__goods-info {
|
||||
box-sizing: border-box;
|
||||
min-height: 82px;
|
||||
padding: 10px 60px 10px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__goods-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__price-symbol {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__price-num {
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__goods-price {
|
||||
margin-top: 10px;
|
||||
color: @red;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__close-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
color: @gray-dark;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-group-container {
|
||||
margin-left: 15px;
|
||||
padding: 12px 0 2px;
|
||||
|
||||
&--hide-soldout {
|
||||
.van-sku-row__item--disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* sku row */
|
||||
&-row {
|
||||
margin: 0 15px 10px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
min-width: 52px;
|
||||
height: 28px;
|
||||
margin: 0 10px 10px 0;
|
||||
padding: 5px 9px;
|
||||
color: @text-color;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
border: 1px solid @gray-dark;
|
||||
border-radius: 3px;
|
||||
|
||||
&--active {
|
||||
color: @white;
|
||||
background: @red;
|
||||
border-color: @red;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: @gray;
|
||||
background: @active-color;
|
||||
border-color: @border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* sku stepper */
|
||||
&-stepper {
|
||||
&-stock {
|
||||
margin-left: 15px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
&-container {
|
||||
height: 30px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__stepper {
|
||||
float: right;
|
||||
|
||||
&-title {
|
||||
float: left;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&__stock {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
color: @gray-dark;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__quota {
|
||||
display: inline-block;
|
||||
color: @red;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-messages {
|
||||
padding-bottom: 10px;
|
||||
background: @background-color;
|
||||
|
||||
&__image-cell {
|
||||
.van-cell__title {
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
.van-cell__value {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-img-uploader {
|
||||
display: inline-block;
|
||||
|
||||
&__header {
|
||||
padding: 0 10px;
|
||||
color: @text-color;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
border: 1px solid @border-color;
|
||||
border-radius: 3px;
|
||||
|
||||
.van-icon {
|
||||
top: 3px;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 10px 10px 0 0;
|
||||
border: 1px solid @border-color;
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__delete {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: -14px;
|
||||
z-index: 1;
|
||||
padding: 6px;
|
||||
color: @red;
|
||||
|
||||
&::before {
|
||||
background-color: @white;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__uploading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* sku actions */
|
||||
&-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders demo correctly 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<div class="sku-container">
|
||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block"><span class="van-button__text">
|
||||
基础用法
|
||||
</span></button></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sku-container">
|
||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block"><span class="van-button__text">
|
||||
自定义步进器
|
||||
</span></button></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sku-container">
|
||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block"><span class="van-button__text">
|
||||
隐藏售罄规格
|
||||
</span></button></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sku-container">
|
||||
<!----> <button class="van-button van-button--primary van-button--normal van-button--block"><span class="van-button__text">
|
||||
高级用法
|
||||
</span></button></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Demo from '../demo';
|
||||
import demoTest from '../../../test/demo-test';
|
||||
|
||||
demoTest(Demo);
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export type SkuData = {
|
||||
price: string;
|
||||
none_sku: boolean;
|
||||
stock_num: number;
|
||||
hide_stock: boolean;
|
||||
collection_id: number;
|
||||
tree: SkuTreeItemData[];
|
||||
list: SkuListItemData[];
|
||||
messages: SkuMessageData[];
|
||||
};
|
||||
|
||||
export type SkuTreeItemData = {
|
||||
k: string;
|
||||
v: SkuTreeItemValueData[];
|
||||
k_s: string;
|
||||
};
|
||||
|
||||
export type SkuTreeItemValueData = {
|
||||
id: string;
|
||||
name: string;
|
||||
imgUrl?: string;
|
||||
img_url?: string;
|
||||
};
|
||||
|
||||
export type SkuListItemData = {
|
||||
id: number;
|
||||
s1: string;
|
||||
s2: string;
|
||||
s3: string;
|
||||
price: number;
|
||||
stock_num: number;
|
||||
};
|
||||
|
||||
export type SkuMessageData = {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: string;
|
||||
datetime?: string;
|
||||
multiple?: string;
|
||||
};
|
||||
|
||||
export type SkuGoodsData = {
|
||||
title: string;
|
||||
picture: string;
|
||||
};
|
||||
|
||||
export type SelectedSkuData = {
|
||||
s1: string;
|
||||
s2: string;
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { UNSELECTED_SKU_VALUE_ID } from '../constants';
|
||||
|
||||
/*
|
||||
normalize sku tree
|
||||
|
||||
[
|
||||
{
|
||||
count: 2,
|
||||
k: "品种", // 规格名称 skuKeyName
|
||||
k_id: "1200", // skuKeyId
|
||||
k_s: "s1" // skuKeyStr
|
||||
v: [ // skuValues
|
||||
{ // skuValue
|
||||
id: "1201", // skuValueId
|
||||
name: "萌" // 具体的规格值 skuValueName
|
||||
}, {
|
||||
id: "973",
|
||||
name: "帅"
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
|
|
||||
v
|
||||
{
|
||||
s1: [{
|
||||
id: "1201",
|
||||
name: "萌"
|
||||
}, {
|
||||
id: "973",
|
||||
name: "帅"
|
||||
}],
|
||||
...
|
||||
}
|
||||
*/
|
||||
export const normalizeSkuTree = skuTree => {
|
||||
const normalizedTree = {};
|
||||
skuTree.forEach(treeItem => {
|
||||
normalizedTree[treeItem.k_s] = treeItem.v;
|
||||
});
|
||||
return normalizedTree;
|
||||
};
|
||||
|
||||
// 判断是否所有的sku都已经选中
|
||||
export const isAllSelected = (skuTree, selectedSku) => {
|
||||
// 筛选selectedSku对象中key值不为空的值
|
||||
const selected = Object.keys(selectedSku).filter(
|
||||
skuKeyStr => selectedSku[skuKeyStr] !== UNSELECTED_SKU_VALUE_ID
|
||||
);
|
||||
return skuTree.length === selected.length;
|
||||
};
|
||||
|
||||
// 根据已选择的 sku 获取 skuComb
|
||||
export const getSkuComb = (skuList, selectedSku) => {
|
||||
const skuComb = skuList.filter(item => (
|
||||
Object.keys(selectedSku).every(
|
||||
skuKeyStr => String(item[skuKeyStr]) === String(selectedSku[skuKeyStr])
|
||||
)
|
||||
));
|
||||
return skuComb[0];
|
||||
};
|
||||
|
||||
// 获取已选择的sku名称
|
||||
export const getSelectedSkuValues = (skuTree, selectedSku) => {
|
||||
const normalizedTree = normalizeSkuTree(skuTree);
|
||||
return Object.keys(selectedSku).reduce((selectedValues, skuKeyStr) => {
|
||||
const skuValues = normalizedTree[skuKeyStr];
|
||||
const skuValueId = selectedSku[skuKeyStr];
|
||||
|
||||
if (skuValueId !== UNSELECTED_SKU_VALUE_ID) {
|
||||
const skuValue = skuValues.filter(value => value.id === skuValueId)[0];
|
||||
skuValue && selectedValues.push(skuValue);
|
||||
}
|
||||
return selectedValues;
|
||||
}, []);
|
||||
};
|
||||
|
||||
// 判断sku是否可选
|
||||
export const isSkuChoosable = (skuList, selectedSku, skuToChoose) => {
|
||||
const { key, valueId } = skuToChoose;
|
||||
|
||||
// 先假设sku已选中,拼入已选中sku对象中
|
||||
const matchedSku = Object.assign({}, selectedSku, {
|
||||
[key]: valueId
|
||||
});
|
||||
|
||||
// 再判断剩余sku是否全部不可选,若不可选则当前sku不可选中
|
||||
const skusToCheck = Object.keys(matchedSku).filter(
|
||||
skuKey => matchedSku[skuKey] !== UNSELECTED_SKU_VALUE_ID
|
||||
);
|
||||
|
||||
const filteredSku = skuList.filter(sku => (
|
||||
skusToCheck.every(
|
||||
skuKey => String(matchedSku[skuKey]) === String(sku[skuKey])
|
||||
)
|
||||
));
|
||||
|
||||
const stock = filteredSku.reduce((total, sku) => {
|
||||
total += sku.stock_num;
|
||||
return total;
|
||||
}, 0);
|
||||
return stock > 0;
|
||||
};
|
||||
|
||||
export default {
|
||||
normalizeSkuTree,
|
||||
getSkuComb,
|
||||
getSelectedSkuValues,
|
||||
isAllSelected,
|
||||
isSkuChoosable
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
# Sku 商品规格
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { Sku } from 'vant';
|
||||
|
||||
Vue.use(Sku);
|
||||
```
|
||||
|
||||
## 代码演示
|
||||
|
||||
### 基础用法
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showBase"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
:reset-stepper-on-hide="resetStepperOnHide"
|
||||
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
|
||||
:close-on-click-overlay="closeOnClickOverlay"
|
||||
:disable-stepper-input="disableStepperInput"
|
||||
:message-config="messageConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
```
|
||||
|
||||
### 自定义步进器
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showBase"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
:custom-stepper-config="customStepperConfig"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
/>
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
```html
|
||||
<van-sku
|
||||
v-model="showCustomAction"
|
||||
stepper-title="我要买"
|
||||
:sku="sku"
|
||||
:goods="goods"
|
||||
:goods-id="goodsId"
|
||||
:hide-stock="sku.hide_stock"
|
||||
:quota="quota"
|
||||
:quota-used="quotaUsed"
|
||||
show-add-cart-btn
|
||||
reset-stepper-on-hide
|
||||
:initial-sku="initialSku"
|
||||
@buy-clicked="onBuyClicked"
|
||||
@add-cart="onAddCartClicked"
|
||||
>
|
||||
<!-- 自定义 sku-header-price -->
|
||||
<template slot="sku-header-price" slot-scope="props">
|
||||
<div class="van-sku__goods-price">
|
||||
<span class="van-sku__price-symbol">¥</span><span class="van-sku__price-num">{{ props.price }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 自定义 sku actions -->
|
||||
<template slot="sku-actions" slot-scope="props">
|
||||
<div class="van-sku-actions">
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="warning"
|
||||
@click="onPointClicked"
|
||||
>
|
||||
积分兑换
|
||||
</van-button>
|
||||
<!-- 直接触发 sku 内部事件,通过内部事件执行 onBuyClicked 回调 -->
|
||||
<van-button
|
||||
square
|
||||
size="large"
|
||||
type="danger"
|
||||
@click="props.skuEventBus.$emit('sku:buy')"
|
||||
>
|
||||
买买买
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</van-sku>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
|------|------|------|------|------|
|
||||
| v-model | 是否显示sku | `Boolean` | `false` | - |
|
||||
| sku | 商品sku数据 | `Object` | - | - |
|
||||
| goods | 商品信息 | `Object` | - | - |
|
||||
| goods-id | 商品 id | `String | Number` | - | - |
|
||||
| hide-stock | 是否显示商品剩余库存 | `Boolean` | `false` | - |
|
||||
| hide-quota-text | 是否显示限购提示 | `Boolean` | `false` | 1.4.8 |
|
||||
| show-add-cart-btn | 是否显示加入购物车按钮 | `Boolean` | `true` | - |
|
||||
| quota | 限购数,0 表示不限购 | `Number` | `0` | - |
|
||||
| quota-used | 已经购买过的数量 | `Number` | `0` | - |
|
||||
| reset-stepper-on-hide | 隐藏时重置选择的商品数量 | `Boolean` | `false` | - |
|
||||
| reset-selected-sku-on-hide | 隐藏时重置已选择的 sku | `Boolean` | `false` | - |
|
||||
| disable-stepper-input | 是否禁用步进器输入 | `Boolean` | `false` | - |
|
||||
| close-on-click-overlay | 是否在点击遮罩层后关闭 | `Boolean` | `false` | - |
|
||||
| stepper-title | 数量选择组件左侧文案 | `String` | `购买数量` | - |
|
||||
| custom-stepper-config | 步进器相关自定义配置 | `Object` | `{}` | - |
|
||||
| message-config | 留言相关配置 | `Object` | `{}` | - |
|
||||
| get-container | 指定挂载的节点,可以传入选择器,<br>或一个返回节点的函数 | `String | () => HTMLElement` | - | - |
|
||||
| initial-sku | 默认选中的sku,具体参考高级用法 | `Object` | `{}` | - |
|
||||
| show-soldout-sku | 是否展示售罄的sku,如果展示,则为置灰不可选状态,不展示则直接隐藏 | `Boolean` | `true` | - |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|------|------|------|
|
||||
| add-cart | 点击添加购物车回调 | skuData: Object |
|
||||
| buy-clicked | 点击购买回调 | skuData: Object |
|
||||
| stepper-change | 购买数量变化时触发 | value: number |
|
||||
| sku-selected | 切换规格类目时触发 | { skuValue, selectedSku, selectedSkuComb } |
|
||||
| open-preview | 打开商品图片预览时触发 | data: Object |
|
||||
| close-preview | 关闭商品图片预览时触发 | data: Object |
|
||||
|
||||
### 方法
|
||||
|
||||
通过 ref 可以获取到 sku 实例并调用实例方法
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 介绍 |
|
||||
|------|------|------|------|
|
||||
| getSkuData | - | skuData | 获取当前 skuData |
|
||||
|
||||
### Slots
|
||||
|
||||
Sku 组件默认划分好了若干区块,这些区块都定义成了插槽,可以按需进行替换。区块顺序见下表:
|
||||
|
||||
| 名称 | 说明 |
|
||||
|------|------|
|
||||
| sku-header | 商品信息展示区,包含商品图片、名称、价格等信息 |
|
||||
| sku-header-price | 自定义 sku 头部价格展示 |
|
||||
| sku-body-top | sku 展示区上方的内容,无默认展示内容,按需使用 |
|
||||
| sku-group | 商品 sku 展示区 |
|
||||
| extra-sku-group | 额外商品 sku 展示区,一般用不到 |
|
||||
| sku-stepper | 商品数量选择区 |
|
||||
| sku-messages | 商品留言区 |
|
||||
| sku-actions | 操作按钮区 |
|
||||
|
||||
### sku 对象结构
|
||||
|
||||
```javascript
|
||||
sku: {
|
||||
// 所有sku规格类目与其值的从属关系,比如商品有颜色和尺码两大类规格,颜色下面又有红色和蓝色两个规格值。
|
||||
// 可以理解为一个商品可以有多个规格类目,一个规格类目下可以有多个规格值。
|
||||
tree: [
|
||||
{
|
||||
k: '颜色', // skuKeyName:规格类目名称
|
||||
v: [
|
||||
{
|
||||
id: '30349', // skuValueId:规格值 id
|
||||
name: '红色', // skuValueName:规格值名称
|
||||
imgUrl: 'https://img.yzcdn.cn/1.jpg' // 规格类目图片,只有第一个规格类目可以定义图片
|
||||
},
|
||||
{
|
||||
id: '1215',
|
||||
name: '蓝色',
|
||||
imgUrl: 'https://img.yzcdn.cn/2.jpg'
|
||||
}
|
||||
],
|
||||
k_s: 's1' // skuKeyStr:sku 组合列表(下方 list)中当前类目对应的 key 值,value 值会是从属于当前类目的一个规格值 id
|
||||
}
|
||||
],
|
||||
// 所有 sku 的组合列表,比如红色、M 码为一个 sku 组合,红色、S 码为另一个组合
|
||||
list: [
|
||||
{
|
||||
id: 2259, // skuId,下单时后端需要
|
||||
price: 100, // 价格(单位分)
|
||||
s1: '1215', // 规格类目 k_s 为 s1 的对应规格值 id
|
||||
s2: '1193', // 规格类目 k_s 为 s2 的对应规格值 id
|
||||
s3: '0', // 最多包含3个规格值,为0表示不存在该规格
|
||||
stock_num: 110 // 当前 sku 组合对应的库存
|
||||
}
|
||||
],
|
||||
price: '1.00', // 默认价格(单位元)
|
||||
stock_num: 227, // 商品总库存
|
||||
collection_id: 2261, // 无规格商品 skuId 取 collection_id,否则取所选 sku 组合对应的 id
|
||||
none_sku: false, // 是否无规格商品
|
||||
messages: [
|
||||
{
|
||||
// 商品留言
|
||||
datetime: '0', // 留言类型为 time 时,是否含日期。'1' 表示包含
|
||||
multiple: '0', // 留言类型为 text 时,是否多行文本。'1' 表示多行
|
||||
name: '留言', // 留言名称
|
||||
type: 'text', // 留言类型,可选: id_no(身份证), text, tel, date, time, email
|
||||
required: '1' // 是否必填 '1' 表示必填
|
||||
}
|
||||
],
|
||||
hide_stock: false // 是否隐藏剩余库存
|
||||
}
|
||||
```
|
||||
|
||||
### initialSku 对象结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
// 键:skuKeyStr(sku 组合列表中当前类目对应的 key 值)
|
||||
// 值:skuValueId(规格值 id)
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
// 初始选中数量
|
||||
selectedNum: 3
|
||||
}
|
||||
```
|
||||
|
||||
### goods 对象结构
|
||||
|
||||
```javascript
|
||||
goods: {
|
||||
// 商品标题
|
||||
title: '测试商品',
|
||||
// 默认商品 sku 缩略图
|
||||
picture: 'https://img.yzcdn.cn/1.jpg'
|
||||
}
|
||||
```
|
||||
|
||||
### customStepperConfig 对象结构
|
||||
|
||||
```javascript
|
||||
customStepperConfig: {
|
||||
// 自定义限购文案
|
||||
quotaText: '每次限购xxx件',
|
||||
// 自定义步进器超过限制时的回调
|
||||
handleOverLimit: (data) => {
|
||||
const { action, limitType, quota, quotaUsed } = data;
|
||||
|
||||
if (action === 'minus') {
|
||||
Toast('至少选择一件商品');
|
||||
} else if (action === 'plus') {
|
||||
// const { LIMIT_TYPE } = Sku.skuConstants;
|
||||
if (limitType === LIMIT_TYPE.QUOTA_LIMIT) {
|
||||
let msg = `单次限购${quota}件`;
|
||||
if (quotaUsed > 0) msg += `,你已购买${quotaUsed}`;
|
||||
Toast(msg);
|
||||
} else {
|
||||
Toast('库存不够了');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### messageConfig Data Structure
|
||||
|
||||
```javascript
|
||||
messageConfig: {
|
||||
// 图片上传回调,需要返回一个promise,promise正确执行的结果需要是一个图片url
|
||||
uploadImg: () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
|
||||
});
|
||||
},
|
||||
// 最大上传体积 (MB)
|
||||
uploadMaxSize: 3,
|
||||
// placehold配置
|
||||
placeholderMap: {
|
||||
text: 'xxx',
|
||||
tel: 'xxx',
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加购物车和点击购买回调函数接收的 skuData 对象结构
|
||||
|
||||
```javascript
|
||||
skuData: {
|
||||
// 商品 id
|
||||
goodsId: '946755',
|
||||
// 留言信息
|
||||
messages: {
|
||||
message_0: '12',
|
||||
message_1: ''
|
||||
},
|
||||
// 另一种格式的留言,key 不同
|
||||
cartMessages: {
|
||||
'留言1': 'xxxx'
|
||||
},
|
||||
// 选择的商品数量
|
||||
selectedNum: 1,
|
||||
// 选择的 sku 组合
|
||||
selectedSkuComb: {
|
||||
id: 2257,
|
||||
price: 100,
|
||||
s1: '30349',
|
||||
s2: '1193',
|
||||
s3: '0',
|
||||
stock_num: 111
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user