diff --git a/Api/ConfigController.cs b/Api/ConfigController.cs index 6aee70c..81db693 100644 --- a/Api/ConfigController.cs +++ b/Api/ConfigController.cs @@ -87,4 +87,39 @@ public class ConfigController(IConfigService configService) : BaseApiController return Error($"删除配置失败: {ex.Message}", 500); } } + + [HttpGet("backup")] + public async Task>>> BackupConfigs() + { + try + { + var backup = await configService.BackupConfigsAsync(); + return Success(backup, "配置备份成功"); + } + catch (Exception ex) + { + return Error>($"配置备份失败: {ex.Message}", 500); + } + } + + [HttpPost("restore")] + public async Task>> RestoreConfigs([FromBody] Dictionary configBackup) + { + try + { + if (configBackup == null || configBackup.Count == 0) + return Error("配置备份数据无效"); + + var result = await configService.RestoreConfigsAsync(configBackup); + + if (result) + return Success(true, "配置恢复成功"); + else + return Error("配置恢复失败", 500); + } + catch (Exception ex) + { + return Error($"配置恢复失败: {ex.Message}", 500); + } + } } \ No newline at end of file diff --git a/Services/Configuration/ConfigService.cs b/Services/Configuration/ConfigService.cs index 3d54f60..556d636 100644 --- a/Services/Configuration/ConfigService.cs +++ b/Services/Configuration/ConfigService.cs @@ -136,4 +136,50 @@ public class ConfigService( await using var context = await contextFactory.CreateDbContextAsync(); return await context.Configs.AnyAsync(c => c.Key == key); } + + public async Task> BackupConfigsAsync() + { + var configs = await GetAllConfigsAsync(); + var backup = new Dictionary(); + + foreach (var config in configs) + { + backup[config.Key] = config.Value; + } + + return backup; + } + + public async Task RestoreConfigsAsync(Dictionary configBackup) + { + if (configBackup == null || configBackup.Count == 0) + return false; + + try + { + await using var context = await contextFactory.CreateDbContextAsync(); + await using var transaction = await context.Database.BeginTransactionAsync(); + + try + { + foreach (var (key, value) in configBackup) + { + await SetConfigAsync(key, value); + } + + await transaction.CommitAsync(); + return true; + } + catch (Exception) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception ex) + { + logger.LogError(ex, "恢复配置时出错"); + return false; + } + } } diff --git a/Services/Configuration/IConfigService.cs b/Services/Configuration/IConfigService.cs index 3b4210d..8fcd560 100644 --- a/Services/Configuration/IConfigService.cs +++ b/Services/Configuration/IConfigService.cs @@ -16,4 +16,8 @@ public interface IConfigService Task DeleteConfigAsync(string key); Task ExistsAsync(string key); + + Task> BackupConfigsAsync(); + + Task RestoreConfigsAsync(Dictionary configBackup); } diff --git a/Web/src/api/configApi.ts b/Web/src/api/configApi.ts index 5c6de00..847c7f2 100644 --- a/Web/src/api/configApi.ts +++ b/Web/src/api/configApi.ts @@ -72,3 +72,32 @@ export const hasRole = (userRole: string | undefined, requiredRole: UserRole): b // 精确匹配角色 return userRole === requiredRole; }; + +// 备份所有配置 +export const backupConfigs = async (): Promise>> => { + try { + return await fetchApi>('/config/backup'); + } catch (error: any) { + return { + success: false, + message: `备份配置失败: ${error.message}`, + code: 500 + }; + } +}; + +// 恢复配置 +export const restoreConfigs = async (configBackup: Record): Promise> => { + try { + return await fetchApi('/config/restore', { + method: 'POST', + body: JSON.stringify(configBackup), + }); + } catch (error: any) { + return { + success: false, + message: `恢复配置失败: ${error.message}`, + code: 500 + }; + } +}; diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 73ed660..3ce6a2c 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -51,6 +51,8 @@ export { getConfig, setConfig, deleteConfig, - hasRole + hasRole, + backupConfigs, + restoreConfigs } from './configApi'; diff --git a/Web/src/pages/settings/SystemConfig.tsx b/Web/src/pages/settings/SystemConfig.tsx index afb73d1..0fa0ec1 100644 --- a/Web/src/pages/settings/SystemConfig.tsx +++ b/Web/src/pages/settings/SystemConfig.tsx @@ -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({}); 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 | 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: }, @@ -97,7 +178,38 @@ const SystemConfig: React.FC = () => { return ( + 系统配置 + + + + + + + + + + + + + } className="system-config-card" bodyStyle={{ padding: isMobile ? '12px 8px' : '24px' @@ -427,6 +539,43 @@ const SystemConfig: React.FC = () => { )} + + {/* 恢复配置确认对话框 */} + + 确认恢复配置 + + + + + } + open={restoreModalVisible} + onCancel={() => setRestoreModalVisible(false)} + footer={[ + <> + + + + ]} + > +

您确定要从上传的备份文件恢复配置吗?

+

警告:此操作将覆盖当前系统中的所有配置设置!

+ {restoreConfig && ( +
+

备份文件包含 {Object.keys(restoreConfig).length} 条配置项

+
+ )} +
); };