-
- 导入结果:
+
+
+
+ 导入结果:
+
+
+ ✓ 总记录数:{importResult.total || 0}
+
+
+ ✓ 成功导入:{importResult.successCount || 0}
+
+ {importResult.failedCount > 0 && (
+
+ ✗ 导入失败:{importResult.failedCount}
-
- ✓ 总记录数:{importResult.total || 0}
-
-
- ✓ 成功:{importResult.imported || 0}
-
-
0 ? '#faad14' : '#52c41a' }}>
- {importResult.duplicates > 0 ? '⚠' : '✓'} 重复:{importResult.duplicates || 0}
-
- {importResult.details && importResult.details.length > 0 && (
-
-
- 导入失败记录:
-
-
- {importResult.details.map((item, index) => (
- -
- 第 {item.row} 行
-
- {item.errors.map((err, errIndex) => (
- - {err}
- ))}
-
-
- ))}
-
+ )}
+
+ {importResult.errors && importResult.errors.length > 0 && (
+
+
错误详情:
+ {importResult.errors.slice(0, 5).map((err, idx) => (
+
+ 第{err.row || idx + 1}行:{err.error}
+ ))}
+ {importResult.errors.length > 5 && (
+
+ 还有 {importResult.errors.length - 5} 处错误...
+
)}
-
)}
+
+
+ )}
-
);
}
-export default React.memo(RackManagement);
\ No newline at end of file
+export default React.memo(RackManagement);
diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx
index 9a4c73a..a66deef 100644
--- a/frontend/src/pages/RackVisualization.jsx
+++ b/frontend/src/pages/RackVisualization.jsx
@@ -1,26 +1,174 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
-import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox, Input, Badge, Typography } from 'antd';
-import {
- ReloadOutlined,
- ZoomInOutlined,
- ZoomOutOutlined,
- RotateRightOutlined,
- CloudServerOutlined,
- SwitcherOutlined,
- DatabaseOutlined,
- CloudOutlined,
- LaptopOutlined,
- MobileOutlined,
- PrinterOutlined,
- SettingOutlined,
- SearchOutlined,
- ClearOutlined,
- EnvironmentOutlined
+import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox, Input, Badge, Typography, Row, Col, Empty, Spin } from 'antd';
+import {
+ ReloadOutlined, ZoomInOutlined, ZoomOutOutlined,
+ RotateRightOutlined, CloudServerOutlined, SwitcherOutlined,
+ DatabaseOutlined, CloudOutlined, LaptopOutlined,
+ MobileOutlined, PrinterOutlined, SettingOutlined,
+ SearchOutlined, ClearOutlined, EnvironmentOutlined,
+ FilterOutlined, AppstoreOutlined, UnorderedListOutlined,
+ FullscreenOutlined, CompressOutlined, EyeOutlined
} from '@ant-design/icons';
import axios from 'axios';
const { Option } = Select;
-const { Text } = Typography;
+const { Text, Title } = Typography;
+
+const designTokens = {
+ colors: {
+ primary: {
+ main: '#667eea',
+ gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ light: '#8b9ff0',
+ dark: '#4f5db8'
+ },
+ success: {
+ main: '#10b981',
+ gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
+ light: '#34d399',
+ dark: '#047857'
+ },
+ warning: {
+ main: '#f59e0b',
+ gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
+ light: '#fbbf24',
+ dark: '#b45309'
+ },
+ error: {
+ main: '#ef4444',
+ gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
+ light: '#f87171',
+ dark: '#b91c1c'
+ },
+ purple: {
+ main: '#8b5cf6',
+ gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)'
+ },
+ cyan: {
+ main: '#06b6d4',
+ gradient: 'linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)'
+ },
+ text: {
+ primary: '#1e293b',
+ secondary: '#64748b',
+ tertiary: '#94a3b8',
+ inverse: '#ffffff'
+ },
+ background: {
+ primary: '#ffffff',
+ secondary: '#f8fafc',
+ tertiary: '#f1f5f9',
+ dark: '#1e293b'
+ },
+ border: {
+ light: '#e2e8f0',
+ medium: '#cbd5e1',
+ dark: '#94a3b8'
+ },
+ device: {
+ server: '#3b82f6',
+ switch: '#22c55e',
+ router: '#f59e0b',
+ storage: '#8b5cf6',
+ firewall: '#ef4444',
+ ups: '#14b8a6',
+ pdu: '#64748b',
+ other: '#94a3b8'
+ },
+ status: {
+ normal: '#10b981',
+ running: '#10b981',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ fault: '#ef4444',
+ offline: '#6b7280',
+ maintenance: '#3b82f6'
+ }
+ },
+ 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), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
+ glow: '0 0 20px rgba(102, 126, 234, 0.3)'
+ },
+ borderRadius: {
+ small: '6px',
+ medium: '10px',
+ large: '16px',
+ xl: '24px',
+ round: '50%'
+ },
+ transitions: {
+ fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
+ normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
+ slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)'
+ },
+ spacing: {
+ xs: '4px',
+ sm: '8px',
+ md: '16px',
+ lg: '24px',
+ xl: '32px'
+ }
+};
+
+const pageContainerStyle = {
+ minHeight: '100vh',
+ background: designTokens.colors.background.secondary,
+ padding: designTokens.spacing.lg
+};
+
+const headerStyle = {
+ marginBottom: designTokens.spacing.lg,
+ padding: `${designTokens.spacing.lg}px ${designTokens.spacing.xl}px`,
+ background: designTokens.colors.primary.gradient,
+ borderRadius: designTokens.borderRadius.large,
+ boxShadow: designTokens.shadows.large,
+ color: designTokens.colors.text.inverse
+};
+
+const cardStyle = {
+ borderRadius: designTokens.borderRadius.large,
+ border: 'none',
+ boxShadow: designTokens.shadows.medium,
+ background: designTokens.colors.background.primary,
+ overflow: 'hidden'
+};
+
+const filterCardStyle = {
+ borderRadius: designTokens.borderRadius.medium,
+ border: 'none',
+ boxShadow: designTokens.shadows.small,
+ background: designTokens.colors.background.primary,
+ marginBottom: designTokens.spacing.lg
+};
+
+const primaryButtonStyle = {
+ height: '40px',
+ borderRadius: designTokens.borderRadius.medium,
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ boxShadow: designTokens.shadows.medium,
+ fontWeight: '500',
+ transition: `all ${designTokens.transitions.normal}`
+};
+
+const secondaryButtonStyle = {
+ height: '40px',
+ borderRadius: designTokens.borderRadius.medium,
+ border: `1px solid ${designTokens.colors.border.light}`,
+ background: designTokens.colors.background.primary,
+ transition: `all ${designTokens.transitions.fast}`
+};
+
+const statCardStyle = () => ({
+ background: 'rgba(255, 255, 255, 0.15)',
+ borderRadius: designTokens.borderRadius.medium,
+ padding: `${designTokens.spacing.md}px`,
+ border: '1px solid rgba(255, 255, 255, 0.2)',
+ backdropFilter: 'blur(10px)'
+});
// 工具函数提取到组件外部,避免每次渲染重复创建
const getDeviceIcon = (deviceType) => {
@@ -339,6 +487,30 @@ const initAnimationStyles = () => {
white-space: nowrap;
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
}
+
+ @media (max-width: 768px) {
+ .device-tooltip {
+ font-size: 10px;
+ padding: 6px 10px;
+ }
+
+ .device-count-badge {
+ padding: 4px 10px;
+ font-size: 11px;
+ }
+ }
+
+ @media (max-width: 480px) {
+ .device-tooltip {
+ font-size: 9px;
+ padding: 4px 8px;
+ }
+
+ .device-count-badge {
+ padding: 3px 8px;
+ font-size: 10px;
+ }
+ }
`;
document.head.appendChild(style);
return style;
@@ -398,56 +570,6 @@ class ErrorBoundary extends React.Component {
}
function RackVisualization() {
- const pageHeaderStyle = {
- marginBottom: '24px',
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- flexWrap: 'wrap',
- gap: '16px'
- };
-
- const titleStyle = {
- fontSize: '24px',
- fontWeight: '700',
- margin: 0,
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
- WebkitBackgroundClip: 'text',
- WebkitTextFillColor: 'transparent',
- backgroundClip: 'text'
- };
-
- const primaryButtonStyle = {
- height: '40px',
- borderRadius: '8px',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
- border: 'none',
- boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)',
- fontWeight: '500',
- transition: 'all 0.3s ease'
- };
-
- const secondaryButtonStyle = {
- height: '40px',
- borderRadius: '8px',
- border: '1px solid #e8e8e8',
- transition: 'all 0.3s ease'
- };
-
- const cardStyle = {
- borderRadius: '16px',
- border: 'none',
- boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
- overflow: 'hidden'
- };
-
- const searchCardStyle = {
- borderRadius: '12px',
- border: 'none',
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
- marginBottom: '20px'
- };
-
const [racks, setRacks] = useState([]);
const [selectedRack, setSelectedRack] = useState(null);
const [selectedRoom, setSelectedRoom] = useState(null);
@@ -1166,24 +1288,69 @@ function RackVisualization() {
// 无需额外的初始化,CSS 3D变换直接在JSX中实现
return (
-
-
-
-
- 机柜可视化
-
-
+
+
+
+
+
+
+ 机柜可视化
+
+
+ 实时监控机房机柜设备分布与状态
+
+
+
+
+
+
机柜总数
+
{racks.length}
+
+
+
+
+
设备总数
+
+ {racks.reduce((sum, rack) => sum + (rack.deviceCount || 0), 0)}
+
+
+
+
+
+
在线设备
+
+ {devices.filter(d => d.status === 'running' || d.status === 'normal').length}
+
+
+
+
+
+
+
+
+
handleSearch(e.target.value)}
onPressEnter={(e) => handleSearch(e.target.value)}
- style={{ width: 220, height: '40px', borderRadius: '8px' }}
+ style={{ width: 240, height: '40px', borderRadius: designTokens.borderRadius.medium }}
allowClear
prefix={}
suffix={
searchMatchCount > 0 ? (
-
+
) : null
}
/>
@@ -1197,7 +1364,7 @@ function RackVisualization() {
)}
-
+
+
+
{searchKeyword && (
-
-
+
-
0 ? '#52c41a' : '#ff4d4f'
+
- 0 ? '#135200' : '#cf1322', margin: 0 }}>
- {searchResults.length > 0
- ? `找到 ${searchResults.length} 个设备`
+
+ {searchResults.length > 0
+ ? `找到 ${searchResults.length} 个设备`
: searching ? '搜索中...' : '未找到匹配的设备'}
{searching && (
-
+
)}
{searchResults.length > 0 && (
-
+
{searchResults.slice(0, 5).map(result => {
const isCurrentRack = selectedRack && result.rackId === selectedRack.rackId;
return (
-
{!isCurrentRack && (
-
+
)}
@@ -1314,7 +1486,7 @@ function RackVisualization() {
);
})}
{searchResults.length > 5 && (
- + 还有 {searchResults.length - 5} 个
+ + 还有 {searchResults.length - 5} 个
)}
)}
@@ -1322,15 +1494,15 @@ function RackVisualization() {
)}
-
-
-
+
+
+
} onClick={handleZoomIn}>放大
} onClick={handleZoomOut}>缩小
} onClick={handleResetView}>重置视角
{
setBackgroundType(type);
@@ -1341,11 +1513,18 @@ function RackVisualization() {
{backgroundType === 'image' && (
-
+
{
const image = e.target.value;
setBackgroundImage(image);
@@ -1364,13 +1543,13 @@ function RackVisualization() {
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'background');
-
+
const response = await axios.post('/api/background/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
-
+
if (response.data && response.data.path) {
setBackgroundImage(response.data.path);
message.success('背景图片上传成功');
@@ -1392,7 +1571,7 @@ function RackVisualization() {
{
setBackgroundSize(size);
@@ -2219,40 +2398,50 @@ function RackVisualization() {
{/* 设备详情字段配置模态框 */}
+
+ 设备详情字段配置
+
+ }
open={showTooltipConfig}
onCancel={() => setShowTooltipConfig(false)}
footer={null}
- width={500}
+ width={520}
+ styles={{ body: { padding: designTokens.spacing.lg } }}
>
{loadingTooltipFields ? (
-
- 加载字段配置中...
+
) : Object.keys(tooltipFields).length === 0 ? (
-
- 暂无字段配置,请先在设备字段管理中添加字段
+
+
+
+ 请先在设备字段管理中添加字段
+
) : (
-
-
+
+
选择要在设备详情中显示的字段: ({Object.keys(tooltipFields).length} 个字段)
-
+
{Object.entries(tooltipFields).map(([key, field]) => (
{
setTooltipFields(prev => ({
@@ -2262,32 +2451,45 @@ function RackVisualization() {
}}
>
- {field.label}
+
+ {field.label}
+
))}
-
-
+
+
-
diff --git a/frontend/src/pages/RoomManagement.jsx b/frontend/src/pages/RoomManagement.jsx
index c7af47d..701266e 100644
--- a/frontend/src/pages/RoomManagement.jsx
+++ b/frontend/src/pages/RoomManagement.jsx
@@ -1,21 +1,260 @@
-import React, { useState, useEffect } from 'react';
-import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Upload, Progress, Alert } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined } from '@ant-design/icons';
+import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import {
+ Table, Button, Modal, Form, Input, Select, message, Card, Space,
+ InputNumber, Progress, Drawer, Tag, Tooltip, Dropdown, Badge,
+ Row, Col, Statistic, Typography, Empty, Spin, Alert, Checkbox
+} from 'antd';
+import {
+ PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined,
+ SearchOutlined, FilterOutlined, MoreOutlined, EyeOutlined,
+ CloudOutlined, EnvironmentOutlined, DashboardOutlined,
+ ExpandOutlined, CompressOutlined, CheckCircleOutlined,
+ WarningOutlined, SyncOutlined, DeleteFilled
+} from '@ant-design/icons';
import axios from 'axios';
const { Option } = Select;
+const { Title, Text } = Typography;
+
+const designTokens = {
+ colors: {
+ primary: {
+ main: '#1890ff',
+ gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ bgGradient: 'linear-gradient(135deg, #667eea15 0%, #764ba208 100%)'
+ },
+ success: {
+ main: '#52c41a',
+ gradient: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)'
+ },
+ warning: {
+ main: '#faad14',
+ gradient: 'linear-gradient(135deg, #faad14 0%, #d48806 100%)'
+ },
+ error: {
+ main: '#ff4d4f',
+ gradient: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)'
+ },
+ text: {
+ primary: '#262626',
+ secondary: '#8c8c8c',
+ tertiary: '#bfbfbf'
+ }
+ },
+ shadows: {
+ small: '0 2px 8px rgba(0, 0, 0, 0.06)',
+ medium: '0 4px 16px rgba(0, 0, 0, 0.08)',
+ large: '0 8px 24px rgba(0, 0, 0, 0.12)'
+ },
+ borderRadius: {
+ small: '8px',
+ medium: '12px',
+ large: '16px'
+ },
+ transitions: {
+ normal: '0.3s cubic-bezier(0.4, 0, 0.2, 1)'
+ }
+};
+
+const containerStyle = {
+ minHeight: '100vh',
+ background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)',
+ padding: '24px'
+};
+
+const headerStyle = {
+ marginBottom: '24px',
+ padding: '24px',
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ borderRadius: '20px',
+ color: '#fff',
+ boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)'
+};
+
+const statCardStyle = (color) => ({
+ background: 'rgba(255, 255, 255, 0.15)',
+ borderRadius: designTokens.borderRadius.medium,
+ padding: '16px',
+ border: '1px solid rgba(255, 255, 255, 0.2)',
+ backdropFilter: 'blur(10px)'
+});
+
+const cardStyle = {
+ borderRadius: designTokens.borderRadius.large,
+ border: 'none',
+ boxShadow: designTokens.shadows.medium,
+ background: '#fff',
+ overflow: 'hidden'
+};
+
+const cardHeadStyle = {
+ borderBottom: '1px solid #f0f0f0',
+ padding: '16px 24px',
+ background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)'
+};
+
+const primaryButtonStyle = {
+ height: '42px',
+ borderRadius: '10px',
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ boxShadow: '0 4px 16px rgba(102, 126, 234, 0.35)',
+ fontWeight: '500'
+};
+
+const actionButtonStyle = {
+ height: '36px',
+ borderRadius: '8px',
+ border: '1px solid #e8e8e8'
+};
+
+const searchInputStyle = {
+ borderRadius: '10px',
+ height: '42px',
+ border: '1px solid #e8e8e8'
+};
+
+const statusConfig = {
+ active: { text: '在用', color: 'success', icon:
},
+ maintenance: { text: '维护中', color: 'warning', icon:
},
+ inactive: { text: '停用', color: 'default', icon:
}
+};
+
+const CapacityProgress = ({ used, capacity, color }) => {
+ const percentage = Math.min((used / capacity) * 100, 100);
+ const status = percentage >= 90 ? 'exception' : percentage >= 70 ? 'active' : 'success';
+
+ return (
+
+
+
+ {used} / {capacity}
+
+
+ {percentage.toFixed(1)}%
+
+
+
+
+ );
+};
+
+const RoomCard = ({ room, onEdit, onDelete, onView, selected, onSelect }) => {
+ const rackCount = room.Racks?.length || 0;
+ const capacityUsage = (rackCount / room.capacity) * 100;
+ const statusInfo = statusConfig[room.status];
+
+ return (
+
onSelect(room.roomId)}
+ onDoubleClick={() => onView(room)}
+ bodyStyle={{ padding: '20px' }}
+ >
+
+
+
+
+
+ {room.name}
+
+
+
{room.roomId}
+
+
+ {statusInfo.text}
+
+
+
+
+
+
+ {room.location}
+
+
+
+
+ = 90 ? designTokens.colors.error.main : capacityUsage >= 70 ? designTokens.colors.warning.main : designTokens.colors.success.main}
+ />
+
+
+
+
+
+
面积
+
+ {room.area} ㎡
+
+
+
+
+
+
+ }
+ onClick={(e) => { e.stopPropagation(); onView(room); }}
+ style={{ color: designTokens.colors.text.secondary }}
+ />
+
+
+ }
+ onClick={(e) => { e.stopPropagation(); onEdit(room); }}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ }
+ danger
+ onClick={(e) => { e.stopPropagation(); onDelete(room.roomId); }}
+ />
+
+
+
+
+ );
+};
function RoomManagement() {
const [rooms, setRooms] = useState([]);
const [loading, setLoading] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
+ const [drawerVisible, setDrawerVisible] = useState(false);
const [editingRoom, setEditingRoom] = useState(null);
+ const [viewingRoom, setViewingRoom] = useState(null);
+ const [selectedRoomIds, setSelectedRoomIds] = useState([]);
+ const [viewMode, setViewMode] = useState('table');
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
const [form] = Form.useForm();
-
-
- // 获取所有机房
- const fetchRooms = async () => {
+ const fetchRooms = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get('/api/rooms');
@@ -26,13 +265,12 @@ function RoomManagement() {
} finally {
setLoading(false);
}
- };
+ }, []);
useEffect(() => {
fetchRooms();
- }, []);
+ }, [fetchRooms]);
- // 打开模态框
const showModal = (room = null) => {
setEditingRoom(room);
if (room) {
@@ -43,25 +281,20 @@ function RoomManagement() {
setModalVisible(true);
};
- // 关闭模态框
const handleCancel = () => {
setModalVisible(false);
setEditingRoom(null);
};
- // 提交表单
const handleSubmit = async (values) => {
try {
if (editingRoom) {
- // 更新机房
await axios.put(`/api/rooms/${editingRoom.roomId}`, values);
message.success('机房更新成功');
} else {
- // 创建机房
await axios.post('/api/rooms', values);
message.success('机房创建成功');
}
-
setModalVisible(false);
fetchRooms();
setEditingRoom(null);
@@ -71,11 +304,10 @@ function RoomManagement() {
}
};
- // 删除机房
const handleDelete = async (roomId) => {
Modal.confirm({
title: '确认删除',
- content: '确定要删除这个机房吗?',
+ content: '确定要删除这个机房吗?删除后无法恢复。',
okText: '删除',
okType: 'danger',
cancelText: '取消',
@@ -92,209 +324,380 @@ function RoomManagement() {
});
};
+ const handleBatchDelete = async () => {
+ if (selectedRoomIds.length === 0) {
+ message.warning('请先选择要删除的机房');
+ return;
+ }
-
-
-
-
-
- // 状态标签映射
- const statusMap = {
- active: { text: '在用', color: 'green' },
- maintenance: { text: '维护中', color: 'orange' },
- inactive: { text: '停用', color: 'gray' }
+ Modal.confirm({
+ title: '批量删除',
+ content: `确定要删除选中的 ${selectedRoomIds.length} 个机房吗?`,
+ okText: '删除',
+ okType: 'danger',
+ cancelText: '取消',
+ onOk: async () => {
+ try {
+ await Promise.all(selectedRoomIds.map(id => axios.delete(`/api/rooms/${id}`)));
+ message.success(`成功删除 ${selectedRoomIds.length} 个机房`);
+ setSelectedRoomIds([]);
+ fetchRooms();
+ } catch (error) {
+ message.error('批量删除失败');
+ console.error('批量删除失败:', error);
+ }
+ }
+ });
};
- // 表格列配置
- const columns = [
+ const handleView = (room) => {
+ setViewingRoom(room);
+ setDrawerVisible(true);
+ };
+
+ const filteredRooms = useMemo(() => {
+ return rooms.filter(room => {
+ const matchKeyword = !searchKeyword ||
+ room.name?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
+ room.roomId?.toLowerCase().includes(searchKeyword.toLowerCase()) ||
+ room.location?.toLowerCase().includes(searchKeyword.toLowerCase());
+
+ const matchStatus = statusFilter === 'all' || room.status === statusFilter;
+
+ return matchKeyword && matchStatus;
+ });
+ }, [rooms, searchKeyword, statusFilter]);
+
+ const stats = useMemo(() => ({
+ total: rooms.length,
+ active: rooms.filter(r => r.status === 'active').length,
+ maintenance: rooms.filter(r => r.status === 'maintenance').length,
+ inactive: rooms.filter(r => r.status === 'inactive').length,
+ totalRacks: rooms.reduce((sum, r) => sum + (r.Racks?.length || 0), 0),
+ totalCapacity: rooms.reduce((sum, r) => sum + (r.capacity || 0), 0)
+ }), [rooms]);
+
+ const tableColumns = [
{
- title: '机房ID',
- dataIndex: 'roomId',
- key: 'roomId',
+ title: (
+
0}
+ indeterminate={selectedRoomIds.length > 0 && selectedRoomIds.length < filteredRooms.length}
+ onChange={(e) => {
+ if (e.target.checked) {
+ setSelectedRoomIds(filteredRooms.map(r => r.roomId));
+ } else {
+ setSelectedRoomIds([]);
+ }
+ }}
+ />
+ ),
+ key: 'selection',
+ width: 50,
+ render: (_, record) => (
+ {
+ if (e.target.checked) {
+ setSelectedRoomIds([...selectedRoomIds, record.roomId]);
+ } else {
+ setSelectedRoomIds(selectedRoomIds.filter(id => id !== record.roomId));
+ }
+ }}
+ />
+ )
},
{
- title: '机房名称',
- dataIndex: 'name',
- key: 'name',
+ title: '机房信息',
+ key: 'roomInfo',
+ render: (_, record) => (
+
+
+
+
+
+
+ {record.name}
+
+
+ {record.roomId}
+
+
+
+ )
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
+ render: (location) => (
+
+
+ {location}
+
+ )
},
{
- title: '面积(㎡)',
- dataIndex: 'area',
- key: 'area',
- },
- {
- title: '容量(机柜数)',
- dataIndex: 'capacity',
- key: 'capacity',
+ title: '面积/容量',
+ key: 'areaCapacity',
+ render: (_, record) => (
+
+
{record.area} ㎡
+
+
+
+
+ )
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
- render: (status) => (
-
- {statusMap[status].text}
-
- ),
+ render: (status) => {
+ const config = statusConfig[status];
+ return (
+
+ {config.text}
+
+ );
+ },
+ filters: [
+ { text: '在用', value: 'active' },
+ { text: '维护中', value: 'maintenance' },
+ { text: '停用', value: 'inactive' }
+ ],
+ onFilter: (value, record) => record.status === value
},
{
- title: '机柜数量',
- dataIndex: 'Racks',
+ title: '机柜数',
key: 'rackCount',
- render: (racks) => racks ? racks.length : 0,
- },
- {
- title: '描述',
- dataIndex: 'description',
- key: 'description',
- ellipsis: true,
+ render: (_, record) => (
+
+
+ {record.Racks?.length || 0}
+
+ ),
+ sorter: (a, b) => (a.Racks?.length || 0) - (b.Racks?.length || 0)
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
- render: (date) => date ? new Date(date).toLocaleString() : '',
+ render: (date) => date ? new Date(date).toLocaleString() : '-',
+ sorter: (a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)
},
{
title: '操作',
key: 'action',
+ fixed: 'right',
+ width: 160,
render: (_, record) => (
-
- } onClick={() => showModal(record)} size="small">
- 编辑
-
- } onClick={() => handleDelete(record.roomId)} size="small">
- 删除
-
+
+
+ }
+ onClick={() => handleView(record)}
+ style={{ color: designTokens.colors.text.secondary }}
+ />
+
+
+ }
+ onClick={() => showModal(record)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ }
+ danger
+ onClick={() => handleDelete(record.roomId)}
+ />
+
- ),
- },
+ )
+ }
];
- const pageHeaderStyle = {
- marginBottom: '24px',
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- flexWrap: 'wrap',
- gap: '16px'
- };
-
- const titleStyle = {
- fontSize: '24px',
- fontWeight: '700',
- margin: 0,
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
- WebkitBackgroundClip: 'text',
- WebkitTextFillColor: 'transparent',
- backgroundClip: 'text'
- };
-
- const cardStyle = {
- borderRadius: '16px',
- border: 'none',
- boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
- overflow: 'hidden'
- };
-
- const cardHeadStyle = {
- borderBottom: '1px solid #f0f0f0',
- padding: '16px 24px',
- background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)'
- };
-
- const tableStyle = {
- borderRadius: '12px',
- overflow: 'hidden'
- };
-
- const statusTagStyle = (color) => ({
- padding: '4px 12px',
- borderRadius: '20px',
- fontSize: '12px',
- fontWeight: '500',
- border: 'none',
- background: color === 'green' ? '#f6ffed' : color === 'orange' ? '#fff7e6' : '#f5f5f5',
- color: color === 'green' ? '#52c41a' : color === 'orange' ? '#faad14' : '#8c8c8c'
- });
-
- const primaryButtonStyle = {
- height: '40px',
- borderRadius: '8px',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
- border: 'none',
- boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)',
- fontWeight: '500',
- transition: 'all 0.3s ease'
- };
-
- const actionButtonStyle = {
- height: '32px',
- borderRadius: '6px',
- border: '1px solid #e8e8e8',
- transition: 'all 0.3s ease'
+ const rowSelection = {
+ selectedRowKeys: selectedRoomIds,
+ onChange: (selectedRowKeys) => {
+ setSelectedRoomIds(selectedRowKeys);
+ }
};
return (
-
-
-
机房管理
-
- }
- onClick={fetchRooms}
- style={actionButtonStyle}
- >
- 刷新
-
- }
- onClick={() => showModal()}
- style={primaryButtonStyle}
- >
- 添加机房
-
-
+
+
+
+
+
+
+ 机房管理
+
+
+ 管理和监控所有机房设施
+
+
+
+
+
+
在用机房
+
{stats.active}
+
+
+
总机柜
+
{stats.totalRacks}
+
+
+
- `共 ${total} 条记录`
- }}
- style={tableStyle}
- rowClassName={() => 'table-row'}
- />
+
+
+ }
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ style={searchInputStyle}
+ allowClear
+ />
+
+
+
+
+
+
+
+
+ {selectedRoomIds.length > 0 && (
+ }
+ onClick={handleBatchDelete}
+ style={{ borderRadius: '10px', height: '42px' }}
+ >
+ 批量删除 ({selectedRoomIds.length})
+
+ )}
+ }
+ onClick={fetchRooms}
+ loading={loading}
+ style={actionButtonStyle}
+ >
+ 刷新
+
+ }
+ onClick={() => showModal()}
+ style={primaryButtonStyle}
+ >
+ 添加机房
+
+
+
+
+
+ }
+ onClick={() => setViewMode('table')}
+ style={viewMode === 'table' ? primaryButtonStyle : actionButtonStyle}
+ >
+ 表格视图
+
+ }
+ onClick={() => setViewMode('card')}
+ style={viewMode === 'card' ? primaryButtonStyle : actionButtonStyle}
+ >
+ 卡片视图
+
+
+
+ {viewMode === 'table' ? (
+ `共 ${total} 条记录`
+ }}
+ scroll={{ x: 1000 }}
+ rowClassName={() => 'table-row'}
+ />
+ ) : (
+
+ {filteredRooms.length > 0 ? (
+ filteredRooms.map(room => (
+
+ {
+ if (selectedRoomIds.includes(id)) {
+ setSelectedRoomIds(selectedRoomIds.filter(rid => rid !== id));
+ } else {
+ setSelectedRoomIds([...selectedRoomIds, id]);
+ }
+ }}
+ />
+
+ ))
+ ) : (
+
+
+
+ )}
+
+ )}
-
+
+ }} />
{editingRoom ? '编辑机房' : '添加机房'}
}
@@ -305,61 +708,60 @@ function RoomManagement() {
destroyOnHidden
styles={{
body: { padding: '24px' },
- header: {
- borderBottom: '1px solid #f0f0f0',
- padding: '16px 24px',
- marginBottom: '0'
- }
- }}
- style={{
- borderRadius: '16px',
- overflow: 'hidden'
+ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
}}
+ style={{ borderRadius: '16px' }}
>
-
-
-
-
-
-
-
+
-
+ } placeholder="请输入机房位置" style={{ borderRadius: '8px' }} />
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
- 取消
-
-
- 确定
-
-
-
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+ 机房详情 - {viewingRoom?.name}
+
+ }
+ open={drawerVisible}
+ onClose={() => setDrawerVisible(false)}
+ width={480}
+ styles={{
+ header: { borderBottom: '1px solid #f0f0f0' },
+ body: { padding: '24px' }
+ }}
+ >
+ {viewingRoom && (
+
+
+
+
+
+
+ {viewingRoom.name}
+
+
+ {viewingRoom.roomId}
+
+
+
+
+ {statusConfig[viewingRoom.status].text}
+
+
+
+
位置
+
+
+ {viewingRoom.location}
+
+
+
+
+
+
面积
+
+ {viewingRoom.area} ㎡
+
+
+
+
+
+
容量
+
+ {viewingRoom.capacity} 机柜
+
+
+
+
+
+
+ {viewingRoom.description && (
+
+
描述
+
+ {viewingRoom.description}
+
+
+ )}
+
+
+ }
+ onClick={() => {
+ setDrawerVisible(false);
+ showModal(viewingRoom);
+ }}
+ style={primaryButtonStyle}
+ >
+ 编辑机房
+
+ }
+ onClick={() => {
+ setDrawerVisible(false);
+ handleDelete(viewingRoom.roomId);
+ }}
+ style={{ borderRadius: '8px', height: '42px' }}
+ >
+ 删除机房
+
+
+
+ )}
+
);
}
-export default RoomManagement;
\ No newline at end of file
+export default RoomManagement;