[improvement] rename packages dir to src (#3659)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<demo-section>
|
||||
<demo-block :title="$t('basicUsage')">
|
||||
<van-uploader :after-read="afterRead" />
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="$t('preview')">
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
multiple
|
||||
accept="*"
|
||||
/>
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="$t('maxCount')">
|
||||
<van-uploader
|
||||
v-model="fileList2"
|
||||
multiple
|
||||
:max-count="2"
|
||||
/>
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="$t('uploadStyle')">
|
||||
<van-uploader>
|
||||
<van-button
|
||||
type="primary"
|
||||
icon="photo"
|
||||
>
|
||||
{{ this.$t('upload') }}
|
||||
</van-button>
|
||||
</van-uploader>
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="$t('beforeRead')">
|
||||
<van-uploader
|
||||
v-model="fileList3"
|
||||
:before-read="beforeRead"
|
||||
/>
|
||||
</demo-block>
|
||||
</demo-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18n: {
|
||||
'zh-CN': {
|
||||
upload: '上传文件',
|
||||
preview: '文件预览',
|
||||
maxCount: '限制上传数量',
|
||||
beforeRead: '上传前校验',
|
||||
uploadStyle: '自定义上传样式',
|
||||
invalidType: '请上传 jpg 格式图片'
|
||||
},
|
||||
'en-US': {
|
||||
upload: 'Upload File',
|
||||
preview: 'Preview File',
|
||||
maxCount: 'Max Count',
|
||||
beforeRead: 'Before Read',
|
||||
uploadStyle: 'Upload Style',
|
||||
invalidType: 'Please upload an image in jpg format'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
fileList: [],
|
||||
fileList2: [],
|
||||
fileList3: []
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeRead(file) {
|
||||
if (file.type !== 'image/jpeg') {
|
||||
this.$toast(this.$t('invalidType'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
afterRead(file) {
|
||||
console.log(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.demo-uploader {
|
||||
background-color: #fff;
|
||||
|
||||
.van-uploader {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
# Uploader
|
||||
|
||||
### Install
|
||||
|
||||
``` javascript
|
||||
import { Uploader } from 'vant';
|
||||
|
||||
Vue.use(Uploader);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<van-uploader :after-read="afterRead" />
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
methods: {
|
||||
afterRead(file) {
|
||||
console.log(file)
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Preview File
|
||||
|
||||
```html
|
||||
<van-uploader v-model="fileList" multiple />
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: []
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Max Count
|
||||
|
||||
```html
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
multiple
|
||||
:max-count="2"
|
||||
/>
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: []
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Upload Style
|
||||
|
||||
```html
|
||||
<van-uploader>
|
||||
<van-button icon="photo" type="primary">Upload Image</van-button>
|
||||
</van-uploader>
|
||||
```
|
||||
|
||||
### Before Read
|
||||
|
||||
```html
|
||||
<van-uploader :before-read="beforeRead" />
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
methods: {
|
||||
beforeRead(file) {
|
||||
if (file.type !== 'image/jpeg') {
|
||||
Toast('Please upload an image in jpg format');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
asyncBeforeRead(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file.type !== 'image/jpeg') {
|
||||
Toast('Please upload an image in jpg format');
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| Attribute | Description | Type | Default |
|
||||
|------|------|------|------|
|
||||
| name | Input name | `String` | - |
|
||||
| accept | Accepted file type | `String` | `image/*` |
|
||||
| preview-image | Whether to show image preview | `Boolean` | `true` |
|
||||
| preview-size | Size of preview image | `String | Number` | `80px` |
|
||||
| multiple | Whether to enable multiple selection pictures | `Boolean` | `false` |
|
||||
| disabled | Whether to disabled the upload | `Boolean` | `false` |
|
||||
| capture | Capture,can be set to `camera` | `String` | - |
|
||||
| before-read | Hook before reading the file, return false to stop reading the file, can return Promise | `Function` | - |
|
||||
| after-read | Hook after reading the file | `Function` | - |
|
||||
| max-size | Max size of file | `Number` | - |
|
||||
| max-count | Max count of image | `Number` | - |
|
||||
| result-type | Type of file read result, can be set to `dataUrl` `text` | `String` | `dataUrl` |
|
||||
| upload-text | Upload text | `String` | - |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Description | Arguments |
|
||||
|------|------|------|
|
||||
| oversize | Triggered when file size over limit | Same as after-read |
|
||||
| delete | Triggered when delete preview file | file |
|
||||
|
||||
### Slots
|
||||
|
||||
| Name | Description |
|
||||
|------|------|
|
||||
| default | Custom icon |
|
||||
|
||||
### Parematers of before-read、after-read
|
||||
|
||||
| Attribute | Description | Type |
|
||||
|------|------|------|
|
||||
| file | File object | `Object` |
|
||||
| detail | Detail info | `Object` |
|
||||
@@ -0,0 +1,249 @@
|
||||
import { createNamespace, suffixPx } from '../utils';
|
||||
import { toArray, readFile, isOversize } from './utils';
|
||||
import Icon from '../icon';
|
||||
import Image from '../image';
|
||||
import ImagePreview from '../image-preview';
|
||||
|
||||
const [createComponent, bem] = createNamespace('uploader');
|
||||
|
||||
function isImageDataUrl(dataUrl) {
|
||||
return dataUrl.indexOf('data:image') === 0;
|
||||
}
|
||||
|
||||
export default createComponent({
|
||||
inheritAttrs: false,
|
||||
|
||||
model: {
|
||||
prop: 'fileList'
|
||||
},
|
||||
|
||||
props: {
|
||||
fileList: Array,
|
||||
disabled: Boolean,
|
||||
uploadText: String,
|
||||
afterRead: Function,
|
||||
beforeRead: Function,
|
||||
previewSize: [Number, String],
|
||||
previewImage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: 'image/*'
|
||||
},
|
||||
resultType: {
|
||||
type: String,
|
||||
default: 'dataUrl'
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: Number.MAX_VALUE
|
||||
},
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: Number.MAX_VALUE
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
detail() {
|
||||
return {
|
||||
name: this.$attrs.name || ''
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChange(event) {
|
||||
let { files } = event.target;
|
||||
|
||||
if (this.disabled || !files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
files = files.length === 1 ? files[0] : [].slice.call(files);
|
||||
|
||||
if (this.beforeRead) {
|
||||
const response = this.beforeRead(files, this.detail);
|
||||
|
||||
if (!response) {
|
||||
this.resetInput();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.then) {
|
||||
response
|
||||
.then(() => {
|
||||
this.readFile(files);
|
||||
})
|
||||
.catch(this.resetInput);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.readFile(files);
|
||||
},
|
||||
|
||||
readFile(files) {
|
||||
const oversize = isOversize(files, this.maxSize);
|
||||
|
||||
if (Array.isArray(files)) {
|
||||
const maxCount = this.maxCount - this.fileList.length;
|
||||
|
||||
if (files.length > maxCount) {
|
||||
files = files.slice(0, maxCount);
|
||||
}
|
||||
|
||||
Promise.all(files.map(file => readFile(file, this.resultType))).then(contents => {
|
||||
const fileList = files.map((file, index) => ({
|
||||
file,
|
||||
content: contents[index]
|
||||
}));
|
||||
|
||||
this.onAfterRead(fileList, oversize);
|
||||
});
|
||||
} else {
|
||||
readFile(files, this.resultType).then(content => {
|
||||
this.onAfterRead({ file: files, content }, oversize);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onAfterRead(files, oversize) {
|
||||
if (oversize) {
|
||||
this.$emit('oversize', files, this.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetInput();
|
||||
this.$emit('input', [...this.fileList, ...toArray(files)]);
|
||||
|
||||
if (this.afterRead) {
|
||||
this.afterRead(files, this.detail);
|
||||
}
|
||||
},
|
||||
|
||||
onDelete(file, index) {
|
||||
const fileList = this.fileList.slice(0);
|
||||
fileList.splice(index, 1);
|
||||
|
||||
this.$emit('input', fileList);
|
||||
this.$emit('delete', file);
|
||||
},
|
||||
|
||||
resetInput() {
|
||||
/* istanbul ignore else */
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
onPreviewImage(item) {
|
||||
const imageFiles = this.fileList
|
||||
.map(item => item.content)
|
||||
.filter(content => isImageDataUrl(content));
|
||||
|
||||
ImagePreview({
|
||||
images: imageFiles,
|
||||
startPosition: imageFiles.indexOf(item.content)
|
||||
});
|
||||
},
|
||||
|
||||
renderPreview() {
|
||||
if (!this.previewImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.fileList.map((item, index) => (
|
||||
<div class={bem('preview')}>
|
||||
<Image
|
||||
fit="cover"
|
||||
src={item.content}
|
||||
class={bem('preview-image')}
|
||||
width={this.previewSize}
|
||||
height={this.previewSize}
|
||||
scopedSlots={{
|
||||
error() {
|
||||
return [
|
||||
<Icon class={bem('file-icon')} name="description" />,
|
||||
<div class={[bem('file-name'), 'van-ellipsis']}>{item.file.name}</div>
|
||||
];
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isImageDataUrl(item.content)) {
|
||||
this.onPreviewImage(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
name="delete"
|
||||
class={bem('preview-delete')}
|
||||
onClick={() => {
|
||||
this.onDelete(item, index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
},
|
||||
|
||||
renderUpload() {
|
||||
if (this.fileList.length >= this.maxCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slot = this.slots();
|
||||
|
||||
const Input = (
|
||||
<input
|
||||
{...{ attrs: this.$attrs }}
|
||||
ref="input"
|
||||
type="file"
|
||||
accept={this.accept}
|
||||
class={bem('input')}
|
||||
disabled={this.disabled}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
if (slot) {
|
||||
return (
|
||||
<div class={bem('input-wrapper')}>
|
||||
{slot}
|
||||
{Input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let style;
|
||||
if (this.previewSize) {
|
||||
const size = suffixPx(this.previewSize);
|
||||
style = {
|
||||
width: size,
|
||||
height: size
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={bem('upload')} style={style}>
|
||||
<Icon name="plus" class={bem('upload-icon')} />
|
||||
{this.uploadText && <span class={bem('upload-text')}>{this.uploadText}</span>}
|
||||
{Input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
return (
|
||||
<div class={bem()}>
|
||||
<div class={bem('wrapper')}>
|
||||
{this.renderPreview()}
|
||||
{this.renderUpload()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
@import '../style/var';
|
||||
|
||||
.van-uploader {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden; // to clip file-upload-button
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: @uploader-size;
|
||||
height: @uploader-size;
|
||||
margin: 0 10px 10px 0;
|
||||
background-color: @uploader-upload-background-color;
|
||||
border: 1px dashed @uploader-upload-border-color;
|
||||
|
||||
&-icon {
|
||||
color: @uploader-icon-color;
|
||||
font-size: @uploader-icon-size;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 10px;
|
||||
color: @uploader-text-color;
|
||||
font-size: @uploader-text-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
position: relative;
|
||||
margin: 0 10px 10px 0;
|
||||
|
||||
&-image {
|
||||
display: block;
|
||||
width: @uploader-size;
|
||||
height: @uploader-size;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1px;
|
||||
color: @uploader-delete-color;
|
||||
background-color: @uploader-delete-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__file-icon {
|
||||
color: @gray-darker;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
color: @gray-darker;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders demo correctly 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input multiple="multiple" type="file" accept="*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input multiple="multiple" type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__input-wrapper"><button class="van-button van-button--primary van-button--normal"><i class="van-icon van-icon-photo van-button__icon">
|
||||
<!----></i><span class="van-button__text">
|
||||
上传文件
|
||||
</span></button><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`delete preview image 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload" style="width: 30px; height: 30px;"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`disable preview image 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`max-count prop 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__preview">
|
||||
<div class="van-image van-uploader__preview-image"><img src="data:image/test" class="van-image__img" style="object-fit: cover;">
|
||||
<div class="van-image__loading"><i class="van-icon van-icon-photo-o" style="font-size: 22px;">
|
||||
<!----></i></div>
|
||||
</div><i class="van-icon van-icon-delete van-uploader__preview-delete">
|
||||
<!----></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`preview not image file 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__preview">
|
||||
<div class="van-image van-uploader__preview-image">
|
||||
<div class="van-image__error"><i class="van-icon van-icon-description van-uploader__file-icon" style="">
|
||||
<!----></i>
|
||||
<div class="van-uploader__file-name van-ellipsis">test.md</div>
|
||||
</div>
|
||||
</div><i class="van-icon van-icon-delete van-uploader__preview-delete">
|
||||
<!----></i>
|
||||
</div>
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`preview-size prop 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__preview" style="">
|
||||
<div class="van-image van-uploader__preview-image" style="width: 30px; height: 30px;"><img src="data:image/test" class="van-image__img" style="object-fit: cover;">
|
||||
<div class="van-image__loading"><i class="van-icon van-icon-photo-o" style="font-size: 22px;">
|
||||
<!----></i></div>
|
||||
</div><i class="van-icon van-icon-delete van-uploader__preview-delete">
|
||||
<!----></i>
|
||||
</div>
|
||||
<div class="van-uploader__upload" style="width: 30px; height: 30px;"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`render preview image 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__preview">
|
||||
<div class="van-image van-uploader__preview-image"><img src="data:image/test" class="van-image__img" style="object-fit: cover;">
|
||||
<div class="van-image__loading"><i class="van-icon van-icon-photo-o" style="font-size: 22px;">
|
||||
<!----></i></div>
|
||||
</div><i class="van-icon van-icon-delete van-uploader__preview-delete">
|
||||
<!----></i>
|
||||
</div>
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`render upload-text 1`] = `
|
||||
<div class="van-uploader">
|
||||
<div class="van-uploader__wrapper">
|
||||
<div class="van-uploader__upload"><i class="van-icon van-icon-plus van-uploader__upload-icon">
|
||||
<!----></i><span class="van-uploader__upload-text">Text</span><input type="file" accept="image/*" class="van-uploader__input"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Demo from '../demo';
|
||||
import demoTest from '../../../test/demo-test';
|
||||
|
||||
demoTest(Demo);
|
||||
@@ -0,0 +1,302 @@
|
||||
import Uploader from '..';
|
||||
import { mount, later } from '../../../test/utils';
|
||||
|
||||
window.File = function () {
|
||||
this.size = 10000;
|
||||
};
|
||||
|
||||
const mockFileDataUrl = 'data:image/test';
|
||||
const mockFile = new File([], '/Users');
|
||||
const file = { target: { files: [mockFile] } };
|
||||
const multiFile = { target: { files: [mockFile, mockFile] } };
|
||||
|
||||
window.FileReader = function () {
|
||||
this.readAsText = function () {
|
||||
this.onload &&
|
||||
this.onload({
|
||||
target: {
|
||||
result: mockFileDataUrl
|
||||
}
|
||||
});
|
||||
};
|
||||
this.readAsDataURL = this.readAsText;
|
||||
};
|
||||
|
||||
test('disabled', () => {
|
||||
const afterRead = jest.fn();
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
disabled: true,
|
||||
afterRead
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
expect(afterRead).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('read text', done => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
resultType: 'text',
|
||||
afterRead: readFile => {
|
||||
expect(readFile.content).toEqual(mockFileDataUrl);
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
});
|
||||
|
||||
it('set input name', done => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
name: 'uploader',
|
||||
beforeRead: (readFile, detail) => {
|
||||
expect(detail.name).toEqual('uploader');
|
||||
return true;
|
||||
},
|
||||
afterRead: (readFile, detail) => {
|
||||
expect(detail.name).toEqual('uploader');
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
});
|
||||
|
||||
it('unknown resultType', () => {
|
||||
const afterRead = jest.fn();
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
resultType: 'xxxx',
|
||||
afterRead
|
||||
}
|
||||
});
|
||||
wrapper.vm.onChange(file);
|
||||
expect(afterRead).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('before read return false', () => {
|
||||
const afterRead = jest.fn();
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
beforeRead: () => false,
|
||||
afterRead
|
||||
}
|
||||
});
|
||||
|
||||
const input = wrapper.find('input');
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
expect(afterRead).toHaveBeenCalledTimes(0);
|
||||
expect(input.element.value).toEqual('');
|
||||
});
|
||||
|
||||
it('before read return promise and resolve', async () => {
|
||||
const afterRead = jest.fn();
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
beforeRead: () => new Promise(resolve => {
|
||||
resolve();
|
||||
}),
|
||||
afterRead
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
|
||||
await later();
|
||||
expect(afterRead).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('before read return promise and reject', async () => {
|
||||
const afterRead = jest.fn();
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
beforeRead: () => new Promise((resolve, reject) => {
|
||||
reject();
|
||||
}),
|
||||
afterRead
|
||||
}
|
||||
});
|
||||
|
||||
const input = wrapper.find('input');
|
||||
|
||||
await later();
|
||||
wrapper.vm.onChange(file);
|
||||
expect(afterRead).toHaveBeenCalledTimes(0);
|
||||
expect(input.element.value).toEqual('');
|
||||
});
|
||||
|
||||
test('file size overlimit', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
maxSize: 1
|
||||
}
|
||||
});
|
||||
wrapper.vm.onChange(file);
|
||||
wrapper.vm.onChange(multiFile);
|
||||
|
||||
await later();
|
||||
expect(wrapper.emitted('oversize')[0]).toBeTruthy();
|
||||
expect(wrapper.emitted('oversize')[1]).toBeTruthy();
|
||||
|
||||
wrapper.vm.maxSize = 100000;
|
||||
wrapper.vm.onChange(multiFile);
|
||||
|
||||
await later();
|
||||
expect(wrapper.emitted('oversize')[2]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('render upload-text', () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
uploadText: 'Text'
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render preview image', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: []
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
await later();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disable preview image', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [],
|
||||
previewImage: false
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
await later();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('max-count prop', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [],
|
||||
maxCount: 1
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(multiFile);
|
||||
await later();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('preview-size prop', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [],
|
||||
previewSize: 30
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
await later();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('delete preview image', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [],
|
||||
previewSize: 30
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
await later();
|
||||
|
||||
wrapper.find('.van-uploader__preview-delete').trigger('click');
|
||||
expect(wrapper.vm.fileList.length).toEqual(0);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.emitted('delete')[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('click to preview image', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [],
|
||||
previewSize: 30
|
||||
},
|
||||
listeners: {
|
||||
input(fileList) {
|
||||
wrapper.setProps({ fileList });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.vm.onChange(file);
|
||||
await later();
|
||||
|
||||
wrapper.find('.van-image').trigger('click');
|
||||
|
||||
const imagePreviewNode = document.querySelector('.van-image-preview');
|
||||
expect(imagePreviewNode).toBeTruthy();
|
||||
imagePreviewNode.remove();
|
||||
});
|
||||
|
||||
it('preview not image file', async () => {
|
||||
const wrapper = mount(Uploader, {
|
||||
propsData: {
|
||||
fileList: [{
|
||||
content: 'data:application',
|
||||
file: {
|
||||
name: 'test.md'
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.find('img').trigger('error');
|
||||
wrapper.find('.van-image').trigger('click');
|
||||
|
||||
expect(document.querySelector('.van-image-preview')).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
export function toArray(item) {
|
||||
if (Array.isArray(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return [item];
|
||||
}
|
||||
|
||||
export function readFile(file, resultType) {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = event => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
if (resultType === 'dataUrl') {
|
||||
reader.readAsDataURL(file);
|
||||
} else if (resultType === 'text') {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function isOversize(files, maxSize) {
|
||||
return toArray(files).some(file => file.size > maxSize);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# Uploader 文件上传
|
||||
|
||||
### 引入
|
||||
|
||||
``` javascript
|
||||
import { Uploader } from 'vant';
|
||||
|
||||
Vue.use(Uploader);
|
||||
```
|
||||
|
||||
## 代码演示
|
||||
|
||||
### 基础用法
|
||||
|
||||
文件上传完毕后会触发`after-read`回调函数,获取到对应的`file`对象
|
||||
|
||||
```html
|
||||
<van-uploader :after-read="afterRead" />
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
methods: {
|
||||
afterRead(file) {
|
||||
// 此时可以自行将文件上传至服务器
|
||||
console.log(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 图片预览
|
||||
|
||||
通过`v-model`可以绑定已经上传的图片列表,并展示图片列表的预览图
|
||||
|
||||
```html
|
||||
<van-uploader v-model="fileList" multiple />
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: []
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 限制上传数量
|
||||
|
||||
通过`max-count`属性可以限制上传文件的数量,上传数量达到限制后,会自动隐藏上传区域
|
||||
|
||||
```html
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
multiple
|
||||
:max-count="2"
|
||||
/>
|
||||
```
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fileList: []
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 自定义上传样式
|
||||
|
||||
通过插槽可以自定义上传区域的样式
|
||||
|
||||
```html
|
||||
<van-uploader>
|
||||
<van-button icon="photo" type="primary">上传图片</van-button>
|
||||
</van-uploader>
|
||||
```
|
||||
|
||||
### 上传前校验
|
||||
|
||||
通过传入`beforeRead`函数可以在上传前进行校验,返回`true`表示校验通过,返回`false`表示校验失败。支持返回`Promise`进行异步校验
|
||||
|
||||
```html
|
||||
<van-uploader :before-read="beforeRead" />
|
||||
```
|
||||
|
||||
```js
|
||||
export default {
|
||||
methods: {
|
||||
// 返回布尔值
|
||||
beforeRead(file) {
|
||||
if (file.type !== 'image/jpeg') {
|
||||
Toast('请上传 jpg 格式图片');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// 返回 Promise
|
||||
asyncBeforeRead(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file.type !== 'image/jpeg') {
|
||||
Toast('请上传 jpg 格式图片');
|
||||
reject();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
|------|------|------|------|------|
|
||||
| name | 标识符,可以在回调函数的第二项参数中获取 | `String` | - | 1.6.13 |
|
||||
| accept | 接受的文件类型 | `String` | `image/*` | - |
|
||||
| preview-image | 是否在上传完成后展示预览图 | `Boolean` | `true` | 2.0.0 |
|
||||
| preview-size | 预览图和上传区域的尺寸,默认单位为`px` | `String | Number` | `80px` | 2.0.0 |
|
||||
| multiple | 是否开启图片多选,部分安卓机型不支持 | `Boolean` | `false` | 2.0.0 |
|
||||
| disabled | 是否禁用文件上传 | `Boolean` | `false` | - |
|
||||
| capture | 图片选取模式,可选值为`camera`(直接调起摄像头) | `String` | - | 2.0.0 |
|
||||
| before-read | 文件读取前的回调函数,返回`false`可终止文件读取,支持返回`Promise` | `Function` | - | - |
|
||||
| after-read | 文件读取完成后的回调函数 | `Function` | - | - |
|
||||
| max-size | 文件大小限制,单位为`byte` | `Number` | - | - |
|
||||
| max-count | 文件上传数量限制 | `Number` | - | 2.0.0 |
|
||||
| result-type | 文件读取结果类型,可选值为`text` | `String` | `dataUrl` | - |
|
||||
| upload-text | 上传区域文字提示 | `String` | - | 2.0.0 |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|------|------|------|
|
||||
| oversize | 文件大小超过限制时触发 | 同`after-read` |
|
||||
| delete | 删除文件预览时触发 | file: 被删除的文件对象 |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
|------|------|
|
||||
| default | 自定义上传区域 |
|
||||
|
||||
### before-read、after-read 回调参数
|
||||
|
||||
| 参数名 | 说明 | 类型 |
|
||||
|------|------|------|
|
||||
| file | 文件解析后的 file 对象 | `Object` |
|
||||
| detail | 额外信息,包含 name 字段 | `Object` |
|
||||
Reference in New Issue
Block a user