mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
refactor(Uploader): refactor with composition api
This commit is contained in:
parent
c8d573cbe7
commit
595b062c34
@ -1,15 +1,13 @@
|
|||||||
// Utils
|
import { ref } from 'vue';
|
||||||
import {
|
|
||||||
noop,
|
|
||||||
isDef,
|
|
||||||
isPromise,
|
|
||||||
getSizeStyle,
|
|
||||||
createNamespace,
|
|
||||||
} from '../utils';
|
|
||||||
import { toArray, readFile, isOversize, isImageFile } from './utils';
|
|
||||||
|
|
||||||
// Mixins
|
// Utils
|
||||||
import { FieldMixin } from '../mixins/field';
|
import { callInterceptor } from '../utils/interceptor';
|
||||||
|
import { isDef, isPromise, getSizeStyle, createNamespace } from '../utils';
|
||||||
|
import { toArray, isOversize, isImageFile, readFileContent } from './utils';
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
import { usePublicApi } from '../composition/use-public-api';
|
||||||
|
import { useParentField } from '../composition/use-parent-field';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Icon from '../icon';
|
import Icon from '../icon';
|
||||||
@ -20,8 +18,6 @@ import ImagePreview from '../image-preview';
|
|||||||
const [createComponent, bem] = createNamespace('uploader');
|
const [createComponent, bem] = createNamespace('uploader');
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
mixins: [FieldMixin],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
capture: String,
|
capture: String,
|
||||||
multiple: Boolean,
|
multiple: Boolean,
|
||||||
@ -91,89 +87,22 @@ export default createComponent({
|
|||||||
'update:modelValue',
|
'update:modelValue',
|
||||||
],
|
],
|
||||||
|
|
||||||
methods: {
|
setup(props, { emit, slots }) {
|
||||||
getDetail(index = this.modelValue.length) {
|
const inputRef = ref();
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
index,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onChange(event) {
|
const getDetail = (index = props.modelValue.length) => ({
|
||||||
let { files } = event.target;
|
name: props.name,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
if (this.disabled || !files.length) {
|
const resetInput = () => {
|
||||||
return;
|
if (inputRef.value) {
|
||||||
|
inputRef.value.value = '';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
files = files.length === 1 ? files[0] : [].slice.call(files);
|
const onAfterRead = (files, oversize) => {
|
||||||
|
resetInput();
|
||||||
if (this.beforeRead) {
|
|
||||||
const response = this.beforeRead(files, this.getDetail());
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
this.resetInput();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPromise(response)) {
|
|
||||||
response
|
|
||||||
.then((data) => {
|
|
||||||
if (data) {
|
|
||||||
this.readFile(data);
|
|
||||||
} else {
|
|
||||||
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.modelValue.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) => {
|
|
||||||
const result = { file, status: '', message: '' };
|
|
||||||
|
|
||||||
if (contents[index]) {
|
|
||||||
result.content = contents[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onAfterRead(fileList, oversize);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
readFile(files, this.resultType).then((content) => {
|
|
||||||
const result = { file: files, status: '', message: '' };
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
result.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onAfterRead(result, oversize);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onAfterRead(files, oversize) {
|
|
||||||
this.resetInput();
|
|
||||||
|
|
||||||
let validFiles = files;
|
let validFiles = files;
|
||||||
|
|
||||||
@ -184,7 +113,7 @@ export default createComponent({
|
|||||||
validFiles = [];
|
validFiles = [];
|
||||||
files.forEach((item) => {
|
files.forEach((item) => {
|
||||||
if (item.file) {
|
if (item.file) {
|
||||||
if (item.file.size > this.maxSize) {
|
if (item.file.size > props.maxSize) {
|
||||||
oversizeFiles.push(item);
|
oversizeFiles.push(item);
|
||||||
} else {
|
} else {
|
||||||
validFiles.push(item);
|
validFiles.push(item);
|
||||||
@ -194,7 +123,7 @@ export default createComponent({
|
|||||||
} else {
|
} else {
|
||||||
validFiles = null;
|
validFiles = null;
|
||||||
}
|
}
|
||||||
this.$emit('oversize', oversizeFiles, this.getDetail());
|
emit('oversize', oversizeFiles, getDetail());
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidFiles = Array.isArray(validFiles)
|
const isValidFiles = Array.isArray(validFiles)
|
||||||
@ -202,90 +131,140 @@ export default createComponent({
|
|||||||
: Boolean(validFiles);
|
: Boolean(validFiles);
|
||||||
|
|
||||||
if (isValidFiles) {
|
if (isValidFiles) {
|
||||||
this.$emit('update:modelValue', [
|
emit('update:modelValue', [
|
||||||
...this.modelValue,
|
...props.modelValue,
|
||||||
...toArray(validFiles),
|
...toArray(validFiles),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (this.afterRead) {
|
if (props.afterRead) {
|
||||||
this.afterRead(validFiles, this.getDetail());
|
props.afterRead(validFiles, getDetail());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onDelete(file, index) {
|
const readFile = (files) => {
|
||||||
if (this.beforeDelete) {
|
const { maxSize, maxCount, modelValue, resultType } = props;
|
||||||
const response = this.beforeDelete(file, this.getDetail(index));
|
const oversize = isOversize(files, maxSize);
|
||||||
|
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
const remainCount = maxCount - modelValue.length;
|
||||||
|
|
||||||
|
if (files.length > remainCount) {
|
||||||
|
files = files.slice(0, remainCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
files.map((file) => readFileContent(file, resultType))
|
||||||
|
).then((contents) => {
|
||||||
|
const fileList = files.map((file, index) => {
|
||||||
|
const result = { file, status: '', message: '' };
|
||||||
|
|
||||||
|
if (contents[index]) {
|
||||||
|
result.content = contents[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
onAfterRead(fileList, oversize);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
readFileContent(files, resultType).then((content) => {
|
||||||
|
const result = { file: files, status: '', message: '' };
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
result.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAfterRead(result, oversize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = (file, index) => {
|
||||||
|
const fileList = props.modelValue.slice(0);
|
||||||
|
fileList.splice(index, 1);
|
||||||
|
|
||||||
|
emit('update:modelValue', fileList);
|
||||||
|
emit('delete', file, getDetail(index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (file, index) => {
|
||||||
|
callInterceptor({
|
||||||
|
interceptor: props.beforeDelete,
|
||||||
|
args: [file, getDetail(index)],
|
||||||
|
done() {
|
||||||
|
deleteFile(file, index);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (event) => {
|
||||||
|
let { files } = event.target;
|
||||||
|
|
||||||
|
if (props.disabled || !files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
files = files.length === 1 ? files[0] : [].slice.call(files);
|
||||||
|
|
||||||
|
if (props.beforeRead) {
|
||||||
|
const response = props.beforeRead(files, getDetail());
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
|
resetInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPromise(response)) {
|
if (isPromise(response)) {
|
||||||
response
|
response
|
||||||
.then(() => {
|
.then((data) => {
|
||||||
this.deleteFile(file, index);
|
if (data) {
|
||||||
|
readFile(data);
|
||||||
|
} else {
|
||||||
|
readFile(files);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(noop);
|
.catch(resetInput);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
readFile(files);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.deleteFile(file, index);
|
let imagePreview;
|
||||||
},
|
|
||||||
|
|
||||||
deleteFile(file, index) {
|
const onPreviewImage = (item) => {
|
||||||
const fileList = this.modelValue.slice(0);
|
if (props.previewFullImage) {
|
||||||
fileList.splice(index, 1);
|
const imageFiles = props.modelValue.filter((item) => isImageFile(item));
|
||||||
|
const imageContents = imageFiles.map(
|
||||||
|
(item) => item.content || item.url
|
||||||
|
);
|
||||||
|
|
||||||
this.$emit('update:modelValue', fileList);
|
imagePreview = ImagePreview({
|
||||||
this.$emit('delete', file, this.getDetail(index));
|
images: imageContents,
|
||||||
},
|
startPosition: imageFiles.indexOf(item),
|
||||||
|
onClose: () => {
|
||||||
resetInput() {
|
emit('close-preview');
|
||||||
/* istanbul ignore else */
|
},
|
||||||
if (this.$refs.input) {
|
...props.previewOptions,
|
||||||
this.$refs.input.value = '';
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onPreviewImage(item) {
|
const closeImagePreview = () => {
|
||||||
if (!this.previewFullImage) {
|
if (imagePreview) {
|
||||||
return;
|
imagePreview.close();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const imageFiles = this.modelValue.filter((item) => isImageFile(item));
|
const chooseFile = () => {
|
||||||
const imageContents = imageFiles.map((item) => item.content || item.url);
|
if (inputRef.value && !props.disabled) {
|
||||||
|
inputRef.value.click();
|
||||||
this.imagePreview = ImagePreview({
|
|
||||||
images: imageContents,
|
|
||||||
startPosition: imageFiles.indexOf(item),
|
|
||||||
onClose: () => {
|
|
||||||
this.$emit('close-preview');
|
|
||||||
},
|
|
||||||
...this.previewOptions,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
closeImagePreview() {
|
|
||||||
if (this.imagePreview) {
|
|
||||||
this.imagePreview.close();
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
// @exposed-api
|
const renderPreviewMask = (item) => {
|
||||||
chooseFile() {
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (this.$refs.input) {
|
|
||||||
this.$refs.input.click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
genPreviewMask(item) {
|
|
||||||
const { status, message } = item;
|
const { status, message } = item;
|
||||||
|
|
||||||
if (status === 'uploading' || status === 'failed') {
|
if (status === 'uploading' || status === 'failed') {
|
||||||
@ -305,26 +284,26 @@ export default createComponent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
genPreviewItem(item, index) {
|
const renderPreviewItem = (item, index) => {
|
||||||
const showDelete = item.status !== 'uploading' && this.deletable;
|
const showDelete = item.status !== 'uploading' && props.deletable;
|
||||||
|
|
||||||
const DeleteIcon = showDelete && (
|
const DeleteIcon = showDelete && (
|
||||||
<div
|
<div
|
||||||
class={bem('preview-delete')}
|
class={bem('preview-delete')}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.onDelete(item, index);
|
onDelete(item, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="cross" class={bem('preview-delete-icon')} />
|
<Icon name="cross" class={bem('preview-delete-icon')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const PreviewCover = this.$slots['preview-cover'] && (
|
const PreviewCover = slots['preview-cover'] && (
|
||||||
<div class={bem('preview-cover')}>
|
<div class={bem('preview-cover')}>
|
||||||
{this.$slots['preview-cover']({
|
{slots['preview-cover']({
|
||||||
index,
|
index,
|
||||||
...item,
|
...item,
|
||||||
})}
|
})}
|
||||||
@ -333,20 +312,20 @@ export default createComponent({
|
|||||||
|
|
||||||
const Preview = isImageFile(item) ? (
|
const Preview = isImageFile(item) ? (
|
||||||
<Image
|
<Image
|
||||||
fit={this.imageFit}
|
fit={props.imageFit}
|
||||||
src={item.content || item.url}
|
src={item.content || item.url}
|
||||||
class={bem('preview-image')}
|
class={bem('preview-image')}
|
||||||
width={this.previewSize}
|
width={props.previewSize}
|
||||||
height={this.previewSize}
|
height={props.previewSize}
|
||||||
lazyLoad={this.lazyLoad}
|
lazyLoad={props.lazyLoad}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.onPreviewImage(item);
|
onPreviewImage(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{PreviewCover}
|
{PreviewCover}
|
||||||
</Image>
|
</Image>
|
||||||
) : (
|
) : (
|
||||||
<div class={bem('file')} style={getSizeStyle(this.previewSize)}>
|
<div class={bem('file')} style={getSizeStyle(props.previewSize)}>
|
||||||
<Icon class={bem('file-icon')} name="description" />
|
<Icon class={bem('file-icon')} name="description" />
|
||||||
<div class={[bem('file-name'), 'van-ellipsis']}>
|
<div class={[bem('file-name'), 'van-ellipsis']}>
|
||||||
{item.file ? item.file.name : item.url}
|
{item.file ? item.file.name : item.url}
|
||||||
@ -359,24 +338,24 @@ export default createComponent({
|
|||||||
<div
|
<div
|
||||||
class={bem('preview')}
|
class={bem('preview')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.$emit('click-preview', item, this.getDetail(index));
|
emit('click-preview', item, getDetail(index));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Preview}
|
{Preview}
|
||||||
{this.genPreviewMask(item)}
|
{renderPreviewMask(item)}
|
||||||
{DeleteIcon}
|
{DeleteIcon}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
|
|
||||||
genPreviewList() {
|
const renderPreviewList = () => {
|
||||||
if (this.previewImage) {
|
if (props.previewImage) {
|
||||||
return this.modelValue.map(this.genPreviewItem);
|
return props.modelValue.map(renderPreviewItem);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
genUpload() {
|
const renderUpload = () => {
|
||||||
if (this.modelValue.length >= this.maxCount || !this.showUpload) {
|
if (props.modelValue.length >= props.maxCount || !props.showUpload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,41 +364,46 @@ export default createComponent({
|
|||||||
ref="input"
|
ref="input"
|
||||||
type="file"
|
type="file"
|
||||||
class={bem('input')}
|
class={bem('input')}
|
||||||
accept={this.accept}
|
accept={props.accept}
|
||||||
capture={this.capture}
|
capture={props.capture}
|
||||||
multiple={this.multiple}
|
multiple={props.multiple}
|
||||||
disabled={this.disabled}
|
disabled={props.disabled}
|
||||||
onChange={this.onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.$slots.default) {
|
if (slots.default) {
|
||||||
return (
|
return (
|
||||||
<div class={bem('input-wrapper')}>
|
<div class={bem('input-wrapper')}>
|
||||||
{this.$slots.default()}
|
{slots.default()}
|
||||||
{Input}
|
{Input}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={bem('upload')} style={getSizeStyle(this.previewSize)}>
|
<div class={bem('upload')} style={getSizeStyle(props.previewSize)}>
|
||||||
<Icon name={this.uploadIcon} class={bem('upload-icon')} />
|
<Icon name={props.uploadIcon} class={bem('upload-icon')} />
|
||||||
{this.uploadText && (
|
{props.uploadText && (
|
||||||
<span class={bem('upload-text')}>{this.uploadText}</span>
|
<span class={bem('upload-text')}>{props.uploadText}</span>
|
||||||
)}
|
)}
|
||||||
{Input}
|
{Input}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
usePublicApi({
|
||||||
return (
|
chooseFile,
|
||||||
|
closeImagePreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
useParentField(() => props.modelValue);
|
||||||
|
|
||||||
|
return () => (
|
||||||
<div class={bem()}>
|
<div class={bem()}>
|
||||||
<div class={bem('wrapper', { disabled: this.disabled })}>
|
<div class={bem('wrapper', { disabled: props.disabled })}>
|
||||||
{this.genPreviewList()}
|
{renderPreviewList()}
|
||||||
{this.genUpload()}
|
{renderUpload()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ export function toArray<T>(item: T | T[]): T[] {
|
|||||||
return [item];
|
return [item];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readFile(file: File, resultType: ResultType) {
|
export function readFileContent(file: File, resultType: ResultType) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (resultType === 'file') {
|
if (resultType === 'file') {
|
||||||
resolve();
|
resolve();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user