feat: Form增加ImageUpload组件 (#3172)

* feat: Form增加图片上传组件

* fix: 还原表单组件引用

* chore: ImageUpload demo

* chore: update ImageUpload demo

* fix: 'visible' will be removed in next major version

* chore: 修改api接口返回值参数

---------

Co-authored-by: Li Kui <90845831+likui628@users.noreply.github.com>
This commit is contained in:
1455668754 2023-10-21 19:29:53 +08:00 committed by GitHub
parent e4bcf8c899
commit b776ac4cd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 276 additions and 9 deletions

View File

@ -13,5 +13,6 @@ export { default as ApiTree } from './src/components/ApiTree.vue';
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export { default as ApiCascader } from './src/components/ApiCascader.vue'; export { default as ApiCascader } from './src/components/ApiCascader.vue';
export { default as ApiTransfer } from './src/components/ApiTransfer.vue'; export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
export { default as ImageUpload } from './src/components/ImageUpload.vue';
export { BasicForm }; export { BasicForm };

View File

@ -5,22 +5,21 @@ import type { ComponentType } from './types/index';
* Component list, register here to setting it in the form * Component list, register here to setting it in the form
*/ */
import { import {
Input,
Select,
Radio,
Checkbox,
AutoComplete, AutoComplete,
Cascader, Cascader,
Checkbox,
DatePicker, DatePicker,
Divider,
Input,
InputNumber, InputNumber,
Radio,
Rate,
Select,
Slider,
Switch, Switch,
TimePicker, TimePicker,
TreeSelect, TreeSelect,
Slider,
Rate,
Divider,
} from 'ant-design-vue'; } from 'ant-design-vue';
import ApiRadioGroup from './components/ApiRadioGroup.vue'; import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue'; import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue'; import ApiSelect from './components/ApiSelect.vue';
@ -28,6 +27,7 @@ import ApiTree from './components/ApiTree.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue'; import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue'; import ApiCascader from './components/ApiCascader.vue';
import ApiTransfer from './components/ApiTransfer.vue'; import ApiTransfer from './components/ApiTransfer.vue';
import ImageUpload from './components/ImageUpload.vue';
import { BasicUpload } from '/@/components/Upload'; import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter'; import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon'; import { IconPicker } from '/@/components/Icon';
@ -42,7 +42,7 @@ componentMap.set('InputSearch', Input.Search);
componentMap.set('InputTextArea', Input.TextArea); componentMap.set('InputTextArea', Input.TextArea);
componentMap.set('InputNumber', InputNumber); componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete); componentMap.set('AutoComplete', AutoComplete);
componentMap.set('ImageUpload', ImageUpload);
componentMap.set('Select', Select); componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect); componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTree', ApiTree); componentMap.set('ApiTree', ApiTree);

View File

