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