From 81a61558cb786068bb59bd9af2132eade0f89a88 Mon Sep 17 00:00:00 2001 From: JyQAQ <45193678+jyqwq@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:19:40 +0800 Subject: [PATCH] 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 --- apps/web-antd/src/adapter/component/index.ts | 299 ++++++++++--------- packages/locales/src/langs/en-US/ui.json | 7 +- packages/locales/src/langs/zh-CN/ui.json | 7 +- playground/src/adapter/component/index.ts | 299 ++++++++++--------- playground/src/views/examples/form/basic.vue | 4 +- 5 files changed, 323 insertions(+), 293 deletions(-) diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 1d288009..79b9bf6f 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/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 = ( }; 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, + fileList: Ref, + ) => { + // 检查是否为图片文件的辅助函数 + 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, - fileList: Ref, -) => { - // 检查是否为图片文件的辅助函数 - 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' diff --git a/packages/locales/src/langs/en-US/ui.json b/packages/locales/src/langs/en-US/ui.json index f920ee33..f5166cdf 100644 --- a/packages/locales/src/langs/en-US/ui.json +++ b/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", diff --git a/packages/locales/src/langs/zh-CN/ui.json b/packages/locales/src/langs/zh-CN/ui.json index 3433bcb5..46093189 100644 --- a/packages/locales/src/langs/zh-CN/ui.json +++ b/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": "请完成安全验证", diff --git a/playground/src/adapter/component/index.ts b/playground/src/adapter/component/index.ts index 261a7d3b..b0e43c9a 100644 --- a/playground/src/adapter/component/index.ts +++ b/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 = ( }; 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, + fileList: Ref, + ) => { + // 检查是否为图片文件的辅助函数 + 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, - fileList: Ref, -) => { - // 检查是否为图片文件的辅助函数 - 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' diff --git a/playground/src/views/examples/form/basic.vue b/playground/src/views/examples/form/basic.vue index d0e91d33..7cb08522 100644 --- a/playground/src/views/examples/form/basic.vue +++ b/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个