mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-05 19:41:42 +08:00
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:
parent
b275e1867d
commit
b94c8c3333
@ -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 |
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ export type UploaderFileListItem = {
|
||||
message?: string;
|
||||
imageFit?: ImageFit;
|
||||
deletable?: boolean;
|
||||
reupload?: boolean;
|
||||
previewSize?: Numeric;
|
||||
beforeDelete?: Interceptor;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user