feat: add backup and restore

This commit is contained in:
engigu
2025-12-22 11:10:46 +08:00
parent 179aad970f
commit 03f4fa7b5c
8 changed files with 569 additions and 2 deletions
+141
View File
@@ -0,0 +1,141 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { api } from '@/api'
import { toast } from 'vue-sonner'
import { Download, Upload, Archive } from 'lucide-vue-next'
const hasBackup = ref(false)
const backupTime = ref('')
const backupLoading = ref(false)
const restoreLoading = ref(false)
const fileInput = ref<HTMLInputElement>()
const showConfirm = ref(false)
const pendingFile = ref<File | null>(null)
async function checkBackupStatus() {
try {
const res = await api.settings.getBackupStatus()
hasBackup.value = res.has_backup
backupTime.value = res.backup_time || ''
} catch {}
}
async function createBackup() {
backupLoading.value = true
try {
await api.settings.createBackup()
toast.success('备份创建成功')
await checkBackupStatus()
} catch (e: any) {
toast.error(e.message || '备份失败')
} finally {
backupLoading.value = false
}
}
function downloadBackup() {
window.open(api.settings.downloadBackup(), '_blank')
setTimeout(checkBackupStatus, 6000)
}
function triggerFileSelect() {
fileInput.value?.click()
}
function handleFileSelect(e: Event) {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
if (!file.name.endsWith('.zip')) {
toast.error('请选择 .zip 备份文件')
target.value = ''
return
}
pendingFile.value = file
showConfirm.value = true
target.value = ''
}
async function confirmRestore() {
if (!pendingFile.value) return
showConfirm.value = false
restoreLoading.value = true
try {
await api.settings.restoreBackup(pendingFile.value)
toast.success('恢复成功,页面即将刷新')
setTimeout(() => window.location.reload(), 1500)
} catch (e: any) {
toast.error(e.message || '恢复失败')
} finally {
restoreLoading.value = false
pendingFile.value = null
}
}
function cancelRestore() {
showConfirm.value = false
pendingFile.value = null
}
onMounted(checkBackupStatus)
</script>
<template>
<div class="space-y-4">
<div class="flex items-center gap-4">
<Button @click="createBackup" :disabled="backupLoading" variant="outline">
<Archive class="w-4 h-4 mr-2" />
{{ backupLoading ? '备份中...' : '创建备份' }}
</Button>
<Button v-if="hasBackup" @click="downloadBackup" variant="outline">
<Download class="w-4 h-4 mr-2" />
下载备份
</Button>
<span v-if="hasBackup && backupTime" class="text-xs text-muted-foreground">{{ backupTime }}</span>
</div>
<div class="text-xs text-muted-foreground">
备份包含任务执行日志环境变量脚本设置及 scripts 文件夹第一次下载后5分钟后文件将被删除
</div>
<div class="border-t pt-4 mt-4">
<div class="flex items-center gap-4">
<Button @click="triggerFileSelect" :disabled="restoreLoading" variant="outline">
<Upload class="w-4 h-4 mr-2" />
{{ restoreLoading ? '恢复中...' : '恢复备份' }}
</Button>
<input ref="fileInput" type="file" accept=".zip" class="hidden" @change="handleFileSelect" />
</div>
<div class="text-xs text-muted-foreground mt-2">
上传 .zip 备份文件进行恢复恢复会覆盖现有数据
</div>
</div>
<AlertDialog :open="showConfirm" @update:open="showConfirm = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认恢复</AlertDialogTitle>
<AlertDialogDescription>
恢复备份将覆盖现有所有数据此操作不可撤销确定要继续吗
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="cancelRestore">取消</AlertDialogCancel>
<AlertDialogAction @click="confirmRestore">确认恢复</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
+14
View File
@@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import PasswordSettings from './PasswordSettings.vue'
import SiteSettings from './SiteSettings.vue'
import BackupSettings from './BackupSettings.vue'
import AboutSettings from './AboutSettings.vue'
const activeTab = ref('password')
@@ -20,6 +21,7 @@ const activeTab = ref('password')
<TabsList>
<TabsTrigger value="password">密码修改</TabsTrigger>
<TabsTrigger value="site">站点设置</TabsTrigger>
<TabsTrigger value="backup">备份恢复</TabsTrigger>
<TabsTrigger value="about">关于</TabsTrigger>
</TabsList>
@@ -47,6 +49,18 @@ const activeTab = ref('password')
</Card>
</TabsContent>
<TabsContent value="backup" class="mt-6">
<Card class="max-w-xl">
<CardHeader>
<CardTitle>备份恢复</CardTitle>
<CardDescription>备份和恢复系统数据</CardDescription>
</CardHeader>
<CardContent>
<BackupSettings />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="about" class="mt-6">
<Card class="max-w-xl">
<CardContent class="pt-6">
+5 -1
View File
@@ -37,7 +37,11 @@ async function loadSettings() {
async function saveSettings() {
loading.value = true
try {
await api.settings.updateSite(form.value)
await api.settings.updateSite({
...form.value,
page_size: String(form.value.page_size),
cookie_days: String(form.value.cookie_days)
})
await refreshSettings()
toast.success('保存成功')
} catch {