@@ -0,0 +1,141 @@
< script setup lang = "ts" >
import { RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed , onMounted , reactive , ref } from 'vue'
import {
fetchBackupSchedule ,
fetchBackupStatus ,
updateBackupSchedule ,
type BackupStatus ,
} from '@/features/admin/api/backup'
import { useAdminSessionStore } from '@/stores/adminSession'
import { formatDateTime } from '@/shared/utils/time'
const adminSession = useAdminSessionStore ( )
const loading = ref ( false )
const saving = ref ( false )
const status = ref < BackupStatus | null > ( null )
const statusError = ref ( '' )
const canUpdateSchedule = computed ( ( ) => adminSession . isSuperAdmin || adminSession . hasPermission ( 'backup:schedule:update' ) )
const schedule = reactive ( { enabled : true , full _hour : 4 , full _minute : 30 , binlog _interval _minutes : 1 } )
const healthLabel = computed ( ( ) => ( status . value ? . health === 'healthy' ? '链路正常' : '需要检查' ) )
const healthType = computed ( ( ) => ( status . value ? . health === 'healthy' ? 'success' : 'warning' ) )
async function load ( ) {
loading . value = true
statusError . value = ''
try {
const [ nextStatus , nextSchedule ] = await Promise . all ( [ fetchBackupStatus ( ) , fetchBackupSchedule ( ) ] )
status . value = nextStatus
Object . assign ( schedule , nextSchedule )
} catch ( error ) {
statusError . value = error instanceof Error ? error . message : '无法读取备份状态'
} finally {
loading . value = false
}
}
async function saveSchedule ( ) {
saving . value = true
try {
Object . assign ( schedule , await updateBackupSchedule ( { ... schedule } ) )
ElMessage . success ( '定时配置已保存,宿主机调度器将在下一分钟读取' )
} finally {
saving . value = false
}
}
onMounted ( load )
< / script >
< template >
< section v-loading = "loading" class="backup-page" >
< div class = "page-header" >
< div >
< h1 > 数据备份 < / h1 >
< / div >
< el-button :loading = "loading" circle title = "刷新状态" @click ="load" >
< el -icon > < RefreshRight / > < / el-icon >
< / el-button >
< / div >
< el-alert v-if = "statusError" type="warning" :title="statusError" :closable="false" show -icon / >
< div v-else class = "status-grid" >
< el-card shadow = "never" >
< template # header > < span > 备份健康状态 < / span > < / template >
< el-tag :type = "healthType" effect = "light" > { { healthLabel } } < / el-tag >
< dl >
< dt > 状态更新时间 < / dt >
< dd > { { status ? . generated _at ? formatDateTime ( status . generated _at ) : '-' } } < / dd >
< dt > 最近任务 < / dt >
< dd > { { status ? . last _job . type || '-' } } { { status ? . last _job . status || '' } } < / dd >
< dt > 任务时间 < / dt >
< dd > { { status ? . last _job . at ? formatDateTime ( status . last _job . at ) : '-' } } < / dd >
< / dl >
< / el-card >
< el-card shadow = "never" >
< template # header > < span > 最新全量备份 < / span > < / template >
< dl >
< dt > 本地完成标识 < / dt >
< dd class = "path" > { { status ? . last _full . local _complete || '-' } } < / dd >
< dt > OSS 完成标识 < / dt >
< dd class = "path" > { { status ? . last _full . remote _complete || '-' } } < / dd >
< / dl >
< / el-card >
< el-card shadow = "never" >
< template # header > < span > 最新 Binlog 归档 < / span > < / template >
< dl >
< dt > Binlog 文件 < / dt >
< dd > { { status ? . last _binlog . file || '-' } } < / dd >
< dt > 归档对象 < / dt >
< dd class = "path" > { { status ? . last _binlog . remote _dir || '-' } } < / dd >
< / dl >
< / el-card >
< / div >
< el-card shadow = "never" class = "schedule-card" >
< template # header > < span > 定时配置 < / span > < / template >
< el-form label -position = " top " class = "schedule-form" >
< el-form-item label = "启用服务器定时备份" >
< el-switch v-model = "schedule.enabled" :disabled="!canUpdateSchedule" / >
< / el-form-item >
< el-form-item label = "每日全量备份时间(Asia/Shanghai) " >
< div class = "time-inputs" >
< el-input-number v-model = "schedule.full_hour" :min="0" :max="23" :disabled="!canUpdateSchedule" controls-position="right" / >
< span > : < / span >
< el-input-number v-model = "schedule.full_minute" :min="0" :max="59" :disabled="!canUpdateSchedule" controls-position="right" / >
< / div >
< / el-form-item >
< el-form-item label = "Binlog 归档间隔" >
< el-select v-model = "schedule.binlog_interval_minutes" :disabled="!canUpdateSchedule" >
< el -option :value = "1" label = "每 1 分钟" / >
< el-option :value = "2" label = "每 2 分钟" / >
< el-option :value = "3" label = "每 3 分钟" / >
< el-option :value = "5" label = "每 5 分钟" / >
< / el-select >
< / el-form-item >
< el-button v-if = "canUpdateSchedule" type="primary" :loading="saving" @click="saveSchedule" > 保存定时配置 < / el -button >
< / el-form >
< / el-card >
< / section >
< / template >
< style scoped >
. backup - page { padding : 20 px ; }
. page - header { display : flex ; align - items : flex - start ; justify - content : space - between ; margin - bottom : 18 px ; }
. page - header h1 { margin : 0 ; font - size : 22 px ; font - weight : 600 ; }
. status - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; gap : 16 px ; }
dl { margin : 16 px 0 0 ; display : grid ; gap : 8 px ; }
dt { color : var ( -- el - text - color - secondary ) ; font - size : 13 px ; }
dd { margin : 0 ; overflow - wrap : anywhere ; }
. path { color : var ( -- el - text - color - regular ) ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; }
. schedule - card { margin - top : 16 px ; }
. schedule - form { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 180 px , 260 px ) ) ; gap : 0 18 px ; align - items : end ; }
. time - inputs { display : flex ; align - items : center ; gap : 8 px ; }
@ media ( max - width : 900 px ) { . status - grid , . schedule - form { grid - template - columns : 1 fr ; } }
< / style >