import React, { useState, useEffect, useCallback } from 'react'; import { Card, Button, Table, Tag, Space, Modal, Form, Input, Select, InputNumber, Switch, message, Popconfirm, Divider, Alert, Row, Col, Typography, Tooltip, Steps, Skeleton, Badge, Progress, Empty, Result, Collapse, Statistic, } from 'antd'; import { CloudOutlined, PlusOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined, ArrowLeftOutlined, SettingOutlined, CloudUploadOutlined, GlobalOutlined, LockOutlined, UserOutlined, DatabaseOutlined, ApiOutlined, SafetyOutlined, InfoCircleOutlined, WarningOutlined, SyncOutlined, QuestionCircleOutlined, CopyOutlined, ReloadOutlined, LinkOutlined, HddOutlined, FolderOutlined, } from '@ant-design/icons'; import api from '../api'; import { useNavigate } from 'react-router-dom'; const { Title, Text, Paragraph } = Typography; const { TextArea } = Input; const { Panel } = Collapse; // 设计系统 - 统一视觉风格 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', gradient: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)', }, danger: { main: '#ef4444', light: '#f87171', gradient: 'linear-gradient(135deg, #ef4444 0%, #f87171 100%)', }, info: { main: '#3b82f6', light: '#60a5fa', gradient: 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)', }, background: { primary: '#f8fafc', secondary: '#ffffff', accent: '#f1f5f9', gradient: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)', }, text: { primary: '#1e293b', secondary: '#64748b', muted: '#94a3b8', }, border: { light: '#e2e8f0', medium: '#cbd5e1', }, }, 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.15)', }, borderRadius: { sm: '8px', md: '12px', lg: '16px', xl: '20px', }, transitions: { fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)', normal: '250ms cubic-bezier(0.4, 0, 0.2, 1)', slow: '350ms cubic-bezier(0.4, 0, 0.2, 1)', }, }; // 按钮样式系统 const buttonStyles = { primary: { background: designTokens.colors.primary.gradient, border: 'none', borderRadius: designTokens.borderRadius.md, fontWeight: 600, boxShadow: designTokens.shadows.md, transition: `all ${designTokens.transitions.normal}`, }, secondary: { background: designTokens.colors.background.secondary, border: `1px solid ${designTokens.colors.border.light}`, borderRadius: designTokens.borderRadius.md, fontWeight: 500, color: designTokens.colors.text.primary, transition: `all ${designTokens.transitions.normal}`, }, danger: { background: designTokens.colors.danger.gradient, border: 'none', borderRadius: designTokens.borderRadius.md, fontWeight: 600, boxShadow: designTokens.shadows.md, transition: `all ${designTokens.transitions.normal}`, }, success: { background: designTokens.colors.success.gradient, border: 'none', borderRadius: designTokens.borderRadius.md, fontWeight: 600, boxShadow: designTokens.shadows.md, transition: `all ${designTokens.transitions.normal}`, }, }; const RemoteBackupSettings = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [targets, setTargets] = useState([]); const [globalSettings, setGlobalSettings] = useState({ enabled: false, uploadAfterBackup: true, deleteLocalAfterUpload: false, retryCount: 3, retryDelay: 5000, timeout: 300000, }); const [modalVisible, setModalVisible] = useState(false); const [editingTarget, setEditingTarget] = useState(null); const [protocols, setProtocols] = useState([]); const [form] = Form.useForm(); const [currentStep, setCurrentStep] = useState(0); const [testStatus, setTestStatus] = useState(null); const [testing, setTesting] = useState(false); const protocolsList = [ { value: 'sftp', label: 'SFTP', icon: , category: 'server', description: 'SSH 安全文件传输协议', }, { value: 'ftp', label: 'FTP/FTPS', icon: , category: 'server', description: '传统文件传输协议(支持 FTPS)', }, { value: 'webdav', label: 'WebDAV', icon: , category: 'server', description: '基于 HTTP 的文件传输协议', }, { value: 'smb', label: 'SMB/CIFS', icon: , category: 'server', description: 'Windows 网络共享文件夹', }, ]; useEffect(() => { loadInitialData(); }, []); const loadInitialData = async () => { try { setInitialLoading(true); await Promise.all([fetchTargets(), fetchGlobalSettings(), fetchProtocols()]); } finally { setInitialLoading(false); } }; const fetchTargets = async () => { try { const response = await api.get('/backup/remote/targets'); if (response?.success) { setTargets(response.data.targets || []); } } catch (error) { console.error('获取远端目标列表失败:', error); } }; const fetchGlobalSettings = async () => { try { const response = await api.get('/backup/remote/settings'); if (response?.success) { setGlobalSettings(response.data.settings || {}); } } catch (error) { console.error('获取全局设置失败:', error); } }; const fetchProtocols = async () => { try { const response = await api.get('/backup/remote/protocols'); if (response?.success) { setProtocols(response.data.protocols || []); } } catch (error) { console.error('获取协议列表失败:', error); } }; const handleAdd = () => { setEditingTarget(null); setCurrentStep(0); setTestStatus(null); form.resetFields(); form.setFieldsValue({ enabled: true }); setModalVisible(true); }; const handleEdit = record => { setEditingTarget(record); setCurrentStep(1); setTestStatus(null); form.setFieldsValue(record); setModalVisible(true); }; const handleDelete = async id => { try { setLoading(true); const response = await api.delete(`/backup/remote/targets/${id}`); if (response?.success) { message.success('删除成功'); fetchTargets(); } } catch (error) { message.error('删除失败:' + (error.response?.data?.message || error.message)); } finally { setLoading(false); } }; const handleTest = async record => { try { setTesting(true); const response = await api.post(`/backup/remote/targets/${record.id}/test`); if (response?.success) { message.success('连接测试成功!'); } else { message.error('连接测试失败:' + (response?.data?.message || '未知错误')); } } catch (error) { message.error('连接测试失败:' + (error.response?.data?.message || error.message)); } finally { setTesting(false); } }; const handleTestConnection = async () => { try { setTesting(true); setTestStatus('testing'); await form.validateFields(); const values = form.getFieldsValue(true); const response = await api.post('/backup/remote/test', values); if (response?.success) { setTestStatus('success'); message.success('连接测试成功!'); } else { setTestStatus('error'); message.error('连接测试失败:' + (response?.data?.message || '未知错误')); } } catch (error) { setTestStatus('error'); if (error.errorFields) { message.error('请先完善表单信息'); } else { message.error('连接测试失败:' + (error.response?.data?.message || error.message)); } } finally { setTesting(false); } }; const handleSubmit = async () => { try { setLoading(true); const values = await form.validateFields(); if (editingTarget) { const response = await api.put(`/backup/remote/targets/${editingTarget.id}`, values); if (response?.success) { message.success('更新成功'); setModalVisible(false); fetchTargets(); } } else { const response = await api.post('/backup/remote/targets', values); if (response?.success) { message.success('添加成功'); setModalVisible(false); fetchTargets(); } } } catch (error) { if (!error.errorFields) { message.error('保存失败:' + (error.response?.data?.message || error.message)); } } finally { setLoading(false); } }; const handleUpdateGlobalSettings = async (key, value) => { try { const newSettings = { ...globalSettings, [key]: value }; const response = await api.put('/backup/remote/settings', newSettings); if (response?.success) { setGlobalSettings(newSettings); message.success('设置已更新'); } } catch (error) { message.error('更新设置失败:' + (error.response?.data?.message || error.message)); } }; const getProtocolLabel = protocol => { const proto = protocolsList.find(p => p.value === protocol); return proto ? proto.label : protocol; }; const getProtocolIcon = protocol => { const proto = protocolsList.find(p => p.value === protocol); return proto?.icon || ; }; const getStatusColor = enabled => (enabled ? 'success' : 'default'); const columns = [ { title: ( 目标名称 ), dataIndex: 'name', key: 'name', width: 200, render: (text, record) => (
{React.cloneElement(getProtocolIcon(record.protocol), { style: { color: '#fff', fontSize: 18 }, })}
{text} {getProtocolLabel(record.protocol)}
), }, { title: ( 连接地址 ), key: 'host', width: 220, render: (_, record) => { if (record.protocol === 'webdav') { return ( {record.url?.length > 30 ? `${record.url.substring(0, 30)}...` : record.url} ); } return ( {record.host}:{record.port || (record.protocol === 'ftp' ? 21 : 22)} ); }, }, { title: ( 远程路径 ), key: 'path', width: 140, render: (_, record) => ( {record.prefix || record.rootPath || '/'} ), }, { title: 状态, dataIndex: 'enabled', key: 'enabled', width: 100, render: enabled => ( {enabled ? '启用' : '禁用'} ), }, { title: 操作, key: 'action', width: 200, fixed: 'right', render: (_, record) => ( handleDelete(record.id)} okText="确定删除" cancelText="取消" okButtonProps={{ danger: true }} >
<CloudOutlined style={{ marginRight: 12, fontSize: 32, background: designTokens.colors.primary.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', }} /> 远端备份配置 配置多个远端存储目标,实现备份数据异地容灾
{/* 统计概览 */}
远端备份状态
{globalSettings.enabled ? '已启用' : '已禁用'}
handleUpdateGlobalSettings('enabled', checked)} checkedChildren="开" unCheckedChildren="关" style={{ background: globalSettings.enabled ? 'rgba(255,255,255,0.3)' : undefined, }} />
远端目标
{targets.length}
t.enabled).length / Math.max(targets.length, 1)) * 100 } showInfo={false} strokeColor={designTokens.colors.primary.main} size="small" style={{ marginTop: 8 }} /> {targets.filter(t => t.enabled).length} 个已启用
自动上传
{globalSettings.uploadAfterBackup ? '已开启' : '已关闭'} handleUpdateGlobalSettings('uploadAfterBackup', checked)} disabled={!globalSettings.enabled} checkedChildren="开" unCheckedChildren="关" />
删除本地文件
{globalSettings.deleteLocalAfterUpload ? '已开启' : '已关闭'} handleUpdateGlobalSettings('deleteLocalAfterUpload', checked) } disabled={!globalSettings.enabled} checkedChildren="开" unCheckedChildren="关" />
{/* 远端目标列表 */}
远端备份目标列表
} extra={ 共 {targets.length} 个目标 } > `共 ${total} 个目标`, }} scroll={{ x: 1100 }} locale={{ emptyText: (
暂无远端备份目标 点击下方按钮添加您的第一个远端备份目标 } /> ), }} /> {/* 帮助文档 */}
使用帮助与最佳实践 } key="help" style={{ border: 'none', background: designTokens.colors.background.secondary, borderRadius: designTokens.borderRadius.lg, }} >
🎯 推荐配置 } style={{ borderRadius: designTokens.borderRadius.md, border: `1px solid ${designTokens.colors.border.light}`, }} >
  • 多地备份:配置至少 2 个不同地理位置的目标
  • 加密传输:敏感数据使用 SFTP 或 HTTPS 协议
  • 定期测试:每月至少测试一次连接
