mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
refactor(Uploader): extract PreviewItem component
This commit is contained in:
parent
595b062c34
commit
632a54d672
123
src/uploader/PreviewItem.js
Normal file
123
src/uploader/PreviewItem.js
Normal 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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { callInterceptor } from '../utils/interceptor';
|
import { bem, createComponent } from './shared';
|
||||||
import { isDef, isPromise, getSizeStyle, createNamespace } from '../utils';
|
import { isPromise, getSizeStyle, pick } from '../utils';
|
||||||
import { toArray, isOversize, isImageFile, readFileContent } from './utils';
|
import { toArray, isOversize, isImageFile, readFileContent } from './utils';
|
||||||
|
|
||||||
// Composition
|
// Composition
|
||||||
@ -11,12 +11,9 @@ import { useParentField } from '../composition/use-parent-field';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Icon from '../icon';
|
import Icon from '../icon';
|
||||||
import Image from '../image';
|
import PreviewItem from './PreviewItem';
|
||||||
import Loading from '../loading';
|
|
||||||
import ImagePreview from '../image-preview';
|
import ImagePreview from '../image-preview';
|
||||||
|
|
||||||
const [createComponent, bem] = createNamespace('uploader');
|
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
props: {
|
props: {
|
||||||
capture: String,
|
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();
|
resetInput();
|
||||||
|
|
||||||
let validFiles = files;
|
let validFiles = items;
|
||||||
|
|
||||||
if (oversize) {
|
const hasOversize = isOversize(items, props.maxSize);
|
||||||
let oversizeFiles = files;
|
|
||||||
if (Array.isArray(files)) {
|
if (hasOversize) {
|
||||||
oversizeFiles = [];
|
if (Array.isArray(items)) {
|
||||||
validFiles = [];
|
const result = findOversizeFiles(items);
|
||||||
files.forEach((item) => {
|
validFiles = result.valid;
|
||||||
if (item.file) {
|
emit('oversize', result.oversizeFiles, getDetail());
|
||||||
if (item.file.size > props.maxSize) {
|
|
||||||
oversizeFiles.push(item);
|
|
||||||
} else {
|
|
||||||
validFiles.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
validFiles = null;
|
emit('oversize', items, getDetail());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
emit('oversize', oversizeFiles, getDetail());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidFiles = Array.isArray(validFiles)
|
const isValidFiles = Boolean(
|
||||||
? Boolean(validFiles.length)
|
Array.isArray(validFiles) ? validFiles.length : validFiles
|
||||||
: Boolean(validFiles);
|
);
|
||||||
|
|
||||||
if (isValidFiles) {
|
if (isValidFiles) {
|
||||||
emit('update:modelValue', [
|
emit('update:modelValue', [
|
||||||
@ -143,8 +148,7 @@ export default createComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const readFile = (files) => {
|
const readFile = (files) => {
|
||||||
const { maxSize, maxCount, modelValue, resultType } = props;
|
const { maxCount, modelValue, resultType } = props;
|
||||||
const oversize = isOversize(files, maxSize);
|
|
||||||
|
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
const remainCount = maxCount - modelValue.length;
|
const remainCount = maxCount - modelValue.length;
|
||||||
@ -166,7 +170,7 @@ export default createComponent({
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
onAfterRead(fileList, oversize);
|
onAfterRead(fileList);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
readFileContent(files, resultType).then((content) => {
|
readFileContent(files, resultType).then((content) => {
|
||||||
@ -176,29 +180,11 @@ export default createComponent({
|
|||||||
result.content = content;
|
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) => {
|
const onChange = (event) => {
|
||||||
let { files } = event.target;
|
let { files } = event.target;
|
||||||
|
|
||||||
@ -234,19 +220,19 @@ export default createComponent({
|
|||||||
|
|
||||||
let imagePreview;
|
let imagePreview;
|
||||||
|
|
||||||
const onPreviewImage = (item) => {
|
const onClosePreview = () => {
|
||||||
|
emit('close-preview');
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewImage = (item) => {
|
||||||
if (props.previewFullImage) {
|
if (props.previewFullImage) {
|
||||||
const imageFiles = props.modelValue.filter((item) => isImageFile(item));
|
const imageFiles = props.modelValue.filter(isImageFile);
|
||||||
const imageContents = imageFiles.map(
|
const images = imageFiles.map((item) => item.content || item.url);
|
||||||
(item) => item.content || item.url
|
|
||||||
);
|
|
||||||
|
|
||||||
imagePreview = ImagePreview({
|
imagePreview = ImagePreview({
|
||||||
images: imageContents,
|
images,
|
||||||
startPosition: imageFiles.indexOf(item),
|
startPosition: imageFiles.indexOf(item),
|
||||||
onClose: () => {
|
onClose: onClosePreview,
|
||||||
emit('close-preview');
|
|
||||||
},
|
|
||||||
...props.previewOptions,
|
...props.previewOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -258,95 +244,37 @@ export default createComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const chooseFile = () => {
|
const deleteFile = (item, index) => {
|
||||||
if (inputRef.value && !props.disabled) {
|
const fileList = props.modelValue.slice(0);
|
||||||
inputRef.value.click();
|
fileList.splice(index, 1);
|
||||||
}
|
|
||||||
|
emit('update:modelValue', fileList);
|
||||||
|
emit('delete', item, getDetail(index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPreviewMask = (item) => {
|
const renderPreviewItem = (item, index) => (
|
||||||
const { status, message } = item;
|
<PreviewItem
|
||||||
|
v-slots={{ 'preview-cover': slots['preview-cover'] }}
|
||||||
if (status === 'uploading' || status === 'failed') {
|
item={item}
|
||||||
const MaskIcon =
|
onClick={() => {
|
||||||
status === 'failed' ? (
|
emit('click-preview', item, getDetail(index));
|
||||||
<Icon name="close" class={bem('mask-icon')} />
|
}}
|
||||||
) : (
|
onDelete={() => {
|
||||||
<Loading class={bem('loading')} />
|
deleteFile(item, index);
|
||||||
);
|
}}
|
||||||
|
onPreview={() => {
|
||||||
const showMessage = isDef(message) && message !== '';
|
previewImage(item);
|
||||||
|
}}
|
||||||
return (
|
{...pick(props, [
|
||||||
<div class={bem('mask')}>
|
'name',
|
||||||
{MaskIcon}
|
'lazyLoad',
|
||||||
{showMessage && <div class={bem('mask-message')}>{message}</div>}
|
'imageFit',
|
||||||
</div>
|
'deletable',
|
||||||
);
|
'previewSize',
|
||||||
}
|
'beforeDelete',
|
||||||
};
|
])}
|
||||||
|
/>
|
||||||
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 renderPreviewList = () => {
|
const renderPreviewList = () => {
|
||||||
if (props.previewImage) {
|
if (props.previewImage) {
|
||||||
@ -392,6 +320,12 @@ export default createComponent({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const chooseFile = () => {
|
||||||
|
if (inputRef.value && !props.disabled) {
|
||||||
|
inputRef.value.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
usePublicApi({
|
usePublicApi({
|
||||||
chooseFile,
|
chooseFile,
|
||||||
closeImagePreview,
|
closeImagePreview,
|
||||||
|
5
src/uploader/shared.js
Normal file
5
src/uploader/shared.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createNamespace } from '../utils';
|
||||||
|
|
||||||
|
const [createComponent, bem] = createNamespace('uploader');
|
||||||
|
|
||||||
|
export { bem, createComponent };
|
@ -30,10 +30,10 @@ export function readFileContent(file: File, resultType: ResultType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isOversize(
|
export function isOversize(
|
||||||
files: File | File[],
|
items: FileListItem | FileListItem[],
|
||||||
maxSize: number | string
|
maxSize: number | string
|
||||||
): boolean {
|
): boolean {
|
||||||
return toArray(files).some((file) => file.size > maxSize);
|
return toArray(items).some((item) => item.file && item.file.size > maxSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileListItem = {
|
export type FileListItem = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user