From 2885dc43bfa2cf0a645bd7318c338d3c6824eaa7 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Sun, 15 Jun 2025 19:46:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20download=20=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8@vben/utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/utils/download.ts | 215 ------------------ apps/web-antd/src/utils/index.ts | 1 - .../detail/modules/signature.vue | 7 +- .../@core/base/shared/src/utils/download.ts | 57 +++++ 4 files changed, 59 insertions(+), 221 deletions(-) delete mode 100644 apps/web-antd/src/utils/download.ts diff --git a/apps/web-antd/src/utils/download.ts b/apps/web-antd/src/utils/download.ts deleted file mode 100644 index e50f2997..00000000 --- a/apps/web-antd/src/utils/download.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * 下载工具模块 - * 提供多种文件格式的下载功能 - */ -// TODO @ziye:请使用 @vben/utils/download 代替 packages/@core/base/shared/src/utils/download.ts - -/** - * 图片下载配置接口 - */ -interface ImageDownloadOptions { - /** 图片 URL */ - url: string; - /** 指定画布宽度 */ - canvasWidth?: number; - /** 指定画布高度 */ - canvasHeight?: number; - /** 将图片绘制在画布上时带上图片的宽高值,默认为 true */ - drawWithImageSize?: boolean; -} - -/** - * 基础文件下载函数 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - * @param mimeType - MIME 类型 - */ -export const download0 = (data: Blob, fileName: string, mimeType: string) => { - try { - // 创建 blob - const blob = new Blob([data], { type: mimeType }); - // 创建 href 超链接,点击进行下载 - window.URL = window.URL || window.webkitURL; - const href = URL.createObjectURL(blob); - const downA = document.createElement('a'); - downA.href = href; - downA.download = fileName; - downA.click(); - // 销毁超链接 - window.URL.revokeObjectURL(href); - } catch (error) { - console.error('文件下载失败:', error); - throw new Error( - `文件下载失败: ${error instanceof Error ? error.message : '未知错误'}`, - ); - } -}; - -/** - * 触发文件下载的通用方法 - * @param url - 下载链接 - * @param fileName - 文件名 - */ -const triggerDownload = (url: string, fileName: string) => { - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - a.click(); -}; - -export const download = { - /** - * 下载 Excel 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - excel: (data: Blob, fileName: string) => { - download0(data, fileName, 'application/vnd.ms-excel'); - }, - - /** - * 下载 Word 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - word: (data: Blob, fileName: string) => { - download0(data, fileName, 'application/msword'); - }, - - /** - * 下载 Zip 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - zip: (data: Blob, fileName: string) => { - download0(data, fileName, 'application/zip'); - }, - - /** - * 下载 HTML 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - html: (data: Blob, fileName: string) => { - download0(data, fileName, 'text/html'); - }, - - /** - * 下载 Markdown 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - markdown: (data: Blob, fileName: string) => { - download0(data, fileName, 'text/markdown'); - }, - - /** - * 下载 JSON 文件 - * @param data - 文件数据 Blob - * @param fileName - 文件名 - */ - json: (data: Blob, fileName: string) => { - download0(data, fileName, 'application/json'); - }, - - /** - * 下载图片(允许跨域) - * @param options - 图片下载配置 - */ - image: (options: ImageDownloadOptions) => { - const { - url, - canvasWidth, - canvasHeight, - drawWithImageSize = true, - } = options; - - const image = new Image(); - // image.setAttribute('crossOrigin', 'anonymous') - image.src = url; - image.addEventListener('load', () => { - try { - const canvas = document.createElement('canvas'); - canvas.width = canvasWidth || image.width; - canvas.height = canvasHeight || image.height; - const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; - ctx?.clearRect(0, 0, canvas.width, canvas.height); - - if (drawWithImageSize) { - ctx.drawImage(image, 0, 0, image.width, image.height); - } else { - ctx.drawImage(image, 0, 0); - } - - const dataUrl = canvas.toDataURL('image/png'); - triggerDownload(dataUrl, 'image.png'); - } catch (error) { - console.error('图片下载失败:', error); - throw new Error( - `图片下载失败: ${error instanceof Error ? error.message : '未知错误'}`, - ); - } - }); - - image.addEventListener('error', () => { - throw new Error('图片加载失败'); - }); - }, - - /** - * 将 Base64 字符串转换为文件对象 - * @param base64 - Base64 字符串 - * @param fileName - 文件名 - * @returns File 对象 - */ - base64ToFile: (base64: string, fileName: string): File => { - // 输入验证 - if (!base64 || typeof base64 !== 'string') { - throw new Error('base64 参数必须是非空字符串'); - } - - // 将 base64 按照逗号进行分割,将前缀与后续内容分隔开 - const data = base64.split(','); - if (data.length !== 2 || !data[0] || !data[1]) { - throw new Error('无效的 base64 格式'); - } - - // 利用正则表达式从前缀中获取类型信息(image/png、image/jpeg、image/webp等) - const typeMatch = data[0].match(/:(.*?);/); - if (!typeMatch || !typeMatch[1]) { - throw new Error('无法解析 base64 类型信息'); - } - const type = typeMatch[1]; - - // 从类型信息中获取具体的文件格式后缀(png、jpeg、webp) - const typeParts = type.split('/'); - if (typeParts.length !== 2 || !typeParts[1]) { - throw new Error('无效的 MIME 类型格式'); - } - const suffix = typeParts[1]; - - try { - // 使用 atob() 对 base64 数据进行解码,结果是一个文件数据流以字符串的格式输出 - const bstr = window.atob(data[1]); - - // 获取解码结果字符串的长度 - const n = bstr.length; - // 根据解码结果字符串的长度创建一个等长的整型数字数组 - const u8arr = new Uint8Array(n); - - // 优化的 Uint8Array 填充逻辑 - for (let i = 0; i < n; i++) { - // 使用 charCodeAt() 获取字符对应的字节值(Base64 解码后的字符串是字节级别的) - // eslint-disable-next-line unicorn/prefer-code-point - u8arr[i] = bstr.charCodeAt(i); - } - - // 返回 File 文件对象 - return new File([u8arr], `${fileName}.${suffix}`, { type }); - } catch (error) { - throw new Error( - `Base64 解码失败: ${error instanceof Error ? error.message : '未知错误'}`, - ); - } - }, -}; diff --git a/apps/web-antd/src/utils/index.ts b/apps/web-antd/src/utils/index.ts index aaa0682f..dcf28573 100644 --- a/apps/web-antd/src/utils/index.ts +++ b/apps/web-antd/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './constants'; export * from './dict'; -export * from './download'; export * from './formCreate'; export * from './rangePickerProps'; export * from './routerHelper'; diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/signature.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/signature.vue index 82d7dfd6..ae22ccad 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/signature.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/signature.vue @@ -3,13 +3,13 @@ import { ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { IconifyIcon } from '@vben/icons'; +import { base64ToFile } from '@vben/utils'; import { Button, message, Space, Tooltip } from 'ant-design-vue'; // TODO @ziye:这个可能,适合放到全局?!因为 element-plus 也用这个; import Vue3Signature from 'vue3-signature'; import { uploadFile } from '#/api/infra/file'; -import { download } from '#/utils'; defineOptions({ name: 'BpmProcessInstanceSignature', @@ -31,10 +31,7 @@ const [Modal, modalApi] = useVbenModal({ content: '签名上传中请稍等。。。', }); const signFileUrl = await uploadFile({ - file: download.base64ToFile( - signature?.value?.save('image/jpeg') || '', - '签名', - ), + file: base64ToFile(signature?.value?.save('image/jpeg') || '', '签名'), }); emits('success', signFileUrl); // TODO @ziye:下面有个告警哈;ps:所有告警,皆是错误,可以关注 ide 给的提示哈; diff --git a/packages/@core/base/shared/src/utils/download.ts b/packages/@core/base/shared/src/utils/download.ts index d1cb0d30..3e85339a 100644 --- a/packages/@core/base/shared/src/utils/download.ts +++ b/packages/@core/base/shared/src/utils/download.ts @@ -140,6 +140,63 @@ export function urlToBase64(url: string, mineType?: string): Promise { }); } +/** + * 将 Base64 字符串转换为文件对象 + * @param base64 - Base64 字符串 + * @param fileName - 文件名 + * @returns File 对象 + */ +export function base64ToFile(base64: string, fileName: string): File { + // 输入验证 + if (!base64 || typeof base64 !== 'string') { + throw new Error('base64 参数必须是非空字符串'); + } + + // 将 base64 按照逗号进行分割,将前缀与后续内容分隔开 + const data = base64.split(','); + if (data.length !== 2 || !data[0] || !data[1]) { + throw new Error('无效的 base64 格式'); + } + + // 利用正则表达式从前缀中获取类型信息(image/png、image/jpeg、image/webp等) + const typeMatch = data[0].match(/:(.*?);/); + if (!typeMatch || !typeMatch[1]) { + throw new Error('无法解析 base64 类型信息'); + } + const type = typeMatch[1]; + + // 从类型信息中获取具体的文件格式后缀(png、jpeg、webp) + const typeParts = type.split('/'); + if (typeParts.length !== 2 || !typeParts[1]) { + throw new Error('无效的 MIME 类型格式'); + } + const suffix = typeParts[1]; + + try { + // 使用 atob() 对 base64 数据进行解码,结果是一个文件数据流以字符串的格式输出 + const bstr = window.atob(data[1]); + + // 获取解码结果字符串的长度 + const n = bstr.length; + // 根据解码结果字符串的长度创建一个等长的整型数字数组 + const u8arr = new Uint8Array(n); + + // 优化的 Uint8Array 填充逻辑 + for (let i = 0; i < n; i++) { + // 使用 charCodeAt() 获取字符对应的字节值(Base64 解码后的字符串是字节级别的) + // eslint-disable-next-line unicorn/prefer-code-point + u8arr[i] = bstr.charCodeAt(i); + } + + // 返回 File 文件对象 + return new File([u8arr], `${fileName}.${suffix}`, { type }); + } catch (error) { + throw new Error( + `Base64 解码失败: ${error instanceof Error ? error.message : '未知错误'}`, + ); + } +} + /** * 通用下载触发函数 * @param href - 文件下载的 URL