diff --git a/backend/config/routes.js b/backend/config/routes.js index 4941aea..c991141 100644 --- a/backend/config/routes.js +++ b/backend/config/routes.js @@ -105,6 +105,10 @@ const routesConfig = [ file: 'dangerousOperations.js', path: '/api/dangerous-operations', }, + { + file: 'topology.js', + path: '/api/topology', + }, ]; module.exports = routesConfig; diff --git a/backend/routes/topology.js b/backend/routes/topology.js new file mode 100644 index 0000000..7ec9819 --- /dev/null +++ b/backend/routes/topology.js @@ -0,0 +1,323 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); +const Cable = require('../models/Cable'); +const Device = require('../models/Device'); +const DevicePort = require('../models/DevicePort'); +const Rack = require('../models/Rack'); +const Room = require('../models/Room'); + +router.get('/switch/:switchId', async (req, res) => { + try { + const { switchId } = req.params; + const { maxNodes = 100 } = req.query; + + const centerDevice = await Device.findByPk(switchId, { + include: [ + { + model: Rack, + as: 'Rack', + include: [{ model: Room, as: 'Room' }] + } + ] + }); + + if (!centerDevice) { + return res.status(404).json({ success: false, error: '交换机不存在' }); + } + + if (centerDevice.type !== 'switch') { + return res.status(400).json({ success: false, error: '指定设备不是交换机' }); + } + + const cables = await Cable.findAll({ + where: { + [Op.or]: [ + { sourceDeviceId: switchId }, + { targetDeviceId: switchId } + ] + }, + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'model', 'status', 'ipAddress', 'rackId', 'position'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'model', 'status', 'ipAddress', 'rackId', 'position'] + } + ] + }); + + const connectedDeviceIds = new Set(); + cables.forEach(cable => { + if (cable.sourceDeviceId !== switchId) { + connectedDeviceIds.add(cable.sourceDeviceId); + } + if (cable.targetDeviceId !== switchId) { + connectedDeviceIds.add(cable.targetDeviceId); + } + }); + + if (connectedDeviceIds.size > parseInt(maxNodes)) { + return res.status(400).json({ + success: false, + error: `连接设备数量(${connectedDeviceIds.size})超过限制(${maxNodes}),请使用更具体的筛选条件` + }); + } + + const connectedDevices = await Device.findAll({ + where: { deviceId: { [Op.in]: Array.from(connectedDeviceIds) } }, + include: [ + { + model: Rack, + as: 'Rack', + include: [{ model: Room, as: 'Room' }] + } + ] + }); + + const deviceMap = {}; + connectedDevices.forEach(device => { + deviceMap[device.deviceId] = device.toJSON(); + }); + + const centerRack = centerDevice.Rack; + const centerRoom = centerRack?.Room; + + const nodes = [ + { + id: centerDevice.deviceId, + deviceId: centerDevice.deviceId, + name: centerDevice.name, + type: centerDevice.type, + model: centerDevice.model, + status: centerDevice.status, + ipAddress: centerDevice.ipAddress, + rackId: centerDevice.rackId, + rackName: centerRack?.name, + roomId: centerRoom?.roomId, + roomName: centerRoom?.name, + position: centerDevice.position, + isCenter: true + }, + ...connectedDevices.map(device => { + const d = device.toJSON(); + const rack = d.Rack; + const room = rack?.Room; + return { + id: d.deviceId, + deviceId: d.deviceId, + name: d.name, + type: d.type, + model: d.model, + status: d.status, + ipAddress: d.ipAddress, + rackId: d.rackId, + rackName: rack?.name, + roomId: room?.roomId, + roomName: room?.name, + position: d.position, + isCenter: false + }; + }) + ]; + + const edges = cables.map(cable => { + const sourceId = cable.sourceDeviceId === switchId ? cable.targetDeviceId : cable.sourceDeviceId; + return { + id: cable.cableId, + source: cable.sourceDeviceId, + target: cable.targetDeviceId, + sourcePort: cable.sourcePort, + targetPort: cable.targetPort, + cableId: cable.cableId, + cableType: cable.cableType, + cableLength: cable.cableLength, + cableLabel: cable.cableLabel, + cableColor: cable.cableColor, + status: cable.status, + description: cable.description, + installedAt: cable.installedAt + }; + }); + + const portStats = {}; + const allDeviceIds = [switchId, ...Array.from(connectedDeviceIds)]; + const ports = await DevicePort.findAll({ + where: { deviceId: { [Op.in]: allDeviceIds } }, + attributes: ['deviceId', 'status'] + }); + + ports.forEach(port => { + if (!portStats[port.deviceId]) { + portStats[port.deviceId] = { total: 0, used: 0, free: 0, fault: 0 }; + } + portStats[port.deviceId].total++; + if (port.status === 'occupied') portStats[port.deviceId].used++; + else if (port.status === 'free') portStats[port.deviceId].free++; + else if (port.status === 'fault') portStats[port.deviceId].fault++; + }); + + nodes.forEach(node => { + node.portCount = portStats[node.deviceId] || { total: 0, used: 0, free: 0, fault: 0 }; + }); + + const statistics = { + totalDevices: nodes.length, + totalCables: edges.length, + normalCables: edges.filter(e => e.status === 'normal').length, + faultCables: edges.filter(e => e.status === 'fault').length, + disconnectedCables: edges.filter(e => e.status === 'disconnected').length, + byDeviceType: {}, + byCableType: {} + }; + + nodes.forEach(node => { + statistics.byDeviceType[node.type] = (statistics.byDeviceType[node.type] || 0) + 1; + }); + + edges.forEach(edge => { + statistics.byCableType[edge.cableType] = (statistics.byCableType[edge.cableType] || 0) + 1; + }); + + res.json({ + success: true, + data: { + centerDevice: nodes[0], + nodes: nodes.slice(1), + edges, + statistics + } + }); + } catch (error) { + console.error('获取拓扑数据失败:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/rack/:rackId', async (req, res) => { + try { + const { rackId } = req.params; + const { maxNodes = 100 } = req.query; + + const devices = await Device.findAll({ + where: { rackId }, + attributes: ['deviceId'] + }); + + const deviceIds = devices.map(d => d.deviceId); + + if (deviceIds.length === 0) { + return res.json({ + success: true, + data: { + nodes: [], + edges: [], + statistics: { + totalDevices: 0, + totalCables: 0, + normalCables: 0, + faultCables: 0, + disconnectedCables: 0 + } + } + }); + } + + const cables = await Cable.findAll({ + where: { + [Op.or]: [ + { sourceDeviceId: { [Op.in]: deviceIds } }, + { targetDeviceId: { [Op.in]: deviceIds } } + ] + }, + include: [ + { + model: Device, + as: 'sourceDevice', + attributes: ['deviceId', 'name', 'type', 'model', 'status', 'ipAddress', 'rackId', 'position'] + }, + { + model: Device, + as: 'targetDevice', + attributes: ['deviceId', 'name', 'type', 'model', 'status', 'ipAddress', 'rackId', 'position'] + } + ] + }); + + const relatedDeviceIds = new Set(deviceIds); + cables.forEach(cable => { + relatedDeviceIds.add(cable.sourceDeviceId); + relatedDeviceIds.add(cable.targetDeviceId); + }); + + if (relatedDeviceIds.size > parseInt(maxNodes)) { + return res.status(400).json({ + success: false, + error: `设备数量(${relatedDeviceIds.size})超过限制(${maxNodes})` + }); + } + + const allDevices = await Device.findAll({ + where: { deviceId: { [Op.in]: Array.from(relatedDeviceIds) } }, + include: [ + { + model: Rack, + as: 'Rack', + include: [{ model: Room, as: 'Room' }] + } + ] + }); + + const nodes = allDevices.map(device => { + const d = device.toJSON(); + const rack = d.Rack; + const room = rack?.Room; + return { + id: d.deviceId, + deviceId: d.deviceId, + name: d.name, + type: d.type, + model: d.model, + status: d.status, + ipAddress: d.ipAddress, + rackId: d.rackId, + rackName: rack?.name, + roomId: room?.roomId, + roomName: room?.name, + position: d.position, + isCenter: deviceIds.includes(d.deviceId) + }; + }); + + const edges = cables.map(cable => ({ + id: cable.cableId, + source: cable.sourceDeviceId, + target: cable.targetDeviceId, + sourcePort: cable.sourcePort, + targetPort: cable.targetPort, + cableId: cable.cableId, + cableType: cable.cableType, + cableLength: cable.cableLength, + status: cable.status + })); + + const statistics = { + totalDevices: nodes.length, + totalCables: edges.length, + normalCables: edges.filter(e => e.status === 'normal').length, + faultCables: edges.filter(e => e.status === 'fault').length, + disconnectedCables: edges.filter(e => e.status === 'disconnected').length + }; + + res.json({ success: true, data: { nodes, edges, statistics } }); + } catch (error) { + console.error('获取机柜拓扑数据失败:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +module.exports = router; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b6411b8..565b034 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,8 +11,10 @@ "@ant-design/icons": "^6.1.0", "@react-three/drei": "^9.122.0", "@react-three/fiber": "^8.18.0", + "@types/dagre": "^0.7.54", "antd": "^5.8.6", "axios": "^1.13.6", + "dagre": "^0.8.5", "dayjs": "^1.11.19", "framer-motion": "^12.34.0", "html2canvas": "^1.4.1", @@ -2823,6 +2825,12 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.54", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz", + "integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4172,6 +4180,16 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/data-urls": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", @@ -5328,6 +5346,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6249,6 +6276,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index b715f8b..cd36079 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,8 +16,10 @@ "@ant-design/icons": "^6.1.0", "@react-three/drei": "^9.122.0", "@react-three/fiber": "^8.18.0", + "@types/dagre": "^0.7.54", "antd": "^5.8.6", "axios": "^1.13.6", + "dagre": "^0.8.5", "dayjs": "^1.11.19", "framer-motion": "^12.34.0", "html2canvas": "^1.4.1", diff --git a/frontend/public/png/交换机.png b/frontend/public/png/交换机.png new file mode 100644 index 0000000..9de9a0a Binary files /dev/null and b/frontend/public/png/交换机.png differ diff --git a/frontend/public/png/服务器.png b/frontend/public/png/服务器.png new file mode 100644 index 0000000..7613a29 Binary files /dev/null and b/frontend/public/png/服务器.png differ diff --git a/frontend/public/png/路由器.png b/frontend/public/png/路由器.png new file mode 100644 index 0000000..3a3c9f6 Binary files /dev/null and b/frontend/public/png/路由器.png differ diff --git a/frontend/public/png/防火墙.png b/frontend/public/png/防火墙.png new file mode 100644 index 0000000..baa33b1 Binary files /dev/null and b/frontend/public/png/防火墙.png differ diff --git a/frontend/src/components/CableWizardModal.jsx b/frontend/src/components/CableWizardModal.jsx index de39a1a..b639941 100644 --- a/frontend/src/components/CableWizardModal.jsx +++ b/frontend/src/components/CableWizardModal.jsx @@ -189,7 +189,6 @@ const CableWizardModal = ({ visible, onClose, onSuccess, initialSourceDevice, ed useEffect(() => { if (visible) { setCurrentStep(0); - setSourceDevice(null); setTargetDevice(null); setSourcePort(null); setTargetPort(null); @@ -197,22 +196,30 @@ const CableWizardModal = ({ visible, onClose, onSuccess, initialSourceDevice, ed setSelectedCableLength(3); setCableLabel(''); setCableDescription(''); - setSourcePorts([]); setTargetPorts([]); setConflicts([]); setCompatibilityWarning(null); - setDevices([]); setCablesData([]); - fetchDevices('', 'switch').then(deviceList => { - if (initialSourceDevice?.deviceId) { - const device = deviceList.find(d => d.deviceId === initialSourceDevice.deviceId); - if (device) { - setSourceDevice(device); - fetchDevicePorts(device.deviceId, 'source'); - } - } - }); + if (initialSourceDevice?.deviceId) { + const deviceWithId = { + deviceId: initialSourceDevice.deviceId, + name: initialSourceDevice.name || initialSourceDevice.deviceId, + type: initialSourceDevice.type || 'switch', + model: initialSourceDevice.model, + status: initialSourceDevice.status, + Rack: initialSourceDevice.Rack, + ipAddress: initialSourceDevice.ipAddress, + position: initialSourceDevice.position, + }; + setSourceDevice(deviceWithId); + fetchDevicePorts(deviceWithId.deviceId, 'source'); + fetchDevices('', 'switch'); + } else { + setSourceDevice(null); + setSourcePorts([]); + fetchDevices('', 'switch'); + } fetchCables(); } }, [visible, initialSourceDevice, fetchDevices, fetchDevicePorts, fetchCables]); @@ -677,152 +684,165 @@ const Step1SourceDevice = ({ onPortSelect, onDevicesChange, }) => { + const isSourcePreSelected = !!sourceDevice; + return (
{ e.target.style.display = 'none'; }}
+ />
+
{ e.target.style.display = 'none'; }}
+ />
+
{ e.target.style.display = 'none'; }}
+ />
+
{ e.target.style.display = 'none'; }}
+ />
+