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>
| | |
| | | export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; |
| | | export { default as ApiCascader } from './src/components/ApiCascader.vue'; |
| | | export { default as ApiTransfer } from './src/components/ApiTransfer.vue'; |
| | | export { default as ImageUpload } from './src/components/ImageUpload.vue'; |
| | | |
| | | export { BasicForm }; |
| | |
| | | * Component list, register here to setting it in the form |
| | | */ |
| | | import { |
| | | Input, |
| | | Select, |
| | | Radio, |
| | | Checkbox, |
| | | AutoComplete, |
| | | Cascader, |
| | | Checkbox, |
| | | DatePicker, |
| | | Divider, |
| | | Input, |
| | | InputNumber, |
| | | Radio, |
| | | Rate, |
| | | Select, |
| | | Slider, |
| | | Switch, |
| | | TimePicker, |
| | | TreeSelect, |
| | | Slider, |
| | | Rate, |
| | | Divider, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import ApiRadioGroup from './components/ApiRadioGroup.vue'; |
| | | import RadioButtonGroup from './components/RadioButtonGroup.vue'; |
| | | import ApiSelect from './components/ApiSelect.vue'; |
| | |
| | | import ApiTreeSelect from './components/ApiTreeSelect.vue'; |
| | | import ApiCascader from './components/ApiCascader.vue'; |
| | | import ApiTransfer from './components/ApiTransfer.vue'; |
| | | import ImageUpload from './components/ImageUpload.vue'; |
| | | import { BasicUpload } from '/@/components/Upload'; |
| | | import { StrengthMeter } from '/@/components/StrengthMeter'; |
| | | import { IconPicker } from '/@/components/Icon'; |
| | |
| | | componentMap.set('InputTextArea', Input.TextArea); |
| | | componentMap.set('InputNumber', InputNumber); |
| | | componentMap.set('AutoComplete', AutoComplete); |
| | | |
| | | componentMap.set('ImageUpload', ImageUpload); |
| | | componentMap.set('Select', Select); |
| | | componentMap.set('ApiSelect', ApiSelect); |
| | | componentMap.set('ApiTree', ApiTree); |
New file |
| | |
| | | <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> |
| | |
| | | 'ApiCascader', |
| | | 'AutoComplete', |
| | | 'RadioButtonGroup', |
| | | 'ImageUpload', |
| | | 'ApiSelect', |
| | | ]; |
| | |
| | | | 'Switch' |
| | | | 'StrengthMeter' |
| | | | 'Upload' |
| | | | 'ImageUpload' |
| | | | 'IconPicker' |
| | | | 'Render' |
| | | | 'Slider' |
| | |
| | | allowHalf: true, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'field23', |
| | | component: 'ImageUpload', |
| | | label: '字段23', |
| | | colProps: { |
| | | span: 8, |
| | | }, |
| | | componentProps: { |
| | | api: () => Promise.resolve('https://via.placeholder.com/600/92c952'), |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | export default defineComponent({ |