refactor(Uploader): extract PreviewItem component

This commit is contained in:
chenjiahan 2020-09-15 19:01:10 +08:00
parent 595b062c34
commit 632a54d672
4 changed files with 209 additions and 147 deletions

123
src/uploader/PreviewItem.js Normal file
View File

@ -0,0 +1,123 @@
import { bem } from './shared';
import { isImageFile } from './utils';
import { isDef, getSizeStyle } from '../utils';
import { callInterceptor } from '../utils/interceptor';
// Components
import Icon from '../icon';
import Image from '../image';
import Loading from '../loading';
export default {
props: {
name: String,
item: Object,
imageFit: String,
lazyLoad: Boolean,
deletable: Boolean,
previewSize: [Number, String],
beforeDelete: Function,
},
emits: ['delete', 'preview'],
setup(props, { emit, slots }) {
const renderPreviewMask = () => {
const { status, message } = props.item;
if (status === 'uploading' || status === 'failed') {
const MaskIcon =
status === 'failed' ? (
<Icon name="close" class={bem('mask-icon')} />
) : (
<Loading class={bem('loading')} />
);
const showMessage = isDef(message) && message !== '';
return (
<div class={bem('mask')}>
{MaskIcon}
{showMessage && <div class={bem('mask-message')}>{message}</div>}
</div>
);
}
};
const onDelete = (event) => {
const { name, item, index, beforeDelete } = props;
event.stopPropagation();
callInterceptor({
interceptor: beforeDelete,
args: [item, { name, index }],
done() {
emit('delete');
},
});
};
const onPreview = () => {
emit('preview');
};
const renderDeleteIcon = () => {
if (props.deletable && props.item.status !== 'uploading') {
return (
<div class={bem('preview-delete')} onClick={onDelete}>
<Icon name="cross" class={bem('preview-delete-icon')} />
</div>
);
}
};
const renderPreviewCover = () => {
if (slots['preview-cover']) {
const { index, item } = props;
return (
<div class={bem('preview-cover')}>
{slots['preview-cover']({ index, ...item })}
</div>
);
}
};
const renderPreview = () => {
const { item } = props;
if (isImageFile(item)) {
return (
<Image
fit={props.imageFit}
src={item.content || item.url}
class={bem('preview-image')}
width={props.previewSize}
height={props.previewSize}
lazyLoad={props.lazyLoad}
onClick={onPreview}
>
{renderPreviewCover()}
</Image>
);
}
return (
<div class={bem('file')} style={getSizeStyle(props.previewSize)}>
<Icon class={bem('file-icon')} name="description" />
<div class={[bem('file-name'), 'van-ellipsis']}>
{item.file ? item.file.name : item.url}
</div>
{renderPreviewCover()}
</div>
);
};
return () => (
<div class={bem('preview')}>
{renderPreview()}
{renderPreviewMask()}
{renderDeleteIcon()}
</div>
);
},
};

View File

