Merge branch 'feature/update_sku_doc' into dev

This commit is contained in:
niunai 2017-09-19 18:17:45 +08:00
commit a574ee29f1
9 changed files with 230 additions and 103 deletions

View File

@ -15,6 +15,10 @@ export default {
goods: goods,
quota: data.quota,
quotaUsed: data.quota_used,
initialSku: {
s1: '30349',
s2: '1193'
}
}
},
@ -85,6 +89,7 @@ Vue.component(Sku.name, Sku);
:quota="quota"
:quota-used="quotaUsed"
:reset-stepper-on-hide="true"
:initial-sku="initialSku"
@buy-clicked="handleBuyClicked"
@add-cart="handleAddCartClicked"
>
@ -201,6 +206,11 @@ skuData: {
message_1:"",
... // 有几个留言就有几条
},
// 另一种格式的留言key不同
cartMessages: {
'留言1': 'xxxx',
... // key是message的name
},
// 选择的商品数量
selectedNum:1,
// 选择的sku组合

View File

@ -1,12 +1,12 @@
<template>
<van-cell-group>
<van-cell-group class="van-sku-messages">
<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="点击填写段落文本"
:placeholder="placeholderMap.textarea"
type="textarea"
v-model="messageValues[index]">
</van-field>
@ -14,7 +14,7 @@
:key="index"
:required="message.required == '1'"
:label="message.name"
:placeholder="PLACEHOLDER_MAP[message.type]"
:placeholder="placeholderMap[message.type]"
:type="getType(message)"
v-model="messageValues[index]">
</van-field>
@ -23,20 +23,11 @@
</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: '点击选择时间'
};
import { DEFAULT_PLACEHOLDER_MAP } from '../constants';
export default {
name: 'van-sku-messages',
@ -47,12 +38,13 @@ export default {
},
props: {
messages: Array
messages: Array,
messagePlaceholderMap: Object
},
data() {
return {
PLACEHOLDER_MAP
placeholderMap: Object.assign({}, DEFAULT_PLACEHOLDER_MAP, this.messagePlaceholderMap)
};
},
@ -117,7 +109,7 @@ export default {
const value = values[i];
const message = this.internalMessages[i];
if (isEmpty(value)) {
if (value === '') {
//
if (message.required == '1') { // eslint-disable-line
if (message.type === 'image') {
@ -128,12 +120,6 @@ export default {
}
}
} 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 '请填写正确的数字格式留言';
}

View File

@ -2,33 +2,15 @@
<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>
<slot></slot>
</div>
</div>
</template>
<script>
import SkuRowItem from './SkuRowItem';
export default {
name: 'van-sku-row',
inheritAttrs: false,
components: {
[SkuRowItem.name]: SkuRowItem
},
props: {
skuRow: Object
}

View File

@ -8,11 +8,6 @@
</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',
@ -29,12 +24,12 @@ export default {
return this.skuValue.id === this.selectedSku[this.skuKeyStr];
},
isChoosable() {
const matchedSku = assign({}, this.selectedSku, {
const matchedSku = Object.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) {
const skusToCheck = Object.keys(matchedSku).filter(skuKey => matchedSku[skuKey] !== '');
const filteredSku = this.skuList.filter(sku => {
return skusToCheck.every(skuKey => {
// skuValue.id
return matchedSku[skuKey] == sku[skuKey]; // eslint-disable-line
});
@ -47,7 +42,7 @@ export default {
methods: {
onSkuSelected() {
this.skuEventBus.$emit('sku:select', assign({}, this.skuValue, { skuKeyStr: this.skuKeyStr }));
this.skuEventBus.$emit('sku:select', Object.assign({}, this.skuValue, { skuKeyStr: this.skuKeyStr }));
}
}
};

View File

@ -11,7 +11,7 @@
<script>
import Stepper from '../../stepper';
import { LIMIT_TYPE } from '../constants';
import { LIMIT_TYPE, DEFAULT_BUY_TEXT } from '../constants';
const { QUOTA_LIMIT, STOCK_LIMIT } = LIMIT_TYPE;
@ -24,7 +24,7 @@ export default {
props: {
skuEventBus: Object,
sku: Object,
skuStockNum: Number,
selectedSku: Object,
selectedSkuComb: Object,
selectedNum: Number,
@ -36,7 +36,7 @@ export default {
},
stepperTitle: {
type: String,
default: '购买数量'
default: DEFAULT_BUY_TEXT
}
},
@ -64,7 +64,7 @@ export default {
if (this.selectedSkuComb) {
return this.selectedSkuComb.stock_num;
}
return this.sku.stock_num;
return this.skuStockNum;
},
stepperLimit() {
const quotaLimit = this.quota - this.quotaUsed;

View File

@ -2,3 +2,17 @@ export const LIMIT_TYPE = {
QUOTA_LIMIT: 0,
STOCK_LIMIT: 1
};
export const DEFAULT_BUY_TEXT = '立即购买';
export const DEFAULT_STEPPER_TITLE = '购买数量';
export const DEFAULT_PLACEHOLDER_MAP = {
'id_no': '输入18位身份证号码',
text: '输入文本',
tel: '输入数字',
email: '输入邮箱',
date: '点击选择日期',
time: '点击选择时间',
textarea: '点击填写段落文本'
};

View File

@ -1,5 +1,5 @@
<template>
<van-popup v-model="show" position="bottom" :lock-scroll="true">
<van-popup v-model="show" v-if="!isSkuEmpty" 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">
@ -14,15 +14,21 @@
<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"
<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">
:skuRow="skuTreeItem">
<van-sku-row-item
v-for="(skuValue, index) in skuTreeItem.v"
:key="index"
:skuKeyStr="skuTreeItem.k_s"
:skuValue="skuValue"
:skuEventBus="skuEventBus"
:selectedSku="selectedSku"
:skuList="sku.list">
</van-sku-row-item>
</van-sku-row>
</div>
</div>
@ -36,7 +42,7 @@
:selectedSkuComb="selectedSkuComb"
:selectedNum="selectedNum"
:stepperTitle="stepperTitle"
:sku="sku"
:skuStockNum="sku.stock_num"
:quota="quota"
:quotaUsed="quotaUsed"
:hideStock="hideStock">
@ -45,17 +51,18 @@
<slot name="sku-messages">
<van-sku-messages
ref="skuMessages"
:messagePlaceholderMap="messagePlaceholderMap"
: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>
<slot name="sku-actions" :skuEventBus="skuEventBus">
<van-sku-actions
:skuEventBus="skuEventBus"
:buyText="buyText"
:showAddCartBtn="showAddCartBtn">
</van-sku-actions>
</slot>
</div>
</div>
</van-popup>
@ -67,11 +74,12 @@ import Popup from '../../popup';
import Toast from '../../toast';
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 { isAllSelected, getSkuComb, getSelectedSkuValues } from '../utils/skuHelper';
import { LIMIT_TYPE } from '../constants';
import { LIMIT_TYPE, DEFAULT_STEPPER_TITLE } from '../constants';
const { QUOTA_LIMIT } = LIMIT_TYPE;
@ -82,6 +90,7 @@ export default {
[Popup.name]: Popup,
[SkuHeader.name]: SkuHeader,
[SkuRow.name]: SkuRow,
[SkuRowItem.name]: SkuRowItem,
[SkuStepper.name]: SkuStepper,
[SkuMessages.name]: SkuMessages,
[SkuActions.name]: SkuActions
@ -115,13 +124,19 @@ export default {
buyText: String,
stepperTitle: {
type: String,
default: '购买数量'
default: DEFAULT_STEPPER_TITLE
},
bodyOffsetTop: {
type: Number,
default: 150
default: 200
},
resetStepperOnHide: Boolean,
messagePlaceholderMap: {
type: Object,
default() {
return {};
}
},
value: Boolean
},
@ -161,7 +176,7 @@ export default {
computed: {
bodyStyle() {
const windowHeight = window.innerHeight;
// header82px
// header82px, sku actions50pxbodyOffsetTop
const maxHeight = windowHeight - this.bodyOffsetTop;
return {
@ -171,6 +186,13 @@ export default {
isSkuCombSelected() {
return isAllSelected(this.sku.tree, this.selectedSku);
},
// sku
isSkuEmpty() {
for (var key in this.sku) {
if (Object.prototype.hasOwnProperty.call(this.sku, key)) return false;
}
return true;
},
hasSku() {
return !this.sku.none_sku;
},
@ -242,7 +264,7 @@ export default {
if (this.isSkuCombSelected) {
const error = this.validateSkuMessages();
// sku
return error ? error : '';
return error;
} else {
return '请选择完整的规格';
}

View File

@ -1,10 +1,4 @@
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
@ -49,21 +43,15 @@ export const normalizeSkuTree = (skuTree) => {
// 判断是否所有的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] !== '');
// 筛选selectedSku对象中key值不为空的值
const selected = Object.keys(selectedSku).filter(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 => {
const skuComb = find(skuList, skuComb => {
return Object.keys(selectedSku).every(skuKeyStr => {
// 后端给的key有时数字有时字符串需要兼容=。=
return skuComb[skuKeyStr] == selectedSku[skuKeyStr]; // eslint-disable-line
});
@ -74,7 +62,7 @@ export const getSkuComb = (skuList, selectedSku) => {
// 获取已选择的sku名称
export const getSelectedSkuValues = (skuTree, selectedSku) => {
const normalizedTree = normalizeSkuTree(skuTree);
return keys(selectedSku).reduce((selectedValues, skuKeyStr) => {
return Object.keys(selectedSku).reduce((selectedValues, skuKeyStr) => {
const skuValues = normalizedTree[skuKeyStr];
const skuValueId = selectedSku[skuKeyStr];

View File

@ -2,9 +2,14 @@ import Sku from 'packages/sku';
import { mount } from 'avoriaz';
import { DOMChecker } from '../utils';
import data from '../mock/sku';
import repeat from 'lodash/repeat';
const { skuHelper } = Sku;
const goods = data.goods_info;
const initialSku = {
s1: '30349',
s2: '1193'
};
goods.picture = goods.picture[0];
describe('Sku', (done) => {
@ -41,18 +46,29 @@ describe('Sku', (done) => {
const selectedSku = skuHelper.getSelectedSkuValues(data.sku.tree, wrapper.vm.selectedSku);
expect(selectedSku[0].id).to.equal('30349');
// 关闭sku弹层
const closeCallback = sinon.spy();
const closeIcon = wrapper.find('.van-sku__close-icon')[0];
wrapper.vm.$on('sku-close', closeCallback);
closeIcon.trigger('click');
// 测试sku图片
const firstSku = wrapper.find('.van-sku-row__item')[0];
firstSku.trigger('click');
wrapper.vm.$nextTick(() => {
expect(closeCallback.calledOnce).to.be.true;
done();
DOMChecker(wrapper, {
src: {
'.van-sku__goods-img': 'https://img.yzcdn.cn/upload_files/2017/03/16/Fs_OMbSFPa183sBwvG_94llUYiLa.jpeg?imageView2/2/w/100/h/100/q/75/format/jpg'
}
});
// 关闭sku弹层
const closeCallback = sinon.spy();
const closeIcon = wrapper.find('.van-sku__close-icon')[0];
wrapper.vm.$on('sku-close', closeCallback);
closeIcon.trigger('click');
wrapper.vm.$nextTick(() => {
expect(closeCallback.calledOnce).to.be.true;
done();
});
});
});
it('click buy and addCart', (done) => {
it('should trigger an event or toast error when click buy and addCart', (done) => {
wrapper = mount(Sku, {
attachToDocument: true,
propsData: {
@ -98,7 +114,7 @@ describe('Sku', (done) => {
});
});
it('change step value', (done) =>{
it('should modify current num or toast error when change step value', (done) => {
wrapper = mount(Sku, {
attachToDocument: true,
propsData: {
@ -135,14 +151,15 @@ describe('Sku', (done) => {
});
});
it('test none sku', (done) => {
data.sku.none_sku = true; // eslint-disable-line
it('should not render sku group when none_sku is true', (done) => {
const newData = Object.assign({}, data);
newData.sku.none_sku = true; // eslint-disable-line
wrapper = mount(Sku, {
attachToDocument: true,
propsData: {
value: false,
sku: data.sku,
sku: newData.sku,
goods: goods
}
});
@ -153,4 +170,117 @@ describe('Sku', (done) => {
done();
});
});
it('should toast error when sku messages fail to pass validation', (done) => {
wrapper = mount(Sku, {
attachToDocument: true,
propsData: {
initialSku,
value: true,
sku: data.sku,
goods: goods
}
});
const buyBtn = wrapper.find('.van-sku__buy-btn')[0];
const skuMessages = wrapper.find('.van-sku-messages')[0];
const inputs = skuMessages.find('input');
const textarea = skuMessages.find('textarea')[0];
// 修改留言内容
inputs[0].element.value = 123;
// 测试身份证号
inputs[1].element.value = 234;
inputs[0].trigger('input');
inputs[1].trigger('input');
wrapper.vm.$nextTick(() => {
// 点击购买
buyBtn.trigger('click');
wrapper.vm.$nextTick(() => {
const toastText = document.querySelector('.van-toast__text');
expect(toastText.textContent).to.equal('请填写正确的身份证号码');
inputs[1].element.value = 330101198801012211;
// 测试textarea字数限制
textarea.element.value = repeat('*', 201);
inputs[1].trigger('input');
textarea.trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('留言4 写的太多了<br/>不要超过200字');
textarea.element.value = '';
// 测试数字留言
inputs[2].element.value = 'abc';
textarea.trigger('input');
inputs[2].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('请填写正确的数字格式留言');
inputs[2].element.value = 0;
inputs[3].element.value = 345;
inputs[2].trigger('input');
inputs[3].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('请填写正确的邮箱');
done();
});
});
});
});
});
});
});
});
});
it('should toast error when there is no stock', (done) => {
/* eslint-disable */
const newData = Object.assign({}, data);
newData.sku.stock_num = 0;
newData.sku.messages = [];
newData.sku.list.forEach((item) => {
item.stock_num = 0;
});
/* eslint-enable */
wrapper = mount(Sku, {
attachToDocument: true,
propsData: {
initialSku,
value: true,
sku: newData.sku,
goods: goods
}
});
const buyBtn = wrapper.find('.van-sku__buy-btn')[0];
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
wrapper.vm.$nextTick(() => {
const toastText = document.querySelector('.van-toast__text');
expect(toastText.textContent).to.equal('商品已经无法购买啦');
const plusBtn = wrapper.find('.van-stepper__plus')[0];
plusBtn.trigger('click');
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('库存不足');
done();
});
});
});
});
});