feat(Uploader): add reupload (#11854)

* feat(Uploader): add reupload

* feat(Uploader): add reupload

* Update packages/vant/src/uploader/README.md

Co-authored-by: neverland <jait.chen@foxmail.com>

* Update packages/vant/src/uploader/README.zh-CN.md

Co-authored-by: neverland <jait.chen@foxmail.com>

---------

Co-authored-by: neverland <jait.chen@foxmail.com>
This commit is contained in:
Zhousg 2023-05-20 19:15:29 +08:00 committed by GitHub
parent b275e1867d
commit b94c8c3333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 214 additions and 17 deletions

View File

@ -298,6 +298,26 @@ export default {
};
```
### Open Reupload
```html
<van-uploader v-model="fileList" reupload max-count="2" />
```
```ts
import { ref } from 'vue';
export default {
setup() {
const fileList = ref([
{ url: 'https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg' },
]);
return { fileList };
},
};
```
## API
### Props
@ -315,6 +335,7 @@ export default {
| disabled | Whether to disabled the upload | _boolean_ | `false` |
| readonly | Whether to make upload area readonly | _boolean_ | `false` |
| deletable | Whether to show delete icon | _boolean_ | `true` |
| reupload | Whether to enable reupload, if enabled, the image preview will be disabled | _boolean_ | `false` |
| show-upload | Whether to show upload area | _boolean_ | `true` |
| lazy-load | Whether to enable lazy load, should register [Lazyload](#/en-US/lazyload) component | _boolean_ | `false` |
| capture | Capture, can be set to `camera` | _string_ | - |
@ -337,6 +358,7 @@ export default {
| oversize | Emitted when file size over limit | Same as after-read |
| click-upload | Emitted when click upload area | _event: MouseEvent_ |
| click-preview | Emitted when preview image is clicked | Same as after-read |
| click-reupload | Emitted when reupload image is clicked | Same as after-read |
| close-preview | Emitted when the full screen image preview is closed | - |
| delete | Emitted when preview file is deleted | Same as after-read |

View File

@ -317,6 +317,26 @@ export default {
};
```
### 开启覆盖上传
```html
<van-uploader v-model="fileList" reupload max-count="2" />
```
```ts
import { ref } from 'vue';
export default {
setup() {
const fileList = ref([
{ url: 'https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg' },
]);
return { fileList };
},
};
```
## API
### Props
@ -334,6 +354,7 @@ export default {
| disabled | 是否禁用文件上传 | _boolean_ | `false` |
| readonly | 是否将上传区域设置为只读状态 | _boolean_ | `false` |
| deletable | 是否展示删除按钮 | _boolean_ | `true` |
| reupload | 是否开启覆盖上传,开启后会关闭图片预览 | _boolean_ | `false` |
| show-upload | 是否展示上传区域 | _boolean_ | `true` |
| lazy-load | 是否开启图片懒加载,须配合 [Lazyload](#/zh-CN/lazyload) 组件使用 | _boolean_ | `false` |
| capture | 图片选取模式,可选值为 `camera` (直接调起摄像头) | _string_ | - |
@ -351,13 +372,14 @@ export default {
### Events
| 事件名 | 说明 | 回调参数 |
| ------------- | ---------------------- | ------------------- |
| oversize | 文件大小超过限制时触发 | 同 `after-read` |
| click-upload | 点击上传区域时触发 | _event: MouseEvent_ |
| click-preview | 点击预览图时触发 | 同 `after-read` |
| close-preview | 关闭全屏图片预览时触发 | - |
| delete | 删除文件预览时触发 | 同 `after-read` |
| 事件名 | 说明 | 回调参数 |
| -------------- | ---------------------- | ------------------- |
| oversize | 文件大小超过限制时触发 | 同 `after-read` |
| click-upload | 点击上传区域时触发 | _event: MouseEvent_ |
| click-preview | 点击预览图时触发 | 同 `after-read` |
| click-reupload | 点击覆盖上传时触发 | 同 `after-read` |
| close-preview | 关闭全屏图片预览时触发 | - |
| delete | 删除文件预览时触发 | 同 `after-read` |
### Slots

View File

@ -65,6 +65,7 @@ export const uploaderProps = {
uploadIcon: makeStringProp('photograph'),
uploadText: String,
deletable: truthProp,
reupload: Boolean,
afterRead: Function as PropType<UploaderAfterRead>,
showUpload: truthProp,
modelValue: makeArrayProp<UploaderFileListItem>(),
@ -95,12 +96,14 @@ export default defineComponent({
'clickUpload',
'closePreview',
'clickPreview',
'clickReupload',
'update:modelValue',
],
setup(props, { emit, slots }) {
const inputRef = ref();
const urls: string[] = [];
const reuploadIndex = ref(-1);
const getDetail = (index = props.modelValue.length) => ({
name: props.name,
@ -133,7 +136,14 @@ export default defineComponent({
}
}
items = reactive(items);
emit('update:modelValue', [...props.modelValue, ...toArray(items)]);
if (reuploadIndex.value > -1) {
const arr = [...props.modelValue];
arr.splice(reuploadIndex.value, 1, items as UploaderFileListItem);
emit('update:modelValue', arr);
reuploadIndex.value = -1;
} else {
emit('update:modelValue', [...props.modelValue, ...toArray(items)]);
}
if (props.afterRead) {
props.afterRead(items, getDetail());
@ -265,10 +275,17 @@ export default defineComponent({
emit('delete', item, getDetail(index));
};
const reuploadImage = (index: number) => {
// eslint-disable-next-line no-use-before-define
chooseFile();
reuploadIndex.value = index;
};
const renderPreviewItem = (item: UploaderFileListItem, index: number) => {
const needPickData = [
'imageFit',
'deletable',
'reupload',
'previewSize',
'beforeDelete',
] as const;
@ -283,9 +300,16 @@ export default defineComponent({
v-slots={pick(slots, ['preview-cover', 'preview-delete'])}
item={item}
index={index}
onClick={() => emit('clickPreview', item, getDetail(index))}
onClick={() =>
emit(
props.reupload ? 'clickReupload' : 'clickPreview',
item,
getDetail(index)
)
}
onDelete={() => deleteFile(item, index)}
onPreview={() => previewImage(item)}
onReupload={() => reuploadImage(index)}
{...pick(props, ['name', 'lazyLoad'])}
{...previewData}
/>
@ -301,10 +325,13 @@ export default defineComponent({
const onClickUpload = (event: MouseEvent) => emit('clickUpload', event);
const renderUpload = () => {
if (props.modelValue.length >= +props.maxCount) {
if (props.modelValue.length >= +props.maxCount && !props.reupload) {
return;
}
const hideUploader =
props.modelValue.length >= +props.maxCount && props.reupload;
const Input = props.readonly ? null : (
<input
ref={inputRef}
@ -312,7 +339,7 @@ export default defineComponent({
class={bem('input')}
accept={props.accept}
capture={props.capture as unknown as boolean}
multiple={props.multiple}
multiple={props.multiple && reuploadIndex.value === -1}
disabled={props.disabled}
onChange={onChange}
/>
@ -320,7 +347,11 @@ export default defineComponent({
if (slots.default) {
return (
<div class={bem('input-wrapper')} onClick={onClickUpload}>
<div
v-show={!hideUploader}
class={bem('input-wrapper')}
onClick={onClickUpload}
>
{slots.default()}
{Input}
</div>
@ -329,7 +360,7 @@ export default defineComponent({
return (
<div
v-show={props.showUpload}
v-show={props.showUpload && !hideUploader}
class={bem('upload', { readonly: props.readonly })}
style={getSizeStyle(props.previewSize)}
onClick={onClickUpload}

View File

@ -29,13 +29,14 @@ export default defineComponent({
imageFit: String as PropType<ImageFit>,
lazyLoad: Boolean,
deletable: Boolean,
reupload: Boolean,
previewSize: [Number, String, Array] as PropType<
Numeric | [Numeric, Numeric]
>,
beforeDelete: Function as PropType<Interceptor>,
},
emits: ['delete', 'preview'],
emits: ['delete', 'preview', 'reupload'],
setup(props, { emit, slots }) {
const renderMask = () => {
@ -71,6 +72,8 @@ export default defineComponent({
const onPreview = () => emit('preview');
const onReupload = () => emit('reupload');
const renderDeleteIcon = () => {
if (props.deletable && props.item.status !== 'uploading') {
const slot = slots['preview-delete'];
@ -104,7 +107,7 @@ export default defineComponent({
};
const renderPreview = () => {
const { item, lazyLoad, imageFit, previewSize } = props;
const { item, lazyLoad, imageFit, previewSize, reupload } = props;
if (isImageFile(item)) {
return (
@ -116,7 +119,7 @@ export default defineComponent({
width={Array.isArray(previewSize) ? previewSize[0] : previewSize}
height={Array.isArray(previewSize) ? previewSize[1] : previewSize}
lazyLoad={lazyLoad}
onClick={onPreview}
onClick={reupload ? onReupload : onPreview}
/>
);
}

View File

@ -25,6 +25,7 @@ const t = useTranslate({
previewCover: '自定义预览样式',
deleteMessage: '删除前置处理',
customPreviewImage: '自定义单个图片预览',
reupload: '开启覆盖上传',
},
'en-US': {
status: 'Upload Status',
@ -44,6 +45,7 @@ const t = useTranslate({
previewCover: 'Preview Cover',
deleteMessage: 'Before Delete',
customPreviewImage: 'Custom single preview image',
reupload: 'Open Reupload',
},
});
@ -142,6 +144,8 @@ const onOversize = (file: UploaderFileListItem, detail: unknown) => {
console.log(file, detail);
showToast(t('overSizeTip'));
};
const fileList6 = ref([{ url: cdnURL('leaf.jpeg') }]);
</script>
<template>
@ -201,6 +205,10 @@ const onOversize = (file: UploaderFileListItem, detail: unknown) => {
<demo-block :title="t('customPreviewImage')">
<van-uploader v-model="fileList5" multiple accept="*" :deletable="false" />
</demo-block>
<demo-block :title="t('reupload')">
<van-uploader v-model="fileList6" reupload max-count="2" />
</demo-block>
</template>
<style lang="less">

View File

@ -556,4 +556,54 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div>
<!--[-->
<div class="van-uploader">
<div class="van-uploader__wrapper">
<!--[-->
<div class="van-uploader__preview">
<div class="van-image van-uploader__preview-image"
style
>
<img src="https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg"
class="van-image__img"
style="object-fit:cover;"
>
<div class="van-image__loading">
<i class="van-badge__wrapper van-icon van-icon-photo van-image__loading-icon"
style
>
<!--[-->
</i>
</div>
<!--[-->
</div>
<div role="button"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
<i class="van-badge__wrapper van-icon van-icon-cross van-uploader__preview-delete-icon"
style
>
<!--[-->
</i>
</div>
</div>
<div class="van-uploader__upload"
style
>
<i class="van-badge__wrapper van-icon van-icon-photograph van-uploader__upload-icon"
style
>
<!--[-->
</i>
<input type="file"
class="van-uploader__input"
accept="image/*"
>
</div>
</div>
</div>
</div>
`;

View File

@ -391,4 +391,38 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div>
<div class="van-uploader">
<div class="van-uploader__wrapper">
<div class="van-uploader__preview">
<div class="van-image van-uploader__preview-image">
<img src="https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg"
class="van-image__img"
style="object-fit: cover;"
>
<div class="van-image__loading">
<i class="van-badge__wrapper van-icon van-icon-photo van-image__loading-icon">
</i>
</div>
</div>
<div role="button"
class="van-uploader__preview-delete van-uploader__preview-delete--shadow"
tabindex="0"
aria-label="Delete"
>
<i class="van-badge__wrapper van-icon van-icon-cross van-uploader__preview-delete-icon">
</i>
</div>
</div>
<div class="van-uploader__upload">
<i class="van-badge__wrapper van-icon van-icon-photograph van-uploader__upload-icon">
</i>
<input type="file"
class="van-uploader__input"
accept="image/*"
>
</div>
</div>
</div>
</div>
`;

View File

@ -1,7 +1,7 @@
import { nextTick } from 'vue';
import { cdnURL } from '../../../docs/site';
import Uploader, { type UploaderFileListItem } from '..';
import { mount, later, triggerDrag } from '../../../test';
import { mount, later, triggerDrag, trigger } from '../../../test';
import type { Numeric } from '../../utils';
const mockFileDataUrl = 'data:image/test';
@ -670,3 +670,29 @@ test('should emit clickUpload event when upload area is clicked', async () => {
wrapper.find('.van-uploader__upload').trigger('click');
expect(wrapper.emitted('clickUpload')).toBeTruthy();
});
test('should emit clickReUpload event when props reupload true', async () => {
const wrapper = mount(Uploader, {
props: {
maxCount: 1,
modelValue: [{ url: IMAGE }],
},
});
expect(wrapper.find('.van-uploader__upload').exists()).toBeFalsy();
await wrapper.setProps({ reupload: true });
expect(wrapper.find('.van-uploader__upload').style.display).toBe('none');
const previewItem = wrapper.find<HTMLDivElement>(
'.van-uploader__preview-image'
);
await trigger(previewItem, 'click');
expect(wrapper.emitted('clickReupload')).toBeTruthy();
const input = wrapper.find<HTMLInputElement>('.van-uploader__input');
Object.defineProperty(input.element, 'files', {
get: jest.fn().mockReturnValue([mockFile]),
});
await trigger(input, 'change');
expect(wrapper.emitted('update:modelValue')?.[0][0]).toHaveLength(1);
});

View File

@ -14,6 +14,7 @@ export type UploaderFileListItem = {
message?: string;
imageFit?: ImageFit;
deletable?: boolean;
reupload?: boolean;
previewSize?: Numeric;
beforeDelete?: Interceptor;
};