@ -0,0 +1,253 @@
<template>
<div class="clearfix">
<a-upload
v-model:file-list="fileList"
:list-type="listType"
:multiple="multiple"
:max-count="maxCount"
:customRequest="handleCustomRequest"
:before-upload="handleBeforeUpload"
v-bind="$attrs"
@preview="handlePreview"
v-model:value="state"
>
<div v-if="fileList.length < maxCount">
<plus-outlined />
<div style="margin-top: 8px">
{{ t('component.upload.upload') }}
</div>
</div>
</a-upload>
<a-modal :open="previewOpen" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, reactive, ref, watch } from 'vue';
import { message, Modal, Upload, UploadProps } from 'ant-design-vue';
import { UploadFile } from 'ant-design-vue/lib/upload/interface';
import { useI18n } from '@/hooks/web/useI18n';
import { join } from 'lodash-es';
import { buildShortUUID } from '@/utils/uuid';
import { isArray, isNotEmpty, isUrl } from '@/utils/is';
import { useRuleFormItem } from '@/hooks/component/useFormItem';
import { useAttrs } from '@vben/hooks';
import { PlusOutlined } from '@ant-design/icons-vue';
type ImageUploadType = 'text' | 'picture' | 'picture-card';
export default defineComponent({
name: 'ImageUpload',
components: {
PlusOutlined,
AUpload: Upload,
AModal: Modal,
},
inheritAttrs: false,
props: {
value: [Array, String],
api: {
type: Function as PropType<(file: UploadFile) => Promise<string>>,
default: null,
},
listType: {
type: String as PropType<ImageUploadType>,
default: () => 'picture-card',
},
//
fileType: {
type: Array,
default: () => ['image/png', 'image/jpeg'],
},
multiple: {
type: Boolean,
default: () => false,
},
//
maxCount: {
type: Number,
default: () => 1,
},
//
minCount: {
type: Number,
default: () => 0,
},
// MB
maxSize: {
type: Number,
default: () => 2,
},
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const attrs = useAttrs();
const { t } = useI18n();
const previewOpen = ref(false);
const previewImage = ref('');
const emitData = ref<any[] | any | undefined>();
const fileList = ref<UploadFile[]>([]);
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
const fileState = reactive<{
newList: any[];
newStr: string;
oldStr: string;
}>({
newList: [],
newStr: '',
oldStr: '',
});
watch(
() => fileList.value,
(v) => {
fileState.newList = v
.filter((item: any) => {
return item?.url && item.status === 'done' && isUrl(item?.url);
})
.map((item: any) => item?.url);
fileState.newStr = join(fileState.newList);
//
if (fileState.newStr !== fileState.oldStr) {
fileState.oldStr = fileState.newStr;
emitData.value = props.multiple ? fileState.newList : fileState.newStr;
state.value = props.multiple ? fileState.newList : fileState.newStr;
}
},
{
deep: true,
},
);
watch(
() => state.value,
(v) => {
changeFileValue(v);
emit('update:value', v);
},
);
function changeFileValue(value: any) {
const stateStr = props.multiple ? join((value as string[]) || []) : value || '';
if (stateStr !== fileState.oldStr) {
fileState.oldStr = stateStr;
let list: string[] = [];
if (props.multiple) {
if (isNotEmpty(value)) {
if (isArray(value)) {
list = value as string[];
} else {
list.push(value as string);
}
}
} else {
if (isNotEmpty(value)) {
list.push(value as string);
}
}
fileList.value = list.map((item) => {
const uuid = buildShortUUID();
return {
uid: uuid,
name: uuid,
status: 'done',
url: item,
};
});
}
}
/** 关闭查看 */
const handleCancel = () => {
previewOpen.value = false;
};
/** 查看图片 */
// @ts-ignore
const handlePreview = async (file: UploadProps['fileList'][number]) => {
if (!file.url && !file.preview) {
file.preview = (await getBase64(file.originFileObj)) as string;
}
previewImage.value = file.url || file.preview;
previewOpen.value = true;
};
/** 上传前校验 */
const handleBeforeUpload: UploadProps['beforeUpload'] = (file) => {
if (fileList.value.length > props.maxCount) {
fileList.value.splice(props.maxCount, fileList.value.length - props.maxCount);
message.error(t('component.upload.maxNumber', [props.maxCount]));
return Upload.LIST_IGNORE;
}
const isPNG = props.fileType.includes(file.type);
if (!isPNG) {
message.error(t('component.upload.acceptUpload', [props.fileType.toString()]));
}
const isLt2M = file.size / 1024 / 1024 < props.maxSize;
if (!isLt2M) {
message.error(t('component.upload.maxSizeMultiple', [props.maxSize]));
}
if (!(isPNG && isLt2M)) {
fileList.value.pop();
}
return (isPNG && isLt2M) || Upload.LIST_IGNORE;
};
/** 自定义上传 */
const handleCustomRequest = async (option: any) => {
const { file } = option;
await props
.api(option)
.then((url) => {
file.url = url;
file.status = 'done';
fileList.value.pop();
fileList.value.push(file);
})
.catch(() => {
fileList.value.pop();
});
};
function getBase64(file: File) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}
return {
previewOpen,
fileList,
state,
attrs,
t,
handlePreview,
handleBeforeUpload,
handleCustomRequest,
handleCancel,
previewImage,
};
},
});
</script>
<style scoped>
/* you can make up upload button and sample style by using stylesheets */
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

View File

@ -86,5 +86,6 @@ export const NO_AUTO_LINK_COMPONENTS: ComponentType[] = [
'ApiCascader', 'ApiCascader',
'AutoComplete', 'AutoComplete',
'RadioButtonGroup', 'RadioButtonGroup',
'ImageUpload',
'ApiSelect', 'ApiSelect',
]; ];

View File

@ -110,6 +110,7 @@ export type ComponentType =
| 'Switch' | 'Switch'
| 'StrengthMeter' | 'StrengthMeter'
| 'Upload' | 'Upload'
| 'ImageUpload'
| 'IconPicker' | 'IconPicker'
| 'Render' | 'Render'
| 'Slider' | 'Slider'

View File

@ -697,6 +697,17 @@
allowHalf: true, allowHalf: true,
}, },
}, },
{
field: 'field23',
component: 'ImageUpload',
label: '字段23',
colProps: {
span: 8,
},
componentProps: {
api: () => Promise.resolve('https://via.placeholder.com/600/92c952'),
},
},
]; ];
export default defineComponent({ export default defineComponent({