Преглед изворни кода

feat: added file download examples (#4853)

Vben пре 10 месеци
родитељ
комит
a3d0d2ed34

+ 160 - 0
packages/@core/base/shared/src/utils/download.ts

@@ -0,0 +1,160 @@
+import { openWindow } from './window';
+
+interface DownloadOptions<T = string> {
+  fileName?: string;
+  source: T;
+  target?: string;
+}
+
+const DEFAULT_FILENAME = 'downloaded_file';
+
+/**
+ * 通过 URL 下载文件,支持跨域
+ * @throws {Error} - 当下载失败时抛出错误
+ */
+export async function downloadFileFromUrl({
+  fileName,
+  source,
+  target = '_blank',
+}: DownloadOptions): Promise<void> {
+  if (!source || typeof source !== 'string') {
+    throw new Error('Invalid URL.');
+  }
+
+  const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
+  const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
+
+  if (/iP/.test(window.navigator.userAgent)) {
+    console.error('Your browser does not support download!');
+    return;
+  }
+
+  if (isChrome || isSafari) {
+    triggerDownload(source, resolveFileName(source, fileName));
+  }
+  if (!source.includes('?')) {
+    source += '?download';
+  }
+
+  openWindow(source, { target });
+}
+
+/**
+ * 通过 Base64 下载文件
+ */
+export function downloadFileFromBase64({ fileName, source }: DownloadOptions) {
+  if (!source || typeof source !== 'string') {
+    throw new Error('Invalid Base64 data.');
+  }
+
+  const resolvedFileName = fileName || DEFAULT_FILENAME;
+  triggerDownload(source, resolvedFileName);
+}
+
+/**
+ * 通过图片 URL 下载图片文件
+ */
+export async function downloadFileFromImageUrl({
+  fileName,
+  source,
+}: DownloadOptions) {
+  const base64 = await urlToBase64(source);
+  downloadFileFromBase64({ fileName, source: base64 });
+}
+
+/**
+ * 通过 Blob 下载文件
+ * @param blob - 文件的 Blob 对象
+ * @param fileName - 可选,下载的文件名称
+ */
+export function downloadFileFromBlob({
+  fileName = DEFAULT_FILENAME,
+  source,
+}: DownloadOptions<Blob>): void {
+  if (!(source instanceof Blob)) {
+    throw new TypeError('Invalid Blob data.');
+  }
+
+  const url = URL.createObjectURL(source);
+  triggerDownload(url, fileName);
+}
+
+/**
+ * 下载文件,支持 Blob、字符串和其他 BlobPart 类型
+ * @param data - 文件的 BlobPart 数据
+ * @param fileName - 下载的文件名称
+ */
+export function downloadFileFromBlobPart({
+  fileName = DEFAULT_FILENAME,
+  source,
+}: DownloadOptions<BlobPart>): void {
+  // 如果 data 不是 Blob,则转换为 Blob
+  const blob =
+    source instanceof Blob
+      ? source
+      : new Blob([source], { type: 'application/octet-stream' });
+
+  // 创建对象 URL 并触发下载
+  const url = URL.createObjectURL(blob);
+  triggerDownload(url, fileName);
+}
+
+/**
+ * img url to base64
+ * @param url
+ */
+export function urlToBase64(url: string, mineType?: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
+    const ctx = canvas?.getContext('2d');
+    const img = new Image();
+    img.crossOrigin = '';
+    img.addEventListener('load', () => {
+      if (!canvas || !ctx) {
+        return reject(new Error('Failed to create canvas.'));
+      }
+      canvas.height = img.height;
+      canvas.width = img.width;
+      ctx.drawImage(img, 0, 0);
+      const dataURL = canvas.toDataURL(mineType || 'image/png');
+      canvas = null;
+      resolve(dataURL);
+    });
+    img.src = url;
+  });
+}
+
+/**
+ * 通用下载触发函数
+ * @param href - 文件下载的 URL
+ * @param fileName - 下载文件的名称,如果未提供则自动识别
+ * @param revokeDelay - 清理 URL 的延迟时间 (毫秒)
+ */
+export function triggerDownload(
+  href: string,
+  fileName: string | undefined,
+  revokeDelay: number = 100,
+): void {
+  const defaultFileName = 'downloaded_file';
+  const finalFileName = fileName || defaultFileName;
+
+  const link = document.createElement('a');
+  link.href = href;
+  link.download = finalFileName;
+  link.style.display = 'none';
+
+  if (link.download === undefined) {
+    link.setAttribute('target', '_blank');
+  }
+
+  document.body.append(link);
+  link.click();
+  link.remove();
+
+  // 清理临时 URL 以释放内存
+  setTimeout(() => URL.revokeObjectURL(href), revokeDelay);
+}
+
+function resolveFileName(url: string, fileName?: string): string {
+  return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;
+}

