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) => (
}
onClick={() => handleTest(record)}
loading={testing}
style={{
...buttonStyles.success,
color: '#fff',
padding: '4px 12px',
height: '32px',
fontSize: '13px',
}}
>
测试
}
onClick={() => handleEdit(record)}
style={{
...buttonStyles.secondary,
padding: '4px 12px',
height: '32px',
fontSize: '13px',
}}
>
编辑
handleDelete(record.id)}
okText="确定删除"
cancelText="取消"
okButtonProps={{ danger: true }}
>
}
style={{
background: designTokens.colors.background.accent,
border: 'none',
borderRadius: designTokens.borderRadius.sm,
color: designTokens.colors.danger.main,
width: '32px',
height: '32px',
}}
/>
),
},
];
const renderProtocolFields = protocol => {
if (!protocol) {
return ;
}
const fieldsMap = {
ftp: {
title: 'FTP/FTPS 配置',
icon: ,
fields: (
<>
}
placeholder="例如:192.168.1.100 或 ftp.example.com"
/>
} placeholder="FTP 用户名" />
} placeholder="FTP 密码" />
>
),
},
sftp: {
title: 'SFTP 配置',
icon: ,
fields: (
<>
} placeholder="例如:192.168.1.100" />
} placeholder="SSH 用户名" />
} placeholder="SSH 密码" />
>
),
},
webdav: {
title: 'WebDAV 配置',
icon: ,
fields: (
<>
} placeholder="例如:https://dav.jianguoyun.com/dav" />
} placeholder="WebDAV 用户名" />
} placeholder="WebDAV 密码" />
>
),
},
smb: {
title: 'SMB/CIFS 配置',
icon: ,
fields: (
<>
} placeholder="例如:192.168.1.100" />
} placeholder="SMB 用户名" />
} placeholder="SMB 密码" />
>
),
},
};
const config = fieldsMap[protocol];
if (!config) return null;
return (
{config.icon}
{config.title}
}
style={{ marginTop: 16 }}
>
{config.fields}
);
};
const renderProtocolSelector = () => (
选择适合你的远端存储类型,支持 FTP/SFTP、WebDAV、SMB 等多种协议
{() => {
const protocol = form.getFieldValue('protocol');
if (protocol) {
const proto = protocolsList.find(p => p.value === protocol);
return (
);
}
return null;
}}
);
const renderSkeleton = () => (
);
// 页面容器样式
const containerStyle = {
minHeight: '100vh',
background: designTokens.colors.background.gradient,
padding: '24px',
};
// 页面头部样式
const headerCardStyle = {
borderRadius: designTokens.borderRadius.lg,
border: 'none',
boxShadow: designTokens.shadows.lg,
background: designTokens.colors.background.secondary,
marginBottom: 24,
overflow: 'hidden',
};
// 卡片样式
const cardStyle = {
borderRadius: designTokens.borderRadius.lg,
border: 'none',
boxShadow: designTokens.shadows.md,
background: designTokens.colors.background.secondary,
transition: `all ${designTokens.transitions.normal}`,
};
if (initialLoading) {
return (
);
}
return (
{/* 页面头部 */}
}
onClick={() => navigate('/backup')}
style={{
...buttonStyles.secondary,
marginBottom: 16,
padding: '8px 16px',
height: '40px',
}}
>
返回备份管理
远端备份配置
配置多个远端存储目标,实现备份数据异地容灾
}
onClick={loadInitialData}
loading={initialLoading}
style={{
...buttonStyles.secondary,
padding: '8px 16px',
height: '40px',
}}
>
刷新
}
onClick={handleAdd}
style={{
...buttonStyles.primary,
padding: '8px 24px',
height: '40px',
color: '#fff',
}}
>
添加远端目标
{/* 统计概览 */}
远端备份状态
{globalSettings.enabled ? '已启用' : '已禁用'}
handleUpdateGlobalSettings('enabled', checked)}
checkedChildren="开"
unCheckedChildren="关"
style={{
background: globalSettings.enabled ? 'rgba(255,255,255,0.3)' : undefined,
}}
/>
{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: (
暂无远端备份目标
点击下方按钮添加您的第一个远端备份目标
}
onClick={handleAdd}
style={{
...buttonStyles.primary,
color: '#fff',
marginTop: 8,
}}
>
添加远端目标
}
/>
),
}}
/>
{/* 帮助文档 */}
使用帮助与最佳实践
}
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 ? '编辑远端备份目标' : '添加远端备份目标'}
} />
} />
} />
)}
);
};
export default RemoteBackupSettings;