feat(Config): add backup and restore configuration functionality

This commit is contained in:
ShiYu
2025-05-24 00:49:48 +08:00
parent 5c5b63e82f
commit b76d462b12
6 changed files with 270 additions and 5 deletions

View File

@@ -72,3 +72,32 @@ export const hasRole = (userRole: string | undefined, requiredRole: UserRole): b
// 精确匹配角色
return userRole === requiredRole;
};
// 备份所有配置
export const backupConfigs = async (): Promise<BaseResult<Record<string, string>>> => {
try {
return await fetchApi<Record<string, string>>('/config/backup');
} catch (error: any) {
return {
success: false,
message: `备份配置失败: ${error.message}`,
code: 500
};
}
};
// 恢复配置
export const restoreConfigs = async (configBackup: Record<string, string>): Promise<BaseResult<boolean>> => {
try {
return await fetchApi<boolean>('/config/restore', {
method: 'POST',
body: JSON.stringify(configBackup),
});
} catch (error: any) {
return {
success: false,
message: `恢复配置失败: ${error.message}`,
code: 500
};
}
};

View File

@@ -51,6 +51,8 @@ export {
getConfig,
setConfig,
deleteConfig,
hasRole
hasRole,
backupConfigs,
restoreConfigs
} from './configApi';

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Tabs, Card, message, Spin, Select } from 'antd';
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined } from '@ant-design/icons';
import { getAllConfigs, setConfig } from '../../api';
import { Tabs, Card, message, Spin, Select, Button, Upload, Modal, Space, Tooltip } from 'antd';
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined, DownloadOutlined, UploadOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { getAllConfigs, setConfig, backupConfigs, restoreConfigs } from '../../api';
import ConfigGroup from './ConfigGroup.tsx';
import useIsMobile from '../../hooks/useIsMobile';
@@ -20,6 +20,10 @@ const SystemConfig: React.FC = () => {
const [configs, setConfigs] = useState<ConfigStructure>({});
const [activeKey, setActiveKey] = useState('AI');
const [storageType, setStorageType] = useState('Telegram');
const [backupLoading, setBackupLoading] = useState(false);
const [restoreLoading, setRestoreLoading] = useState(false);
const [restoreModalVisible, setRestoreModalVisible] = useState(false);
const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null);
// 获取所有配置项
const fetchConfigs = async () => {
@@ -82,6 +86,83 @@ const SystemConfig: React.FC = () => {
}
};
// 备份配置
const handleBackupConfigs = async () => {
setBackupLoading(true);
try {
const response = await backupConfigs();
if (response.success && response.data) {
const configData = JSON.stringify(response.data, null, 2);
const blob = new Blob([configData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
a.download = `foxel-config-backup-${timestamp}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
message.success('配置备份已下载');
} else {
message.error('备份配置失败: ' + response.message);
}
} catch (error) {
message.error('备份配置出错');
console.error(error);
} finally {
setBackupLoading(false);
}
};
// 上传配置文件
const handleFileUpload = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const config = JSON.parse(content);
setRestoreConfig(config);
setRestoreModalVisible(true);
} catch (error) {
message.error('无效的配置文件格式');
}
};
reader.readAsText(file);
return false; // 阻止自动上传
};
// 确认恢复配置
const handleRestoreConfigs = async () => {
if (!restoreConfig) return;
setRestoreLoading(true);
try {
const response = await restoreConfigs(restoreConfig);
if (response.success) {
message.success('配置恢复成功将在3秒后刷新页面');
setRestoreModalVisible(false);
// 重新加载配置
setTimeout(() => {
fetchConfigs();
// 可选:刷新页面以确保所有配置生效
// window.location.reload();
}, 3000);
} else {
message.error('恢复配置失败: ' + response.message);
}
} catch (error) {
message.error('恢复配置出错');
console.error(error);
} finally {
setRestoreLoading(false);
}
};
// 存储类型选项
const storageOptions = [
{ value: 'Local', label: '本地存储', icon: <DatabaseOutlined style={{ color: '#52c41a' }} /> },
@@ -97,7 +178,38 @@ const SystemConfig: React.FC = () => {
return (
<Card
title="系统配置"
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span></span>
<Space>
<Tooltip title="下载当前所有配置的备份">
<Button
icon={<DownloadOutlined />}
onClick={handleBackupConfigs}
loading={backupLoading}
size={isMobile ? "small" : "middle"}
>
{isMobile ? '' : '备份配置'}
</Button>
</Tooltip>
<Upload
beforeUpload={handleFileUpload}
showUploadList={false}
accept=".json"
>
<Tooltip title="从备份文件恢复配置">
<Button
icon={<UploadOutlined />}
size={isMobile ? "small" : "middle"}
>
{isMobile ? '' : '恢复配置'}
</Button>
</Tooltip>
</Upload>
</Space>
</div>
}
className="system-config-card"
bodyStyle={{
padding: isMobile ? '12px 8px' : '24px'
@@ -427,6 +539,43 @@ const SystemConfig: React.FC = () => {
</TabPane>
</Tabs>
)}
{/* 恢复配置确认对话框 */}
<Modal
title={
<div>
<span></span>
<Tooltip title="恢复配置将覆盖当前所有配置设置,请确认备份文件正确无误">
<QuestionCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
</div>
}
open={restoreModalVisible}
onCancel={() => setRestoreModalVisible(false)}
footer={[
<>
<Button key="cancel" onClick={() => setRestoreModalVisible(false)}>
</Button>
<Button
key="submit"
type="primary"
loading={restoreLoading}
onClick={handleRestoreConfigs}
>
</Button>
</>
]}
>
<p></p>
<p style={{ color: '#ff4d4f' }}></p>
{restoreConfig && (
<div>
<p> {Object.keys(restoreConfig).length} </p>
</div>
)}
</Modal>
</Card>
);
};