⚠️ 注意事项 } style={{ borderRadius: designTokens.borderRadius.md, border: `1px solid ${designTokens.colors.border.light}`, }} >
  • 首次使用前请先测试连接
  • 密码等敏感信息已加密存储
  • 启用"删除本地文件"前请确保远端上传成功
  • 建议定期检查远端备份文件完整性
{/* 添加/编辑弹窗 */} setModalVisible(false)} footer={null} width={800} destroyOnClose centered styles={{ body: { padding: '24px 32px' }, }} >
{editingTarget ? '编辑远端备份目标' : '添加远端备份目标'} } /> } /> } />
{/* 步骤 1: 基本信息 */} {currentStep === 0 && (
{renderProtocolSelector()}
)} {/* 步骤 2: 协议配置 */} {currentStep === 1 && (
{() => renderProtocolFields(form.getFieldValue('protocol'))}
)} {/* 步骤 3: 测试保存 */} {currentStep === 2 && (
请确认以下配置信息是否正确:
  • 目标名称:{form.getFieldValue('name')}
  • 协议类型:{getProtocolLabel(form.getFieldValue('protocol'))}
  • 启用状态:{form.getFieldValue('enabled') ? '启用' : '禁用'}
} type="info" showIcon style={{ marginBottom: 16 }} /> {testStatus === 'success' && ( )} {testStatus === 'error' && ( )}
)}
); }; export default RemoteBackupSettings;