@ -1,8 +1,8 @@
import { ref } from 'vue';
// Utils
import { callInterceptor } from '../utils/interceptor';
import { isDef, isPromise, getSizeStyle, createNamespace } from '../utils';
import { bem, createComponent } from './shared';
import { isPromise, getSizeStyle, pick } from '../utils';
import { toArray, isOversize, isImageFile, readFileContent } from './utils';
// Composition
@ -11,12 +11,9 @@ import { useParentField } from '../composition/use-parent-field';
// Components
import Icon from '../icon';
import Image from '../image';
import Loading from '../loading';
import PreviewItem from './PreviewItem';
import ImagePreview from '../image-preview';
const [createComponent, bem] = createNamespace('uploader');
export default createComponent({
props: {
capture: String,
@ -101,34 +98,42 @@ export default createComponent({
}
};
const onAfterRead = (files, oversize) => {
const findOversizeFiles = (files) => {
const valid = [];
const oversize = [];
files.forEach((item) => {
if (item.file && item.file.size > props.maxSize) {
oversize.push(item);
} else {
valid.push(item);
}
});
return { valid, oversize };
};
const onAfterRead = (items) => {
resetInput();
let validFiles = files;
let validFiles = items;
if (oversize) {
let oversizeFiles = files;
if (Array.isArray(files)) {
oversizeFiles = [];
validFiles = [];
files.forEach((item) => {
if (item.file) {
if (item.file.size > props.maxSize) {
oversizeFiles.push(item);
} else {
validFiles.push(item);
}
}
});
const hasOversize = isOversize(items, props.maxSize);
if (hasOversize) {
if (Array.isArray(items)) {
const result = findOversizeFiles(items);
validFiles = result.valid;
emit('oversize', result.oversizeFiles, getDetail());
} else {
validFiles = null;
emit('oversize', items, getDetail());
return;
}
emit('oversize', oversizeFiles, getDetail());
}
const isValidFiles = Array.isArray(validFiles)
? Boolean(validFiles.length)
: Boolean(validFiles);
const isValidFiles = Boolean(
Array.isArray(validFiles) ? validFiles.length : validFiles
);
if (isValidFiles) {
emit('update:modelValue', [
@ -143,8 +148,7 @@ export default createComponent({
};
const readFile = (files) => {
const { maxSize, maxCount, modelValue, resultType } = props;
const oversize = isOversize(files, maxSize);
const { maxCount, modelValue, resultType } = props;
if (Array.isArray(files)) {
const remainCount = maxCount - modelValue.length;
@ -166,7 +170,7 @@ export default createComponent({
return result;
});
onAfterRead(fileList, oversize);
onAfterRead(fileList);
});
} else {
readFileContent(files, resultType).then((content) => {
@ -176,29 +180,11 @@ export default createComponent({
result.content = content;
}
onAfterRead(result, oversize);
onAfterRead(result);
});
}
};
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;
@ -234,19 +220,19 @@ export default createComponent({
let imagePreview;
const onPreviewImage = (item) => {
const onClosePreview = () => {
emit('close-preview');
};
const previewImage = (item) => {
if (props.previewFullImage) {
const imageFiles = props.modelValue.filter((item) => isImageFile(item));
const imageContents = imageFiles.map(
(item) => item.content || item.url
);
const imageFiles = props.modelValue.filter(isImageFile);
const images = imageFiles.map((item) => item.content || item.url);
imagePreview = ImagePreview({
images: imageContents,
images,
startPosition: imageFiles.indexOf(item),
onClose: () => {
emit('close-preview');
},
onClose: onClosePreview,
...props.previewOptions,
});
}
@ -258,95 +244,37 @@ export default createComponent({
}
};
const chooseFile = () => {
if (inputRef.value && !props.disabled) {
inputRef.value.click();
}
const deleteFile = (item, index) => {
const fileList = props.modelValue.slice(0);
fileList.splice(index, 1);
emit('update:modelValue', fileList);
emit('delete', item, getDetail(index));
};
const renderPreviewMask = (item) => {
const { status, message } = item;
if (status === 'uploading' || status === 'failed') {
const MaskIcon =
status === 'failed' ? (
<Icon name="close" class={bem('mask-icon')} />
) : (
<Loading class={bem('loading')} />
);
const showMessage = isDef(message) && message !== '';
return (
<div class={bem('mask')}>
{MaskIcon}
{showMessage && <div class={bem('mask-message')}>{message}</div>}
</div>
);
}
};
const renderPreviewItem = (item, index) => {
const showDelete = item.status !== 'uploading' && props.deletable;
const DeleteIcon = showDelete && (
<div
class={bem('preview-delete')}
onClick={(event) => {
event.stopPropagation();
onDelete(item, index);
}}
>
<Icon name="cross" class={bem('preview-delete-icon')} />
</div>
);
const PreviewCover = slots['preview-cover'] && (
<div class={bem('preview-cover')}>
{slots['preview-cover']({
index,
...item,
})}
</div>
);
const Preview = isImageFile(item) ? (
<Image
fit={props.imageFit}
src={item.content || item.url}
class={bem('preview-image')}
width={props.previewSize}
height={props.previewSize}
lazyLoad={props.lazyLoad}
onClick={() => {
onPreviewImage(item);
}}
>
{PreviewCover}
</Image>
) : (
<div class={bem('file')} style={getSizeStyle(props.previewSize)}>
<Icon class={bem('file-icon')} name="description" />
<div class={[bem('file-name'), 'van-ellipsis']}>
{item.file ? item.file.name : item.url}
</div>
{PreviewCover}
</div>
);
return (
<div
class={bem('preview')}
onClick={() => {
emit('click-preview', item, getDetail(index));
}}
>
{Preview}
{renderPreviewMask(item)}
{DeleteIcon}
</div>
);
};
const renderPreviewItem = (item, index) => (
<PreviewItem
v-slots={{ 'preview-cover': slots['preview-cover'] }}
item={item}
onClick={() => {
emit('click-preview', item, getDetail(index));
}}
onDelete={() => {
deleteFile(item, index);
}}
onPreview={() => {
previewImage(item);
}}
{...pick(props, [
'name',
'lazyLoad',
'imageFit',
'deletable',
'previewSize',
'beforeDelete',
])}
/>
);
const renderPreviewList = () => {
if (props.previewImage) {
@ -392,6 +320,12 @@ export default createComponent({
);
};
const chooseFile = () => {
if (inputRef.value && !props.disabled) {
inputRef.value.click();
}
};
usePublicApi({
chooseFile,
closeImagePreview,

5
src/uploader/shared.js Normal file
View File

@ -0,0 +1,5 @@
import { createNamespace } from '../utils';
const [createComponent, bem] = createNamespace('uploader');
export { bem, createComponent };

View File

@ -30,10 +30,10 @@ export function readFileContent(file: File, resultType: ResultType) {
}
export function isOversize(
files: File | File[],
items: FileListItem | FileListItem[],
maxSize: number | string
): boolean {
return toArray(files).some((file) => file.size > maxSize);
return toArray(items).some((item) => item.file && item.file.size > maxSize);
}
export type FileListItem = {