+ 1 - 0
packages/@core/base/shared/src/utils/index.ts

@@ -2,6 +2,7 @@ export * from './cn';
 export * from './date';
 export * from './diff';
 export * from './dom';
+export * from './download';
 export * from './inference';
 export * from './letter';
 export * from './merge';

+ 2 - 1
playground/src/locales/langs/en-US/demos.json

@@ -49,7 +49,8 @@
     "fullScreen": "FullScreen",
     "clipboard": "Clipboard",
     "menuWithQuery": "Menu With Query",
-    "openInNewWindow": "Open in New Window"
+    "openInNewWindow": "Open in New Window",
+    "fileDownload": "File Download"
   },
   "breadcrumb": {
     "navigation": "Breadcrumb Navigation",

+ 2 - 1
playground/src/locales/langs/zh-CN/demos.json

@@ -49,7 +49,8 @@
     "fullScreen": "全屏",
     "clipboard": "剪贴板",
     "menuWithQuery": "带参菜单",
-    "openInNewWindow": "新窗口打开"
+    "openInNewWindow": "新窗口打开",
+    "fileDownload": "文件下载"
   },
   "breadcrumb": {
     "navigation": "面包屑导航",

+ 10 - 0
playground/src/router/routes/modules/demos.ts

@@ -177,6 +177,16 @@ const routes: RouteRecordRaw[] = [
               title: $t('demos.features.fullScreen'),
             },
           },
+          {
+            name: 'FileDownloadDemo',
+            path: '/demos/features/file-download',
+            component: () =>
+              import('#/views/demos/features/file-download/index.vue'),
+            meta: {
+              icon: 'lucide:hard-drive-download',
+              title: $t('demos.features.fileDownload'),
+            },
+          },
           {
             name: 'ClipboardDemo',
             path: '/demos/features/clipboard',

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
playground/src/views/demos/features/file-download/base64.ts


+ 74 - 0
playground/src/views/demos/features/file-download/index.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+import {
+  downloadFileFromBase64,
+  downloadFileFromBlobPart,
+  downloadFileFromImageUrl,
+  downloadFileFromUrl,
+} from '@vben/utils';
+
+import { Button, Card } from 'ant-design-vue';
+
+import imageBase64 from './base64';
+</script>
+
+<template>
+  <Page title="文件下载示例">
+    <Card title="根据文件地址下载文件">
+      <Button
+        type="primary"
+        @click="
+          downloadFileFromUrl({
+            source:
+              'https://codeload.github.com/vbenjs/vue-vben-admin-doc/zip/main',
+            target: '_self',
+          })
+        "
+      >
+        Download File
+      </Button>
+    </Card>
+
+    <Card class="my-5" title="根据地址下载图片">
+      <Button
+        type="primary"
+        @click="
+          downloadFileFromImageUrl({
+            source:
+              'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
+            fileName: 'vben-logo.png',
+          })
+        "
+      >
+        Download File
+      </Button>
+    </Card>
+
+    <Card class="my-5" title="base64流下载">
+      <Button
+        type="primary"
+        @click="
+          downloadFileFromBase64({
+            source: imageBase64,
+            fileName: 'image.png',
+          })
+        "
+      >
+        Download Image
+      </Button>
+    </Card>
+    <Card class="my-5" title="文本下载">
+      <Button
+        type="primary"
+        @click="
+          downloadFileFromBlobPart({
+            source: 'text content',
+            fileName: 'test.txt',
+          })
+        "
+      >
+        Download TxT
+      </Button>
+    </Card>
+  </Page>
+</template>

Неке датотеке нису приказане због велике количине промена