feat(系统配置): 实现全局配置管理功能
- 新增ConfigContext提供全局配置状态管理 - 重构系统设置页面,移除语言和深色模式选项 - 优化备份功能,支持自定义备份路径 - 应用主题颜色动态更新功能 - 添加配置变更自动重载机制
This commit is contained in:
+141
-27
@@ -1,8 +1,9 @@
|
||||
import React, { useState, Suspense, lazy } from 'react';
|
||||
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd';
|
||||
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider, ConfigProvider as AntdConfigProvider } from 'antd';
|
||||
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from './context/AuthContext';
|
||||
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
@@ -131,6 +132,7 @@ const AppLayout = ({ children }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
@@ -149,6 +151,56 @@ const AppLayout = ({ children }) => {
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
// 动态设计令牌
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: config.primary_color || '#667eea',
|
||||
gradient: `linear-gradient(135deg, ${config.primary_color || '#667eea'} 0%, ${config.secondary_color || '#764ba2'} 100%)`,
|
||||
light: '#8b9ff0'
|
||||
},
|
||||
success: { main: '#10b981' },
|
||||
warning: { main: '#f59e0b' },
|
||||
error: { main: '#ef4444' },
|
||||
text: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
inverse: '#ffffff'
|
||||
},
|
||||
background: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#f8fafc',
|
||||
dark: '#1e293b'
|
||||
},
|
||||
border: {
|
||||
light: '#e2e8f0'
|
||||
},
|
||||
sidebar: {
|
||||
bg: '#ffffff',
|
||||
bgHover: 'rgba(102, 126, 234, 0.08)',
|
||||
bgActive: 'rgba(102, 126, 234, 0.15)',
|
||||
text: '#475569',
|
||||
textHover: config.primary_color || '#667eea',
|
||||
textActive: config.primary_color || '#667eea',
|
||||
border: '#e2e8f0'
|
||||
}
|
||||
},
|
||||
shadows: {
|
||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
borderRadius: {
|
||||
small: '6px',
|
||||
medium: '10px'
|
||||
},
|
||||
spacing: {
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px'
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'dashboard',
|
||||
@@ -315,7 +367,7 @@ const AppLayout = ({ children }) => {
|
||||
color: designTokens.colors.primary.main,
|
||||
lineHeight: 1.2
|
||||
}}>
|
||||
IDC管理
|
||||
{config.site_name || 'IDC管理'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
@@ -336,7 +388,7 @@ const AppLayout = ({ children }) => {
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[getSelectedKey()]}
|
||||
defaultOpenKeys={['room-management', 'asset-management', 'consumables-management', 'system-management', 'ticket-management']}
|
||||
defaultOpenKeys={[]}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
borderRight: 0,
|
||||
@@ -451,32 +503,94 @@ const AppLayout = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ThemeConfig = () => {
|
||||
const { config } = useConfig();
|
||||
|
||||
// 动态设计令牌
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: config.primary_color || '#667eea',
|
||||
gradient: `linear-gradient(135deg, ${config.primary_color || '#667eea'} 0%, ${config.secondary_color || '#764ba2'} 100%)`,
|
||||
light: '#8b9ff0'
|
||||
},
|
||||
success: { main: '#10b981' },
|
||||
warning: { main: '#f59e0b' },
|
||||
error: { main: '#ef4444' },
|
||||
text: {
|
||||
primary: '#1e293b',
|
||||
secondary: '#64748b',
|
||||
inverse: '#ffffff'
|
||||
},
|
||||
background: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#f8fafc',
|
||||
dark: '#1e293b'
|
||||
},
|
||||
border: {
|
||||
light: '#e2e8f0'
|
||||
},
|
||||
sidebar: {
|
||||
bg: '#ffffff',
|
||||
bgHover: 'rgba(102, 126, 234, 0.08)',
|
||||
bgActive: 'rgba(102, 126, 234, 0.15)',
|
||||
text: '#475569',
|
||||
textHover: config.primary_color || '#667eea',
|
||||
textActive: config.primary_color || '#667eea',
|
||||
border: '#e2e8f0'
|
||||
}
|
||||
},
|
||||
shadows: {
|
||||
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
borderRadius: {
|
||||
small: '6px',
|
||||
medium: '10px'
|
||||
},
|
||||
spacing: {
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px'
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AntdConfigProvider theme={{ token: designTokens }}>
|
||||
<Router>
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
|
||||
<Route path="/devices" element={<PrivateRoute><DeviceManagement /></PrivateRoute>} />
|
||||
<Route path="/racks" element={<PrivateRoute><RackManagement /></PrivateRoute>} />
|
||||
<Route path="/rooms" element={<PrivateRoute><RoomManagement /></PrivateRoute>} />
|
||||
<Route path="/fields" element={<PrivateRoute><DeviceFieldManagement /></PrivateRoute>} />
|
||||
<Route path="/visualization" element={<PrivateRoute><RackVisualization /></PrivateRoute>} />
|
||||
<Route path="/consumables" element={<PrivateRoute><ConsumableManagement /></PrivateRoute>} />
|
||||
<Route path="/consumables-categories" element={<PrivateRoute><CategoryManagement /></PrivateRoute>} />
|
||||
<Route path="/consumables-stats" element={<PrivateRoute><ConsumableStatistics /></PrivateRoute>} />
|
||||
<Route path="/consumables-logs" element={<PrivateRoute><ConsumableLogs /></PrivateRoute>} />
|
||||
<Route path="/users" element={<PrivateRoute><UserManagement /></PrivateRoute>} />
|
||||
<Route path="/tickets" element={<PrivateRoute><TicketManagement /></PrivateRoute>} />
|
||||
<Route path="/ticket-categories" element={<PrivateRoute><TicketCategoryManagement /></PrivateRoute>} />
|
||||
<Route path="/ticket-statistics" element={<PrivateRoute><TicketStatistics /></PrivateRoute>} />
|
||||
<Route path="/ticket-fields" element={<PrivateRoute><TicketFieldManagement /></PrivateRoute>} />
|
||||
<Route path="/settings" element={<PrivateRoute><SystemSettings /></PrivateRoute>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
</AntdConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Suspense fallback={<PageLoading />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
|
||||
<Route path="/devices" element={<PrivateRoute><DeviceManagement /></PrivateRoute>} />
|
||||
<Route path="/racks" element={<PrivateRoute><RackManagement /></PrivateRoute>} />
|
||||
<Route path="/rooms" element={<PrivateRoute><RoomManagement /></PrivateRoute>} />
|
||||
<Route path="/fields" element={<PrivateRoute><DeviceFieldManagement /></PrivateRoute>} />
|
||||
<Route path="/visualization" element={<PrivateRoute><RackVisualization /></PrivateRoute>} />
|
||||
<Route path="/consumables" element={<PrivateRoute><ConsumableManagement /></PrivateRoute>} />
|
||||
<Route path="/consumables-categories" element={<PrivateRoute><CategoryManagement /></PrivateRoute>} />
|
||||
<Route path="/consumables-stats" element={<PrivateRoute><ConsumableStatistics /></PrivateRoute>} />
|
||||
<Route path="/consumables-logs" element={<PrivateRoute><ConsumableLogs /></PrivateRoute>} />
|
||||
<Route path="/users" element={<PrivateRoute><UserManagement /></PrivateRoute>} />
|
||||
<Route path="/tickets" element={<PrivateRoute><TicketManagement /></PrivateRoute>} />
|
||||
<Route path="/ticket-categories" element={<PrivateRoute><TicketCategoryManagement /></PrivateRoute>} />
|
||||
<Route path="/ticket-statistics" element={<PrivateRoute><TicketStatistics /></PrivateRoute>} />
|
||||
<Route path="/ticket-fields" element={<PrivateRoute><TicketFieldManagement /></PrivateRoute>} />
|
||||
<Route path="/settings" element={<PrivateRoute><SystemSettings /></PrivateRoute>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
<ConfigProvider>
|
||||
<ThemeConfig />
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { createContext, useState, useEffect, useContext } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
const ConfigContext = createContext();
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState({
|
||||
// 默认配置
|
||||
site_name: '机柜管理系统',
|
||||
primary_color: '#667eea',
|
||||
secondary_color: '#764ba2',
|
||||
sidebar_collapsed: false,
|
||||
compact_mode: false,
|
||||
animation_enabled: true,
|
||||
language: 'zh-CN',
|
||||
timezone: 'Asia/Shanghai',
|
||||
date_format: 'YYYY-MM-DD',
|
||||
session_timeout: 30,
|
||||
max_login_attempts: 5,
|
||||
maintenance_mode: false
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 加载系统配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/system-settings');
|
||||
const settings = response.data;
|
||||
const configValues = {};
|
||||
|
||||
// 将配置转换为扁平结构
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
configValues[key] = value.value;
|
||||
});
|
||||
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
...configValues
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('加载系统配置失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载配置
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
// 更新配置
|
||||
const updateConfig = (newConfig) => {
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
...newConfig
|
||||
}));
|
||||
};
|
||||
|
||||
// 重新加载配置
|
||||
const reloadConfig = async () => {
|
||||
await loadConfig();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, loading, updateConfig, reloadConfig }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// 自定义钩子,方便组件使用配置
|
||||
export const useConfig = () => {
|
||||
const context = useContext(ConfigContext);
|
||||
if (!context) {
|
||||
throw new Error('useConfig must be used within a ConfigProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Table, Tag, Progress, Divider, Descriptions, Alert } from 'antd';
|
||||
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, DatabaseOutlined, InfoCircleOutlined, CloudUploadOutlined, DeleteOutlined, ReloadOutlined, DownloadOutlined, SyncOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { useConfig } from '../context/ConfigContext';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
@@ -15,6 +16,7 @@ const SystemSettings = () => {
|
||||
const [backupLoading, setBackupLoading] = useState(false);
|
||||
const [systemInfo, setSystemInfo] = useState(null);
|
||||
const [form] = Form.useForm();
|
||||
const { reloadConfig } = useConfig();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -74,6 +76,8 @@ const SystemSettings = () => {
|
||||
await axios.put('/api/system-settings', { settings: updates });
|
||||
message.success('设置保存成功');
|
||||
fetchSettings();
|
||||
// 重新加载全局配置,使更改立即生效
|
||||
await reloadConfig();
|
||||
} catch (error) {
|
||||
message.error('保存设置失败');
|
||||
} finally {
|
||||
@@ -151,6 +155,8 @@ const SystemSettings = () => {
|
||||
await axios.post(`/api/system-settings/reset/${key}`);
|
||||
message.success('重置成功');
|
||||
fetchSettings();
|
||||
// 重新加载全局配置,使更改立即生效
|
||||
await reloadConfig();
|
||||
} catch (error) {
|
||||
message.error('重置失败');
|
||||
}
|
||||
@@ -237,10 +243,29 @@ const SystemSettings = () => {
|
||||
{ value: 'DD/MM/YYYY', label: '01/01/2024' },
|
||||
{ value: 'MM/DD/YYYY', label: '01/01/2024' }
|
||||
],
|
||||
language: [
|
||||
{ value: 'zh-CN', label: '简体中文' },
|
||||
{ value: 'zh-TW', label: '繁體中文' },
|
||||
{ value: 'en-US', label: 'English' }
|
||||
primary_color: [
|
||||
{ value: '#667eea', label: '蓝色 (#667eea)' },
|
||||
{ value: '#764ba2', label: '紫色 (#764ba2)' },
|
||||
{ value: '#f093fb', label: '粉色 (#f093fb)' },
|
||||
{ value: '#4facfe', label: '浅蓝色 (#4facfe)' },
|
||||
{ value: '#43e97b', label: '绿色 (#43e97b)' },
|
||||
{ value: '#fa709a', label: '红色 (#fa709a)' },
|
||||
{ value: '#fee140', label: '黄色 (#fee140)' },
|
||||
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
||||
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
||||
{ value: '#fcb045', label: '橙色 (#fcb045)' }
|
||||
],
|
||||
secondary_color: [
|
||||
{ value: '#764ba2', label: '紫色 (#764ba2)' },
|
||||
{ value: '#667eea', label: '蓝色 (#667eea)' },
|
||||
{ value: '#4facfe', label: '浅蓝色 (#4facfe)' },
|
||||
{ value: '#43e97b', label: '绿色 (#43e97b)' },
|
||||
{ value: '#fa709a', label: '红色 (#fa709a)' },
|
||||
{ value: '#fee140', label: '黄色 (#fee140)' },
|
||||
{ value: '#00b4db', label: '青色 (#00b4db)' },
|
||||
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
|
||||
{ value: '#fcb045', label: '橙色 (#fcb045)' },
|
||||
{ value: '#f093fb', label: '粉色 (#f093fb)' }
|
||||
],
|
||||
table_row_height: [
|
||||
{ value: 'small', label: '紧凑 (Small)' },
|
||||
@@ -273,11 +298,18 @@ const SystemSettings = () => {
|
||||
};
|
||||
|
||||
const renderGeneralSettings = () => {
|
||||
const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'language', 'session_timeout', 'max_login_attempts', 'maintenance_mode'];
|
||||
const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'session_timeout', 'max_login_attempts', 'maintenance_mode'];
|
||||
return (
|
||||
<Card title="全局配置" bordered={false}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||
{generalKeys.map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||
{generalKeys.map(key => {
|
||||
// 确保时区和日期格式使用select类型
|
||||
const settingData = { ...settings[key] };
|
||||
if (key === 'timezone' || key === 'date_format') {
|
||||
settingData.type = 'select';
|
||||
}
|
||||
return renderFormItem(key, settingData);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||
@@ -290,18 +322,25 @@ const SystemSettings = () => {
|
||||
};
|
||||
|
||||
const renderAppearanceSettings = () => {
|
||||
const appearanceKeys = ['primary_color', 'secondary_color', 'dark_mode', 'compact_mode', 'sidebar_collapsed', 'table_row_height', 'animation_enabled'];
|
||||
const appearanceKeys = ['primary_color', 'secondary_color', 'compact_mode', 'sidebar_collapsed', 'table_row_height', 'animation_enabled'];
|
||||
return (
|
||||
<Card title="外观设置" bordered={false}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||
<Alert
|
||||
message="主题颜色"
|
||||
description="修改主题颜色后需要刷新页面才能生效。深色模式可以减少眼睛疲劳。"
|
||||
description="修改主题颜色后需要刷新页面才能生效。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
{appearanceKeys.map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||
{appearanceKeys.map(key => {
|
||||
// 确保主题颜色使用select类型
|
||||
const settingData = { ...settings[key] };
|
||||
if (key === 'primary_color' || key === 'secondary_color') {
|
||||
settingData.type = 'select';
|
||||
}
|
||||
return renderFormItem(key, settingData);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||
@@ -314,14 +353,6 @@ const SystemSettings = () => {
|
||||
};
|
||||
|
||||
const renderBackupSettings = () => {
|
||||
const backupInfo = settings.backup_retention ? {
|
||||
auto_backup_enabled: settings.auto_backup_enabled,
|
||||
backup_interval: settings.backup_interval,
|
||||
backup_retention: settings.backup_retention,
|
||||
last_backup_time: settings.last_backup_time,
|
||||
backup_count: settings.backup_count
|
||||
} : null;
|
||||
|
||||
const backupColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
@@ -357,13 +388,19 @@ const SystemSettings = () => {
|
||||
}
|
||||
];
|
||||
|
||||
// 备份设置键列表
|
||||
const backupKeys = ['auto_backup_enabled', 'backup_interval', 'backup_retention', 'backup_path', 'last_backup_time', 'backup_count'];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card title="自动备份设置" bordered={false} style={{ marginBottom: 16 }}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||
{backupInfo && Object.entries(backupInfo).map(([key, data]) => (
|
||||
data && typeof data === 'object' ? renderFormItem(key, data) : null
|
||||
))}
|
||||
{backupKeys.map(key => {
|
||||
if (settings[key]) {
|
||||
return renderFormItem(key, settings[key]);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||
|
||||
Reference in New Issue
Block a user