[new feature] sku组件增加图片上传功能 (#612)

* [new feature] sku组件增加图片上传功能
This commit is contained in:
wny 2018-02-07 11:29:07 +08:00 committed by GitHub
parent 30e42284e9
commit 7b89e11701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 361 additions and 41 deletions

View File

@ -13,6 +13,7 @@
:reset-stepper-on-hide="true"
:reset-selected-sku-on-hide="true"
:disable-stepper-input="true"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -23,7 +24,7 @@
<demo-block :title="$t('title2')">
<div class="sku-container">
<van-sku
v-model="showBase"
v-model="showStepper"
:sku="$t('sku').sku"
:goods="$t('sku').goods_info"
:goods-id="$t('sku').goods_id"
@ -34,7 +35,7 @@
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
<van-button type="primary" @click="showBase = true" block>{{ $t('title2') }}</van-button>
<van-button type="primary" @click="showStepper = true" block>{{ $t('title2') }}</van-button>
</div>
</demo-block>
@ -94,6 +95,7 @@ export default {
return {
showBase: false,
showCustom: false,
showStepper: false,
initialSku: {
s1: '30349',
s2: '1193'
@ -113,6 +115,14 @@ export default {
}
}
}
},
messageConfig: {
uploadImg: () => {
return new Promise((resolve) => {
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
});
},
uploadMaxSize: 3
}
};
},

View File

@ -22,6 +22,7 @@ Vue.use(Sku);
:reset-stepper-on-hide="resetStepperOnHide"
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
:disable-stepper-input="disableStepperInput"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -90,6 +91,7 @@ Vue.use(Sku);
| disable-stepper-input | Whether to disable stepper input | `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 | `Function` | - | `() => HTMLElement` |
### Event
@ -201,6 +203,26 @@ customStepperConfig: {
}
```
#### 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',
...
}
}
```
#### Event Params Data Structure
```javascript

View File

