import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Card, Row, Col, Button, Switch, InputNumber, Input, Space, Tag, Alert, Modal, message, Descriptions, Divider, Typography, Spin, Radio, Table, Select, Pagination, Empty, Tooltip, } from 'antd'; import { ClockCircleOutlined, DatabaseOutlined, CheckCircleOutlined, PlayCircleOutlined, SaveOutlined, SettingOutlined, InfoCircleOutlined, ArrowLeftOutlined, ThunderboltOutlined, SafetyOutlined, CloudDownloadOutlined, FileProtectOutlined, HistoryOutlined, EyeOutlined, ReloadOutlined, UpOutlined, DownOutlined, } from '@ant-design/icons'; import { backupAPI } from '../api'; import { useNavigate } from 'react-router-dom'; const { Title, Text, Paragraph } = Typography; const { Option } = Select; const TimePicker = ({ hour, minute, onChange, disabled }) => { const [hourInput, setHourInput] = useState(hour.toString()); const [minuteInput, setMinuteInput] = useState(minute.toString()); useEffect(() => { setHourInput(hour.toString()); setMinuteInput(minute.toString()); }, [hour, minute]); const handleHourChange = newHour => { const validHour = Math.max(0, Math.min(23, newHour)); onChange(validHour, minute); }; const handleMinuteChange = newMinute => { const validMinute = Math.max(0, Math.min(59, newMinute)); onChange(hour, validMinute); }; const handleHourInputBlur = () => { const value = parseInt(hourInput); if (!isNaN(value)) { handleHourChange(value); } else { setHourInput(hour.toString()); } }; const handleMinuteInputBlur = () => { const value = parseInt(minuteInput); if (!isNaN(value)) { handleMinuteChange(value); } else { setMinuteInput(minute.toString()); } }; const handleHourKeyPress = e => { if (e.key === 'Enter') { handleHourInputBlur(); } }; const handleMinuteKeyPress = e => { if (e.key === 'Enter') { handleMinuteInputBlur(); } }; const incrementHour = () => handleHourChange(hour + 1); const decrementHour = () => handleHourChange(hour - 1); const incrementMinute = () => handleMinuteChange(minute + 1); const decrementMinute = () => handleMinuteChange(minute - 1); const timePeriods = [ { start: 0, end: 5, label: '深夜', icon: '🌙' }, { start: 6, end: 8, label: '清晨', icon: '🌅' }, { start: 9, end: 11, label: '上午', icon: '☀️' }, { start: 12, end: 13, label: '中午', icon: '🌞' }, { start: 14, end: 17, label: '下午', icon: '🌤️' }, { start: 18, end: 21, label: '傍晚', icon: '🌇' }, { start: 22, end: 23, label: '夜晚', icon: '🌙' }, ]; const currentPeriod = timePeriods.find(p => hour >= p.start && hour <= p.end); return (
:
{currentPeriod && (
{currentPeriod.icon} {currentPeriod.label}
)}
{[ { hour: 2, minute: 0, label: '凌晨 2:00', recommended: true }, { hour: 3, minute: 0, label: '凌晨 3:00' }, { hour: 4, minute: 0, label: '凌晨 4:00', recommended: true }, { hour: 20, minute: 0, label: '晚上 8:00' }, { hour: 22, minute: 0, label: '晚上 10:00' }, ].map((preset, index) => ( ))}
); }; const AutoBackupSettings = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(null); const [settings, setSettings] = useState({ enabled: false, hour: 2, minute: 0, includeFiles: true, compress: true, maxCount: 30, maxAgeDays: 90, backupType: 'full', }); const [modified, setModified] = useState(false); const isInitialMount = useRef(true); const isFetching = useRef(false); const [logs, setLogs] = useState([]); const [logsLoading, setLogsLoading] = useState(false); const [logsPagination, setLogsPagination] = useState({ current: 1, pageSize: 10, total: 0, }); const [logFilter, setLogFilter] = useState({ logType: '', status: '', }); const [logDetailModal, setLogDetailModal] = useState(false); const [selectedLog, setSelectedLog] = useState(null); useEffect(() => { if (isInitialMount.current) { fetchStatus(); fetchLogs(); isInitialMount.current = false; } }, []); const fetchStatus = useCallback(async () => { if (isFetching.current) return; try { setLoading(true); isFetching.current = true; const response = await backupAPI.getAutoStatus(); if (response?.success) { const data = response.data; setStatus(data); if (data.cronExpression) { const parts = data.cronExpression.split(' '); const minute = parseInt(parts[0]); const hour = parseInt(parts[1]); setSettings(prev => ({ ...prev, enabled: data.enabled || false, hour: isNaN(hour) ? 2 : hour, minute: isNaN(minute) ? 0 : minute, includeFiles: data.includeFiles !== undefined ? data.includeFiles : true, compress: data.compress !== undefined ? data.compress : true, maxCount: data.maxCount || 30, maxAgeDays: data.maxAgeDays || 90, backupType: data.backupType || 'full', })); } } } catch (error) { console.error('获取自动备份状态失败:', error); } finally { setLoading(false); isFetching.current = false; } }, []); const fetchLogs = useCallback( async (page = 1, pageSize = 10) => { try { setLogsLoading(true); const params = { page, pageSize }; if (logFilter.logType) params.logType = logFilter.logType; if (logFilter.status) params.status = logFilter.status; const response = await backupAPI.getLogs(params); if (response?.success) { setLogs(response.data.logs || []); setLogsPagination({ current: response.data.page || 1, pageSize: response.data.pageSize || 10, total: response.data.total || 0, }); } } catch (error) { console.error('获取备份日志失败:', error); message.error('获取备份日志失败'); } finally { setLogsLoading(false); } }, [logFilter] ); const handleSave = async () => { try { setLoading(true); const response = await backupAPI.updateAutoSettings({ enabled: settings.enabled, hour: settings.hour, minute: settings.minute, includeFiles: settings.includeFiles, compress: settings.compress, maxCount: settings.maxCount, maxAgeDays: settings.maxAgeDays, backupType: settings.backupType, }); if (response?.success) { message.success('自动备份设置已保存'); setModified(false); fetchStatus(); } } catch (error) { message.error('保存失败:' + (error.response?.data?.message || error.message)); } finally { setLoading(false); } }; const handleExecuteNow = async () => { Modal.confirm({ title: (
立即执行备份
), content: '确定要立即执行一次备份吗?这将在后台创建一个新的备份文件。', okText: '立即备份', cancelText: '取消', onOk: async () => { try { setLoading(true); const response = await backupAPI.executeNow({ description: `手动触发 - ${new Date().toLocaleString('zh-CN')}`, includeFiles: settings.includeFiles, compress: settings.compress, backupType: settings.backupType, }); if (response?.success) { message.success('备份执行成功!'); fetchLogs(logsPagination.current, logsPagination.pageSize); } } catch (error) { message.error('执行失败:' + (error.response?.data?.message || error.message)); } finally { setLoading(false); } }, }); }; const formatTime = (hour, minute) => { return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; }; const formatNextRun = nextRun => { if (!nextRun) return '未知'; return new Date(nextRun).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); }; const formatDateTime = dateTime => { if (!dateTime) return '-'; return new Date(dateTime).toLocaleString('zh-CN'); }; const formatDuration = ms => { if (!ms) return '-'; const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes > 0) { return `${minutes}分${remainingSeconds}秒`; } return `${remainingSeconds}秒`; }; const formatFileSize = bytes => { if (!bytes) return '-'; const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; }; const getStatusColor = () => { if (!status) return 'default'; if (status.enabled && status.isActive) return 'success'; if (status.enabled && !status.isActive) return 'warning'; return 'default'; }; const getStatusText = () => { if (!status) return '加载中...'; if (status.enabled && status.isActive) return '自动备份运行中'; if (status.enabled && !status.isActive) return '自动备份已启用但未运行'; return '自动备份已禁用'; }; const getLogStatusTag = status => { const statusMap = { pending: { color: 'default', text: '待执行' }, running: { color: 'processing', text: '执行中' }, success: { color: 'success', text: '成功' }, failed: { color: 'error', text: '失败' }, }; const info = statusMap[status] || { color: 'default', text: status }; return {info.text}; }; const getLogTypeTag = type => { const typeMap = { auto: { color: 'blue', text: '自动备份' }, manual: { color: 'green', text: '手动备份' }, }; const info = typeMap[type] || { color: 'default', text: type }; return {info.text}; }; const handleLogPageChange = (page, pageSize) => { fetchLogs(page, pageSize); }; const handleLogFilterChange = (key, value) => { setLogFilter(prev => ({ ...prev, [key]: value })); }; const handleViewLogDetail = log => { setSelectedLog(log); setLogDetailModal(true); }; const handleRefreshLogs = () => { fetchLogs(logsPagination.current, logsPagination.pageSize); }; const StatusCard = ({ icon, title, value, subtitle, gradient }) => (
{React.cloneElement(icon, { style: { fontSize: 120 } })}
{title}
{value}
{subtitle && {subtitle}}
); const SettingItem = ({ title, description, children, bordered = true }) => (
{title}
{description && ( {description} )}
{children}
); const logColumns = [ { title: '类型', dataIndex: 'logType', key: 'logType', width: 120, render: type => getLogTypeTag(type), }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: status => getLogStatusTag(status), }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, }, { title: '文件大小', dataIndex: 'fileSize', key: 'fileSize', width: 120, render: size => formatFileSize(size), }, { title: '执行时间', dataIndex: 'duration', key: 'duration', width: 120, render: duration => formatDuration(duration), }, { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 180, render: date => formatDateTime(date), }, { title: '操作', key: 'actions', width: 100, render: (_, record) => (
<SettingOutlined style={{ marginRight: 12, verticalAlign: 'middle' }} /> 自动备份设置 配置定时自动备份,确保数据安全无忧
<ThunderboltOutlined style={{ marginRight: 8 }} /> 状态概览 } title="当前状态" value={getStatusText()} gradient={ status?.enabled && status.isActive ? 'linear-gradient(135deg, #10b981 0%, #34d399 100%)' : 'linear-gradient(135deg, #6b7280 0%, #9ca3af 100%)' } /> } title="下次执行时间" value={formatNextRun(status?.nextRun)} gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)" /> } title="备份时间" value={formatTime(settings.hour, settings.minute)} gradient="linear-gradient(135deg, #10b981 0%, #34d399 100%)" /> } title="保留策略" value={`${settings.maxCount}个 / ${settings.maxAgeDays}天`} gradient="linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)" />
基本设置 } style={{ borderRadius: '20px', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.05)', height: '100%', }} > { setSettings({ ...settings, enabled: checked }); setModified(true); }} size="default" checkedChildren="开启" unCheckedChildren="关闭" /> { setSettings(prev => ({ ...prev, hour: newHour, minute: newMinute, })); setModified(true); }} disabled={!settings.enabled} />
高级设置 } style={{ borderRadius: '20px', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.05)', height: '100%', }} > { setSettings({ ...settings, backupType: e.target.value }); setModified(true); }} disabled={!settings.enabled} > 全量备份 增量备份 { setSettings({ ...settings, includeFiles: checked }); setModified(true); }} disabled={!settings.enabled} checkedChildren="包含" unCheckedChildren="不包含" /> { setSettings({ ...settings, compress: checked }); setModified(true); }} disabled={!settings.enabled} checkedChildren="压缩" unCheckedChildren="不压缩" /> { setSettings({ ...settings, maxCount: value }); setModified(true); }} addonAfter="个" disabled={!settings.enabled} style={{ width: 120 }} /> { setSettings({ ...settings, maxAgeDays: value }); setModified(true); }} addonAfter="天" disabled={!settings.enabled} style={{ width: 120 }} />
备份日志 } style={{ borderRadius: '20px', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.05)', marginTop: 24, }} extra={ } >
类型: 状态:
, }} />
`共 ${total} 条`} />
最佳实践建议 } description={
备份时间:建议设置在凌晨 2-4 点业务低峰期,避免影响正常使用
保留策略:开发环境建议 7 个/7 天,生产环境建议 30 个/90 天
压缩备份:强烈建议开启,可显著减少磁盘空间占用
定期测试:每月至少手动执行一次备份,验证功能正常
} type="info" showIcon={false} style={{ borderRadius: '16px', border: '1px solid #dbeafe', background: 'linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%)', }} />
安全提示 } description="自动备份功能需要服务器持续运行。如果服务器重启,自动备份任务会自动恢复。请确保服务器时间设置正确。" type="warning" showIcon={false} style={{ borderRadius: '16px', border: '1px solid #fef3c7', background: 'linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%)', }} />
setLogDetailModal(false)} footer={[ , ]} width={700} > {selectedLog && ( {getLogTypeTag(selectedLog.logType)} {getLogStatusTag(selectedLog.status)} {selectedLog.description || '-'} {selectedLog.backupType === 'full' ? '全量备份' : '增量备份'} {selectedLog.filename || '-'} {formatFileSize(selectedLog.fileSize)} {selectedLog.includeFiles ? '是' : '否'} {selectedLog.compressed ? '是' : '否'} {formatDateTime(selectedLog.startTime)} {formatDateTime(selectedLog.endTime)} {formatDuration(selectedLog.duration)} {formatDateTime(selectedLog.createdAt)} {selectedLog.errorMessage && ( {selectedLog.errorMessage} )} {selectedLog.remoteUploads && selectedLog.remoteUploads.length > 0 && (
{selectedLog.remoteUploads.map((upload, index) => (
{upload.targetName} {upload.success ? '上传成功' : `失败: ${upload.error}`}
))}
)}
)}
); }; export default AutoBackupSettings;