import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Layout, Select, Card, Spin, message, Typography, Descriptions, Tag, Button, Space, Empty, Modal, Form, Input, InputNumber, DatePicker, Checkbox, Switch, } from 'antd'; import { CloudServerOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, UpOutlined, DownOutlined, EditOutlined, SettingOutlined, FullscreenOutlined, EyeOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import dayjs from 'dayjs'; import Scene from '../components/3d/Scene'; import NetworkCardCreateModal from '../components/NetworkCardCreateModal'; import PortCreateModal from '../components/PortCreateModal'; import CableCreateModal from '../components/CableCreateModal'; import DeviceDetailDrawer from '../components/DeviceDetailDrawer'; import { useScene3D } from '../context/Scene3DContext'; const { Header, Content, Sider } = Layout; const { Title, Text } = Typography; const { Option } = Select; const Rack3DVisualization = () => { const navigate = useNavigate(); // Scene 组件的 ref,用于调用重置视角方法 const sceneRef = useRef(null); // 使用 Scene3DContext 管理3D场景状态 const { devices, setDevices, selectedDevice, setSelectedDevice, hoveredDevice, setHoveredDevice, deviceSlideEnabled, setDeviceSlideEnabled, selectedRack, setSelectedRack, racks, setRacks, deviceCables, setDeviceCables, loadingDevices, setLoadingDevices, } = useScene3D(); const [selectedRoom, setSelectedRoom] = useState(null); const [loading, setLoading] = useState(false); const [isRackInfoCollapsed, setIsRackInfoCollapsed] = useState(false); // Rack Info Card collapsed state // Edit Modal State const [modalVisible, setModalVisible] = useState(false); const [editingDevice, setEditingDevice] = useState(null); const [form] = Form.useForm(); // Field configuration state const [tooltipFields, setTooltipFields] = useState({}); const [showTooltipConfig, setShowTooltipConfig] = useState(false); const [loadingTooltipFields, setLoadingTooltipFields] = useState(false); const [savingTooltipConfig, setSavingTooltipConfig] = useState(false); // Add Port/NIC/Cable Modal State const [nicModalVisible, setNicModalVisible] = useState(false); const [portModalVisible, setPortModalVisible] = useState(false); const [cableModalVisible, setCableModalVisible] = useState(false); const [operatingDevice, setOperatingDevice] = useState(null); const [refreshTrigger, setRefreshTrigger] = useState(0); // 使用 ref 存储模态框状态,避免 handleDeviceHover 频繁重新创建 const modalsOpenRef = useRef(false); // 同步 ref 与 state useEffect(() => { modalsOpenRef.current = modalVisible || nicModalVisible || portModalVisible || cableModalVisible; }, [modalVisible, nicModalVisible, portModalVisible, cableModalVisible]); const fetchDeviceCables = useCallback(async deviceId => { if (!deviceId) return; try { const response = await axios.get(`/api/cables/device/${deviceId}`); setDeviceCables(response.data || []); } catch (error) { console.error('Failed to fetch device cables:', error); setDeviceCables([]); } }, []); const handleAddNic = device => { setOperatingDevice(device); setNicModalVisible(true); }; const handleAddPort = device => { setOperatingDevice(device); setPortModalVisible(true); }; const handleAddCable = device => { setOperatingDevice(device); setCableModalVisible(true); }; // Handle Edit Device Click const handleEditDevice = device => { setEditingDevice(device); setModalVisible(true); }; // 当 Modal 打开时,初始化表单数据 useEffect(() => { if (modalVisible && editingDevice) { // Prepare form data (similar to DeviceManagement) const deviceData = { ...editingDevice }; if (deviceData.purchaseDate) deviceData.purchaseDate = dayjs(deviceData.purchaseDate); if (deviceData.warrantyExpiry) deviceData.warrantyExpiry = dayjs(deviceData.warrantyExpiry); // Fixed fields const fixedFields = [ 'deviceId', 'name', 'type', 'model', 'serialNumber', 'rackId', 'position', 'height', 'powerConsumption', 'status', 'purchaseDate', 'warrantyExpiry', 'ipAddress', 'description', ]; const cleanDeviceData = {}; fixedFields.forEach(field => { if (deviceData[field] !== undefined) { cleanDeviceData[field] = deviceData[field]; } }); // Custom fields if any (simplified for 3D view context) if (deviceData.customFields && typeof deviceData.customFields === 'object') { Object.entries(deviceData.customFields).forEach(([key, value]) => { cleanDeviceData[key] = value; }); } form.setFieldsValue(cleanDeviceData); } }, [modalVisible, editingDevice, form]); const handleModalCancel = () => { setModalVisible(false); setEditingDevice(null); form.resetFields(); }; const handleModalSubmit = async values => { try { const deviceData = { ...values, purchaseDate: values.purchaseDate ? values.purchaseDate.format('YYYY-MM-DD') : null, warrantyExpiry: values.warrantyExpiry ? values.warrantyExpiry.format('YYYY-MM-DD') : null, }; if (editingDevice) { await axios.put(`/api/devices/${editingDevice.deviceId || editingDevice.id}`, deviceData); message.success('设备更新成功'); // Refresh devices if (selectedRack) { fetchDevices(selectedRack.rackId); } setModalVisible(false); setEditingDevice(null); } } catch (error) { message.error('设备更新失败'); console.error(error); } }; // Fetch Racks const fetchRacks = useCallback(async () => { try { setLoading(true); const response = await axios.get('/api/racks', { params: { pageSize: 1000 } }); const racksArray = response.data.racks || []; // Filter valid racks const validRacks = racksArray.filter(r => r && r.rackId && typeof r.height === 'number'); setRacks(validRacks); if (validRacks.length > 0) { // Default select first rack const firstRack = validRacks[0]; setSelectedRack(firstRack); if (firstRack?.Room) { const roomKey = firstRack.Room.roomId || firstRack.Room.id || firstRack.Room.name; setSelectedRoom(roomKey); } } } catch (error) { console.error('Failed to fetch racks:', error); message.error('获取机柜列表失败'); } finally { setLoading(false); } }, []); // Fetch Devices for Rack const fetchDevices = useCallback(async rackId => { if (!rackId) return; try { setLoadingDevices(true); const response = await axios.get(`/api/devices?rackId=${rackId}`); let devicesData = []; if (Array.isArray(response.data)) { devicesData = response.data; } else if (response.data && Array.isArray(response.data.data)) { devicesData = response.data.data; } else if (response.data && Array.isArray(response.data.devices)) { devicesData = response.data.devices; } // Process devices (ensure position/height are numbers) const validDevices = devicesData.filter(d => { if (!d.deviceId && !d.id) return false; // Normalize deviceId if (!d.deviceId && d.id) d.deviceId = d.id; let pos = d.position || d.u_position; let h = d.height || d.u_height || 1; if (typeof pos === 'string') pos = parseInt(pos, 10); if (typeof h === 'string') h = parseInt(h, 10); // Mutate for consistency d.position = pos; d.height = h; return typeof pos === 'number' && !isNaN(pos); }); setDevices(validDevices); } catch (error) { console.error('Failed to fetch devices:', error); message.error('获取设备列表失败'); setDevices([]); } finally { setLoadingDevices(false); } }, []); // Fetch Tooltip Fields from API const fetchTooltipDeviceFields = useCallback(async () => { try { setLoadingTooltipFields(true); const response = await axios.get('/api/deviceFields'); const fields = response.data; const newTooltipFields = {}; if (fields && fields.length > 0) { fields.forEach(field => { newTooltipFields[field.fieldName] = { label: field.displayName, enabled: field.visible, field: field.fieldName, fieldType: field.fieldType || 'text', }; }); } setTooltipFields(newTooltipFields); } catch (error) { console.error('Failed to fetch device fields:', error); message.error('获取字段配置失败'); } finally { setLoadingTooltipFields(false); } }, []); const saveTooltipConfig = async () => { try { setSavingTooltipConfig(true); // Transform object to array for backend API const fieldConfigs = Object.values(tooltipFields).map(field => ({ fieldName: field.field, visible: field.enabled, displayName: field.label, fieldType: field.fieldType, })); await axios.post('/api/deviceFields/config', fieldConfigs); message.success('字段配置保存成功'); setShowTooltipConfig(false); // Refresh fields to sync with DB fetchTooltipDeviceFields(); } catch (error) { console.error('Failed to save tooltip config:', error); message.error('保存失败'); } finally { setSavingTooltipConfig(false); } }; useEffect(() => { fetchRacks(); fetchTooltipDeviceFields(); }, [fetchRacks, fetchTooltipDeviceFields]); useEffect(() => { if (selectedRack?.rackId) { fetchDevices(selectedRack.rackId); setSelectedDevice(null); // Reset selection on rack change } }, [selectedRack, fetchDevices]); // Group racks by room const rooms = useMemo(() => { const roomMap = new Map(); racks.forEach(rack => { if (rack?.Room) { const roomKey = rack.Room.roomId || rack.Room.id || rack.Room.name; if (!roomMap.has(roomKey)) { roomMap.set(roomKey, { ...rack.Room, key: roomKey }); } } }); return Array.from(roomMap.values()); }, [racks]); const racksInSelectedRoom = useMemo(() => { if (!selectedRoom) return []; return racks.filter(r => { const rKey = r.Room?.roomId || r.Room?.id || r.Room?.name; return rKey === selectedRoom; }); }, [selectedRoom, racks]); // 使用 useCallback 稳定回调函数引用,避免 DeviceModel 不必要的重渲染 const handleDeviceClick = useCallback( device => { setSelectedDevice(device); if (device) { fetchDeviceCables(device.deviceId || device.id); } }, [fetchDeviceCables] ); const handleDeviceLeave = device => { // 移除鼠标离开事件,避免抽屉意外关闭 // setSelectedDevice((current) => { // const deviceId = device.deviceId || device.id; // const currentId = current?.deviceId || current?.id; // return currentId === deviceId ? null : current; // }); }; // 优化 handleDeviceHover - 使用 ref 获取最新状态,减少依赖项 const handleDeviceHover = useCallback(device => { // 使用 ref 检查模态框状态,避免频繁重新创建回调 if (modalsOpenRef.current) return; setHoveredDevice(device); }, []); // 空依赖项,回调引用永远稳定 const handleClosePortModal = useCallback(() => { setPortModalVisible(false); setOperatingDevice(null); }, []); const handlePortSuccess = useCallback(() => { message.success('端口添加成功'); }, []); const handleCloseNicModal = useCallback(() => { setNicModalVisible(false); setOperatingDevice(null); }, []); const handleNicSuccess = useCallback(() => { message.success('网卡添加成功'); }, []); const handleCloseCableModal = useCallback(() => { setCableModalVisible(false); setOperatingDevice(null); }, []); const handleCableSuccess = useCallback(() => { message.success('接线添加成功'); setRefreshTrigger(prev => prev + 1); if (selectedDevice) { fetchDeviceCables(selectedDevice.deviceId || selectedDevice.id); } }, [selectedDevice, fetchDeviceCables]); const handleDeleteCable = useCallback( async cableId => { try { await axios.delete(`/api/cables/${cableId}`); message.success('接线删除成功'); setRefreshTrigger(prev => prev + 1); if (selectedDevice) { fetchDeviceCables(selectedDevice.deviceId || selectedDevice.id); } } catch (error) { console.error('删除接线失败:', error); message.error('删除接线失败'); } }, [selectedDevice, fetchDeviceCables] ); return (
设备弹出
{/* Inject custom styles for glass selects */} {loading ? (
加载资源中...
) : selectedRack ? ( <> {/* Rack Info Overlay (New) */} {selectedRack && (
isRackInfoCollapsed && setIsRackInfoCollapsed(false)} > {/* Header */}
{selectedRack.name}
ID: {selectedRack.rackId}
{/* Content Area - Collapsible */}
{/* Stats Grid */}
总高度
{selectedRack.height} U
设备数
{devices.length}
{/* Guide Section */}
操作指南
{[ { icon: '🖱️', text: '左键旋转视图' }, { icon: '🔍', text: '滚轮缩放视图' }, { icon: '👆', text: '点击设备查看详情' }, ].map((item, i) => (
{item.icon} {item.text}
))}
)} {/* Overlay Info for Hovered Device */} {hoveredDevice && !selectedDevice && (
{hoveredDevice.name}
位置: U{hoveredDevice.position}
高度: {hoveredDevice.height}U
状态: {hoveredDevice.status}
)} {/* Device Detail Floating Popup - Removed, now handled in DeviceModel */} ) : (
请选择一个机柜以查看 3D 视图 } />
)} 编辑设备信息 } open={modalVisible} onCancel={handleModalCancel} footer={null} width={700} >
{/* Field Configuration Modal */} 设备详情字段配置 } open={showTooltipConfig} onCancel={() => setShowTooltipConfig(false)} footer={null} width={520} > {loadingTooltipFields ? (
加载配置中...
) : (
选择要在设备详情卡片中显示的字段:
{Object.entries(tooltipFields).map(([key, field]) => (
{ setTooltipFields(prev => ({ ...prev, [key]: { ...prev[key], enabled: !prev[key].enabled }, })); }} > {field.label}
))}
)}
setSelectedDevice(null)} onEdit={handleEditDevice} onAddNic={handleAddNic} onAddPort={handleAddPort} onAddCable={handleAddCable} tooltipFields={tooltipFields} cables={deviceCables} onRefreshCables={() => selectedDevice && fetchDeviceCables(selectedDevice.deviceId || selectedDevice.id) } onDeleteCable={handleDeleteCable} refreshTrigger={refreshTrigger} />
); }; export default Rack3DVisualization;