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