Merge remote-tracking branch 'main/dev' into dev
This commit is contained in:
+4
-1
@@ -34,6 +34,7 @@ import Radio from './radio';
|
||||
import RadioGroup from './radio-group';
|
||||
import Row from './row';
|
||||
import Search from './search';
|
||||
import Sku from './sku';
|
||||
import Step from './step';
|
||||
import Stepper from './stepper';
|
||||
import Steps from './steps';
|
||||
@@ -50,7 +51,7 @@ import TreeSelect from './tree-select';
|
||||
import Uploader from './uploader';
|
||||
import Waterfall from './waterfall';
|
||||
|
||||
const version = '0.9.1';
|
||||
const version = '0.9.2';
|
||||
const components = [
|
||||
Actionsheet,
|
||||
Area,
|
||||
@@ -85,6 +86,7 @@ const components = [
|
||||
RadioGroup,
|
||||
Row,
|
||||
Search,
|
||||
Sku,
|
||||
Step,
|
||||
Stepper,
|
||||
Steps,
|
||||
@@ -152,6 +154,7 @@ export {
|
||||
RadioGroup,
|
||||
Row,
|
||||
Search,
|
||||
Sku,
|
||||
Step,
|
||||
Stepper,
|
||||
Steps,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="van-sku-actions">
|
||||
<button v-if="showAddCartBtn" class="van-sku__add-cart-btn" @click="onAddCartClicked">
|
||||
加入购物车
|
||||
</button>
|
||||
<button class="van-sku__buy-btn" @click="onBuyClicked">
|
||||
{{ buyText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'van-sku-actions',
|
||||
|
||||
props: {
|
||||
skuEventBus: Object,
|
||||
showAddCartBtn: Boolean,
|
||||
buyText: {
|
||||
type: String,
|
||||
default: '立即购买'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAddCartClicked() {
|
||||
this.skuEventBus.$emit('sku:addCart');
|
||||
},
|
||||
onBuyClicked() {
|
||||
this.skuEventBus.$emit('sku:buy');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="van-sku-header">
|
||||
<div class="van-sku-header__img-wrap">
|
||||
<img class="van-sku__goods-img" :src="goodsImg">
|
||||
</div>
|
||||
<div class="van-sku-header__goods-info">
|
||||
<div class="van-sku__goods-name">{{ goods.title }}</div>
|
||||
<div class="van-sku__goods-price"><span class="van-sku__price-symbol">¥</span><span class="van-sku__price-num">{{ price }}</span></div>
|
||||
<span class="van-sku__close-icon" @click="onCloseClicked"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import urlHelper from 'zan-utils/url/helper';
|
||||
|
||||
export default {
|
||||
name: 'van-sku-header',
|
||||
|
||||
props: {
|
||||
skuEventBus: Object,
|
||||
sku: Object,
|
||||
selectedSku: Object,
|
||||
selectedSkuComb: Object,
|
||||
goods: Object
|
||||
},
|
||||
|
||||
computed: {
|
||||
skuTree() {
|
||||
return this.sku.tree;
|
||||
},
|
||||
goodsImg() {
|
||||
const s1Id = this.selectedSku.s1;
|
||||
const skuImg = this.getSkuImg(s1Id);
|
||||
|
||||
return skuImg || this.goods.picture;
|
||||
},
|
||||
price() {
|
||||
if (this.selectedSkuComb) {
|
||||
return (this.selectedSkuComb.price / 100).toFixed(2);
|
||||
}
|
||||
// sku.price是一个格式化好的价格区间
|
||||
return this.sku.price;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCloseClicked() {
|
||||
this.skuEventBus.$emit('sku:close');
|
||||
},
|
||||
getSkuImg(id) {
|
||||
if (!id) return;
|
||||
|
||||
// 目前skuImg都挂载在skuTree中s1那类sku上
|
||||
const treeItem = find(this.skuTree, (treeItem) => treeItem.k_s === 's1') || {};
|
||||
const matchedSku = find(treeItem.v, (skuValue) => skuValue.id === id);
|
||||
|
||||
if (matchedSku && matchedSku.imgUrl) {
|
||||
return urlHelper.getCdnImageUrl(matchedSku.imgUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<van-cell-group>
|
||||
<template v-for="(message, index) in internalMessages">
|
||||
<template v-if="message.type === 'image'"></template>
|
||||
<van-field v-else-if="message.multiple == '1'"
|
||||
:key="index"
|
||||
:required="message.required == '1'"
|
||||
:label="message.name"
|
||||
placeholder="点击填写段落文本"
|
||||
type="textarea"
|
||||
v-model="messageValues[index]">
|
||||
</van-field>
|
||||
<van-field v-else
|
||||
:key="index"
|
||||
:required="message.required == '1'"
|
||||
:label="message.name"
|
||||
:placeholder="PLACEHOLDER_MAP[message.type]"
|
||||
:type="getType(message)"
|
||||
v-model="messageValues[index]">
|
||||
</van-field>
|
||||
</template>
|
||||
</van-cell-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import Field from '../../field';
|
||||
import CellGroup from '../../cell-group';
|
||||
import validateEmail from 'zan-utils/validate/email';
|
||||
import validateNumber from 'zan-utils/validate/number';
|
||||
|
||||
const PLACEHOLDER_MAP = {
|
||||
'id_no': '输入18位身份证号码',
|
||||
text: '输入文本',
|
||||
tel: '输入数字',
|
||||
email: '输入邮箱',
|
||||
date: '点击选择日期',
|
||||
time: '点击选择时间'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'van-sku-messages',
|
||||
|
||||
components: {
|
||||
[Field.name]: Field,
|
||||
[CellGroup.name]: CellGroup
|
||||
},
|
||||
|
||||
props: {
|
||||
messages: Array
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
PLACEHOLDER_MAP
|
||||
};
|
||||
},
|
||||
|
||||
// for debug
|
||||
// watch: {
|
||||
// messageValues() {
|
||||
// console.log(this.messageValues);
|
||||
// }
|
||||
// },
|
||||
|
||||
computed: {
|
||||
internalMessages() {
|
||||
if (Object.prototype.toString.call(this.messages) === '[object Array]') {
|
||||
return this.messages;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
messageValues() {
|
||||
const messageValues = [];
|
||||
this.internalMessages.forEach((message, index) => {
|
||||
messageValues[index] = '';
|
||||
});
|
||||
|
||||
return messageValues;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getType({ type, datetime }) {
|
||||
if (type === 'id_no') return 'text';
|
||||
return datetime > 0 ? 'datetime-local' : type;
|
||||
},
|
||||
getMessages() {
|
||||
const messages = {};
|
||||
|
||||
this.messageValues.forEach((value, index) => {
|
||||
if (this.internalMessages[index].datetime > 0) {
|
||||
value = value.replace(/T/g, ' ');
|
||||
}
|
||||
messages[`message_${index}`] = value;
|
||||
});
|
||||
|
||||
return messages;
|
||||
},
|
||||
getCartMessages() {
|
||||
const messages = {};
|
||||
|
||||
this.messageValues.forEach((value, index) => {
|
||||
const message = this.internalMessages[index];
|
||||
if (message.datetime > 0) {
|
||||
value = value.replace(/T/g, ' ');
|
||||
}
|
||||
messages[message.name] = value;
|
||||
});
|
||||
|
||||
return messages;
|
||||
},
|
||||
validateMessages() {
|
||||
const values = this.messageValues;
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i];
|
||||
const message = this.internalMessages[i];
|
||||
|
||||
if (isEmpty(value)) {
|
||||
// 必填字段的校验
|
||||
if (message.required == '1') { // eslint-disable-line
|
||||
if (message.type === 'image') {
|
||||
continue;
|
||||
// return `请上传${message.name}`;
|
||||
} else {
|
||||
return `请填写${message.name}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if (message.type == 'image') {
|
||||
// loaded = _this.$el.find('#ipt-' + j).data('uploaded');
|
||||
// if (loaded == 'false' || !loaded) {
|
||||
// return '图片还在上传中,请稍等。。';
|
||||
// }
|
||||
// }
|
||||
if (message.type === 'tel' && !validateNumber(value)) {
|
||||
return '请填写正确的数字格式留言';
|
||||
}
|
||||
if (message.type === 'email' && !validateEmail(value)) {
|
||||
return '请填写正确的邮箱';
|
||||
}
|
||||
if (message.type === 'id_no' && (value.length < 15 || value.length > 18)) {
|
||||
return '请填写正确的身份证号码';
|
||||
}
|
||||
}
|
||||
|
||||
if (value.length > 200) {
|
||||
return `${message.name} 写的太多了<br/>不要超过200字`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="van-sku-row">
|
||||
<div class="van-sku-row__title">{{ skuRow.k }}:</div>
|
||||
<div class="van-sku-row__items">
|
||||
<slot name="sku-row-items">
|
||||
<van-sku-row-item
|
||||
v-for="(skuValue, index) in skuRow.v"
|
||||
:key="index"
|
||||
:skuKeyStr="skuRow.k_s"
|
||||
:skuValue="skuValue"
|
||||
:skuEventBus="$attrs.skuEventBus"
|
||||
:selectedSku="$attrs.selectedSku"
|
||||
:skuList="$attrs.skuList">
|
||||
</van-sku-row-item>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SkuRowItem from './SkuRowItem';
|
||||
|
||||
export default {
|
||||
name: 'van-sku-row',
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
components: {
|
||||
[SkuRowItem.name]: SkuRowItem
|
||||
},
|
||||
|
||||
props: {
|
||||
skuRow: Object
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<span v-if="isChoosable"
|
||||
@click="onSkuSelected"
|
||||
:class="{ 'van-sku-row__item': true, 'van-sku-row__item--active': isChoosed }">
|
||||
{{ skuValue.name }}
|
||||
</span>
|
||||
<span v-else class="van-sku-row__item van-sku-row__item--disabled">{{ skuValue.name }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import assign from 'lodash/assign';
|
||||
import filter from 'lodash/filter';
|
||||
import keys from 'lodash/keys';
|
||||
import every from 'lodash/every';
|
||||
|
||||
export default {
|
||||
name: 'van-sku-row-item',
|
||||
|
||||
props: {
|
||||
skuEventBus: Object,
|
||||
skuValue: Object,
|
||||
skuList: Array,
|
||||
selectedSku: Object,
|
||||
skuKeyStr: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
isChoosed() {
|
||||
return this.skuValue.id === this.selectedSku[this.skuKeyStr];
|
||||
},
|
||||
isChoosable() {
|
||||
const matchedSku = assign({}, this.selectedSku, {
|
||||
[this.skuKeyStr]: this.skuValue.id
|
||||
});
|
||||
const skusToCheck = filter(keys(matchedSku), skuKey => matchedSku[skuKey] !== '');
|
||||
const filteredSku = filter(this.skuList, sku => {
|
||||
return every(skusToCheck, function(skuKey) {
|
||||
// 后端给的skuValue.id有时候是数字有时候是字符串,全等会匹配不上
|
||||
return matchedSku[skuKey] == sku[skuKey]; // eslint-disable-line
|
||||
});
|
||||
});
|
||||
const stock = filteredSku.reduce((total, sku) => (total += sku.stock_num), 0);
|
||||
|
||||
return stock > 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSkuSelected() {
|
||||
this.skuEventBus.$emit('sku:select', assign({}, this.skuValue, { skuKeyStr: this.skuKeyStr }));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="van-sku-stepper-stock">
|
||||
<div class="van-sku-stepper-container">
|
||||
<div class="van-sku__stepper-title">{{ stepperTitle }}:</div>
|
||||
<van-stepper v-model="currentNum" :min="1" :max="stepperLimit" class="van-sku__stepper" @overlimit="handleOverLimit"></van-stepper>
|
||||
</div>
|
||||
<div v-if="!hideStock" class="van-sku__stock">剩余{{ stock }}件</div>
|
||||
<div v-if="quota > 0" class="van-sku__quota">每人限购{{ quota }}件</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Stepper from '../../stepper';
|
||||
import { LIMIT_TYPE } from '../constants';
|
||||
|
||||
const { QUOTA_LIMIT, STOCK_LIMIT } = LIMIT_TYPE;
|
||||
|
||||
export default {
|
||||
name: 'van-sku-stepper',
|
||||
|
||||
components: {
|
||||
[Stepper.name]: Stepper
|
||||
},
|
||||
|
||||
props: {
|
||||
skuEventBus: Object,
|
||||
sku: Object,
|
||||
selectedSku: Object,
|
||||
selectedSkuComb: Object,
|
||||
selectedNum: Number,
|
||||
quota: Number,
|
||||
quotaUsed: Number,
|
||||
hideStock: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
stepperTitle: {
|
||||
type: String,
|
||||
default: '购买数量'
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
if (this.selectedSkuComb) {
|
||||
return this.selectedSkuComb.stock_num;
|
||||
}
|
||||
return this.sku.stock_num;
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
return limit;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setCurrentNum(num) {
|
||||
this.currentNum = num;
|
||||
},
|
||||
handleOverLimit(action) {
|
||||
this.skuEventBus.$emit('sku:overLimit', {
|
||||
action,
|
||||
limitType: this.limitType,
|
||||
quota: this.quota,
|
||||
quotaUsed: this.quotaUsed
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
export const LIMIT_TYPE = {
|
||||
QUOTA_LIMIT: 0,
|
||||
STOCK_LIMIT: 1
|
||||
};
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<van-popup v-model="show" position="bottom" :lock-scroll="true">
|
||||
<div class="van-sku-container">
|
||||
<div class="van-sku-layout">
|
||||
<slot name="sku-header" :skuEventBus="skuEventBus" :selectedSku="selectedSku" :selectedSkuComb="selectedSkuComb">
|
||||
<van-sku-header
|
||||
:skuEventBus="skuEventBus"
|
||||
:selectedSku="selectedSku"
|
||||
:selectedSkuComb="selectedSkuComb"
|
||||
:goods="goods"
|
||||
:sku="sku">
|
||||
</van-sku-header>
|
||||
</slot>
|
||||
<div class="van-sku-body scroller" :style="bodyStyle">
|
||||
<slot name="sku-group" :selectedSku="selectedSku">
|
||||
<div v-if="hasSku" class="van-sku-group-container">
|
||||
<div v-for="(skutreeItem, index) in skuTree"
|
||||
class="van-sku-row-group"
|
||||
:key="index">
|
||||
<van-sku-row
|
||||
v-bind="$attrs"
|
||||
:skuEventBus="skuEventBus"
|
||||
:skuRow="skutreeItem"
|
||||
:skuList="sku.list"
|
||||
:selectedSku="selectedSku">
|
||||
</van-sku-row>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="extra-sku-group" :skuEventBus="skuEventBus"></slot>
|
||||
<slot name="sku-stepper" :skuEventBus="skuEventBus" :selectedSku="selectedSku" :selectedSkuComb="selectedSkuComb" :selectedNum="selectedNum">
|
||||
<van-sku-stepper
|
||||
ref="skuStepper"
|
||||
:skuEventBus="skuEventBus"
|
||||
:selectedSku="selectedSku"
|
||||
:selectedSkuComb="selectedSkuComb"
|
||||
:selectedNum="selectedNum"
|
||||
:stepperTitle="stepperTitle"
|
||||
:sku="sku"
|
||||
:quota="quota"
|
||||
:quotaUsed="quotaUsed"
|
||||
:hideStock="hideStock">
|
||||
</van-sku-stepper>
|
||||
</slot>
|
||||
<slot name="sku-messages">
|
||||
<van-sku-messages
|
||||
ref="skuMessages"
|
||||
:messages="sku.messages">
|
||||
</van-sku-messages>
|
||||
</slot>
|
||||
<slot name="sku-actions" :skuEventBus="skuEventBus">
|
||||
<van-sku-actions
|
||||
:skuEventBus="skuEventBus"
|
||||
:buyText="buyText"
|
||||
:showAddCartBtn="showAddCartBtn">
|
||||
</van-sku-actions>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import Popup from '../../popup';
|
||||
import Toast from '../../toast';
|
||||
import SkuHeader from '../components/SkuHeader';
|
||||
import SkuRow from '../components/SkuRow';
|
||||
import SkuStepper from '../components/SkuStepper';
|
||||
import SkuMessages from '../components/SkuMessages';
|
||||
import SkuActions from '../components/SkuActions';
|
||||
import { isAllSelected, getSkuComb, getSelectedSkuValues } from '../utils/skuHelper';
|
||||
import { LIMIT_TYPE } from '../constants';
|
||||
|
||||
const { QUOTA_LIMIT } = LIMIT_TYPE;
|
||||
|
||||
export default {
|
||||
name: 'van-sku',
|
||||
|
||||
components: {
|
||||
[Popup.name]: Popup,
|
||||
[SkuHeader.name]: SkuHeader,
|
||||
[SkuRow.name]: SkuRow,
|
||||
[SkuStepper.name]: SkuStepper,
|
||||
[SkuMessages.name]: SkuMessages,
|
||||
[SkuActions.name]: SkuActions
|
||||
},
|
||||
|
||||
props: {
|
||||
goods: Object,
|
||||
initialSku: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
sku: Object,
|
||||
quota: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
quotaUsed: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
hideStock: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showAddCartBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
buyText: String,
|
||||
stepperTitle: {
|
||||
type: String,
|
||||
default: '购买数量'
|
||||
},
|
||||
bodyOffsetTop: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
resetStepperOnHide: Boolean,
|
||||
value: Boolean
|
||||
},
|
||||
|
||||
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.$refs.skuStepper && this.$refs.skuStepper.setCurrentNum(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
value(val) {
|
||||
this.show = val;
|
||||
},
|
||||
skuTree(val) {
|
||||
this.resetSelectedSku(val);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
bodyStyle() {
|
||||
const windowHeight = window.innerHeight;
|
||||
// header高度大概82px
|
||||
const maxHeight = windowHeight - this.bodyOffsetTop;
|
||||
|
||||
return {
|
||||
maxHeight: maxHeight + 'px'
|
||||
};
|
||||
},
|
||||
isSkuCombSelected() {
|
||||
return isAllSelected(this.sku.tree, this.selectedSku);
|
||||
},
|
||||
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
|
||||
};
|
||||
} else if (this.isSkuCombSelected) {
|
||||
return getSkuComb(this.sku.list, this.selectedSku);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
skuTree() {
|
||||
return this.sku.tree || [];
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
var skuEventBus = new Vue();
|
||||
this.skuEventBus = skuEventBus;
|
||||
|
||||
skuEventBus.$on('sku:close', this.handleCloseClicked);
|
||||
skuEventBus.$on('sku:select', this.handleSkuSelected);
|
||||
skuEventBus.$on('sku:numChange', this.handleNumChange);
|
||||
skuEventBus.$on('sku:overLimit', this.handleOverLimit);
|
||||
skuEventBus.$on('sku:addCart', this.handleAddCartClicked);
|
||||
skuEventBus.$on('sku:buy', this.handleBuyClicked);
|
||||
|
||||
this.resetSelectedSku(this.skuTree);
|
||||
// 组件初始化后的钩子,抛出skuEventBus
|
||||
this.$emit('after-sku-create', skuEventBus);
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetSelectedSku(skuTree) {
|
||||
this.selectedSku = {};
|
||||
skuTree.forEach((item) => {
|
||||
// 只有一个sku规格值时默认选中
|
||||
if (item.v.length === 1) {
|
||||
this.selectedSku[item.k_s] = item.v[0].id;
|
||||
} else {
|
||||
this.selectedSku[item.k_s] = this.initialSku[item.k_s] || '';
|
||||
}
|
||||
});
|
||||
},
|
||||
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) {
|
||||
const error = this.validateSkuMessages();
|
||||
// sku留言没有错误则校验通过
|
||||
return error ? error : '';
|
||||
} else {
|
||||
return '请选择完整的规格';
|
||||
}
|
||||
},
|
||||
handleCloseClicked() {
|
||||
this.show = false;
|
||||
},
|
||||
handleSkuSelected(skuValue) {
|
||||
// 点击已选中的sku时则取消选中
|
||||
this.selectedSku = this.selectedSku[skuValue.skuKeyStr] === skuValue.id
|
||||
? { ...this.selectedSku, [skuValue.skuKeyStr]: '' }
|
||||
: { ...this.selectedSku, [skuValue.skuKeyStr]: skuValue.id };
|
||||
|
||||
this.$emit('sku-selected', {
|
||||
skuValue,
|
||||
selectedSku: this.selectedSku,
|
||||
selectedSkuComb: this.selectedSkuComb
|
||||
});
|
||||
},
|
||||
handleNumChange(num) {
|
||||
this.selectedNum = num;
|
||||
},
|
||||
handleOverLimit({ action, limitType, quota, quotaUsed }) {
|
||||
if (action === 'minus') {
|
||||
Toast('至少选择一件');
|
||||
} else if (action === 'plus') {
|
||||
if (limitType === QUOTA_LIMIT) {
|
||||
let msg = `限购${quota}件`;
|
||||
if (quotaUsed > 0) msg += `,您已购买${quotaUsed}件`;
|
||||
Toast(msg);
|
||||
} else {
|
||||
Toast('库存不足');
|
||||
}
|
||||
}
|
||||
},
|
||||
handleAddCartClicked() {
|
||||
this.handleBuyOrAddCart('add-cart');
|
||||
},
|
||||
handleBuyClicked() {
|
||||
this.handleBuyOrAddCart('buy-clicked');
|
||||
},
|
||||
handleBuyOrAddCart(type) {
|
||||
const error = this.validateSku();
|
||||
if (error) {
|
||||
Toast(error);
|
||||
return;
|
||||
}
|
||||
this.$emit(type, {
|
||||
selectedNum: this.selectedNum,
|
||||
selectedSkuComb: this.selectedSkuComb,
|
||||
messages: this.getSkuMessages(),
|
||||
cartMessages: this.getSkuCartMessages()
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
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 SkuContainer from './containers/SkuContainer';
|
||||
|
||||
SkuContainer.SkuActions = SkuActions;
|
||||
SkuContainer.SkuHeader = SkuHeader;
|
||||
SkuContainer.SkuMessages = SkuMessages;
|
||||
SkuContainer.SkuStepper = SkuStepper;
|
||||
SkuContainer.SkuRow = SkuRow;
|
||||
SkuContainer.SkuRowItem = SkuRowItem;
|
||||
SkuContainer.skuHelper = skuHelper;
|
||||
|
||||
export default SkuContainer;
|
||||
@@ -0,0 +1,95 @@
|
||||
import isArray from 'lodash/isArray';
|
||||
import keys from 'lodash/keys';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import isObject from 'lodash/isObject';
|
||||
import every from 'lodash/every';
|
||||
|
||||
/*
|
||||
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) => {
|
||||
if (!isArray(skuTree)) {
|
||||
throw new Error('skuTree must be array');
|
||||
}
|
||||
if (!isObject(selectedSku)) {
|
||||
throw new Error('selectedSku must be object');
|
||||
}
|
||||
|
||||
const selected = filter(keys(selectedSku), skuKeyStr => selectedSku[skuKeyStr] !== '');
|
||||
return skuTree.length === selected.length;
|
||||
};
|
||||
|
||||
// 根据已选择的sku获取skuComb
|
||||
export const getSkuComb = (skuList, selectedSku) => {
|
||||
const skuComb = find(skuList, (skuComb) => {
|
||||
return every(keys(selectedSku), skuKeyStr => {
|
||||
// 后端给的key有时数字有时字符串,需要兼容=。=
|
||||
return skuComb[skuKeyStr] == selectedSku[skuKeyStr]; // eslint-disable-line
|
||||
});
|
||||
});
|
||||
return skuComb;
|
||||
};
|
||||
|
||||
// 获取已选择的sku名称
|
||||
export const getSelectedSkuValues = (skuTree, selectedSku) => {
|
||||
const normalizedTree = normalizeSkuTree(skuTree);
|
||||
return keys(selectedSku).reduce((selectedValues, skuKeyStr) => {
|
||||
const skuValues = normalizedTree[skuKeyStr];
|
||||
const skuValueId = selectedSku[skuKeyStr];
|
||||
|
||||
if (skuValueId) {
|
||||
const skuValue = find(skuValues, skuValue => skuValue.id === skuValueId);
|
||||
skuValue && selectedValues.push(skuValue);
|
||||
}
|
||||
return selectedValues;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const SkuHelper = {
|
||||
normalizeSkuTree,
|
||||
isAllSelected,
|
||||
getSkuComb,
|
||||
getSelectedSkuValues
|
||||
};
|
||||
export default SkuHelper;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vant-css",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"description": "vant css.",
|
||||
"main": "lib/index.css",
|
||||
"style": "lib/index.css",
|
||||
|
||||
@@ -52,3 +52,4 @@
|
||||
@import './coupon-list.css';
|
||||
@import './goods-action.css';
|
||||
@import './submit-bar.css';
|
||||
@import './sku.css';
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
@import './mixins/border_retina.css';
|
||||
|
||||
.van-sku-container {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.van-sku-layout {
|
||||
background: #fff;
|
||||
}
|
||||
.van-sku-body {
|
||||
max-height: 350px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.van-sku-group-container {
|
||||
position: relative;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 2px;
|
||||
margin-left: 15px;
|
||||
|
||||
&::after {
|
||||
@mixin border-retina (bottom);
|
||||
}
|
||||
}
|
||||
|
||||
.van-sku-row-group {
|
||||
margin-right: 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
sku header
|
||||
*/
|
||||
.van-sku-header {
|
||||
position: relative;
|
||||
margin-left: 15px;
|
||||
|
||||
&::after {
|
||||
@mixin border-retina (bottom);
|
||||
}
|
||||
}
|
||||
.van-sku-header__img-wrap {
|
||||
position: relative;
|
||||
float: left;
|
||||
margin-top: -10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #fafafa;
|
||||
border-radius: 2px;
|
||||
|
||||
.van-sku__goods-img {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
.van-sku-header__goods-info {
|
||||
padding: 10px 60px 10px 10px;
|
||||
min-height: 62px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.van-sku__goods-name {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.van-sku__price-symbol {
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.van-sku__price-num {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.van-sku__goods-price {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
color: #f44;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.van-sku__close-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAAUVBMVEUAAACfn5+bm5uampqZmZmoqKiampqampqZmZmampqampqbm5ubm5uZmZmqqqqampqampqZmZmampqampqampqbm5ubm5uampqdnZ2ampqZmZkjZXmqAAAAGnRSTlMAFYBC5wm+rdPv1Ckl8wzZxKWicm9eUjoN8nSyS9UAAAFVSURBVDjLvZXbrsIgEEWhtAUU6M1a3f//oSeHWKfuVhv7IC+TkJVhz4UZ9bsThq5uYmzqbgifSV04LI4r9Fs0FRHw/WhCugUz9h6IRdpmryVgzfLGWKC8brEF0E58ObVAsUIrC1y2fFwAW9GdhTfb6oyHZQ3+/C7us39VcgXE79o3sIgylayXdZdpIaJVH08rQnTE9BmeEPXsWMK1rV6m01Xz/dO1g5H3nH6yNcoZNnCPPoOXTjplemYlnR4h2wG9YppZ1WPItsOomGZWjeiyrWEU08wqgzrbBkERfWJWBTTZRiTFNLMqIWZ7x43atQYa+lA33EmG5KyB0yyDApT8/uvWFCCnTvIrtKRuLgqzQktRpNzMCi3l5kaymZXqiGS3blGX2Zl2ftGi6+avqtcsUvN/8a2++LCHRsH+kDkwvg4Mxt2Re3yY85pIsiaOLKD91faz8wcoUxux/aS9awAAAABJRU5ErkJggg==);
|
||||
background-size: 22px 22px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 7px 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
sku row
|
||||
*/
|
||||
.van-sku-row__title {
|
||||
font-size: 14px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.van-sku-row__item {
|
||||
display: inline-block;
|
||||
padding: 5px 9px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
height: 16px;
|
||||
min-width: 32px;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
|
||||
&.van-sku-row__item--active {
|
||||
border-color: #f44;
|
||||
color: #fff;
|
||||
background: #f44;
|
||||
}
|
||||
&.van-sku-row__item--disabled {
|
||||
background: #efefef;
|
||||
border-color: #e5e5e5;
|
||||
color: #cacaca;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
sku stepper
|
||||
*/
|
||||
.van-sku-stepper-stock {
|
||||
padding: 12px 0;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.van-sku-stepper-container {
|
||||
height: 30px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.van-sku__stepper-title {
|
||||
float: left;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.van-sku__stepper {
|
||||
float: right;
|
||||
top: 7px;
|
||||
left: 4px;
|
||||
}
|
||||
.van-sku__stock {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.van-sku__quota {
|
||||
display: inline-block;
|
||||
color: #f44;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
sku actions
|
||||
*/
|
||||
.van-sku-actions {
|
||||
margin-top: 10px;
|
||||
font-size: 0;
|
||||
}
|
||||
.van-sku__add-cart-btn {
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: #f85;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
|
||||
& + .van-sku__buy-btn {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.van-sku__buy-btn {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: #f44;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
Reference in New Issue
Block a user