From c03223372ac28a4f181a4600d669302174b5b2ca Mon Sep 17 00:00:00 2001
From: hsy <1029150275@qq.com>
Date: 星期一, 31 八月 2026 15:13:57 +0800
Subject: [PATCH] feat(ui): 导航栏新增数据库备份按钮及下载逻辑

---
 src/api/system/database/index.ts                                             |   50 ++++++++++++++++
 src/packages/effects/layouts/src/widgets/database-backup/index.ts            |    1 
 src/packages/effects/layouts/src/widgets/index.ts                            |    1 
 src/packages/effects/layouts/src/basic/header/header.vue                     |   10 +++
 src/packages/@core/base/icons/src/lucide.ts                                  |    1 
 src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue |   70 +++++++++++++++++++++++
 6 files changed, 133 insertions(+), 0 deletions(-)

diff --git a/src/api/system/database/index.ts b/src/api/system/database/index.ts
new file mode 100644
index 0000000..261d7f2
--- /dev/null
+++ b/src/api/system/database/index.ts
@@ -0,0 +1,50 @@
+import { requestClient } from '#/api/request';
+
+function downloadBlob(blob: Blob, headers?: any) {
+  let filename = 'backup.sql';
+  if (headers) {
+    const disposition = headers['content-disposition'] || headers['Content-Disposition'];
+    if (disposition && disposition.indexOf('filename=') !== -1) {
+      const matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
+      if (matches != null && matches[1]) {
+        filename = matches[1].replace(/['"]/g, '');
+      }
+    }
+  }
+  
+  const url = window.URL.createObjectURL(blob);
+  const link = document.createElement('a');
+  link.style.display = 'none';
+  link.href = url;
+  link.setAttribute('download', decodeURIComponent(filename));
+  document.body.append(link);
+  link.click();
+  link.remove();
+  window.URL.revokeObjectURL(url);
+}
+
+/** 涓嬭浇鏁版嵁搴撳浠芥枃浠� */
+export function backupDatabase(onDownloadProgress?: (progressEvent: any) => void) {
+  return requestClient.request('/system/database/backup', {
+    method: 'GET',
+    responseType: 'blob',
+    timeout: 300000, // 5鍒嗛挓
+    onDownloadProgress,
+    responseReturn: 'raw',
+  }).then((res: any) => {
+    const blob = res.data || res;
+    if (blob instanceof Blob) {
+      downloadBlob(blob, res.headers);
+    }
+    return res;
+  }).catch((err: any) => {
+    if (err instanceof Blob) {
+      downloadBlob(err);
+      return err;
+    } else if (err?.response?.data instanceof Blob) {
+      downloadBlob(err.response.data, err.response.headers);
+      return err;
+    }
+    throw err;
+  });
+}
diff --git a/src/packages/@core/base/icons/src/lucide.ts b/src/packages/@core/base/icons/src/lucide.ts
index 01ea54d..63687b1 100644
--- a/src/packages/@core/base/icons/src/lucide.ts
+++ b/src/packages/@core/base/icons/src/lucide.ts
@@ -27,6 +27,7 @@
   CloudUpload,
   Copy,
   CornerDownLeft,
+  DatabaseBackup,
   Download,
   Ellipsis,
   Eraser,
diff --git a/src/packages/effects/layouts/src/basic/header/header.vue b/src/packages/effects/layouts/src/basic/header/header.vue
index f0d020a..1d319ef 100644
--- a/src/packages/effects/layouts/src/basic/header/header.vue
+++ b/src/packages/effects/layouts/src/basic/header/header.vue
@@ -9,6 +9,7 @@
 import { VbenFullScreen, VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src';
 
 import {
+  DatabaseBackupButton,
   GlobalSearch,
   LanguageToggle,
   PreferencesButton,
@@ -56,6 +57,12 @@
   }
   // 鍋忓ソ璁剧疆蹇嵎鍔熻兘
   if (preferencesButtonPosition.value.header) {
+    // 鏁版嵁搴撳浠芥寜閽�
+    list.push({
+      index: REFERENCE_VALUE + 5,
+      name: 'database-backup',
+    });
+
     list.push({
       index: REFERENCE_VALUE + 10,
       name: 'preferences',
@@ -188,6 +195,9 @@
           />
         </template>
 
+        <template v-else-if="slot.name === 'database-backup'">
+          <DatabaseBackupButton class="mr-1" />
+        </template>
         <template v-else-if="slot.name === 'preferences'">
           <PreferencesButton
             class="mr-1"
diff --git a/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue b/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue
new file mode 100644
index 0000000..5cc98f1
--- /dev/null
+++ b/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue
@@ -0,0 +1,70 @@
+<script lang="ts" setup>
+import { DatabaseBackup } from '../../../../../icons/src';
+import { VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src';
+import { useVbenModal } from '../../../../../@core/ui-kit/popup-ui/src';
+import { backupDatabase } from '../../../../../../api/system/database';
+import { ref } from 'vue';
+
+const isDownloading = ref(false);
+const downloadProgress = ref(0);
+
+const [Modal, modalApi] = useVbenModal({
+  onConfirm: async () => {
+    modalApi.close();
+    try {
+      isDownloading.value = true;
+      downloadProgress.value = 0;
+      await backupDatabase((progressEvent) => {
+        if (progressEvent.total) {
+          downloadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+        }
+      });
+    } catch (error: any) {
+      // Ignore if it's the Blob being thrown by the response interceptor
+      if (!(error instanceof Blob) && !(error?.response?.data instanceof Blob)) {
+        console.error('Backup failed:', error);
+      }
+    } finally {
+      isDownloading.value = false;
+      downloadProgress.value = 0;
+    }
+  },
+});
+
+function handleOpen() {
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div class="relative">
+    <VbenIconButton
+      v-access:code="['system:database:backup']"
+      class="hover:animate-[shrink_0.3s_ease-in-out]"
+      @click="handleOpen"
+    >
+      <DatabaseBackup class="size-4 text-foreground" />
+    </VbenIconButton>
+
+    <Modal
+      title="鎻愮ず"
+      content-class="px-8 min-h-10"
+      footer-class="border-none mb-3 mr-3"
+      header-class="border-none"
+    >
+      纭瑕佷笅杞芥暟鎹簱鐨勫浠芥枃浠跺悧锛�
+    </Modal>
+
+    <teleport to="body">
+      <div
+        v-if="isDownloading"
+        class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"
+      >
+        <div class="flex flex-col items-center space-y-4 rounded-lg bg-background p-6 shadow-lg">
+          <div class="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
+          <p class="text-lg font-medium text-foreground">姝e湪涓嬭浇澶囦唤: {{ downloadProgress }}%</p>
+        </div>
+      </div>
+    </teleport>
+  </div>
+</template>
diff --git a/src/packages/effects/layouts/src/widgets/database-backup/index.ts b/src/packages/effects/layouts/src/widgets/database-backup/index.ts
new file mode 100644
index 0000000..f5a75e6
--- /dev/null
+++ b/src/packages/effects/layouts/src/widgets/database-backup/index.ts
@@ -0,0 +1 @@
+export { default as DatabaseBackupButton } from './database-backup.vue';
diff --git a/src/packages/effects/layouts/src/widgets/index.ts b/src/packages/effects/layouts/src/widgets/index.ts
index 665cdb9..7bb762c 100644
--- a/src/packages/effects/layouts/src/widgets/index.ts
+++ b/src/packages/effects/layouts/src/widgets/index.ts
@@ -12,3 +12,4 @@
 export * from './theme-toggle';
 export * from './timezone';
 export * from './user-dropdown';
+export * from './database-backup';

--
Gitblit v1.9.3