From b776ac4cd8a4895804b554949f39c82c03382006 Mon Sep 17 00:00:00 2001 From: 1455668754 <88491446+1455668754@users.noreply.github.com> Date: 星期六, 21 十月 2023 19:29:53 +0800 Subject: [PATCH] feat: Form增加ImageUpload组件 (#3172) --- src/components/Form/index.ts | 1 src/components/Form/src/types/index.ts | 1 src/components/Form/src/components/ImageUpload.vue | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/components/Form/src/helper.ts | 1 src/views/demo/form/index.vue | 11 ++ src/components/Form/src/componentMap.ts | 18 +- 6 files changed, 276 insertions(+), 9 deletions(-) diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index d85b3c5..d3be25e 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -13,5 +13,6 @@ 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 }; diff --git a/src/components/Form/src/componentMap.ts b/src/components/Form/src/componentMap.ts index b448ace..f33a589 100644 --- a/src/components/Form/src/componentMap.ts +++ b/src/components/Form/src/componentMap.ts @@ -5,22 +5,21 @@ * 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'; @@ -28,6 +27,7 @@ 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'; @@ -42,7 +42,7 @@ 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); diff --git a/src/components/Form/src/components/ImageUpload.vue b/src/components/Form/src/components/ImageUpload.vue new file mode 100644 index 0000000..22fec05 --- /dev/null +++ b/src/components/Form/src/components/ImageUpload.vue @@ -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, + }, + // 鏂囦欢鏈�澶у灏慚B + 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> diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts index 66505f5..3e01365 100644 --- a/src/components/Form/src/helper.ts +++ b/src/components/Form/src/helper.ts @@ -86,5 +86,6 @@ 'ApiCascader', 'AutoComplete', 'RadioButtonGroup', + 'ImageUpload', 'ApiSelect', ]; diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index 55f10de..79e4aaa 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -110,6 +110,7 @@ | 'Switch' | 'StrengthMeter' | 'Upload' + | 'ImageUpload' | 'IconPicker' | 'Render' | 'Slider' diff --git a/src/views/demo/form/index.vue b/src/views/demo/form/index.vue index 89fd096..fc58cc2 100644 --- a/src/views/demo/form/index.vue +++ b/src/views/demo/form/index.vue @@ -697,6 +697,17 @@ allowHalf: true, }, }, + { + field: 'field23', + component: 'ImageUpload', + label: '瀛楁23', + colProps: { + span: 8, + }, + componentProps: { + api: () => Promise.resolve('https://via.placeholder.com/600/92c952'), + }, + }, ]; export default defineComponent({ -- Gitblit v1.8.0