@ -22,6 +22,7 @@ Vue.use(Sku);
:reset-stepper-on-hide="resetStepperOnHide"
:reset-selected-sku-on-hide="resetSelectedSkuOnHide"
:disable-stepper-input="disableStepperInput"
:message-config="messageConfig"
@buy-clicked="onBuyClicked"
@add-cart="onAddCartClicked"
/>
@ -91,6 +92,7 @@ Vue.use(Sku);
| disable-stepper-input | 是否禁用sku中stepper的input框 | `Boolean` | `false` | - |
| stepper-title | 数量选择组件左侧文案 | `String` | `购买数量` | - |
| custom-stepper-config | 步进器相关自定义配置 | `Object` | `{}` | - |
| message-config | 留言相关配置 | `Object` | `{}` | - |
| get-container | 指定挂载的 HTML 节点 | `Function` | - | `() => HTMLElement` |
### Event
@ -207,6 +209,26 @@ customStepperConfig: {
}
```
#### messageConfig Data Structure
```javascript
messageConfig: {
// 图片上传回调需要返回一个promisepromise正确执行的结果需要是一个图片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: {

View File

@ -113,10 +113,12 @@ export default {
},
vanSkuMessages: {
fill: 'Please fill',
upload: 'Please upload',
number: 'Please fill in the correct number format message',
email: 'Please fill in the correct email message',
idcard: 'Please fill in the correct ID number message',
overlimit: 'not more than 200 words',
onePic: 'only one picture',
placeholder: {
'id_no': 'Idcard Number',
text: 'Text',
@ -127,6 +129,15 @@ export default {
textarea: 'Text'
}
},
vanSkuImgUploader: {
or: 'Or',
uploading: 'Uploading...',
rephoto: 'Take Again',
photo: 'Take',
reselect: 'Reselect',
select: 'Select Photo',
maxSize: maxSize => `The upload limit is up to ${maxSize}MBplease try to compress the photo`
},
vanSkuStepper: {
title: 'Quantity',
remain: count => `Remain ${count} items`,

View File

@ -117,10 +117,12 @@ export default {
},
vanSkuMessages: {
fill: '请填写',
upload: '请上传',
number: '请填写正确的数字格式留言',
email: '请填写正确的邮箱',
'id_no': '请填写正确的身份证号码',
overlimit: '写的太多了不要超过200字',
onePic: '仅限一张',
placeholder: {
'id_no': '输入18位身份证号码',
text: '输入文本',
@ -131,6 +133,15 @@ export default {
textarea: '点击填写段落文本'
}
},
vanSkuImgUploader: {
or: '或',
uploading: '正在上传...',
rephoto: '重拍',
photo: '拍照',
reselect: '重新选择照片',
select: '选择照片',
maxSize: maxSize => `最大可上传图片为${maxSize}MB请尝试压缩图片尺寸`
},
vanSkuStepper: {
title: '购买数量',
remain: count => `剩余${count}`,

View File

@ -81,7 +81,7 @@
<sku-messages
ref="skuMessages"
:goods-id="goodsId"
:message-placeholder-map="messagePlaceholderMap"
:message-config="messageConfig"
:messages="sku.messages"
/>
</slot>
@ -165,9 +165,13 @@ export default create({
type: Number,
default: 200
},
messagePlaceholderMap: {
messageConfig: {
type: Object,
default: () => ({})
default: () => ({
placeholderMap: {},
uploadImg: () => Promise.resolve(),
uploadMaxSize: 5
})
},
customStepperConfig: {
type: Object,

View File

@ -0,0 +1,122 @@
<template>
<div class="van-sku-img-uploader">
<!-- 头部 -->
<van-uploader
:disabled="!canUpload"
:before-read="beforeReadFile"
:after-read="afterReadFile"
accept="image/*">
<div class="van-sku-img-uploader__header">
<div v-if="paddingImg">{{ $t('uploading') }}</div>
<template v-else>
<van-icon name="photograph" />
<span class="label">{{ getPhotoText(value) }}</span> {{ $t('or') }}
<van-icon name="photo" />
<span class="label">{{ getPicText(value) }}</span>
</template>
</div>
</van-uploader>
<!-- 图片列表区域 -->
<div class="van-sku-img-uploader__imglist" v-if="paddingImg || imgList.length > 0">
<!-- 已有的图片,图片右上角显示删除按钮 -->
<div
v-for="(img, index) in imgList"
:key="index"
class="van-sku-img-uploader__img-container">
<span class="van-sku-img-uploader__delete-picture" @click="deleteImg(index)">
<van-icon name="clear" />
</span>
<img :src="img">
</div>
<!-- 正在上传的图片,有上传等待提示 -->
<div
v-if="paddingImg"
class="van-sku-img-uploader__img-container">
<img :src="paddingImg">
<van-loading class="van-sku-img-uploader__uploading" type="spinner" color="black" />
</div>
</div>
</div>
</template>
<script>
import Icon from '../../icon';
import Uploader from '../../uploader';
import Loading from '../../loading';
import { create } from '../../utils';
export default create({
name: 'van-sku-img-uploader',
components: {
'van-uploader': Uploader,
'van-icon': Icon,
'van-loading': Loading
},
props: {
value: String,
uploadImg: {
type: Function,
required: true
},
maxSize: {
type: Number,
default: 6
}
},
data() {
return {
// base 64
paddingImg: ''
};
},
computed: {
imgList() {
return this.value && !this.paddingImg ? [this.value] : [];
},
canUpload() {
//
if (this.paddingImg) return false;
return true;
}
},
methods: {
getPhotoText(value) {
return value ? this.$t('rephoto') : this.$t('photo');
},
getPicText(value) {
return value ? this.$t('reselect') : this.$t('select');
},
beforeReadFile(file) {
//
if (file.size > this.maxSize * 1024 * 1024) {
Toast(this.$t('maxSize', this.maxSize));
return false;
}
return true;
},
afterReadFile(file) {
//
this.paddingImg = file.content;
this.uploadImg(file.file).then(img => {
this.updateImg(img);
this.$nextTick(() => {
this.paddingImg = '';
});
}).catch(() => {
this.paddingImg = '';
});
},
deleteImg() {
this.$emit('input', '');
},
updateImg(img) {
this.$emit('input', img);
}
}
});
</script>

View File

@ -1,14 +1,29 @@
<template>
<cell-group class="van-sku-messages">
<field
v-for="(message, index) in internalMessages"
v-model="messageValues[index]"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:label="message.name"
:placeholder="getPlaceholder(message)"
:type="getType(message)"
/>
<template v-for="(message, index) in messages">
<cell
v-if="message.type === 'image'"
class="van-sku-messages__image-cell"
:label="$t('onePic')"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:title="message.name">
<sku-img-uploader
v-model="messageValues[index].value"
:upload-img="messageConfig.uploadImg"
:max-size="messageConfig.uploadMaxSize"
/>
</cell>
<field
v-else
v-model="messageValues[index].value"
:key="`${goodsId}-${index}`"
:required="message.required == '1'"
:label="message.name"
:placeholder="getPlaceholder(message)"
:type="getType(message)"
/>
</template>
</cell-group>
</template>
@ -16,30 +31,36 @@
import { create } from '../../utils';
import Field from '../../field';
import CellGroup from '../../cell-group';
import Cell from '../../cell';
import validateEmail from '../../utils/validate/email';
import validateNumber from '../../utils/validate/number';
import SkuImgUploader from './SkuImgUploader';
export default create({
name: 'van-sku-messages',
components: {
SkuImgUploader,
Field,
Cell,
CellGroup
},
props: {
messages: Array,
messagePlaceholderMap: Object,
messageConfig: Object,
goodsId: [Number, String]
},
computed: {
internalMessages() {
return Array.isArray(this.messages) ? this.messages.filter(message => message.type !== 'image') : [];
},
data() {
return {
messageValues: this.messages.map(() => ({ value: '' }))
};
},
messageValues() {
return this.internalMessages.map(() => '');
computed: {
messagePlaceholderMap() {
return this.messageConfig.placeholderMap || {};
}
},
@ -57,8 +78,9 @@ export default create({
getMessages() {
const messages = {};
this.messageValues.forEach((value, index) => {
if (this.internalMessages[index].datetime > 0) {
this.messageValues.forEach((item, index) => {
let value = item.value;
if (this.messages[index].datetime > 0) {
value = value.replace(/T/g, ' ');
}
messages[`message_${index}`] = value;
@ -70,8 +92,9 @@ export default create({
getCartMessages() {
const messages = {};
this.messageValues.forEach((value, index) => {
const message = this.internalMessages[index];
this.messageValues.forEach((item, index) => {
let value = item.value;
const message = this.messages[index];
if (message.datetime > 0) {
value = value.replace(/T/g, ' ');
}
@ -90,17 +113,16 @@ export default create({
const values = this.messageValues;
for (let i = 0; i < values.length; i++) {
const value = values[i];
const message = this.internalMessages[i];
const value = values[i].value;
const message = this.messages[i];
if (value === '') {
//
if (message.required == '1') { // eslint-disable-line
if (message.type === 'image') {
continue;
} else {
return this.$t('fill') + message.name;
}
const textType = message.type === 'image'
? 'upload'
: 'fill';
return this.$t(textType) + message.name;
}
} else {
if (message.type === 'tel' && !validateNumber(value)) {

View File

@ -9,6 +9,7 @@
:max="stepperLimit"
:disable-input="disableStepperInput"
@overlimit="onOverLimit"
@change="onChange"
/>
</div>
<div v-if="!hideStock" class="van-sku__stock">{{ $t('remain', stock) }}</div>
@ -104,7 +105,6 @@ export default create({
setCurrentNum(num) {
this.currentNum = num;
},
onOverLimit(action) {
this.skuEventBus.$emit('sku:overLimit', {
action,
@ -112,6 +112,10 @@ export default create({
quota: this.quota,
quotaUsed: this.quotaUsed
});
},
onChange(currentValue) {
const { handleStepperChange } = this.customStepperConfig;
handleStepperChange && handleStepperChange(currentValue);
}
}
});

View File

@ -1,4 +1,5 @@
@import './common/var.css';
@import './mixins/clearfix.css';
.van-sku {
&-container {
@ -144,7 +145,7 @@
margin-right: 20px;
}
}
&__stepper {
top: 7px;
left: 4px;
@ -163,13 +164,85 @@
color: $gray-dark;
font-size: 12px;
}
&__quota {
display: inline-block;
color: $red;
font-size: 12px;
}
&-messages {
&__image-cell {
.van-cell__title {
width: 90px;
}
.van-cell__value {
text-align: left;
}
}
}
&-img-uploader {
display: inline-block;
&__header {
padding: 0 10px;
border: 1px solid #e5e5e5;
line-height: 24px;
border-radius: 3px;
font-size: 12px;
.van-icon {
top: 3px;
margin-right: 5px;
font-size: 14px;
}
}
&__imglist {
@mixin clearfix;
}
&__img-container {
height: 60px;
width: 60px;
margin-top: 10px;
margin-right: 10px;
float: left;
position: relative;
border: #e5e5e5 1px solid;
img {
max-width: 100%;
max-height: 100%;
top: 50%;
position: relative;
transform: translateY(-50%);
}
}
&__delete-picture {
position: absolute;
color: $red;
top: -10px;
right: -17px;
z-index: 1;
width: 22px;
height: 22px;
}
&__uploading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: 20px;
height: 20px;
}
}
/* sku actions */
&-actions {
display: flex;

View File

@ -1,4 +1,5 @@
import Sku from 'packages/sku';
import Uploader from 'packages/uploader';
import Toast from 'packages/toast';
import { mount } from 'avoriaz';
import { DOMChecker } from '../utils';
@ -13,6 +14,13 @@ const initialSku = {
};
goods.picture = goods.picture[0];
const File = function() {
this.name = 'test';
this.size = 10000;
};
const mockFile = new File([], '/Users');
describe('Sku', (done) => {
let wrapper;
afterEach(() => {
@ -239,7 +247,15 @@ describe('Sku', (done) => {
value: true,
sku: data.sku,
goodsId: data.goods_id,
goods: goods
goods: goods,
messageConfig: {
uploadImg: () => {
return new Promise((resolve) => {
setTimeout(() => resolve('https://img.yzcdn.cn/upload_files/2017/02/21/FjKTOxjVgnUuPmHJRdunvYky9OHP.jpg!100x100.jpg'), 1000);
});
},
uploadMaxSize: 3
}
}
});
@ -247,12 +263,15 @@ describe('Sku', (done) => {
const skuMessages = wrapper.find('.van-sku-messages')[0];
const inputs = skuMessages.find('input');
const textarea = skuMessages.find('textarea')[0];
const uploader = wrapper.find(Uploader)[0];
// 修改留言内容
inputs[0].element.value = 123;
// 测试身份证号
inputs[1].element.value = 234;
inputs[0].trigger('input');
inputs[1].trigger('input');
// 测试图片
uploader.vm.onChange({ target: { files: [mockFile] }});
wrapper.vm.$nextTick(() => {
// 点击购买
@ -276,9 +295,9 @@ describe('Sku', (done) => {
textarea.element.value = '';
// 测试数字留言
inputs[2].element.value = 'abc';
inputs[3].element.value = 'abc';
textarea.trigger('input');
inputs[2].trigger('input');
inputs[3].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');
@ -286,10 +305,10 @@ describe('Sku', (done) => {
wrapper.vm.$nextTick(() => {
expect(toastText.textContent).to.equal('请填写正确的数字格式留言');
inputs[2].element.value = 0;
inputs[3].element.value = 345;
inputs[2].trigger('input');
inputs[3].element.value = 0;
inputs[4].element.value = 345;
inputs[3].trigger('input');
inputs[4].trigger('input');
wrapper.vm.$nextTick(() => {
buyBtn.trigger('click');