Quellcode durchsuchen

feat(upload prop:maxSize): from Upload component accept prop maxSize (AI prompt fixed) (#7059)

* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize

* feat(upload prop:maxSize): from component accept prop maxSize
JyQAQ vor 5 Monaten
Ursprung
Commit
81a61558cb

+ 155 - 144
apps/web-antd/src/adapter/component/index.ts

@@ -29,7 +29,7 @@ import { IconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
 import { isEmpty } from '@vben/utils';
 
-import { notification } from 'ant-design-vue';
+import { message, notification } from 'ant-design-vue';
 
 const AutoComplete = defineAsyncComponent(
   () => import('ant-design-vue/es/auto-complete'),
@@ -119,9 +119,149 @@ const withDefaultPlaceholder = <T extends Component>(
 };
 
 const withPreviewUpload = () => {
+  // 创建默认的上传按钮插槽
+  const createDefaultSlotsWithUpload = (
+    listType: string,
+    placeholder: string,
+  ) => {
+    switch (listType) {
+      case 'picture-card': {
+        return {
+          default: () => placeholder,
+        };
+      }
+      default: {
+        return {
+          default: () =>
+            h(
+              Button,
+              {
+                icon: h(IconifyIcon, {
+                  icon: 'ant-design:upload-outlined',
+                  class: 'mb-1 size-4',
+                }),
+              },
+              () => placeholder,
+            ),
+        };
+      }
+    }
+  };
+  // 构建预览图片组
+  const previewImage = async (
+    file: UploadFile,
+    visible: Ref<boolean>,
+    fileList: Ref<UploadProps['fileList']>,
+  ) => {
+    // 检查是否为图片文件的辅助函数
+    const isImageFile = (file: UploadFile): boolean => {
+      const imageExtensions = new Set([
+        'bmp',
+        'gif',
+        'jpeg',
+        'jpg',
+        'png',
+        'webp',
+      ]);
+      if (file.url) {
+        const ext = file.url?.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      }
+      if (!file.type) {
+        const ext = file.name?.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      }
+      return file.type.startsWith('image/');
+    };
+
+    // 如果当前文件不是图片,直接打开
+    if (!isImageFile(file)) {
+      if (file.url) {
+        window.open(file.url, '_blank');
+      } else if (file.preview) {
+        window.open(file.preview, '_blank');
+      } else {
+        message.error($t('ui.formRules.previewWarning'));
+      }
+      return;
+    }
+
+    // 对于图片文件,继续使用预览组
+    const [ImageComponent, PreviewGroupComponent] = await Promise.all([
+      Image,
+      PreviewGroup,
+    ]);
+
+    const getBase64 = (file: File) => {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.addEventListener('load', () => resolve(reader.result));
+        reader.addEventListener('error', (error) => reject(error));
+      });
+    };
+    // 从fileList中过滤出所有图片文件
+    const imageFiles = (unref(fileList) || []).filter((element) =>
+      isImageFile(element),
+    );
+
+    // 为所有没有预览地址的图片生成预览
+    for (const imgFile of imageFiles) {
+      if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
+        imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
+      }
+    }
+    const container: HTMLElement | null = document.createElement('div');
+    document.body.append(container);
+
+    // 用于追踪组件是否已卸载
+    let isUnmounted = false;
+
+    const PreviewWrapper = {
+      setup() {
+        return () => {
+          if (isUnmounted) return null;
+          return h(
+            PreviewGroupComponent,
+            {
+              class: 'hidden',
+              preview: {
+                visible: visible.value,
+                // 设置初始显示的图片索引
+                current: imageFiles.findIndex((f) => f.uid === file.uid),
+                onVisibleChange: (value: boolean) => {
+                  visible.value = value;
+                  if (!value) {
+                    // 延迟清理,确保动画完成
+                    setTimeout(() => {
+                      if (!isUnmounted && container) {
+                        isUnmounted = true;
+                        render(null, container);
+                        container.remove();
+                      }
+                    }, 300);
+                  }
+                },
+              },
+            },
+            () =>
+              // 渲染所有图片文件
+              imageFiles.map((imgFile) =>
+                h(ImageComponent, {
+                  key: imgFile.uid,
+                  src: imgFile.url || imgFile.preview,
+                }),
+              ),
+          );
+        };
+      },
+    };
+
+    render(h(PreviewWrapper), container);
+  };
   return defineComponent({
     name: Upload.name,
-    emits: ['change', 'update:modelValue'],
+    emits: ['update:modelValue'],
     setup: (
       props: any,
       { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
@@ -136,9 +276,19 @@ const withPreviewUpload = () => {
         attrs?.fileList || attrs?.['file-list'] || [],
       );
 
+      const handleBeforeUpload = (file: UploadFile) => {
+        if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
+          message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
+          file.status = 'removed';
+          return false;
+        }
+        return attrs.beforeUpload?.(file) ?? true;
+      };
+
       const handleChange = async (event: UploadChangeParam) => {
-        fileList.value = event.fileList;
-        emit('change', event);
+        fileList.value = event.fileList.filter(
+          (file) => file.status !== 'removed',
+        );
         emit(
           'update:modelValue',
           event.fileList?.length ? fileList.value : undefined,
@@ -179,6 +329,7 @@ const withPreviewUpload = () => {
             ...props,
             ...attrs,
             fileList: fileList.value,
+            beforeUpload: handleBeforeUpload,
             onChange: handleChange,
             onPreview: handlePreview,
           },
@@ -188,146 +339,6 @@ const withPreviewUpload = () => {
   });
 };
 
-const createDefaultSlotsWithUpload = (
-  listType: string,
-  placeholder: string,
-) => {
-  switch (listType) {
-    case 'picture-card': {
-      return {
-        default: () => placeholder,
-      };
-    }
-    default: {
-      return {
-        default: () =>
-          h(
-            Button,
-            {
-              icon: h(IconifyIcon, {
-                icon: 'ant-design:upload-outlined',
-                class: 'mb-1 size-4',
-              }),
-            },
-            () => placeholder,
-          ),
-      };
-    }
-  }
-};
-
-const previewImage = async (
-  file: UploadFile,
-  visible: Ref<boolean>,
-  fileList: Ref<UploadProps['fileList']>,
-) => {
-  // 检查是否为图片文件的辅助函数
-  const isImageFile = (file: UploadFile): boolean => {
-    const imageExtensions = new Set([
-      'bmp',
-      'gif',
-      'jpeg',
-      'jpg',
-      'png',
-      'webp',
-    ]);
-    if (file.url) {
-      const ext = file.url?.split('.').pop()?.toLowerCase();
-      return ext ? imageExtensions.has(ext) : false;
-    }
-    if (!file.type) {
-      const ext = file.name?.split('.').pop()?.toLowerCase();
-      return ext ? imageExtensions.has(ext) : false;
-    }
-    return file.type.startsWith('image/');
-  };
-
-  // 如果当前文件不是图片,直接打开
-  if (!isImageFile(file)) {
-    if (file.url) {
-      window.open(file.url, '_blank');
-    } else if (file.preview) {
-      window.open(file.preview, '_blank');
-    } else {
-      console.warn('无法打开文件,没有可用的URL或预览地址');
-    }
-    return;
-  }
-
-  // 对于图片文件,继续使用预览组
-  const [ImageComponent, PreviewGroupComponent] = await Promise.all([
-    Image,
-    PreviewGroup,
-  ]);
-
-  const getBase64 = (file: File) => {
-    return new Promise((resolve, reject) => {
-      const reader = new FileReader();
-      reader.readAsDataURL(file);
-      reader.addEventListener('load', () => resolve(reader.result));
-      reader.addEventListener('error', (error) => reject(error));
-    });
-  };
-  // 从fileList中过滤出所有图片文件
-  const imageFiles = (unref(fileList) || []).filter((element) =>
-    isImageFile(element),
-  );
-
-  // 为所有没有预览地址的图片生成预览
-  for (const imgFile of imageFiles) {
-    if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
-      imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
-    }
-  }
-  const container: HTMLElement | null = document.createElement('div');
-  document.body.append(container);
-
-  // 用于追踪组件是否已卸载
-  let isUnmounted = false;
-
-  const PreviewWrapper = {
-    setup() {
-      return () => {
-        if (isUnmounted) return null;
-        return h(
-          PreviewGroupComponent,
-          {
-            class: 'hidden',
-            preview: {
-              visible: visible.value,
-              // 设置初始显示的图片索引
-              current: imageFiles.findIndex((f) => f.uid === file.uid),
-              onVisibleChange: (value: boolean) => {
-                visible.value = value;
-                if (!value) {
-                  // 延迟清理,确保动画完成
-                  setTimeout(() => {
-                    if (!isUnmounted && container) {
-                      isUnmounted = true;
-                      render(null, container);
-                      container.remove();
-                    }
-                  }, 300);
-                }
-              },
-            },
-          },
-          () =>
-            // 渲染所有图片文件
-            imageFiles.map((imgFile) =>
-              h(ImageComponent, {
-                key: imgFile.uid,
-                src: imgFile.url || imgFile.preview,
-              }),
-            ),
-        );
-      };
-    },
-  };
-
-  render(h(PreviewWrapper), container);
-};
-
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiCascader'

+ 5 - 2
packages/locales/src/langs/en-US/ui.json

@@ -7,7 +7,9 @@
     "length": "{0} must be {1} characters long",
     "alreadyExists": "{0} `{1}` already exists",
     "startWith": "{0} must start with `{1}`",
-    "invalidURL": "Please input a valid URL"
+    "invalidURL": "Please input a valid URL",
+    "sizeLimit": "The file size cannot exceed {0}MB",
+    "previewWarning": "Unable to open the file, there is no available URL or preview address"
   },
   "actionTitle": {
     "edit": "Modify {0}",
@@ -24,7 +26,8 @@
   },
   "placeholder": {
     "input": "Please enter",
-    "select": "Please select"
+    "select": "Please select",
+    "upload": "Click to upload"
   },
   "captcha": {
     "title": "Please complete the security verification",

+ 5 - 2
packages/locales/src/langs/zh-CN/ui.json

@@ -7,7 +7,9 @@
     "length": "{0}长度必须为{1}个字符",
     "alreadyExists": "{0} `{1}` 已存在",
     "startWith": "{0}必须以 {1} 开头",
-    "invalidURL": "请输入有效的链接"
+    "invalidURL": "请输入有效的链接",
+    "sizeLimit": "文件大小不能超过 {0}MB",
+    "previewWarning": "无法打开文件,没有可用的URL或预览地址"
   },
   "actionTitle": {
     "edit": "修改{0}",
@@ -24,7 +26,8 @@
   },
   "placeholder": {
     "input": "请输入",
-    "select": "请选择"
+    "select": "请选择",
+    "upload": "点击上传"
   },
   "captcha": {
     "title": "请完成安全验证",

+ 155 - 144
playground/src/adapter/component/index.ts

@@ -29,7 +29,7 @@ import { IconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
 import { isEmpty } from '@vben/utils';
 
-import { notification } from 'ant-design-vue';
+import { message, notification } from 'ant-design-vue';
 
 const AutoComplete = defineAsyncComponent(
   () => import('ant-design-vue/es/auto-complete'),
@@ -128,9 +128,149 @@ const withDefaultPlaceholder = <T extends Component>(
 };
 
 const withPreviewUpload = () => {
+  // 创建默认的上传按钮插槽
+  const createDefaultSlotsWithUpload = (
+    listType: string,
+    placeholder: string,
+  ) => {
+    switch (listType) {
+      case 'picture-card': {
+        return {
+          default: () => placeholder,
+        };
+      }
+      default: {
+        return {
+          default: () =>
+            h(
+              Button,
+              {
+                icon: h(IconifyIcon, {
+                  icon: 'ant-design:upload-outlined',
+                  class: 'mb-1 size-4',
+                }),
+              },
+              () => placeholder,
+            ),
+        };
+      }
+    }
+  };
+  // 构建预览图片组
+  const previewImage = async (
+    file: UploadFile,
+    visible: Ref<boolean>,
+    fileList: Ref<UploadProps['fileList']>,
+  ) => {
+    // 检查是否为图片文件的辅助函数
+    const isImageFile = (file: UploadFile): boolean => {
+      const imageExtensions = new Set([
+        'bmp',
+        'gif',
+        'jpeg',
+        'jpg',
+        'png',
+        'webp',
+      ]);
+      if (file.url) {
+        const ext = file.url?.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      }
+      if (!file.type) {
+        const ext = file.name?.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      }
+      return file.type.startsWith('image/');
+    };
+
+    // 如果当前文件不是图片,直接打开
+    if (!isImageFile(file)) {
+      if (file.url) {
+        window.open(file.url, '_blank');
+      } else if (file.preview) {
+        window.open(file.preview, '_blank');
+      } else {
+        message.error($t('ui.formRules.previewWarning'));
+      }
+      return;
+    }
+
+    // 对于图片文件,继续使用预览组
+    const [ImageComponent, PreviewGroupComponent] = await Promise.all([
+      Image,
+      PreviewGroup,
+    ]);
+
+    const getBase64 = (file: File) => {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.addEventListener('load', () => resolve(reader.result));
+        reader.addEventListener('error', (error) => reject(error));
+      });
+    };
+    // 从fileList中过滤出所有图片文件
+    const imageFiles = (unref(fileList) || []).filter((element) =>
+      isImageFile(element),
+    );
+
+    // 为所有没有预览地址的图片生成预览
+    for (const imgFile of imageFiles) {
+      if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
+        imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
+      }
+    }
+    const container: HTMLElement | null = document.createElement('div');
+    document.body.append(container);
+
+    // 用于追踪组件是否已卸载
+    let isUnmounted = false;
+
+    const PreviewWrapper = {
+      setup() {
+        return () => {
+          if (isUnmounted) return null;
+          return h(
+            PreviewGroupComponent,
+            {
+              class: 'hidden',
+              preview: {
+                visible: visible.value,
+                // 设置初始显示的图片索引
+                current: imageFiles.findIndex((f) => f.uid === file.uid),
+                onVisibleChange: (value: boolean) => {
+                  visible.value = value;
+                  if (!value) {
+                    // 延迟清理,确保动画完成
+                    setTimeout(() => {
+                      if (!isUnmounted && container) {
+                        isUnmounted = true;
+                        render(null, container);
+                        container.remove();
+                      }
+                    }, 300);
+                  }
+                },
+              },
+            },
+            () =>
+              // 渲染所有图片文件
+              imageFiles.map((imgFile) =>
+                h(ImageComponent, {
+                  key: imgFile.uid,
+                  src: imgFile.url || imgFile.preview,
+                }),
+              ),
+          );
+        };
+      },
+    };
+
+    render(h(PreviewWrapper), container);
+  };
   return defineComponent({
     name: Upload.name,
-    emits: ['change', 'update:modelValue'],
+    emits: ['update:modelValue'],
     setup: (
       props: any,
       { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
@@ -145,9 +285,19 @@ const withPreviewUpload = () => {
         attrs?.fileList || attrs?.['file-list'] || [],
       );
 
+      const handleBeforeUpload = (file: UploadFile) => {
+        if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
+          message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
+          file.status = 'removed';
+          return false;
+        }
+        return attrs.beforeUpload?.(file) ?? true;
+      };
+
       const handleChange = async (event: UploadChangeParam) => {
-        fileList.value = event.fileList;
-        emit('change', event);
+        fileList.value = event.fileList.filter(
+          (file) => file.status !== 'removed',
+        );
         emit(
           'update:modelValue',
           event.fileList?.length ? fileList.value : undefined,
@@ -188,6 +338,7 @@ const withPreviewUpload = () => {
             ...props,
             ...attrs,
             fileList: fileList.value,
+            beforeUpload: handleBeforeUpload,
             onChange: handleChange,
             onPreview: handlePreview,
           },
@@ -197,146 +348,6 @@ const withPreviewUpload = () => {
   });
 };
 
-const createDefaultSlotsWithUpload = (
-  listType: string,
-  placeholder: string,
-) => {
-  switch (listType) {
-    case 'picture-card': {
-      return {
-        default: () => placeholder,
-      };
-    }
-    default: {
-      return {
-        default: () =>
-          h(
-            Button,
-            {
-              icon: h(IconifyIcon, {
-                icon: 'ant-design:upload-outlined',
-                class: 'mb-1 size-4',
-              }),
-            },
-            () => placeholder,
-          ),
-      };
-    }
-  }
-};
-
-const previewImage = async (
-  file: UploadFile,
-  visible: Ref<boolean>,
-  fileList: Ref<UploadProps['fileList']>,
-) => {
-  // 检查是否为图片文件的辅助函数
-  const isImageFile = (file: UploadFile): boolean => {
-    const imageExtensions = new Set([
-      'bmp',
-      'gif',
-      'jpeg',
-      'jpg',
-      'png',
-      'webp',
-    ]);
-    if (file.url) {
-      const ext = file.url?.split('.').pop()?.toLowerCase();
-      return ext ? imageExtensions.has(ext) : false;
-    }
-    if (!file.type) {
-      const ext = file.name?.split('.').pop()?.toLowerCase();
-      return ext ? imageExtensions.has(ext) : false;
-    }
-    return file.type.startsWith('image/');
-  };
-
-  // 如果当前文件不是图片,直接打开
-  if (!isImageFile(file)) {
-    if (file.url) {
-      window.open(file.url, '_blank');
-    } else if (file.preview) {
-      window.open(file.preview, '_blank');
-    } else {
-      console.warn('无法打开文件,没有可用的URL或预览地址');
-    }
-    return;
-  }
-
-  // 对于图片文件,继续使用预览组
-  const [ImageComponent, PreviewGroupComponent] = await Promise.all([
-    Image,
-    PreviewGroup,
-  ]);
-
-  const getBase64 = (file: File) => {
-    return new Promise((resolve, reject) => {
-      const reader = new FileReader();
-      reader.readAsDataURL(file);
-      reader.addEventListener('load', () => resolve(reader.result));
-      reader.addEventListener('error', (error) => reject(error));
-    });
-  };
-  // 从fileList中过滤出所有图片文件
-  const imageFiles = (unref(fileList) || []).filter((element) =>
-    isImageFile(element),
-  );
-
-  // 为所有没有预览地址的图片生成预览
-  for (const imgFile of imageFiles) {
-    if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
-      imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
-    }
-  }
-  const container: HTMLElement | null = document.createElement('div');
-  document.body.append(container);
-
-  // 用于追踪组件是否已卸载
-  let isUnmounted = false;
-
-  const PreviewWrapper = {
-    setup() {
-      return () => {
-        if (isUnmounted) return null;
-        return h(
-          PreviewGroupComponent,
-          {
-            class: 'hidden',
-            preview: {
-              visible: visible.value,
-              // 设置初始显示的图片索引
-              current: imageFiles.findIndex((f) => f.uid === file.uid),
-              onVisibleChange: (value: boolean) => {
-                visible.value = value;
-                if (!value) {
-                  // 延迟清理,确保动画完成
-                  setTimeout(() => {
-                    if (!isUnmounted && container) {
-                      isUnmounted = true;
-                      render(null, container);
-                      container.remove();
-                    }
-                  }, 300);
-                }
-              },
-            },
-          },
-          () =>
-            // 渲染所有图片文件
-            imageFiles.map((imgFile) =>
-              h(ImageComponent, {
-                key: imgFile.uid,
-                src: imgFile.url || imgFile.preview,
-              }),
-            ),
-        );
-      };
-    },
-  };
-
-  render(h(PreviewWrapper), container);
-};
-
 // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
 export type ComponentType =
   | 'ApiCascader'

+ 3 - 1
playground/src/views/examples/form/basic.vue

@@ -342,6 +342,8 @@ const [BaseForm, baseFormApi] = useVbenForm({
         customRequest: upload_file,
         disabled: false,
         maxCount: 1,
+        // 单位:MB
+        maxSize: 2,
         multiple: false,
         showUploadList: true,
         // 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
@@ -354,7 +356,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
           default: () => $t('examples.form.upload-image'),
         };
       },
-      rules: 'required',
+      rules: 'selectRequired',
     },
   ],
   // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个