2026-02-11 10:21:32 +08:00
|
|
|
import React, { useState, useEffect, Suspense, lazy } from 'react';
|
2026-02-10 11:04:01 +08:00
|
|
|
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,
|
|
|
|
|
ApiOutlined,
|
|
|
|
|
PartitionOutlined,
|
|
|
|
|
CodepenOutlined,
|
2026-03-10 09:20:31 +08:00
|
|
|
CloudUploadOutlined,
|
2026-02-10 11:04:01 +08:00
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import {
|
|
|
|
|
BrowserRouter as Router,
|
|
|
|
|
Routes,
|
|
|
|
|
Route,
|
|
|
|
|
Link,
|
|
|
|
|
Navigate,
|
|
|
|
|
useLocation,
|
|
|
|
|
useNavigate,
|
|
|
|
|
} from 'react-router-dom';
|
2025-12-25 10:48:35 +08:00
|
|
|
import { useAuth } from './context/AuthContext';
|
2026-01-21 10:40:30 +08:00
|
|
|
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
2026-01-29 13:09:43 +08:00
|
|
|
import { Scene3DProvider } from './context/Scene3DContext';
|
2026-01-29 16:53:29 +08:00
|
|
|
import { useDesignTokens } from './hooks/useDesignTokens';
|
2026-02-11 10:21:32 +08:00
|
|
|
import useIdleTimeout from './hooks/useIdleTimeout';
|
2026-03-10 14:34:39 +08:00
|
|
|
import { SWRConfig, swrConfig } from './hooks/useSWR';
|
2026-02-11 10:21:32 +08:00
|
|
|
import axios from 'axios';
|
2025-12-25 10:48:35 +08:00
|
|
|
import { Spin } from 'antd';
|
2026-03-13 15:13:58 +08:00
|
|
|
import ErrorBoundary from './components/ErrorBoundary';
|
2025-12-18 10:45:13 +08:00
|
|
|
|
2025-12-26 11:03:47 +08:00
|
|
|
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
|
|
|
|
const DeviceManagement = lazy(() => import('./pages/DeviceManagement'));
|
|
|
|
|
const RackManagement = lazy(() => import('./pages/RackManagement'));
|
|
|
|
|
const RoomManagement = lazy(() => import('./pages/RoomManagement'));
|
|
|
|
|
const DeviceFieldManagement = lazy(() => import('./pages/DeviceFieldManagement'));
|
2026-01-26 17:21:20 +08:00
|
|
|
const Rack3DVisualization = lazy(() => import('./pages/Rack3DVisualization'));
|
2025-12-26 11:03:47 +08:00
|
|
|
const ConsumableManagement = lazy(() => import('./pages/ConsumableManagement'));
|
|
|
|
|
const ConsumableStatistics = lazy(() => import('./pages/ConsumableStatistics'));
|
|
|
|
|
const ConsumableLogs = lazy(() => import('./pages/ConsumableLogs'));
|
|
|
|
|
const CategoryManagement = lazy(() => import('./pages/CategoryManagement'));
|
|
|
|
|
const UserManagement = lazy(() => import('./pages/UserManagement'));
|
|
|
|
|
const Login = lazy(() => import('./pages/Login'));
|
|
|
|
|
const TicketManagement = lazy(() => import('./pages/TicketManagement'));
|
|
|
|
|
const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement'));
|
|
|
|
|
const TicketStatistics = lazy(() => import('./pages/TicketStatistics'));
|
2025-12-29 08:40:08 +08:00
|
|
|
const TicketFieldManagement = lazy(() => import('./pages/TicketFieldManagement'));
|
2025-12-29 16:10:42 +08:00
|
|
|
const SystemSettings = lazy(() => import('./pages/SystemSettings'));
|
2026-01-23 08:55:16 +08:00
|
|
|
const CableManagement = lazy(() => import('./pages/CableManagement'));
|
|
|
|
|
const PortManagement = lazy(() => import('./pages/PortManagement'));
|
2026-02-26 11:24:00 +08:00
|
|
|
const InventoryManagement = lazy(() => import('./pages/InventoryManagement'));
|
|
|
|
|
const InventoryTaskExecution = lazy(() => import('./pages/InventoryTaskExecution'));
|
2026-03-10 09:20:31 +08:00
|
|
|
const PendingDeviceManagement = lazy(() => import('./pages/PendingDeviceManagement'));
|
2026-03-13 15:13:58 +08:00
|
|
|
const BackupManagement = lazy(() => import('./pages/BackupManagement'));
|
|
|
|
|
const AutoBackupSettings = lazy(() => import('./pages/AutoBackupSettings'));
|
2026-03-13 22:11:34 +08:00
|
|
|
const RemoteBackupSettings = lazy(() => import('./pages/RemoteBackupSettings'));
|
2026-03-13 15:13:58 +08:00
|
|
|
const ErrorBoundaryTest = lazy(() => import('./pages/ErrorBoundaryTest'));
|
2025-12-26 11:03:47 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
const { Header, Content, Sider } = Layout;
|
2025-12-18 10:45:13 +08:00
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
const PageLoading = () => (
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
height: '100vh',
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
gap: '16px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-20 14:31:20 +08:00
|
|
|
<Spin size="large" />
|
|
|
|
|
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载页面...</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const AuthLoading = () => (
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
height: '100vh',
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
gap: '16px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-20 14:31:20 +08:00
|
|
|
<Spin size="large" />
|
|
|
|
|
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>正在加载认证状态...</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
const PrivateRoute = ({ children }) => {
|
|
|
|
|
const { token, initialized, loading } = useAuth();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
|
|
|
|
|
if (!initialized) {
|
2026-01-20 14:31:20 +08:00
|
|
|
return <AuthLoading />;
|
2025-12-25 10:48:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
return <AppLayout>{children}</AppLayout>;
|
2025-12-25 10:48:35 +08:00
|
|
|
};
|
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
const ProtectedRoute = ({ component: Component }) => (
|
|
|
|
|
<PrivateRoute>
|
|
|
|
|
<Component />
|
|
|
|
|
</PrivateRoute>
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-11 10:21:32 +08:00
|
|
|
// 默认空闲超时配置
|
|
|
|
|
const DEFAULT_IDLE_CONFIG = {
|
2026-03-06 13:57:37 +08:00
|
|
|
timeout: 30 * 60 * 1000, // 30分钟
|
|
|
|
|
warningTime: 60 * 1000, // 60秒
|
2026-02-11 10:21:32 +08:00
|
|
|
};
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
const AppLayout = ({ children }) => {
|
2025-12-19 20:01:15 +08:00
|
|
|
const [collapsed, setCollapsed] = useState(false);
|
2026-01-20 11:28:53 +08:00
|
|
|
const [activeKey, setActiveKey] = useState('dashboard');
|
2026-02-11 10:21:32 +08:00
|
|
|
const [idleConfig, setIdleConfig] = useState(DEFAULT_IDLE_CONFIG);
|
2025-12-25 10:48:35 +08:00
|
|
|
const { user, logout } = useAuth();
|
2026-01-29 16:53:29 +08:00
|
|
|
const { config } = useConfig();
|
2025-12-25 10:48:35 +08:00
|
|
|
const navigate = useNavigate();
|
2026-01-20 11:28:53 +08:00
|
|
|
const location = useLocation();
|
2026-01-29 16:53:29 +08:00
|
|
|
const designTokens = useDesignTokens();
|
2026-02-10 11:04:01 +08:00
|
|
|
|
2026-02-11 10:21:32 +08:00
|
|
|
// 获取空闲超时配置
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchIdleConfig = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/api/system-settings/idle-timeout');
|
|
|
|
|
setIdleConfig({
|
|
|
|
|
timeout: response.data.timeout,
|
|
|
|
|
warningTime: response.data.warningTime,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('获取空闲超时配置失败,使用默认配置:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchIdleConfig();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 启用空闲超时检测
|
|
|
|
|
useIdleTimeout({
|
|
|
|
|
timeout: idleConfig.timeout,
|
|
|
|
|
warningTime: idleConfig.warningTime,
|
|
|
|
|
onLogout: logout,
|
|
|
|
|
enabled: true,
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
const handleLogout = () => {
|
|
|
|
|
logout();
|
|
|
|
|
message.success('已退出登录');
|
|
|
|
|
navigate('/login');
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
const getSelectedKey = () => {
|
|
|
|
|
const path = location.pathname;
|
|
|
|
|
if (path === '/') return 'dashboard';
|
2026-01-26 17:21:20 +08:00
|
|
|
if (path.startsWith('/visualization-3d')) return 'visualization-3d';
|
|
|
|
|
if (path.startsWith('/rooms') || path.startsWith('/racks')) return 'room-management';
|
2026-02-10 11:04:01 +08:00
|
|
|
if (
|
|
|
|
|
path.startsWith('/devices') ||
|
|
|
|
|
path.startsWith('/fields') ||
|
|
|
|
|
path.startsWith('/cables') ||
|
|
|
|
|
path.startsWith('/ports')
|
|
|
|
|
)
|
|
|
|
|
return 'asset-management';
|
2026-01-20 11:28:53 +08:00
|
|
|
if (path.startsWith('/consumables')) return 'consumables-management';
|
2026-02-10 11:04:01 +08:00
|
|
|
if (
|
|
|
|
|
path.startsWith('/users') ||
|
|
|
|
|
path.startsWith('/login-history') ||
|
|
|
|
|
path.startsWith('/operation-logs') ||
|
2026-03-13 15:13:58 +08:00
|
|
|
path.startsWith('/settings') ||
|
|
|
|
|
path.startsWith('/backup')
|
2026-02-10 11:04:01 +08:00
|
|
|
)
|
|
|
|
|
return 'system-management';
|
2026-01-20 11:28:53 +08:00
|
|
|
if (path.startsWith('/tickets')) return 'ticket-management';
|
2026-03-10 09:20:31 +08:00
|
|
|
if (path.startsWith('/inventory') || path.startsWith('/pending-devices')) return 'inventory-management';
|
2026-01-20 11:28:53 +08:00
|
|
|
return 'dashboard';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const menuItems = [
|
2025-12-25 10:48:35 +08:00
|
|
|
{
|
2026-01-20 11:28:53 +08:00
|
|
|
key: 'dashboard',
|
|
|
|
|
icon: <BarChartOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: <Link to="/">仪表盘</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'room-management',
|
|
|
|
|
icon: <HomeOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '机房管理',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'rooms',
|
|
|
|
|
icon: <HomeOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/rooms">机房管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'racks',
|
|
|
|
|
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/racks">机柜管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-01-26 17:21:20 +08:00
|
|
|
key: 'visualization-3d',
|
|
|
|
|
icon: <CodepenOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/visualization-3d">3D机柜可视化</Link>,
|
2026-01-20 11:28:53 +08:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'asset-management',
|
|
|
|
|
icon: <BuildOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '资产管理',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'devices',
|
|
|
|
|
icon: <CloudServerOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/devices">设备管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'fields',
|
|
|
|
|
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/fields">字段管理</Link>,
|
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
{
|
|
|
|
|
key: 'cables',
|
|
|
|
|
icon: <ApiOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/cables">接线管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ports',
|
|
|
|
|
icon: <PartitionOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/ports">端口管理</Link>,
|
|
|
|
|
},
|
2026-01-20 11:28:53 +08:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'consumables-management',
|
|
|
|
|
icon: <ShoppingCartOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '耗材管理',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'consumables-stats',
|
|
|
|
|
icon: <BarChartOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/consumables-stats">耗材统计</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'consumables',
|
|
|
|
|
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/consumables">耗材列表</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'consumables-categories',
|
|
|
|
|
icon: <ImportOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/consumables-categories">分类管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'consumables-logs',
|
|
|
|
|
icon: <FileTextOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/consumables-logs">操作日志</Link>,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ticket-management',
|
|
|
|
|
icon: <ToolOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '工单管理',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'tickets',
|
|
|
|
|
icon: <ScheduleOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/tickets">工单列表</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ticket-categories',
|
|
|
|
|
icon: <InboxOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/ticket-categories">故障分类</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ticket-statistics',
|
|
|
|
|
icon: <BarChartOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/ticket-statistics">统计报表</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'ticket-fields',
|
|
|
|
|
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/ticket-fields">字段管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2026-02-26 11:24:00 +08:00
|
|
|
{
|
|
|
|
|
key: 'inventory-management',
|
|
|
|
|
icon: <InboxOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '资产盘点',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'inventory',
|
|
|
|
|
icon: <InboxOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/inventory">盘点计划</Link>,
|
|
|
|
|
},
|
2026-03-10 09:20:31 +08:00
|
|
|
{
|
|
|
|
|
key: 'pending-devices',
|
|
|
|
|
icon: <CloudUploadOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/pending-devices">暂存设备</Link>,
|
|
|
|
|
},
|
2026-02-26 11:24:00 +08:00
|
|
|
],
|
|
|
|
|
},
|
2026-01-20 16:54:26 +08:00
|
|
|
{
|
2026-02-10 11:04:01 +08:00
|
|
|
key: 'system-management',
|
|
|
|
|
icon: <UserOutlined style={{ fontSize: '18px' }} />,
|
|
|
|
|
label: '系统管理',
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
key: 'users',
|
|
|
|
|
icon: <UserOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/users">用户管理</Link>,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'system-settings',
|
|
|
|
|
icon: <SettingOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/settings">系统设置</Link>,
|
|
|
|
|
},
|
2026-03-13 15:13:58 +08:00
|
|
|
{
|
|
|
|
|
key: 'backup',
|
|
|
|
|
icon: <DatabaseOutlined style={{ fontSize: '16px' }} />,
|
|
|
|
|
label: <Link to="/backup">数据备份</Link>,
|
|
|
|
|
},
|
2026-02-10 11:04:01 +08:00
|
|
|
],
|
|
|
|
|
},
|
2025-12-25 10:48:35 +08:00
|
|
|
];
|
|
|
|
|
|
2025-12-18 10:45:13 +08:00
|
|
|
return (
|
2026-01-20 11:28:53 +08:00
|
|
|
<Layout style={{ minHeight: '100vh' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
<Sider
|
|
|
|
|
width={240}
|
2026-01-20 11:28:53 +08:00
|
|
|
collapsedWidth={72}
|
2025-12-25 10:48:35 +08:00
|
|
|
collapsed={collapsed}
|
|
|
|
|
style={{
|
2026-01-20 11:28:53 +08:00
|
|
|
background: designTokens.colors.sidebar.bg,
|
|
|
|
|
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.06)',
|
|
|
|
|
borderRight: `1px solid ${designTokens.colors.sidebar.border}`,
|
2025-12-25 10:48:35 +08:00
|
|
|
display: 'flex',
|
2026-01-20 11:28:53 +08:00
|
|
|
flexDirection: 'column',
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
left: 0,
|
|
|
|
|
top: 0,
|
|
|
|
|
bottom: 0,
|
2026-02-10 11:04:01 +08:00
|
|
|
zIndex: 100,
|
2025-12-25 10:48:35 +08:00
|
|
|
}}
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
2026-01-20 11:28:53 +08:00
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
2026-02-10 11:04:01 +08:00
|
|
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
|
|
|
|
padding: collapsed ? '16px 0' : '16px 20px',
|
|
|
|
|
borderBottom: `1px solid ${designTokens.colors.sidebar.border}`,
|
|
|
|
|
minHeight: '64px',
|
|
|
|
|
background: designTokens.colors.sidebar.bg,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: '36px',
|
|
|
|
|
height: '36px',
|
|
|
|
|
borderRadius: designTokens.borderRadius.small,
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
<CloudServerOutlined style={{ fontSize: '18px', color: '#ffffff' }} />
|
|
|
|
|
</div>
|
|
|
|
|
{!collapsed && (
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '15px',
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
color: designTokens.colors.primary.main,
|
|
|
|
|
lineHeight: 1.2,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-21 10:40:30 +08:00
|
|
|
{config.site_name || 'IDC管理'}
|
2026-01-20 11:28:53 +08:00
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
color: designTokens.colors.sidebar.text,
|
|
|
|
|
marginTop: '2px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
数据中心管理平台
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
padding: collapsed ? '12px 0' : '12px 8px',
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
flex: 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
<Menu
|
|
|
|
|
mode="inline"
|
|
|
|
|
selectedKeys={[getSelectedKey()]}
|
2026-01-21 10:40:30 +08:00
|
|
|
defaultOpenKeys={[]}
|
2026-01-20 11:28:53 +08:00
|
|
|
style={{
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
borderRight: 0,
|
2026-02-10 11:04:01 +08:00
|
|
|
fontSize: '14px',
|
2026-01-20 11:28:53 +08:00
|
|
|
}}
|
|
|
|
|
items={menuItems}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
padding: '12px',
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.sidebar.border}`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
|
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
|
|
|
style={{
|
2026-01-20 11:28:53 +08:00
|
|
|
width: '100%',
|
|
|
|
|
height: '40px',
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
borderRadius: designTokens.borderRadius.small,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
2026-02-10 11:04:01 +08:00
|
|
|
gap: '8px',
|
2025-12-25 10:48:35 +08:00
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
{!collapsed && (
|
|
|
|
|
<span style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
|
|
|
|
收起菜单
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-01-20 11:28:53 +08:00
|
|
|
</Button>
|
2025-12-25 10:48:35 +08:00
|
|
|
</div>
|
|
|
|
|
</Sider>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
<Layout
|
|
|
|
|
style={{
|
|
|
|
|
marginLeft: collapsed ? 72 : 240,
|
|
|
|
|
transition: 'margin-left 0.2s ease',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Header
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0 24px',
|
|
|
|
|
height: 64,
|
|
|
|
|
background: designTokens.colors.background.primary,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
boxShadow: designTokens.shadows.small,
|
|
|
|
|
position: 'sticky',
|
|
|
|
|
top: 0,
|
|
|
|
|
zIndex: 99,
|
|
|
|
|
overflow: 'visible',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
{user && (
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
2026-01-20 11:28:53 +08:00
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
2026-02-10 11:04:01 +08:00
|
|
|
gap: '12px',
|
|
|
|
|
height: '100%',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: '8px',
|
|
|
|
|
padding: '4px 12px',
|
|
|
|
|
background: designTokens.colors.background.secondary,
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
height: '40px',
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Avatar
|
|
|
|
|
style={{
|
2026-01-20 11:28:53 +08:00
|
|
|
backgroundColor: designTokens.colors.primary.main,
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
width: 32,
|
|
|
|
|
height: 32,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
2026-02-10 11:04:01 +08:00
|
|
|
justifyContent: 'center',
|
2026-01-20 11:28:53 +08:00
|
|
|
}}
|
|
|
|
|
icon={<UserOutlined style={{ fontSize: '14px' }} />}
|
|
|
|
|
/>
|
2026-02-10 11:04:01 +08:00
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{user.username}
|
|
|
|
|
</span>
|
2026-01-20 11:28:53 +08:00
|
|
|
</div>
|
2025-12-25 10:48:35 +08:00
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
danger
|
|
|
|
|
icon={<LogoutOutlined />}
|
|
|
|
|
onClick={handleLogout}
|
2026-03-10 13:12:29 +08:00
|
|
|
className="logout-btn"
|
2025-12-25 10:48:35 +08:00
|
|
|
>
|
|
|
|
|
退出
|
|
|
|
|
</Button>
|
2026-01-20 11:28:53 +08:00
|
|
|
</div>
|
2025-12-25 10:48:35 +08:00
|
|
|
)}
|
|
|
|
|
</Header>
|
|
|
|
|
<Content
|
|
|
|
|
style={{
|
|
|
|
|
padding: 24,
|
|
|
|
|
margin: 0,
|
2026-01-20 11:28:53 +08:00
|
|
|
minHeight: 'calc(100vh - 64px)',
|
2026-02-10 11:04:01 +08:00
|
|
|
background: designTokens.colors.background.secondary,
|
2025-12-19 20:01:15 +08:00
|
|
|
}}
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
{children}
|
|
|
|
|
</Content>
|
2025-12-18 10:45:13 +08:00
|
|
|
</Layout>
|
2025-12-25 10:48:35 +08:00
|
|
|
</Layout>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 14:34:39 +08:00
|
|
|
const routeConfig = [
|
|
|
|
|
{ path: '/', component: Dashboard },
|
|
|
|
|
{ path: '/devices', component: DeviceManagement },
|
|
|
|
|
{ path: '/racks', component: RackManagement },
|
|
|
|
|
{ path: '/rooms', component: RoomManagement },
|
|
|
|
|
{ path: '/fields', component: DeviceFieldManagement },
|
|
|
|
|
{ path: '/consumables', component: ConsumableManagement },
|
|
|
|
|
{ path: '/consumables-categories', component: CategoryManagement },
|
|
|
|
|
{ path: '/consumables-stats', component: ConsumableStatistics },
|
|
|
|
|
{ path: '/consumables-logs', component: ConsumableLogs },
|
|
|
|
|
{ path: '/users', component: UserManagement },
|
|
|
|
|
{ path: '/tickets', component: TicketManagement },
|
|
|
|
|
{ path: '/ticket-categories', component: TicketCategoryManagement },
|
|
|
|
|
{ path: '/ticket-statistics', component: TicketStatistics },
|
|
|
|
|
{ path: '/ticket-fields', component: TicketFieldManagement },
|
|
|
|
|
{ path: '/settings', component: SystemSettings },
|
|
|
|
|
{ path: '/cables', component: CableManagement },
|
|
|
|
|
{ path: '/inventory', component: InventoryManagement },
|
|
|
|
|
{ path: '/inventory/execution', component: InventoryTaskExecution },
|
|
|
|
|
{ path: '/pending-devices', component: PendingDeviceManagement },
|
|
|
|
|
{ path: '/ports', component: PortManagement },
|
2026-03-13 15:13:58 +08:00
|
|
|
{ path: '/backup', component: BackupManagement },
|
|
|
|
|
{ path: '/auto-backup-settings', component: AutoBackupSettings },
|
2026-03-13 22:11:34 +08:00
|
|
|
{ path: '/remote-backup-settings', component: RemoteBackupSettings },
|
2026-03-13 15:13:58 +08:00
|
|
|
{ path: '/error-boundary-test', component: ErrorBoundaryTest },
|
2026-03-10 14:34:39 +08:00
|
|
|
];
|
|
|
|
|
|
2026-01-21 10:40:30 +08:00
|
|
|
const ThemeConfig = () => {
|
2026-01-29 16:53:29 +08:00
|
|
|
const designTokens = useDesignTokens();
|
2026-01-21 10:40:30 +08:00
|
|
|
|
2026-03-13 15:13:58 +08:00
|
|
|
const renderRoute = (path, Component, needsErrorBoundary = true) => {
|
|
|
|
|
const element = (
|
|
|
|
|
<PrivateRoute>
|
|
|
|
|
<Component />
|
|
|
|
|
</PrivateRoute>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (needsErrorBoundary) {
|
|
|
|
|
return (
|
|
|
|
|
<Route
|
|
|
|
|
key={path}
|
|
|
|
|
path={path}
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary
|
|
|
|
|
fullPage={false}
|
|
|
|
|
title="页面加载失败"
|
|
|
|
|
subTitle="该页面在加载过程中遇到了错误,请尝试重新加载"
|
|
|
|
|
>
|
|
|
|
|
{element}
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <Route key={path} path={path} element={element} />;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-21 10:40:30 +08:00
|
|
|
return (
|
|
|
|
|
<AntdConfigProvider theme={{ token: designTokens }}>
|
2026-03-10 14:34:39 +08:00
|
|
|
<SWRConfig value={swrConfig}>
|
|
|
|
|
<Router>
|
|
|
|
|
<Suspense fallback={<PageLoading />}>
|
|
|
|
|
<Routes>
|
|
|
|
|
<Route
|
2026-03-13 15:13:58 +08:00
|
|
|
path="/login"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary fullPage>
|
|
|
|
|
<Login />
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
{routeConfig.map(({ path, component: Component }) =>
|
|
|
|
|
renderRoute(path, Component)
|
|
|
|
|
)}
|
|
|
|
|
<Route
|
|
|
|
|
path="/visualization-3d"
|
2026-03-10 14:34:39 +08:00
|
|
|
element={
|
|
|
|
|
<PrivateRoute>
|
2026-03-13 15:13:58 +08:00
|
|
|
<ErrorBoundary
|
|
|
|
|
fullPage={false}
|
|
|
|
|
title="3D 可视化加载失败"
|
|
|
|
|
subTitle="3D 场景在加载过程中遇到错误,可能是浏览器不支持 WebGL 或模型文件加载失败"
|
|
|
|
|
>
|
|
|
|
|
<Scene3DProvider>
|
|
|
|
|
<Rack3DVisualization />
|
|
|
|
|
</Scene3DProvider>
|
|
|
|
|
</ErrorBoundary>
|
2026-03-10 14:34:39 +08:00
|
|
|
</PrivateRoute>
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-03-13 15:13:58 +08:00
|
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
|
|
|
</Routes>
|
|
|
|
|
</Suspense>
|
|
|
|
|
</Router>
|
2026-03-10 14:34:39 +08:00
|
|
|
</SWRConfig>
|
2026-01-21 10:40:30 +08:00
|
|
|
</AntdConfigProvider>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
function App() {
|
|
|
|
|
return (
|
2026-01-21 10:40:30 +08:00
|
|
|
<ConfigProvider>
|
|
|
|
|
<ThemeConfig />
|
|
|
|
|
</ConfigProvider>
|
2025-12-18 10:45:13 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
export default App;
|