import React, { useState, useEffect, useCallback } from 'react'; import { Card, Table, Button, Space, Modal, message, Popconfirm, Tag, Tooltip, Progress, Descriptions, Alert, Divider, Typography, Upload, Statistic, Row, Col, } from 'antd'; import { CloudDownloadOutlined, CloudUploadOutlined, DeleteOutlined, DownloadOutlined, UploadOutlined, ReloadOutlined, FileTextOutlined, DatabaseOutlined, ClockCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined, InfoCircleOutlined, SettingOutlined, } from '@ant-design/icons'; import api, { backupAPI } from '../api'; import CloseButton from '../components/CloseButton'; import { useNavigate } from 'react-router-dom'; const { Title, Text } = Typography; const formatBytes = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatDateTime = (dateStr) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleString('zh-CN'); }; // 现代化设计系统 const designTokens = { colors: { primary: { main: '#667eea', light: '#764ba2', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', gradientHover: 'linear-gradient(135deg, #764ba2 0%, #667eea 100%)', }, success: { main: '#10b981', light: '#34d399', gradient: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)', }, warning: { main: '#f59e0b', light: '#fbbf24', }, danger: { main: '#ef4444', light: '#f87171', gradient: 'linear-gradient(135deg, #ef4444 0%, #f87171 100%)', }, background: { primary: '#f8fafc', secondary: '#ffffff', accent: '#f1f5f9', }, text: { primary: '#1e293b', secondary: '#64748b', muted: '#94a3b8', }, }, shadows: { sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', glow: '0 0 20px rgba(102, 126, 234, 0.3)', }, borderRadius: { sm: '8px', md: '12px', lg: '16px', xl: '20px', }, spacing: { xs: '8px', sm: '16px', md: '24px', lg: '32px', xl: '40px', }, }; const BackupManagement = () => { const navigate = useNavigate(); const [backups, setBackups] = useState([]); const [loading, setLoading] = useState(false); const [backupLoading, setBackupLoading] = useState(false); const [restoreLoading, setRestoreLoading] = useState(false); const [selectedBackup, setSelectedBackup] = useState(null); const [detailVisible, setDetailVisible] = useState(false); const [restoreVisible, setRestoreVisible] = useState(false); const [backupInfo, setBackupInfo] = useState(null); const [restoreProgress, setRestoreProgress] = useState(0); const [restoreStatus, setRestoreStatus] = useState(''); const fetchBackups = useCallback(async () => { setLoading(true); try { const response = await api.get('/backup/list'); if (response?.success) { setBackups(response.data?.backups || []); } } catch (error) { message.error('获取备份列表失败'); } finally { setLoading(false); } }, []); const fetchBackupInfo = useCallback(async () => { try { const response = await api.get('/backup/info'); if (response?.success) { setBackupInfo(response.data); } } catch (error) { console.error('获取备份信息失败:', error); } }, []); useEffect(() => { fetchBackups(); fetchBackupInfo(); }, [fetchBackups, fetchBackupInfo]); const handleCreateBackup = async () => { setBackupLoading(true); try { const response = await api.post('/backup', { description: '手动备份', includeFiles: true, }); if (response?.success) { message.success('备份创建成功'); fetchBackups(); fetchBackupInfo(); } else { message.error(response.data?.message || '备份创建失败'); } } catch (error) { message.error('备份创建失败: ' + (error.response?.data?.message || error.message || '未知错误')); } finally { setBackupLoading(false); } }; const handleDownload = (filename) => { window.open(`/api/backup/download/${filename}`, '_blank'); }; const handleValidate = async (filename) => { try { const response = await api.get(`/backup/validate/${filename}`); if (response?.success && response.data?.valid) { const data = response.data; Modal.info({ title: (
备份文件验证通过
), width: 700, content: (
{/* 基本信息 */}
{data.version} {data.backupType === 'full' ? '完整备份' : '部分备份'} {formatDateTime(data.timestamp)} {data.compressed ? ( ) : ( )} {data.description && ( {data.description} )}
{/* 系统信息 */} {data.systemInfo && (

系统环境信息

{data.systemInfo.nodeVersion} {data.systemInfo.platform} {data.systemInfo.arch} {data.systemInfo.dbType}
)} {/* 数据表统计 */}

数据表详情(共 {data.metadata?.tableCount} 个表,{data.metadata?.totalRecords} 条记录)

{data.details?.tables && (
{Object.entries(data.details.tables).map(([tableName, tableInfo]) => (
{tableInfo.displayName || tableName} {tableInfo.displayName !== tableName && ( {tableName} )}
{tableInfo.recordCount} 条
))}
)}
{/* 文件备份详情 */} {data.details?.files && data.details.files.total > 0 && (

备份文件详情(共 {data.details.files.total} 个文件)

{data.details.files.avatars > 0 && (

头像文件 ({data.details.files.avatars} 个):

{data.details.files.avatarList.map((file, index) => ( {file.filename} ({formatBytes(file.size)}) ))}
)} {data.details.files.others > 0 && (

其他文件 ({data.details.files.others} 个):

{data.details.files.otherList.map((file, index) => ( {file.filename} ({formatBytes(file.size)}) ))}
)}
)} {/* 汇总信息 */}

备份汇总

{data.metadata?.tableCount}

数据表

{data.metadata?.totalRecords}

记录数

{data.metadata?.fileCount}

文件数

), maskClosable: false, okText: '关闭', okButtonProps: { style: primaryButtonStyle, }, }); } else { Modal.error({ title: '备份文件验证失败', content: response.data?.data?.error || '文件可能已损坏', }); } } catch (error) { message.error('验证失败: ' + (error.response?.data?.message || error.message || '未知错误')); } }; const handleRestore = async (filename) => { setRestoreLoading(true); setRestoreProgress(0); setRestoreStatus('正在验证备份文件...'); try { setRestoreProgress(10); setRestoreStatus('正在读取备份数据...'); const response = await api.post('/backup/restore', { filename, options: { overwriteExisting: true, skipFiles: false, }, }); setRestoreProgress(90); setRestoreStatus('正在完成恢复...'); if (response?.success) { const data = response.data; setRestoreProgress(100); setRestoreStatus('恢复完成!'); setTimeout(() => { setRestoreVisible(false); setRestoreLoading(false); Modal.success({ title: (
数据恢复成功
), width: 700, content: (
{/* 汇总信息 */}

{data.tablesRestored}

恢复表数

{data.recordsRestored}

恢复记录

{data.filesRestored}

恢复文件

{formatDateTime(data.restoredAt)}

恢复时间

{/* 数据表恢复详情 */} {data.tableDetails && Object.keys(data.tableDetails).length > 0 && (

数据表恢复详情

{Object.entries(data.tableDetails).map(([tableName, tableInfo]) => (
{tableInfo.displayName || tableName} {tableInfo.displayName !== tableName && ( {tableName} )}
{tableInfo.recordCount} 条
))}
)} {/* 文件恢复详情 */} {data.fileDetails && (data.fileDetails.avatars > 0 || data.fileDetails.others > 0) && (

文件恢复详情

{data.fileDetails.avatars > 0 && (

头像文件:{data.fileDetails.avatars} 个

)} {data.fileDetails.others > 0 && (

其他文件:{data.fileDetails.others} 个

)}
)}
), maskClosable: false, okText: '完成', okButtonProps: { style: primaryButtonStyle, }, }); }, 500); } else { throw new Error(response.data?.message || '恢复失败'); } } catch (error) { setRestoreLoading(false); Modal.error({ title: '数据恢复失败', content: error.response?.data?.message || error.message || '未知错误', }); } }; const handleDelete = async (filename) => { try { const response = await api.delete(`/backup/${filename}`); if (response?.success) { message.success('删除成功'); fetchBackups(); fetchBackupInfo(); } else { message.error(response?.message || '删除失败'); } } catch (error) { message.error('删除失败: ' + (error.response?.data?.message || error.message || '未知错误')); } }; const handleUpload = async (options) => { const { file, onSuccess, onError } = options; const formData = new FormData(); formData.append('backup', file); try { const response = await api.post('/backup/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); if (response?.success) { message.success('备份文件上传成功'); fetchBackups(); fetchBackupInfo(); onSuccess(response.data); } else { message.error(response?.message || '上传失败'); onError(new Error(response?.message)); } } catch (error) { message.error('上传失败: ' + (error.response?.data?.message || error.message || '未知错误')); onError(error); } }; const handleCleanBackups = async (maxCount = 30, maxAgeDays = 90, dryRun = false) => { try { const response = await api.post('/backup/clean', { maxCount, maxAgeDays, dryRun, }); if (response?.success) { const data = response.data; if (dryRun) { Modal.info({ title: '清理预览', width: 500, content: (

将删除 {data.deletedCount} 个旧备份文件

将保留 {data.keptCount} 个备份文件

预计释放空间: {data.freedSizeFormatted}

{data.deleted.length > 0 && (
待删除文件:
)}
), }); } else { message.success(`已清理 ${data.deletedCount} 个旧备份,释放 ${data.freedSizeFormatted}`); fetchBackups(); fetchBackupInfo(); } return data; } } catch (error) { message.error('清理失败: ' + (error.response?.data?.message || error.message || '未知错误')); } }; const showRestoreConfirm = (record) => { setSelectedBackup(record); setRestoreVisible(true); }; const columns = [ { title: ( 文件名 ), dataIndex: 'filename', key: 'filename', width: 280, render: (text, record) => (
{text} {record.description && (
{record.description}
)} {record.isUploaded && ( 上传文件 )} {record.invalid && ( 无法识别 )}
), }, { title: 文件大小, dataIndex: 'size', key: 'size', width: 140, render: (size) => ( {formatBytes(size)} ), }, { title: ( 创建时间 ), dataIndex: 'createdAt', key: 'createdAt', width: 200, render: (time, record) => { // 优先使用备份文件中的 timestamp,如果没有则使用文件创建时间 const displayTime = record.timestamp ? new Date(record.timestamp) : new Date(time); return ( {formatDateTime(displayTime.toISOString())} ); }, sorter: (a, b) => { const timeA = a.timestamp ? new Date(a.timestamp) : new Date(a.createdAt); const timeB = b.timestamp ? new Date(b.timestamp) : new Date(b.createdAt); return timeA - timeB; }, }, { title: 操作, key: 'action', width: 320, render: (_, record) => (

将删除超过 30 个或超过 90 天的旧备份

建议先点击"预览"查看将删除的文件

} onConfirm={() => handleCleanBackups(30, 90, false)} okText="确认清理" cancelText="取消" >
{backupInfo && (

备份文件数量

{backupInfo.backupCount}

备份总大小

{backupInfo.totalSizeFormatted}

备份存储路径

{backupInfo.backupPath}

)} 备份说明 } description={

✓ 备份包含所有数据库表数据(设备、机柜、用户、工单、耗材等)

✓ 备份包含上传的文件(用户头像等)

✓ 可通过下载备份文件在新环境中恢复所有数据

✓ 建议定期备份以确保数据安全

} type="info" showIcon style={{ marginBottom: 24, borderRadius: designTokens.borderRadius.md, border: `1px solid ${designTokens.colors.text.muted}20`, background: `${designTokens.colors.primary.main}08`, }} /> `共 ${total} 个备份文件`, pageSizeOptions: ['10', '20', '50', '100'], }} locale={{ emptyText: (

暂无备份文件

点击"创建备份"按钮开始备份

), }} rowClassName={(record, index) => index % 2 === 0 ? 'table-row-light' : 'table-row-dark'} style={{ borderRadius: designTokens.borderRadius.md, overflow: 'hidden', }} />
确认恢复数据 } open={restoreVisible} closeIcon={} onCancel={() => { if (!restoreLoading) { setRestoreVisible(false); } }} footer={null} width={560} maskClosable={false} styles={{ body: { padding: '24px' }, header: { padding: '20px 24px', borderBottom: `1px solid ${designTokens.colors.text.muted}20` }, }} >
{restoreLoading ? (
{restoreStatus}
) : ( <> 警告:恢复操作将覆盖当前所有数据 } description="此操作不可撤销,建议先创建当前数据的备份" type="warning" showIcon style={{ marginBottom: 20, borderRadius: designTokens.borderRadius.md, border: `1px solid ${designTokens.colors.warning.main}30`, background: `${designTokens.colors.warning.main}08`, }} /> {selectedBackup && (
{selectedBackup.filename} {formatBytes(selectedBackup.size)} {formatDateTime(selectedBackup.createdAt)}
)} )}
); }; export default BackupManagement;