chore: 统一代码风格并配置ESLint和Prettier
配置ESLint和Prettier规则 添加前端和后端的忽略文件 统一代码格式和缩进 修复代码风格问题
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,12 @@ import * as THREE from 'three';
|
||||
export const LOD_LEVELS = {
|
||||
HIGH: 0,
|
||||
MEDIUM: 1,
|
||||
LOW: 2
|
||||
LOW: 2,
|
||||
};
|
||||
|
||||
export const LOD_DISTANCES = {
|
||||
HIGH: 5,
|
||||
MEDIUM: 10
|
||||
MEDIUM: 10,
|
||||
};
|
||||
|
||||
const createSimplifiedDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusColor) => {
|
||||
@@ -34,7 +34,7 @@ const createSimplifiedDeviceMesh = (device, uHeight, rackDepth, deviceColor, sta
|
||||
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
||||
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
||||
</mesh>
|
||||
<mesh position={[panelWidth/2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||
<mesh position={[panelWidth / 2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||
<circleGeometry args={[0.006, 16]} />
|
||||
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
||||
</mesh>
|
||||
@@ -65,9 +65,9 @@ const createMediumDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusC
|
||||
<meshStandardMaterial color={deviceColor} roughness={0.8} metalness={0.1} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0, frontZ - panelDepth - chassisDepth / 2]}>
|
||||
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
||||
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
||||
</mesh>
|
||||
<boxGeometry args={[chassisWidth, height - 0.002, chassisDepth]} />
|
||||
<meshStandardMaterial color="#333333" roughness={0.9} metalness={0.3} />
|
||||
</mesh>
|
||||
<group position={[-0.18, 0, frontZ + 0.006]}>
|
||||
<mesh position={[-0.02, 0, 0]}>
|
||||
<boxGeometry args={[0.04, height - 0.01, 0.002]} />
|
||||
@@ -86,7 +86,7 @@ const createMediumDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusC
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
<mesh position={[panelWidth/2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||
<mesh position={[panelWidth / 2 - 0.03, halfHeight - 0.02, frontZ + 0.01]}>
|
||||
<circleGeometry args={[0.006, 16]} />
|
||||
<meshBasicMaterial color={statusColor} toneMapped={false} />
|
||||
</mesh>
|
||||
@@ -94,15 +94,15 @@ const createMediumDeviceMesh = (device, uHeight, rackDepth, deviceColor, statusC
|
||||
);
|
||||
};
|
||||
|
||||
const LODManager = ({
|
||||
device,
|
||||
uHeight,
|
||||
rackDepth,
|
||||
position,
|
||||
deviceColor,
|
||||
statusColor,
|
||||
const LODManager = ({
|
||||
device,
|
||||
uHeight,
|
||||
rackDepth,
|
||||
position,
|
||||
deviceColor,
|
||||
statusColor,
|
||||
children,
|
||||
level = LOD_LEVELS.HIGH
|
||||
level = LOD_LEVELS.HIGH,
|
||||
}) => {
|
||||
const groupRef = useRef();
|
||||
const highDetailRef = useRef();
|
||||
@@ -119,26 +119,26 @@ const LODManager = ({
|
||||
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
|
||||
// 节流:每5帧检查一次
|
||||
frameCount.current++;
|
||||
if (frameCount.current % 5 !== 0) return;
|
||||
|
||||
|
||||
const distance = camera.position.distanceTo(groupRef.current.position);
|
||||
distanceRef.current = distance;
|
||||
|
||||
|
||||
// 添加缓冲避免频繁切换(10% 缓冲)
|
||||
const buffer = 0.1;
|
||||
const highThreshold = LOD_DISTANCES.HIGH * (1 + buffer);
|
||||
const mediumThreshold = LOD_DISTANCES.MEDIUM * (1 + buffer);
|
||||
|
||||
|
||||
let newLevel = LOD_LEVELS.HIGH;
|
||||
if (distance > mediumThreshold) {
|
||||
newLevel = LOD_LEVELS.LOW;
|
||||
} else if (distance > highThreshold) {
|
||||
newLevel = LOD_LEVELS.MEDIUM;
|
||||
}
|
||||
|
||||
|
||||
// 只有当级别变化时才更新
|
||||
if (newLevel !== lodLevelRef.current) {
|
||||
lodLevelRef.current = newLevel;
|
||||
@@ -169,12 +169,12 @@ const LODManager = ({
|
||||
<group ref={highDetailRef} visible={true}>
|
||||
{children}
|
||||
</group>
|
||||
|
||||
|
||||
{/* 中等细节模型 */}
|
||||
<group ref={mediumDetailRef} visible={false}>
|
||||
{createMediumDeviceMesh(device, uHeight, rackDepth, deviceColor, statusColor)}
|
||||
</group>
|
||||
|
||||
|
||||
{/* 低细节模型 */}
|
||||
<group ref={lowDetailRef} visible={false}>
|
||||
{createSimplifiedDeviceMesh(device, uHeight, rackDepth, deviceColor, statusColor)}
|
||||
|
||||
@@ -3,24 +3,24 @@ import * as THREE from 'three';
|
||||
import DeviceModel from './DeviceModel';
|
||||
import LODManager, { LOD_LEVELS } from './LODManager';
|
||||
|
||||
const RackModel = ({
|
||||
rack,
|
||||
devices = [],
|
||||
selectedDeviceId,
|
||||
onDeviceClick,
|
||||
onDeviceLeave,
|
||||
onDeviceHover,
|
||||
onEditDevice,
|
||||
onAddNic,
|
||||
onAddPort,
|
||||
const RackModel = ({
|
||||
rack,
|
||||
devices = [],
|
||||
selectedDeviceId,
|
||||
onDeviceClick,
|
||||
onDeviceLeave,
|
||||
onDeviceHover,
|
||||
onEditDevice,
|
||||
onAddNic,
|
||||
onAddPort,
|
||||
tooltipFields,
|
||||
deviceSlideEnabled = true
|
||||
deviceSlideEnabled = true,
|
||||
}) => {
|
||||
const width = 0.6;
|
||||
const depth = 1.0;
|
||||
const uHeight = 0.04445;
|
||||
const postWidth = 0.05;
|
||||
|
||||
|
||||
const rackHeight = rack?.height || 45;
|
||||
const height = rackHeight * uHeight + 0.2;
|
||||
|
||||
@@ -33,21 +33,21 @@ const RackModel = ({
|
||||
storage: '#8b5cf6',
|
||||
default: '#3b82f6',
|
||||
status: {
|
||||
running: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
offline: '#6b7280'
|
||||
}
|
||||
running: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
offline: '#6b7280',
|
||||
},
|
||||
};
|
||||
|
||||
const getDeviceColor = (type) => {
|
||||
const t = type?.toLowerCase() || '';
|
||||
if (t.includes('server') || t.includes('服务器')) return colors.server;
|
||||
if (t.includes('switch') || t.includes('交换机')) return colors.switch;
|
||||
if (t.includes('router') || t.includes('路由器')) return colors.router;
|
||||
if (t.includes('firewall') || t.includes('防火墙')) return colors.firewall;
|
||||
if (t.includes('storage') || t.includes('存储')) return colors.storage;
|
||||
return colors.default;
|
||||
const getDeviceColor = type => {
|
||||
const t = type?.toLowerCase() || '';
|
||||
if (t.includes('server') || t.includes('服务器')) return colors.server;
|
||||
if (t.includes('switch') || t.includes('交换机')) return colors.switch;
|
||||
if (t.includes('router') || t.includes('路由器')) return colors.router;
|
||||
if (t.includes('firewall') || t.includes('防火墙')) return colors.firewall;
|
||||
if (t.includes('storage') || t.includes('存储')) return colors.storage;
|
||||
return colors.default;
|
||||
};
|
||||
|
||||
// 设备组的Y偏移量(与下方设备渲染的偏移一致)
|
||||
@@ -62,7 +62,7 @@ const RackModel = ({
|
||||
const yPos = (u - 1) * uHeight + uHeight / 2 + deviceGroupOffset;
|
||||
|
||||
// 创建数字纹理 - 白色数字在深色背景上更清晰
|
||||
const createNumberTexture = (num) => {
|
||||
const createNumberTexture = num => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
@@ -90,9 +90,9 @@ const RackModel = ({
|
||||
const planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize);
|
||||
|
||||
// 前侧柱子的位置: [-width/2 + postWidth/2, y, depth/2 - postWidth/2]
|
||||
const leftPostX = -width/2 + postWidth/2;
|
||||
const rightPostX = width/2 - postWidth/2;
|
||||
const frontPostZ = depth/2 - postWidth/2;
|
||||
const leftPostX = -width / 2 + postWidth / 2;
|
||||
const rightPostX = width / 2 - postWidth / 2;
|
||||
const frontPostZ = depth / 2 - postWidth / 2;
|
||||
|
||||
// 刻度线颜色:每5U使用醒目的黄色,其他使用灰色
|
||||
const tickColor = isMajorU ? '#fbbf24' : '#6b7280';
|
||||
@@ -102,7 +102,7 @@ const RackModel = ({
|
||||
<group key={`u-label-${u}`}>
|
||||
{/* 左前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||
<mesh
|
||||
position={[leftPostX, yPos, frontPostZ + postWidth/2 + 0.001]}
|
||||
position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||
geometry={planeGeometry}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
@@ -114,7 +114,7 @@ const RackModel = ({
|
||||
</mesh>
|
||||
{/* 右前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||
<mesh
|
||||
position={[rightPostX, yPos, frontPostZ + postWidth/2 + 0.001]}
|
||||
position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||
geometry={planeGeometry}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
@@ -125,12 +125,12 @@ const RackModel = ({
|
||||
/>
|
||||
</mesh>
|
||||
{/* 左前侧柱子上的刻度线 */}
|
||||
<mesh position={[leftPostX, yPos, frontPostZ + postWidth/2 + 0.002]}>
|
||||
<mesh position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||
<meshBasicMaterial color={tickColor} />
|
||||
</mesh>
|
||||
{/* 右前侧柱子上的刻度线 */}
|
||||
<mesh position={[rightPostX, yPos, frontPostZ + postWidth/2 + 0.002]}>
|
||||
<mesh position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||
<meshBasicMaterial color={tickColor} />
|
||||
</mesh>
|
||||
@@ -142,53 +142,47 @@ const RackModel = ({
|
||||
|
||||
// 生成机柜框架
|
||||
const frame = useMemo(() => {
|
||||
const materialProps = { color: "#333", roughness: 0.5, metalness: 0.8 };
|
||||
const materialProps = { color: '#333', roughness: 0.5, metalness: 0.8 };
|
||||
const postArgs = [postWidth, height, postWidth];
|
||||
const topBottomArgs = [width + 0.02, 0.02, depth + 0.02];
|
||||
|
||||
return (
|
||||
<group>
|
||||
<mesh position={[-width/2 + postWidth/2, height/2, -depth/2 + postWidth/2]}>
|
||||
<mesh position={[-width / 2 + postWidth / 2, height / 2, -depth / 2 + postWidth / 2]}>
|
||||
<boxGeometry args={postArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
<mesh position={[width/2 - postWidth/2, height/2, -depth/2 + postWidth/2]}>
|
||||
<mesh position={[width / 2 - postWidth / 2, height / 2, -depth / 2 + postWidth / 2]}>
|
||||
<boxGeometry args={postArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
<mesh position={[-width/2 + postWidth/2, height/2, depth/2 - postWidth/2]}>
|
||||
<mesh position={[-width / 2 + postWidth / 2, height / 2, depth / 2 - postWidth / 2]}>
|
||||
<boxGeometry args={postArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
<mesh position={[width/2 - postWidth/2, height/2, depth/2 - postWidth/2]}>
|
||||
<mesh position={[width / 2 - postWidth / 2, height / 2, depth / 2 - postWidth / 2]}>
|
||||
<boxGeometry args={postArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[0, height, 0]}>
|
||||
<boxGeometry args={topBottomArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
<boxGeometry args={topBottomArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0, 0]}>
|
||||
<boxGeometry args={topBottomArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
<boxGeometry args={topBottomArgs} />
|
||||
<meshStandardMaterial {...materialProps} />
|
||||
</mesh>
|
||||
|
||||
{[-1, 1].map((side) => (
|
||||
<mesh key={`side-${side}`} position={[side * (width/2 - 0.005), height/2, 0]}>
|
||||
<boxGeometry args={[0.01, height - 0.04, depth - 0.04]} />
|
||||
<meshStandardMaterial
|
||||
color="#2d3748"
|
||||
roughness={0.4}
|
||||
metalness={0.7}
|
||||
side={2}
|
||||
/>
|
||||
</mesh>
|
||||
{[-1, 1].map(side => (
|
||||
<mesh key={`side-${side}`} position={[side * (width / 2 - 0.005), height / 2, 0]}>
|
||||
<boxGeometry args={[0.01, height - 0.04, depth - 0.04]} />
|
||||
<meshStandardMaterial color="#2d3748" roughness={0.4} metalness={0.7} side={2} />
|
||||
</mesh>
|
||||
))}
|
||||
|
||||
{/* U位刻度标识 */}
|
||||
{uLabels}
|
||||
|
||||
</group>
|
||||
);
|
||||
}, [width, height, depth, postWidth, uLabels]);
|
||||
@@ -197,47 +191,47 @@ const RackModel = ({
|
||||
<group position={[0, 0.5, 0]}>
|
||||
{frame}
|
||||
|
||||
<group position={[0, 0.1, 0]}>
|
||||
{devices.map((device) => {
|
||||
const uStart = device.position || device.u_position || 1;
|
||||
const uSize = device.height || device.u_height || 1;
|
||||
const yPos = (uStart - 1) * uHeight + (uSize * uHeight) / 2;
|
||||
|
||||
const deviceColor = getDeviceColor(device.type);
|
||||
const statusColor = colors.status[device.status] || colors.status.running;
|
||||
|
||||
return (
|
||||
<LODManager
|
||||
key={device.id}
|
||||
device={device}
|
||||
uHeight={uHeight}
|
||||
rackDepth={depth}
|
||||
position={[0, yPos, 0]}
|
||||
deviceColor={deviceColor}
|
||||
statusColor={statusColor}
|
||||
level={LOD_LEVELS.HIGH}
|
||||
>
|
||||
<DeviceModel
|
||||
device={device}
|
||||
uHeight={uHeight}
|
||||
rackDepth={depth}
|
||||
position={[0, 0, 0]}
|
||||
isSelected={selectedDeviceId === device.id}
|
||||
onClick={onDeviceClick}
|
||||
onPointerOver={onDeviceHover}
|
||||
onPointerOut={onDeviceLeave}
|
||||
onEdit={onEditDevice}
|
||||
onAddNic={onAddNic}
|
||||
onAddPort={onAddPort}
|
||||
tooltipFields={tooltipFields}
|
||||
slideEnabled={deviceSlideEnabled}
|
||||
/>
|
||||
</LODManager>
|
||||
);
|
||||
<group position={[0, 0.1, 0]}>
|
||||
{devices.map(device => {
|
||||
const uStart = device.position || device.u_position || 1;
|
||||
const uSize = device.height || device.u_height || 1;
|
||||
const yPos = (uStart - 1) * uHeight + (uSize * uHeight) / 2;
|
||||
|
||||
const deviceColor = getDeviceColor(device.type);
|
||||
const statusColor = colors.status[device.status] || colors.status.running;
|
||||
|
||||
return (
|
||||
<LODManager
|
||||
key={device.id}
|
||||
device={device}
|
||||
uHeight={uHeight}
|
||||
rackDepth={depth}
|
||||
position={[0, yPos, 0]}
|
||||
deviceColor={deviceColor}
|
||||
statusColor={statusColor}
|
||||
level={LOD_LEVELS.HIGH}
|
||||
>
|
||||
<DeviceModel
|
||||
device={device}
|
||||
uHeight={uHeight}
|
||||
rackDepth={depth}
|
||||
position={[0, 0, 0]}
|
||||
isSelected={selectedDeviceId === device.id}
|
||||
onClick={onDeviceClick}
|
||||
onPointerOver={onDeviceHover}
|
||||
onPointerOut={onDeviceLeave}
|
||||
onEdit={onEditDevice}
|
||||
onAddNic={onAddNic}
|
||||
onAddPort={onAddPort}
|
||||
tooltipFields={tooltipFields}
|
||||
slideEnabled={deviceSlideEnabled}
|
||||
/>
|
||||
</LODManager>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default RackModel;
|
||||
export default RackModel;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import React, { Suspense, useMemo, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import React, {
|
||||
Suspense,
|
||||
useMemo,
|
||||
useRef,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
|
||||
const envMapUrl = '/assets/3d/env.hdr';
|
||||
@@ -22,7 +29,7 @@ const Controls = ({ rack, onControlsReady }) => {
|
||||
const { camera } = useThree();
|
||||
// 机柜中心点(中轴线)
|
||||
const rackHeight = rack?.height || 45;
|
||||
const targetY = rackHeight * 0.04445 / 2 + 0.5;
|
||||
const targetY = (rackHeight * 0.04445) / 2 + 0.5;
|
||||
const fixedTarget = useMemo(() => new THREE.Vector3(0, targetY, 0), [targetY]);
|
||||
|
||||
// 根据机柜高度计算合适的相机距离限制
|
||||
@@ -48,7 +55,7 @@ const Controls = ({ rack, onControlsReady }) => {
|
||||
camera.position.copy(initialCameraPosition);
|
||||
controlsRef.current.target.copy(fixedTarget);
|
||||
controlsRef.current.update();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -76,107 +83,122 @@ const Controls = ({ rack, onControlsReady }) => {
|
||||
enableZoom={true}
|
||||
enableRotate={true}
|
||||
mouseButtons={{
|
||||
LEFT: 0, // 左键旋转
|
||||
MIDDLE: 1, // 中键平移
|
||||
RIGHT: 2 // 右键平移
|
||||
LEFT: 0, // 左键旋转
|
||||
MIDDLE: 1, // 中键平移
|
||||
RIGHT: 2, // 右键平移
|
||||
}}
|
||||
touches={{
|
||||
ONE: 1,
|
||||
TWO: 2
|
||||
TWO: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Scene = forwardRef(({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => {
|
||||
// 从 Context 获取3D场景状态
|
||||
const {
|
||||
devices,
|
||||
selectedDevice,
|
||||
deviceSlideEnabled
|
||||
} = useScene3D();
|
||||
const Scene = forwardRef(
|
||||
({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => {
|
||||
// 从 Context 获取3D场景状态
|
||||
const { devices, selectedDevice, deviceSlideEnabled } = useScene3D();
|
||||
|
||||
// 用于存储 controls API
|
||||
const controlsApiRef = useRef(null);
|
||||
// 用于存储 controls API
|
||||
const controlsApiRef = useRef(null);
|
||||
|
||||
// 使用 useImperativeHandle 暴露重置方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetView: () => {
|
||||
if (controlsApiRef.current) {
|
||||
controlsApiRef.current.reset();
|
||||
}
|
||||
}
|
||||
}));
|
||||
// 使用 useImperativeHandle 暴露重置方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetView: () => {
|
||||
if (controlsApiRef.current) {
|
||||
controlsApiRef.current.reset();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// 使用 useMemo 稳定 props 引用
|
||||
const rackModelProps = useMemo(() => ({
|
||||
rack,
|
||||
devices,
|
||||
selectedDeviceId: selectedDevice?.id,
|
||||
onDeviceClick,
|
||||
onDeviceLeave,
|
||||
onDeviceHover,
|
||||
tooltipFields,
|
||||
deviceSlideEnabled
|
||||
}), [rack, devices, selectedDevice, onDeviceClick, onDeviceLeave, onDeviceHover, tooltipFields, deviceSlideEnabled]);
|
||||
// 使用 useMemo 稳定 props 引用
|
||||
const rackModelProps = useMemo(
|
||||
() => ({
|
||||
rack,
|
||||
devices,
|
||||
selectedDeviceId: selectedDevice?.id,
|
||||
onDeviceClick,
|
||||
onDeviceLeave,
|
||||
onDeviceHover,
|
||||
tooltipFields,
|
||||
deviceSlideEnabled,
|
||||
}),
|
||||
[
|
||||
rack,
|
||||
devices,
|
||||
selectedDevice,
|
||||
onDeviceClick,
|
||||
onDeviceLeave,
|
||||
onDeviceHover,
|
||||
tooltipFields,
|
||||
deviceSlideEnabled,
|
||||
]
|
||||
);
|
||||
|
||||
// 根据机柜高度动态计算相机初始位置
|
||||
const rackHeight = rack?.height || 45;
|
||||
const rackHeightMeters = rackHeight * 0.04445;
|
||||
// 相机位置:确保能完整看到机柜,高度随机柜高度调整
|
||||
const cameraPosition = useMemo(() => {
|
||||
const baseHeight = 2;
|
||||
const heightFactor = rackHeightMeters * 0.6;
|
||||
const distance = Math.max(3, rackHeightMeters * 1.2);
|
||||
return [distance * 0.7, baseHeight + heightFactor * 0.3, distance];
|
||||
}, [rackHeightMeters]);
|
||||
// 根据机柜高度动态计算相机初始位置
|
||||
const rackHeight = rack?.height || 45;
|
||||
const rackHeightMeters = rackHeight * 0.04445;
|
||||
// 相机位置:确保能完整看到机柜,高度随机柜高度调整
|
||||
const cameraPosition = useMemo(() => {
|
||||
const baseHeight = 2;
|
||||
const heightFactor = rackHeightMeters * 0.6;
|
||||
const distance = Math.max(3, rackHeightMeters * 1.2);
|
||||
return [distance * 0.7, baseHeight + heightFactor * 0.3, distance];
|
||||
}, [rackHeightMeters]);
|
||||
|
||||
// 相机目标点(机柜中心)
|
||||
const cameraTarget = useMemo(() => {
|
||||
return [0, rackHeightMeters / 2 + 0.5, 0];
|
||||
}, [rackHeightMeters]);
|
||||
// 相机目标点(机柜中心)
|
||||
const cameraTarget = useMemo(() => {
|
||||
return [0, rackHeightMeters / 2 + 0.5, 0];
|
||||
}, [rackHeightMeters]);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={deviceDpr}
|
||||
performance={{ min: 0.5 }}
|
||||
gl={{
|
||||
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
||||
alpha: true, // 必须开启alpha以支持透明背景
|
||||
powerPreference: 'high-performance'
|
||||
}}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={deviceDpr}
|
||||
performance={{ min: 0.5 }}
|
||||
gl={{
|
||||
antialias: true, // 对所有设备开启抗锯齿提升清晰度
|
||||
alpha: true, // 必须开启alpha以支持透明背景
|
||||
powerPreference: 'high-performance',
|
||||
}}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||
|
||||
<ambientLight intensity={0.5} color="#ffffff" />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-far={20}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
shadow-camera-top={10}
|
||||
shadow-camera-bottom={-10}
|
||||
/>
|
||||
<ambientLight intensity={0.5} color="#ffffff" />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-far={20}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
shadow-camera-top={10}
|
||||
shadow-camera-bottom={-10}
|
||||
/>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<Environment files={envMapUrl} blur={0.5} resolution={256} background={false} />
|
||||
</Suspense>
|
||||
|
||||
{/* Models */}
|
||||
<group position={[0, 0, 0]}>
|
||||
<RackModel {...rackModelProps} />
|
||||
</group>
|
||||
{/* Models */}
|
||||
<group position={[0, 0, 0]}>
|
||||
<RackModel {...rackModelProps} />
|
||||
</group>
|
||||
|
||||
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
||||
<Controls rack={rack} onControlsReady={(api) => { controlsApiRef.current = api; }} />
|
||||
</Canvas>
|
||||
);
|
||||
});
|
||||
{/* Controls - 使用独立组件保持旋转中心固定 */}
|
||||
<Controls
|
||||
rack={rack}
|
||||
onControlsReady={api => {
|
||||
controlsApiRef.current = api;
|
||||
}}
|
||||
/>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Scene;
|
||||
export default Scene;
|
||||
|
||||
@@ -3,27 +3,62 @@ import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
||||
|
||||
export const createDeviceChassisMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_CHASSIS];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDevicePanelMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDevicePanelSelectedMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DEVICE_PANEL_SELECTED];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDriveTrayMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDriveTrayHandleMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.DRIVE_TRAY_HANDLE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createLedIndicatorMaterial = (color = '#10b981', options = {}) => {
|
||||
@@ -37,32 +72,78 @@ export const createLedErrorMaterial = (options = {}) => {
|
||||
|
||||
export const createSfpPortMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SFP_PORT];
|
||||
return <meshPhysicalMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} transparent opacity={config.opacity} {...options} />;
|
||||
return (
|
||||
<meshPhysicalMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
transparent
|
||||
opacity={config.opacity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createRj45PortMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RJ45_PORT];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createVentHoleMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.VENT_HOLE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createBackPanelMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BACK_PANEL];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} transparent opacity={config.opacity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
transparent
|
||||
opacity={config.opacity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createPsuModuleMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.PSU_MODULE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createFanModuleMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.FAN_MODULE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createConsolePortMaterial = (color = '#facc15') => {
|
||||
|
||||
@@ -3,32 +3,79 @@ import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
||||
|
||||
export const createRackFrameMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RACK_FRAME];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
envMapIntensity={config.envMapIntensity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createRailMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
envMapIntensity={config.envMapIntensity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createRailHoleMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.RAIL_HOLE];
|
||||
return <meshBasicMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} {...options} />;
|
||||
return (
|
||||
<meshBasicMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createSidePanelMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.SIDE_PANEL];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
envMapIntensity={config.envMapIntensity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createTopPlateMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.TOP_PLATE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
envMapIntensity={config.envMapIntensity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createBottomPlateMaterial = (options = {}) => {
|
||||
const config = MATERIAL_CONFIGS[MATERIAL_TYPES.BOTTOM_PLATE];
|
||||
return <meshStandardMaterial color={config.color} metalness={config.metalness} roughness={config.roughness} envMapIntensity={config.envMapIntensity} {...options} />;
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={config.color}
|
||||
metalness={config.metalness}
|
||||
roughness={config.roughness}
|
||||
envMapIntensity={config.envMapIntensity}
|
||||
{...options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const createTextLabelMaterial = (color = '#ffffff') => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MATERIAL_CONFIGS, MATERIAL_TYPES } from './constants.js';
|
||||
|
||||
export const getMaterialConfig = (type) => {
|
||||
export const getMaterialConfig = type => {
|
||||
return MATERIAL_CONFIGS[type] || null;
|
||||
};
|
||||
|
||||
export const getMaterialType = (type) => {
|
||||
export const getMaterialType = type => {
|
||||
return MATERIAL_TYPES[type] || null;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
form.resetFields();
|
||||
if (sourceDevice) {
|
||||
form.setFieldsValue({
|
||||
sourceDeviceId: sourceDevice.deviceId || sourceDevice.id,
|
||||
sourceDeviceId: sourceDevice.deviceId || sourceDevice.id,
|
||||
});
|
||||
fetchDevicePorts(sourceDevice.deviceId || sourceDevice.id, 'source');
|
||||
}
|
||||
@@ -63,12 +63,12 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSourceDeviceChange = (deviceId) => {
|
||||
const handleSourceDeviceChange = deviceId => {
|
||||
form.setFieldsValue({ sourcePort: undefined });
|
||||
fetchDevicePorts(deviceId, 'source');
|
||||
};
|
||||
|
||||
const handleTargetDeviceChange = (deviceId) => {
|
||||
const handleTargetDeviceChange = deviceId => {
|
||||
form.setFieldsValue({ targetPort: undefined });
|
||||
fetchDevicePorts(deviceId, 'target');
|
||||
};
|
||||
@@ -85,22 +85,19 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
let payload = { ...values };
|
||||
|
||||
// If Source is NOT Switch AND Target IS Switch, swap them
|
||||
if (sourceDev && targetDev &&
|
||||
sourceDev.type !== 'switch' &&
|
||||
targetDev.type === 'switch') {
|
||||
|
||||
payload = {
|
||||
...values,
|
||||
sourceDeviceId: values.targetDeviceId,
|
||||
sourcePort: values.targetPort,
|
||||
targetDeviceId: values.sourceDeviceId,
|
||||
targetPort: values.sourcePort
|
||||
};
|
||||
console.log('Swapped source/target to ensure Switch is Source');
|
||||
if (sourceDev && targetDev && sourceDev.type !== 'switch' && targetDev.type === 'switch') {
|
||||
payload = {
|
||||
...values,
|
||||
sourceDeviceId: values.targetDeviceId,
|
||||
sourcePort: values.targetPort,
|
||||
targetDeviceId: values.sourceDeviceId,
|
||||
targetPort: values.sourcePort,
|
||||
};
|
||||
console.log('Swapped source/target to ensure Switch is Source');
|
||||
}
|
||||
|
||||
await axios.post('/api/cables', payload);
|
||||
|
||||
|
||||
message.success('接线创建成功');
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
@@ -130,122 +127,125 @@ const CableCreateModal = ({ visible, onClose, onSuccess, sourceDevice }) => {
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{/* Source Side */}
|
||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>源设备 (起点)</div>
|
||||
<Form.Item
|
||||
name="sourceDeviceId"
|
||||
label="设备"
|
||||
rules={[{ required: true, message: '请选择源设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择源设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={handleSourceDeviceChange}
|
||||
loading={fetchingDevices}
|
||||
disabled={!!sourceDevice} // Lock source device if provided
|
||||
>
|
||||
{devices.map(d => (
|
||||
<Option key={d.deviceId} value={d.deviceId}>{d.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sourcePort"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请选择源端口' }]}
|
||||
>
|
||||
<Select placeholder="选择源端口" showSearch>
|
||||
{sourcePorts.map(p => (
|
||||
<Option key={p.portId} value={p.portName}>
|
||||
{p.portName} ({p.portType})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* Source Side */}
|
||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>源设备 (起点)</div>
|
||||
<Form.Item
|
||||
name="sourceDeviceId"
|
||||
label="设备"
|
||||
rules={[{ required: true, message: '请选择源设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择源设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={handleSourceDeviceChange}
|
||||
loading={fetchingDevices}
|
||||
disabled={!!sourceDevice} // Lock source device if provided
|
||||
>
|
||||
{devices.map(d => (
|
||||
<Option key={d.deviceId} value={d.deviceId}>
|
||||
{d.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sourcePort"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请选择源端口' }]}
|
||||
>
|
||||
<Select placeholder="选择源端口" showSearch>
|
||||
{sourcePorts.map(p => (
|
||||
<Option key={p.portId} value={p.portName}>
|
||||
{p.portName} ({p.portType})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Target Side */}
|
||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>目标设备 (终点)</div>
|
||||
<Form.Item
|
||||
name="targetDeviceId"
|
||||
label="设备"
|
||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择目标设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={handleTargetDeviceChange}
|
||||
loading={fetchingDevices}
|
||||
>
|
||||
{devices.filter(d => d.deviceId !== form.getFieldValue('sourceDeviceId')).map(d => (
|
||||
<Option key={d.deviceId} value={d.deviceId}>{d.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="targetPort"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请选择目标端口' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择目标端口"
|
||||
showSearch
|
||||
disabled={!form.getFieldValue('targetDeviceId')}
|
||||
>
|
||||
{targetPorts.map(p => (
|
||||
<Option key={p.portId} value={p.portName}>
|
||||
{p.portName} ({p.portType})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{/* Target Side */}
|
||||
<div style={{ padding: '12px', background: '#f9f9f9', borderRadius: '8px' }}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 500, color: '#666' }}>目标设备 (终点)</div>
|
||||
<Form.Item
|
||||
name="targetDeviceId"
|
||||
label="设备"
|
||||
rules={[{ required: true, message: '请选择目标设备' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择目标设备"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.children ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onChange={handleTargetDeviceChange}
|
||||
loading={fetchingDevices}
|
||||
>
|
||||
{devices
|
||||
.filter(d => d.deviceId !== form.getFieldValue('sourceDeviceId'))
|
||||
.map(d => (
|
||||
<Option key={d.deviceId} value={d.deviceId}>
|
||||
{d.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="targetPort"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请选择目标端口' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择目标端口"
|
||||
showSearch
|
||||
disabled={!form.getFieldValue('targetDeviceId')}
|
||||
>
|
||||
{targetPorts.map(p => (
|
||||
<Option key={p.portId} value={p.portName}>
|
||||
{p.portName} ({p.portType})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
|
||||
<Form.Item
|
||||
name="cableType"
|
||||
label="线缆类型"
|
||||
initialValue="ethernet"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="ethernet">网线</Option>
|
||||
<Option value="fiber">光纤</Option>
|
||||
<Option value="copper">铜缆</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
initialValue="normal"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="normal">正常</Option>
|
||||
<Option value="fault">故障</Option>
|
||||
<Option value="disconnected">未连接</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="cableLength"
|
||||
label="长度 (米)"
|
||||
>
|
||||
<Input type="number" min={0} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={2} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16 }}>
|
||||
<Form.Item
|
||||
name="cableType"
|
||||
label="线缆类型"
|
||||
initialValue="ethernet"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="ethernet">网线</Option>
|
||||
<Option value="fiber">光纤</Option>
|
||||
<Option value="copper">铜缆</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="状态"
|
||||
initialValue="normal"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="normal">正常</Option>
|
||||
<Option value="fault">故障</Option>
|
||||
<Option value="disconnected">未连接</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="cableLength" label="长度 (米)">
|
||||
<Input type="number" min={0} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Drawer, Tabs, Tag, Space, Typography, Empty, Card, Tooltip, Button, Popconfirm } from 'antd';
|
||||
import { ApiOutlined, CloudServerOutlined, EnvironmentOutlined, EditOutlined, PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Drawer,
|
||||
Tabs,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Empty,
|
||||
Card,
|
||||
Tooltip,
|
||||
Button,
|
||||
Popconfirm,
|
||||
} from 'antd';
|
||||
import {
|
||||
ApiOutlined,
|
||||
CloudServerOutlined,
|
||||
EnvironmentOutlined,
|
||||
EditOutlined,
|
||||
PlusCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import NetworkCardPanel from './NetworkCardPanel';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -10,25 +28,38 @@ const designTokens = {
|
||||
primary: '#667eea',
|
||||
success: '#10b981',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b'
|
||||
warning: '#f59e0b',
|
||||
},
|
||||
spacing: {
|
||||
sm: 8,
|
||||
md: 16
|
||||
}
|
||||
md: 16,
|
||||
},
|
||||
};
|
||||
|
||||
function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables, onEdit, onAddNic, onAddPort, onAddCable, onDeleteCable, tooltipFields, refreshTrigger }) {
|
||||
function DeviceDetailDrawer({
|
||||
device,
|
||||
visible,
|
||||
onClose,
|
||||
cables,
|
||||
onRefreshCables,
|
||||
onEdit,
|
||||
onAddNic,
|
||||
onAddPort,
|
||||
onAddCable,
|
||||
onDeleteCable,
|
||||
tooltipFields,
|
||||
refreshTrigger,
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState('ports');
|
||||
|
||||
const deviceCables = useMemo(() => {
|
||||
if (!device || !cables) return [];
|
||||
return cables.filter(c =>
|
||||
c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId
|
||||
return cables.filter(
|
||||
c => c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId
|
||||
);
|
||||
}, [device, cables]);
|
||||
|
||||
const getStatusTag = useCallback((status) => {
|
||||
const getStatusTag = useCallback(status => {
|
||||
const config = {
|
||||
running: { color: 'success', text: '运行中' },
|
||||
normal: { color: 'success', text: '正常' },
|
||||
@@ -36,13 +67,13 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
error: { color: 'error', text: '故障' },
|
||||
fault: { color: 'error', text: '故障' },
|
||||
offline: { color: 'default', text: '离线' },
|
||||
maintenance: { color: 'processing', text: '维护中' }
|
||||
maintenance: { color: 'processing', text: '维护中' },
|
||||
};
|
||||
const { color, text } = config[status] || { color: 'default', text: status };
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
}, []);
|
||||
|
||||
const getDeviceTypeName = useCallback((type) => {
|
||||
const getDeviceTypeName = useCallback(type => {
|
||||
const typeMap = {
|
||||
server: '服务器',
|
||||
switch: '交换机',
|
||||
@@ -50,41 +81,53 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
storage: '存储设备',
|
||||
firewall: '防火墙',
|
||||
ups: 'UPS',
|
||||
pdu: 'PDU'
|
||||
pdu: 'PDU',
|
||||
};
|
||||
return typeMap[type?.toLowerCase()] || type || '未知设备';
|
||||
}, []);
|
||||
|
||||
const renderFieldValue = useCallback((field, device) => {
|
||||
const renderFieldValue = useCallback(
|
||||
(field, device) => {
|
||||
const fieldKey = field.field;
|
||||
if (fieldKey === 'status') return getStatusTag(device.status);
|
||||
|
||||
|
||||
// 优先从device对象获取值,如果没有则从customFields中获取
|
||||
let value = device[fieldKey];
|
||||
if ((value === undefined || value === null) && device.customFields && typeof device.customFields === 'object') {
|
||||
value = device.customFields[fieldKey];
|
||||
if (
|
||||
(value === undefined || value === null) &&
|
||||
device.customFields &&
|
||||
typeof device.customFields === 'object'
|
||||
) {
|
||||
value = device.customFields[fieldKey];
|
||||
}
|
||||
|
||||
|
||||
if (fieldKey === 'type') value = getDeviceTypeName(value);
|
||||
else if (fieldKey === 'position') value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
|
||||
|
||||
return <Text strong style={{ fontSize: '14px' }}>{value !== undefined && value !== null ? value : '-'}</Text>;
|
||||
}, [getStatusTag, getDeviceTypeName]);
|
||||
else if (fieldKey === 'position')
|
||||
value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
|
||||
|
||||
return (
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{value !== undefined && value !== null ? value : '-'}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
[getStatusTag, getDeviceTypeName]
|
||||
);
|
||||
|
||||
const displayFields = useMemo(() => {
|
||||
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
|
||||
return Object.values(tooltipFields).filter(f => f.enabled);
|
||||
}
|
||||
|
||||
// Default fallback fields if no config
|
||||
return [
|
||||
{ field: 'deviceId', label: '设备ID' },
|
||||
{ field: 'type', label: '设备类型' },
|
||||
{ field: 'status', label: '设备状态' },
|
||||
{ field: 'position', label: '位置' },
|
||||
{ field: 'ipAddress', label: 'IP地址' },
|
||||
{ field: 'brand', label: '品牌' }
|
||||
];
|
||||
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
|
||||
return Object.values(tooltipFields).filter(f => f.enabled);
|
||||
}
|
||||
|
||||
// Default fallback fields if no config
|
||||
return [
|
||||
{ field: 'deviceId', label: '设备ID' },
|
||||
{ field: 'type', label: '设备类型' },
|
||||
{ field: 'status', label: '设备状态' },
|
||||
{ field: 'position', label: '位置' },
|
||||
{ field: 'ipAddress', label: 'IP地址' },
|
||||
{ field: 'brand', label: '品牌' },
|
||||
];
|
||||
}, [tooltipFields]);
|
||||
|
||||
const tabItems = [
|
||||
@@ -103,7 +146,7 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
onRefresh={onRefreshCables}
|
||||
refreshTrigger={refreshTrigger}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'cables',
|
||||
@@ -138,38 +181,64 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
<div style={{ marginBottom: designTokens.spacing.sm }}>
|
||||
<Space direction="vertical" size={4}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>源设备:</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
源设备:
|
||||
</Text>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{cable.sourceDevice?.name || '-'}
|
||||
<Tag color="blue" style={{ marginLeft: '8px' }}>{cable.sourcePort}</Tag>
|
||||
<Tag color="blue" style={{ marginLeft: '8px' }}>
|
||||
{cable.sourcePort}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>目标设备:</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
目标设备:
|
||||
</Text>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{cable.targetDevice?.name || '-'}
|
||||
<Tag color="green" style={{ marginLeft: '8px' }}>{cable.targetPort}</Tag>
|
||||
<Tag color="green" style={{ marginLeft: '8px' }}>
|
||||
{cable.targetPort}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Space wrap>
|
||||
<Tag color={cable.status === 'normal' ? 'success' : cable.status === 'fault' ? 'error' : 'default'}>
|
||||
{cable.status === 'normal' ? '正常' : cable.status === 'fault' ? '故障' : '未连接'}
|
||||
<Tag
|
||||
color={
|
||||
cable.status === 'normal'
|
||||
? 'success'
|
||||
: cable.status === 'fault'
|
||||
? 'error'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{cable.status === 'normal'
|
||||
? '正常'
|
||||
: cable.status === 'fault'
|
||||
? '故障'
|
||||
: '未连接'}
|
||||
</Tag>
|
||||
<Tag color="purple">
|
||||
{cable.cableType === 'ethernet' ? '网线' : cable.cableType === 'fiber' ? '光纤' : '铜缆'}
|
||||
{cable.cableType === 'ethernet'
|
||||
? '网线'
|
||||
: cable.cableType === 'fiber'
|
||||
? '光纤'
|
||||
: '铜缆'}
|
||||
</Tag>
|
||||
{cable.cableLength && (
|
||||
<Tag color="orange">
|
||||
{cable.cableLength}m
|
||||
</Tag>
|
||||
)}
|
||||
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
|
||||
</Space>
|
||||
|
||||
{cable.description && (
|
||||
<div style={{ marginTop: designTokens.spacing.sm, fontSize: '12px', color: '#666' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: designTokens.spacing.sm,
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{cable.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -178,8 +247,8 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (!device) return null;
|
||||
@@ -189,11 +258,15 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
title={
|
||||
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
|
||||
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
|
||||
<span style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>设备详情 - {device.name}</span>
|
||||
<span
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
设备详情 - {device.name}
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
placement="right"
|
||||
@@ -206,42 +279,51 @@ function DeviceDetailDrawer({ device, visible, onClose, cables, onRefreshCables,
|
||||
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} />
|
||||
</Tooltip>
|
||||
<Tooltip title="添加网卡">
|
||||
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>加网卡</Button>
|
||||
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
|
||||
加网卡
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="添加端口">
|
||||
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>加端口</Button>
|
||||
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>
|
||||
加端口
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="添加接线">
|
||||
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>加接线</Button>
|
||||
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>
|
||||
加接线
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
styles={{ body: { padding: '16px 20px', overflow: 'auto' } }}
|
||||
>
|
||||
<div className="device-info-section" style={{ marginBottom: '20px' }}>
|
||||
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}>基本信息</Title>
|
||||
<div className="info-grid" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '12px',
|
||||
background: '#f8fafc',
|
||||
padding: '16px',
|
||||
borderRadius: '10px'
|
||||
}}>
|
||||
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}>
|
||||
基本信息
|
||||
</Title>
|
||||
<div
|
||||
className="info-grid"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '12px',
|
||||
background: '#f8fafc',
|
||||
padding: '16px',
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
{displayFields.map(field => (
|
||||
<div className="info-item" key={field.field}>
|
||||
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>{field.label}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>
|
||||
{field.label}
|
||||
</Text>
|
||||
{renderFieldValue(field, device)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
/>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ const { TextArea } = Input;
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea'
|
||||
}
|
||||
main: '#667eea',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
medium: '10px'
|
||||
}
|
||||
medium: '10px',
|
||||
},
|
||||
};
|
||||
|
||||
function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
@@ -33,7 +33,7 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
description: values.description,
|
||||
model: values.model,
|
||||
manufacturer: values.manufacturer,
|
||||
status: values.status
|
||||
status: values.status,
|
||||
});
|
||||
|
||||
message.success('网卡创建成功');
|
||||
@@ -77,7 +77,7 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
status: 'normal'
|
||||
status: 'normal',
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
@@ -92,24 +92,15 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
}
|
||||
rules={[
|
||||
{ required: true, message: '请输入网卡名称' },
|
||||
{ max: 50, message: '名称不能超过50个字符' }
|
||||
{ max: 50, message: '名称不能超过50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如: 网卡1、eth0、LAN1" />
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ display: 'flex', width: '100%' }}>
|
||||
<Form.Item
|
||||
name="slotNumber"
|
||||
label="插槽编号"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="可选"
|
||||
min={1}
|
||||
max={100}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Form.Item name="slotNumber" label="插槽编号" style={{ flex: 1 }}>
|
||||
<InputNumber placeholder="可选" min={1} max={100} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -128,27 +119,16 @@ function NetworkCardCreateModal({ device, visible, onClose, onSuccess }) {
|
||||
</Space>
|
||||
|
||||
<Space style={{ display: 'flex', width: '100%' }}>
|
||||
<Form.Item
|
||||
name="manufacturer"
|
||||
label="制造商"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Form.Item name="manufacturer" label="制造商" style={{ flex: 1 }}>
|
||||
<Input placeholder="如: Intel、Realtek、Broadcom" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="model"
|
||||
label="型号"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Form.Item name="model" label="型号" style={{ flex: 1 }}>
|
||||
<Input placeholder="如: X520-DA2" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge, Collapse, Card } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined, ReloadOutlined, ApiOutlined, CloudServerOutlined, FolderOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Popconfirm,
|
||||
Empty,
|
||||
Spin,
|
||||
Badge,
|
||||
Collapse,
|
||||
Card,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
ApiOutlined,
|
||||
CloudServerOutlined,
|
||||
FolderOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import PortCreateModal from './PortCreateModal';
|
||||
import NetworkCardCreateModal from './NetworkCardCreateModal';
|
||||
@@ -10,12 +29,12 @@ const { Panel } = Collapse;
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea'
|
||||
main: '#667eea',
|
||||
},
|
||||
success: '#10b981',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b'
|
||||
}
|
||||
warning: '#f59e0b',
|
||||
},
|
||||
};
|
||||
|
||||
function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
@@ -34,7 +53,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
setLoading(true);
|
||||
const [cardsResponse, networkCardsResponse] = await Promise.all([
|
||||
axios.get(`/api/network-cards/device/${deviceId}/with-ports`),
|
||||
axios.get(`/api/network-cards/device/${deviceId}`)
|
||||
axios.get(`/api/network-cards/device/${deviceId}`),
|
||||
]);
|
||||
|
||||
const cardsData = cardsResponse.data || [];
|
||||
@@ -58,27 +77,35 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
fetchData();
|
||||
}, [fetchData, refreshTrigger]);
|
||||
|
||||
const handleDeleteCard = useCallback(async (card) => {
|
||||
try {
|
||||
await axios.delete(`/api/network-cards/${card.nicId}`);
|
||||
import('antd').then(({ message }) => message.success('网卡删除成功'));
|
||||
fetchData();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) => message.error(error.response?.data?.error || '网卡删除失败'));
|
||||
}
|
||||
}, [fetchData, onRefresh]);
|
||||
const handleDeleteCard = useCallback(
|
||||
async card => {
|
||||
try {
|
||||
await axios.delete(`/api/network-cards/${card.nicId}`);
|
||||
import('antd').then(({ message }) => message.success('网卡删除成功'));
|
||||
fetchData();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) =>
|
||||
message.error(error.response?.data?.error || '网卡删除失败')
|
||||
);
|
||||
}
|
||||
},
|
||||
[fetchData, onRefresh]
|
||||
);
|
||||
|
||||
const handleDeletePort = useCallback(async (port) => {
|
||||
try {
|
||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||
fetchData();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||
}
|
||||
}, [fetchData, onRefresh]);
|
||||
const handleDeletePort = useCallback(
|
||||
async port => {
|
||||
try {
|
||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||
fetchData();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||
}
|
||||
},
|
||||
[fetchData, onRefresh]
|
||||
);
|
||||
|
||||
const handleCreateCardSuccess = useCallback(() => {
|
||||
fetchData();
|
||||
@@ -90,7 +117,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
onRefresh?.();
|
||||
}, [fetchData, onRefresh]);
|
||||
|
||||
const handleExpand = (nicId) => {
|
||||
const handleExpand = nicId => {
|
||||
setExpandedCards(prev => {
|
||||
if (prev.includes(nicId)) {
|
||||
return prev.filter(id => id !== nicId);
|
||||
@@ -99,27 +126,27 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const config = {
|
||||
free: { color: 'success', text: '空闲' },
|
||||
occupied: { color: 'processing', text: '占用' },
|
||||
fault: { color: 'error', text: '故障' },
|
||||
normal: { color: 'success', text: '正常' },
|
||||
warning: { color: 'warning', text: '警告' },
|
||||
offline: { color: 'default', text: '离线' }
|
||||
offline: { color: 'default', text: '离线' },
|
||||
};
|
||||
const { color, text } = config[status] || { color: 'default', text: status };
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
};
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const getTypeTag = type => {
|
||||
const config = {
|
||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
||||
'SFP': { color: 'green', text: 'SFP' },
|
||||
RJ45: { color: 'blue', text: 'RJ45' },
|
||||
SFP: { color: 'green', text: 'SFP' },
|
||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
||||
SFP28: { color: 'purple', text: 'SFP28' },
|
||||
QSFP: { color: 'orange', text: 'QSFP' },
|
||||
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||
};
|
||||
const { color, text } = config[type] || { color: 'default', text: type };
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
@@ -132,34 +159,34 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
dataIndex: 'portName',
|
||||
key: 'portName',
|
||||
width: 120,
|
||||
render: (text) => <span style={{ fontWeight: 500 }}>{text}</span>
|
||||
render: text => <span style={{ fontWeight: 500 }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'portType',
|
||||
key: 'portType',
|
||||
width: 80,
|
||||
render: (type) => getTypeTag(type)
|
||||
render: type => getTypeTag(type),
|
||||
},
|
||||
{
|
||||
title: '速率',
|
||||
dataIndex: 'portSpeed',
|
||||
key: 'portSpeed',
|
||||
width: 70
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 70,
|
||||
render: (status) => getStatusTag(status)
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: 'VLAN',
|
||||
dataIndex: 'vlanId',
|
||||
key: 'vlanId',
|
||||
width: 60,
|
||||
render: (vlanId) => vlanId || '-'
|
||||
render: vlanId => vlanId || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -178,8 +205,8 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -194,30 +221,41 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
);
|
||||
};
|
||||
|
||||
const renderCardHeader = (card) => {
|
||||
const renderCardHeader = card => {
|
||||
const stats = card.stats || { free: 0, occupied: 0, fault: 0, total: 0 };
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderRadius: '8px',
|
||||
background: card.isUngrouped
|
||||
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
|
||||
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderRadius: '8px',
|
||||
background: card.isUngrouped
|
||||
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
|
||||
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{card.isUngrouped ? <FolderOutlined /> : <CloudServerOutlined />}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
|
||||
{card.name}
|
||||
{card.slotNumber && <span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>}
|
||||
{card.slotNumber && (
|
||||
<span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
||||
{card.description || (card.isUngrouped ? '未分配到网卡的端口' : '网卡')}
|
||||
@@ -248,7 +286,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setSelectedCard(card);
|
||||
setCreatePortModalVisible(true);
|
||||
@@ -270,26 +308,35 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
);
|
||||
}
|
||||
|
||||
const totalStats = cards.reduce((acc, card) => {
|
||||
const stats = card.stats || {};
|
||||
acc.total += stats.total || 0;
|
||||
acc.free += stats.free || 0;
|
||||
acc.occupied += stats.occupied || 0;
|
||||
acc.fault += stats.fault || 0;
|
||||
return acc;
|
||||
}, { total: 0, free: 0, occupied: 0, fault: 0 });
|
||||
const totalStats = cards.reduce(
|
||||
(acc, card) => {
|
||||
const stats = card.stats || {};
|
||||
acc.total += stats.total || 0;
|
||||
acc.free += stats.free || 0;
|
||||
acc.occupied += stats.occupied || 0;
|
||||
acc.fault += stats.fault || 0;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, free: 0, occupied: 0, fault: 0 }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="network-card-panel">
|
||||
<div className="panel-header" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div
|
||||
className="panel-header"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<div className="stats" style={{ display: 'flex', gap: '24px' }}>
|
||||
<Space size={16}>
|
||||
<Badge count={networkCards.length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
||||
<Badge
|
||||
count={networkCards.length}
|
||||
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||
/>
|
||||
<span style={{ color: '#64748b', fontSize: '13px' }}>个网卡</span>
|
||||
<Badge count={totalStats.total} style={{ backgroundColor: '#667eea' }} />
|
||||
<span style={{ color: '#64748b', fontSize: '13px' }}>个端口</span>
|
||||
@@ -334,11 +381,11 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
) : (
|
||||
<Collapse
|
||||
activeKey={expandedCards}
|
||||
onChange={(keys) => setExpandedCards(keys)}
|
||||
onChange={keys => setExpandedCards(keys)}
|
||||
expandIconPosition="end"
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
{cards.map((card) => (
|
||||
{cards.map(card => (
|
||||
<Panel
|
||||
key={card.nicId}
|
||||
header={renderCardHeader(card)}
|
||||
@@ -346,7 +393,7 @@ function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '8px',
|
||||
border: '1px solid #e2e8f0'
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
{card.ports && card.ports.length > 0 ? (
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Modal, Form, Input, Select, InputNumber, message, Space, Button, Tooltip, Alert, Tag } from 'antd';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
InputNumber,
|
||||
message,
|
||||
Space,
|
||||
Button,
|
||||
Tooltip,
|
||||
Alert,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -10,12 +22,12 @@ const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
}
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
medium: '10px'
|
||||
}
|
||||
medium: '10px',
|
||||
},
|
||||
};
|
||||
|
||||
function parsePortRange(portName) {
|
||||
@@ -24,34 +36,34 @@ function parsePortRange(portName) {
|
||||
}
|
||||
|
||||
const trimmed = portName.trim();
|
||||
|
||||
|
||||
if (!trimmed.includes('-')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [startPart, endPart] = trimmed.split('-').map(s => s.trim());
|
||||
|
||||
|
||||
if (!startPart || !endPart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startNumMatch = startPart.match(/(\d+)$/);
|
||||
const endNumMatch = endPart.match(/(\d+)$/);
|
||||
|
||||
|
||||
if (!startNumMatch || !endNumMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startNum = parseInt(startNumMatch[1], 10);
|
||||
const endNum = parseInt(endNumMatch[1], 10);
|
||||
|
||||
|
||||
if (startNum >= endNum || endNum - startNum > 1000) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = startPart.replace(startNumMatch[0], '');
|
||||
const portCount = endNum - startNum + 1;
|
||||
|
||||
|
||||
const ports = [];
|
||||
for (let i = 0; i < portCount; i++) {
|
||||
const num = startNum + i;
|
||||
@@ -64,17 +76,17 @@ function parsePortRange(portName) {
|
||||
startNum,
|
||||
endNum,
|
||||
portCount,
|
||||
ports
|
||||
ports,
|
||||
};
|
||||
}
|
||||
|
||||
function generatePortNames(portName) {
|
||||
const result = parsePortRange(portName);
|
||||
|
||||
|
||||
if (result && result.isRange) {
|
||||
return result.ports;
|
||||
}
|
||||
|
||||
|
||||
return [portName];
|
||||
}
|
||||
|
||||
@@ -92,7 +104,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
setPreviewPorts([]);
|
||||
setShowPreview(false);
|
||||
form.resetFields();
|
||||
|
||||
|
||||
if (defaultNicId) {
|
||||
form.setFieldsValue({ nicId: defaultNicId });
|
||||
}
|
||||
@@ -116,10 +128,10 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortNameChange = useCallback((e) => {
|
||||
const handlePortNameChange = useCallback(e => {
|
||||
const value = e.target.value;
|
||||
const ports = generatePortNames(value);
|
||||
|
||||
|
||||
if (ports.length > 1) {
|
||||
setPreviewPorts(ports.slice(0, 20));
|
||||
setShowPreview(true);
|
||||
@@ -135,7 +147,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
setLoading(true);
|
||||
|
||||
const portNames = generatePortNames(values.portName);
|
||||
|
||||
|
||||
if (portNames.length === 1) {
|
||||
await axios.post('/api/device-ports', {
|
||||
deviceId: device.deviceId,
|
||||
@@ -145,7 +157,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
portSpeed: values.portSpeed,
|
||||
vlanId: values.vlanId,
|
||||
status: values.status,
|
||||
description: values.description
|
||||
description: values.description,
|
||||
});
|
||||
message.success('端口创建成功');
|
||||
} else {
|
||||
@@ -158,7 +170,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
portSpeed: values.portSpeed,
|
||||
vlanId: values.vlanId,
|
||||
status: values.status,
|
||||
description: values.description
|
||||
description: values.description,
|
||||
}));
|
||||
|
||||
await axios.post('/api/device-ports/batch', { ports: portsData });
|
||||
@@ -196,9 +208,7 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
<Space>
|
||||
<PlusOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
<span>新增端口 - {device?.name || '设备'}</span>
|
||||
{portCount > 1 && (
|
||||
<Tag color="blue">{portCount} 个端口</Tag>
|
||||
)}
|
||||
{portCount > 1 && <Tag color="blue">{portCount} 个端口</Tag>}
|
||||
</Space>
|
||||
}
|
||||
open={visible}
|
||||
@@ -217,18 +227,11 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
initialValues={{
|
||||
portType: 'RJ45',
|
||||
portSpeed: '1G',
|
||||
status: 'free'
|
||||
status: 'free',
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="deviceId"
|
||||
label="设备"
|
||||
>
|
||||
<Input
|
||||
value={device?.name}
|
||||
disabled
|
||||
placeholder={device?.deviceId}
|
||||
/>
|
||||
<Form.Item name="deviceId" label="设备">
|
||||
<Input value={device?.name} disabled placeholder={device?.deviceId} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -263,16 +266,19 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
label={
|
||||
<Space>
|
||||
端口名称
|
||||
<Tooltip title="支持单个端口(如 eth0/1)或端口范围(如 1/0/1-1/0/48)" mouseEnterDelay={0.5}>
|
||||
<Tooltip
|
||||
title="支持单个端口(如 eth0/1)或端口范围(如 1/0/1-1/0/48)"
|
||||
mouseEnterDelay={0.5}
|
||||
>
|
||||
<InfoCircleOutlined style={{ color: '#999' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
rules={[
|
||||
{ required: true, message: '请输入端口名称' },
|
||||
{
|
||||
pattern: /^[\w\/:\-]+$/,
|
||||
message: '端口名称格式不正确'
|
||||
{
|
||||
pattern: /^[\w\/:\-]+$/,
|
||||
message: '端口名称格式不正确',
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
@@ -282,13 +288,13 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
return Promise.reject(new Error('单次最多创建1000个端口'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
||||
onChange={(e) => {
|
||||
<Input
|
||||
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
||||
onChange={e => {
|
||||
// 确保 Form 值更新
|
||||
form.setFieldValue('portName', e.target.value);
|
||||
handlePortNameChange(e);
|
||||
@@ -303,9 +309,12 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Space wrap size={4}>
|
||||
{previewPorts.map((port, index) => (
|
||||
<Tag key={index} color="blue">{port}</Tag>
|
||||
<Tag key={index} color="blue">
|
||||
{port}
|
||||
</Tag>
|
||||
))}
|
||||
{previewPorts.length < parsePortRange(form.getFieldValue('portName'))?.portCount && (
|
||||
{previewPorts.length <
|
||||
parsePortRange(form.getFieldValue('portName'))?.portCount && (
|
||||
<Tag color="default">...等</Tag>
|
||||
)}
|
||||
</Space>
|
||||
@@ -352,17 +361,8 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
</Space>
|
||||
|
||||
<Space style={{ display: 'flex', width: '100%' }}>
|
||||
<Form.Item
|
||||
name="vlanId"
|
||||
label="VLAN ID"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="1-4094"
|
||||
min={1}
|
||||
max={4094}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Form.Item name="vlanId" label="VLAN ID" style={{ flex: 1 }}>
|
||||
<InputNumber placeholder="1-4094" min={1} max={4094} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -379,25 +379,30 @@ function PortCreateModal({ device, visible, onClose, onSuccess, defaultNicId, ne
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
>
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} placeholder="请输入描述信息(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
<strong>格式说明:</strong>
|
||||
<ul style={{ margin: '8px 0 0 0', paddingLeft: '20px' }}>
|
||||
<li>单个端口:<code>eth0/1</code>、<code>gigabitethernet1/0/1</code></li>
|
||||
<li>端口范围:<code>1/0/1-1/0/48</code>(创建 1/0/1 到 1/0/48 共48个端口)</li>
|
||||
<li>简单范围:<code>eth1-eth24</code>(创建 eth1 到 eth24 共24个端口)</li>
|
||||
<li>
|
||||
单个端口:<code>eth0/1</code>、<code>gigabitethernet1/0/1</code>
|
||||
</li>
|
||||
<li>
|
||||
端口范围:<code>1/0/1-1/0/48</code>(创建 1/0/1 到 1/0/48 共48个端口)
|
||||
</li>
|
||||
<li>
|
||||
简单范围:<code>eth1-eth24</code>(创建 eth1 到 eth24 共24个端口)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, ApiOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import PortCreateModal from './PortCreateModal';
|
||||
|
||||
const designTokens = {
|
||||
colors: {
|
||||
primary: {
|
||||
main: '#667eea'
|
||||
main: '#667eea',
|
||||
},
|
||||
success: '#10b981',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b'
|
||||
}
|
||||
warning: '#f59e0b',
|
||||
},
|
||||
};
|
||||
|
||||
function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
@@ -38,40 +44,43 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
fetchPorts();
|
||||
}, [fetchPorts]);
|
||||
|
||||
const handleDelete = useCallback(async (port) => {
|
||||
try {
|
||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||
fetchPorts();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||
}
|
||||
}, [fetchPorts, onRefresh]);
|
||||
const handleDelete = useCallback(
|
||||
async port => {
|
||||
try {
|
||||
await axios.delete(`/api/device-ports/${port.portId}`);
|
||||
import('antd').then(({ message }) => message.success('端口删除成功'));
|
||||
fetchPorts();
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
import('antd').then(({ message }) => message.error('端口删除失败'));
|
||||
}
|
||||
},
|
||||
[fetchPorts, onRefresh]
|
||||
);
|
||||
|
||||
const handleCreateSuccess = useCallback(() => {
|
||||
fetchPorts();
|
||||
onRefresh?.();
|
||||
}, [fetchPorts, onRefresh]);
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const getStatusTag = status => {
|
||||
const config = {
|
||||
free: { color: 'success', text: '空闲' },
|
||||
occupied: { color: 'processing', text: '占用' },
|
||||
fault: { color: 'error', text: '故障' }
|
||||
fault: { color: 'error', text: '故障' },
|
||||
};
|
||||
const { color, text } = config[status] || { color: 'default', text: status };
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
};
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const getTypeTag = type => {
|
||||
const config = {
|
||||
'RJ45': { color: 'blue', text: 'RJ45' },
|
||||
'SFP': { color: 'green', text: 'SFP' },
|
||||
RJ45: { color: 'blue', text: 'RJ45' },
|
||||
SFP: { color: 'green', text: 'SFP' },
|
||||
'SFP+': { color: 'cyan', text: 'SFP+' },
|
||||
'SFP28': { color: 'purple', text: 'SFP28' },
|
||||
'QSFP': { color: 'orange', text: 'QSFP' },
|
||||
'QSFP28': { color: 'red', text: 'QSFP28' }
|
||||
SFP28: { color: 'purple', text: 'SFP28' },
|
||||
QSFP: { color: 'orange', text: 'QSFP' },
|
||||
QSFP28: { color: 'red', text: 'QSFP28' },
|
||||
};
|
||||
const { color, text } = config[type] || { color: 'default', text: type };
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
@@ -83,38 +92,38 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
dataIndex: 'portName',
|
||||
key: 'portName',
|
||||
width: 120,
|
||||
render: (text) => (
|
||||
render: text => (
|
||||
<Tooltip title={text}>
|
||||
<span style={{ fontWeight: 500 }}>{text}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'portType',
|
||||
key: 'portType',
|
||||
width: 90,
|
||||
render: (type) => getTypeTag(type)
|
||||
render: type => getTypeTag(type),
|
||||
},
|
||||
{
|
||||
title: '速率',
|
||||
dataIndex: 'portSpeed',
|
||||
key: 'portSpeed',
|
||||
width: 80
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80,
|
||||
render: (status) => getStatusTag(status)
|
||||
render: status => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: 'VLAN',
|
||||
dataIndex: 'vlanId',
|
||||
key: 'vlanId',
|
||||
width: 70,
|
||||
render: (vlanId) => vlanId || '-'
|
||||
render: vlanId => vlanId || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -129,18 +138,13 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const freeCount = ports.filter(p => p.status === 'free').length;
|
||||
@@ -169,11 +173,7 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
</Space>
|
||||
</div>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchPorts}
|
||||
size="small"
|
||||
>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchPorts} size="small">
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
@@ -182,7 +182,7 @@ function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
style={{
|
||||
background: designTokens.colors.primary.gradient,
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
新增端口
|
||||
|
||||
@@ -2,28 +2,36 @@ import React, { useState } from 'react';
|
||||
import { Tooltip, Badge, Divider, Pagination } from 'antd';
|
||||
import { LinkOutlined, SwapRightOutlined, AimOutlined, NodeIndexOutlined } from '@ant-design/icons';
|
||||
|
||||
const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onPortClick, compact = false }) => {
|
||||
const PortPanel = ({
|
||||
ports,
|
||||
deviceName,
|
||||
deviceId,
|
||||
cables = [],
|
||||
devices = [],
|
||||
onPortClick,
|
||||
compact = false,
|
||||
}) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(48); // 默认每页48个端口
|
||||
|
||||
// 按端口名称排序(升序)
|
||||
const sortedPorts = [...ports].sort((a, b) => {
|
||||
// 尝试按数字部分排序,支持格式如:1/0/1, eth0/1, GigabitEthernet1/0/1 等
|
||||
const extractNumbers = (str) => {
|
||||
const extractNumbers = str => {
|
||||
const matches = str.match(/\d+/g);
|
||||
return matches ? matches.map(Number) : [];
|
||||
};
|
||||
|
||||
|
||||
const numsA = extractNumbers(a.portName);
|
||||
const numsB = extractNumbers(b.portName);
|
||||
|
||||
|
||||
// 逐个比较数字部分
|
||||
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
||||
if (numsA[i] !== numsB[i]) {
|
||||
return numsA[i] - numsB[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果数字部分相同,按字符串排序
|
||||
return a.portName.localeCompare(b.portName);
|
||||
});
|
||||
@@ -35,7 +43,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
const paginatedPorts = sortedPorts.slice(startIndex, endIndex);
|
||||
|
||||
// 获取端口状态颜色
|
||||
const getPortStatusColor = (status) => {
|
||||
const getPortStatusColor = status => {
|
||||
switch (status) {
|
||||
case 'free':
|
||||
return '#6b7280'; // 灰色 - 空闲
|
||||
@@ -51,7 +59,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
};
|
||||
|
||||
// 获取端口状态文本
|
||||
const getPortStatusText = (status) => {
|
||||
const getPortStatusText = status => {
|
||||
switch (status) {
|
||||
case 'free':
|
||||
return '空闲';
|
||||
@@ -67,7 +75,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
};
|
||||
|
||||
// 获取端口类型图标 - 使用更真实的端口符号
|
||||
const getPortTypeIcon = (portType) => {
|
||||
const getPortTypeIcon = portType => {
|
||||
switch (portType) {
|
||||
case 'RJ45':
|
||||
return '⬡'; // 六边形表示网口
|
||||
@@ -84,7 +92,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
};
|
||||
|
||||
// 获取简化端口显示名称(只显示数字)
|
||||
const getPortDisplayName = (portName) => {
|
||||
const getPortDisplayName = portName => {
|
||||
// 提取最后的数字
|
||||
const match = portName.match(/(\d+)$/);
|
||||
if (match) {
|
||||
@@ -95,47 +103,49 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
};
|
||||
|
||||
// 获取线缆类型文本
|
||||
const getCableTypeText = (cableType) => {
|
||||
const getCableTypeText = cableType => {
|
||||
const typeMap = {
|
||||
'ethernet': '网线',
|
||||
'fiber': '光纤',
|
||||
'copper': '铜缆',
|
||||
'power': '电源线'
|
||||
ethernet: '网线',
|
||||
fiber: '光纤',
|
||||
copper: '铜缆',
|
||||
power: '电源线',
|
||||
};
|
||||
return typeMap[cableType] || cableType || '未知';
|
||||
};
|
||||
|
||||
// 获取线缆类型颜色
|
||||
const getCableTypeColor = (cableType) => {
|
||||
const getCableTypeColor = cableType => {
|
||||
const colorMap = {
|
||||
'ethernet': '#52c41a',
|
||||
'fiber': '#1890ff',
|
||||
'copper': '#faad14',
|
||||
'power': '#ff4d4f'
|
||||
ethernet: '#52c41a',
|
||||
fiber: '#1890ff',
|
||||
copper: '#faad14',
|
||||
power: '#ff4d4f',
|
||||
};
|
||||
return colorMap[cableType] || '#999';
|
||||
};
|
||||
|
||||
// 查找端口关联的接线
|
||||
const findPortCable = (port) => {
|
||||
const findPortCable = port => {
|
||||
if (!cables || cables.length === 0) return null;
|
||||
|
||||
return cables.find(cable =>
|
||||
(cable.sourceDeviceId === deviceId && cable.sourcePortId === port.portId) ||
|
||||
(cable.targetDeviceId === deviceId && cable.targetPortId === port.portId) ||
|
||||
(cable.sourceDeviceId === deviceId && cable.sourcePort === port.portName) ||
|
||||
(cable.targetDeviceId === deviceId && cable.targetPort === port.portName)
|
||||
|
||||
return cables.find(
|
||||
cable =>
|
||||
(cable.sourceDeviceId === deviceId && cable.sourcePortId === port.portId) ||
|
||||
(cable.targetDeviceId === deviceId && cable.targetPortId === port.portId) ||
|
||||
(cable.sourceDeviceId === deviceId && cable.sourcePort === port.portName) ||
|
||||
(cable.targetDeviceId === deviceId && cable.targetPort === port.portName)
|
||||
);
|
||||
};
|
||||
|
||||
// 获取连接的对端信息
|
||||
const getPeerInfo = (cable, currentPort) => {
|
||||
if (!cable) return null;
|
||||
|
||||
const isSource = cable.sourceDeviceId === deviceId ||
|
||||
(cable.sourcePortId && cable.sourcePortId === currentPort.portId) ||
|
||||
cable.sourcePort === currentPort.portName;
|
||||
|
||||
|
||||
const isSource =
|
||||
cable.sourceDeviceId === deviceId ||
|
||||
(cable.sourcePortId && cable.sourcePortId === currentPort.portId) ||
|
||||
cable.sourcePort === currentPort.portName;
|
||||
|
||||
if (isSource) {
|
||||
// 当前是源端,返回目标端信息
|
||||
const targetDevice = devices.find(d => d.deviceId === cable.targetDeviceId);
|
||||
@@ -143,7 +153,7 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
deviceName: targetDevice?.name || cable.targetDeviceId,
|
||||
deviceId: cable.targetDeviceId,
|
||||
portName: cable.targetPort || cable.targetPortId,
|
||||
direction: 'out'
|
||||
direction: 'out',
|
||||
};
|
||||
} else {
|
||||
// 当前是目标端,返回源端信息
|
||||
@@ -152,37 +162,60 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
deviceName: sourceDevice?.name || cable.sourceDeviceId,
|
||||
deviceId: cable.sourceDeviceId,
|
||||
portName: cable.sourcePort || cable.sourcePortId,
|
||||
direction: 'in'
|
||||
direction: 'in',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染端口详情提示
|
||||
const renderPortTooltip = (port) => {
|
||||
const renderPortTooltip = port => {
|
||||
const cable = findPortCable(port);
|
||||
const peerInfo = cable ? getPeerInfo(cable, port) : null;
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px 4px', minWidth: '220px' }}>
|
||||
{/* 端口基本信息 */}
|
||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.2)', paddingBottom: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
marginBottom: 8,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.2)',
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
>
|
||||
<NodeIndexOutlined style={{ marginRight: 6 }} />
|
||||
{port.portName}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||
<div><span style={{ opacity: 0.7 }}>端口类型:</span> {port.portType}</div>
|
||||
<div><span style={{ opacity: 0.7 }}>端口速率:</span> {port.portSpeed}</div>
|
||||
<div><span style={{ opacity: 0.7 }}>状态:</span>
|
||||
<span style={{
|
||||
color: getPortStatusColor(port.status),
|
||||
marginLeft: 4,
|
||||
fontWeight: 500
|
||||
}}>
|
||||
<div>
|
||||
<span style={{ opacity: 0.7 }}>端口类型:</span> {port.portType}
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ opacity: 0.7 }}>端口速率:</span> {port.portSpeed}
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ opacity: 0.7 }}>状态:</span>
|
||||
<span
|
||||
style={{
|
||||
color: getPortStatusColor(port.status),
|
||||
marginLeft: 4,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{getPortStatusText(port.status)}
|
||||
</span>
|
||||
</div>
|
||||
{port.vlanId && <div><span style={{ opacity: 0.7 }}>VLAN:</span> {port.vlanId}</div>}
|
||||
{port.description && <div><span style={{ opacity: 0.7 }}>描述:</span> {port.description}</div>}
|
||||
{port.vlanId && (
|
||||
<div>
|
||||
<span style={{ opacity: 0.7 }}>VLAN:</span> {port.vlanId}
|
||||
</div>
|
||||
)}
|
||||
{port.description && (
|
||||
<div>
|
||||
<span style={{ opacity: 0.7 }}>描述:</span> {port.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 接线信息 */}
|
||||
@@ -196,62 +229,60 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
<div style={{ fontSize: 12, lineHeight: '1.8' }}>
|
||||
{/* 线缆类型和长度 */}
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
background: getCableTypeColor(cable.cableType) + '20',
|
||||
color: getCableTypeColor(cable.cableType),
|
||||
fontSize: '11px',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
background: getCableTypeColor(cable.cableType) + '20',
|
||||
color: getCableTypeColor(cable.cableType),
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{getCableTypeText(cable.cableType)}
|
||||
</span>
|
||||
{cable.cableLength && (
|
||||
<span style={{ marginLeft: 8, opacity: 0.8 }}>
|
||||
{cable.cableLength}m
|
||||
</span>
|
||||
<span style={{ marginLeft: 8, opacity: 0.8 }}>{cable.cableLength}m</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 连接方向 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '6px',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '6px',
|
||||
marginTop: '8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: peerInfo.direction === 'out' ? '#52c41a20' : '#1890ff20',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: peerInfo.direction === 'out' ? '#52c41a20' : '#1890ff20',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{peerInfo.direction === 'out' ? '📤' : '📥'}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', marginTop: '2px', opacity: 0.6 }}>
|
||||
{peerInfo.direction === 'out' ? '输出' : '输入'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500, color: '#fff' }}>
|
||||
{peerInfo.deviceName}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', opacity: 0.7 }}>
|
||||
端口: {peerInfo.portName}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', opacity: 0.5 }}>
|
||||
ID: {peerInfo.deviceId}
|
||||
</div>
|
||||
<div style={{ fontWeight: 500, color: '#fff' }}>{peerInfo.deviceName}</div>
|
||||
<div style={{ fontSize: '11px', opacity: 0.7 }}>端口: {peerInfo.portName}</div>
|
||||
<div style={{ fontSize: '10px', opacity: 0.5 }}>ID: {peerInfo.deviceId}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -285,34 +316,40 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(145deg, #1e293b 0%, #0f172a 100%)',
|
||||
borderRadius: compact ? '12px' : '16px',
|
||||
padding: compact ? '16px' : '24px',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(145deg, #1e293b 0%, #0f172a 100%)',
|
||||
borderRadius: compact ? '12px' : '16px',
|
||||
padding: compact ? '16px' : '24px',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
{/* 设备标题 - compact 模式下隐藏 */}
|
||||
{!compact && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
paddingBottom: '16px',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
paddingBottom: '16px',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
}}
|
||||
>
|
||||
🔌
|
||||
</div>
|
||||
<div>
|
||||
@@ -324,37 +361,43 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 状态图例 */}
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#6b7280',
|
||||
boxShadow: '0 0 8px #6b7280'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#6b7280',
|
||||
boxShadow: '0 0 8px #6b7280',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>空闲</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
boxShadow: '0 0 8px #10b981'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
boxShadow: '0 0 8px #10b981',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>已连接</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#ef4444',
|
||||
boxShadow: '0 0 8px #ef4444'
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: '#ef4444',
|
||||
boxShadow: '0 0 8px #ef4444',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'rgba(255,255,255,0.6)' }}>故障</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,29 +405,31 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
)}
|
||||
|
||||
{/* 端口网格 - 固定每行24个端口 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(24, 1fr)',
|
||||
gap: '8px',
|
||||
padding: '16px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)'
|
||||
}}>
|
||||
{paginatedPorts.map((port) => {
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(24, 1fr)',
|
||||
gap: '8px',
|
||||
padding: '16px',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
>
|
||||
{paginatedPorts.map(port => {
|
||||
const statusColor = getPortStatusColor(port.status);
|
||||
const isClickable = onPortClick && port.status !== 'disabled';
|
||||
const cable = findPortCable(port);
|
||||
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={port.portId}
|
||||
<Tooltip
|
||||
key={port.portId}
|
||||
title={renderPortTooltip(port)}
|
||||
placement="top"
|
||||
color="#1e293b"
|
||||
overlayStyle={{
|
||||
overlayStyle={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -397,69 +442,79 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
cursor: isClickable ? 'pointer' : 'not-allowed',
|
||||
transition: 'all 0.2s ease',
|
||||
position: 'relative',
|
||||
minWidth: '0'
|
||||
minWidth: '0',
|
||||
}}
|
||||
>
|
||||
{/* LED 指示灯 - 在端口上方 */}
|
||||
<div style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: statusColor,
|
||||
boxShadow: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
|
||||
marginBottom: '4px',
|
||||
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none'
|
||||
}} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: statusColor,
|
||||
boxShadow: `0 0 6px ${statusColor}, 0 0 12px ${statusColor}50`,
|
||||
marginBottom: '4px',
|
||||
animation: port.status === 'fault' ? 'pulse 1.5s infinite' : 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 端口主体 - 矩形样式 */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1 / 1.2',
|
||||
background: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
|
||||
border: `2px solid ${statusColor}`,
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)`
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1 / 1.2',
|
||||
background: 'linear-gradient(180deg, #2a3441 0%, #1e2530 100%)',
|
||||
border: `2px solid ${statusColor}`,
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 4px rgba(0,0,0,0.3)`,
|
||||
}}
|
||||
>
|
||||
{/* 端口内部图标 */}
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: statusColor,
|
||||
opacity: 0.8
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: statusColor,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{getPortTypeIcon(port.portType)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 接线指示标记 */}
|
||||
{cable && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '1px',
|
||||
right: '1px',
|
||||
width: '4px',
|
||||
height: '4px',
|
||||
borderRadius: '50%',
|
||||
background: getCableTypeColor(cable.cableType),
|
||||
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '1px',
|
||||
right: '1px',
|
||||
width: '4px',
|
||||
height: '4px',
|
||||
borderRadius: '50%',
|
||||
background: getCableTypeColor(cable.cableType),
|
||||
boxShadow: `0 0 3px ${getCableTypeColor(cable.cableType)}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 端口名称 - 在端口下方 */}
|
||||
<div style={{
|
||||
fontSize: '9px',
|
||||
fontWeight: 500,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
textAlign: 'center',
|
||||
marginTop: '3px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '9px',
|
||||
fontWeight: 500,
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
textAlign: 'center',
|
||||
marginTop: '3px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{getPortDisplayName(port.portName)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,13 +525,15 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPorts > pageSize && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '16px 0 0 0',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
marginTop: '16px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '16px 0 0 0',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
marginTop: '16px',
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
total={totalPorts}
|
||||
@@ -487,11 +544,11 @@ const PortPanel = ({ ports, deviceName, deviceId, cables = [], devices = [], onP
|
||||
}}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `共 ${total} 个端口`}
|
||||
showTotal={total => `共 ${total} 个端口`}
|
||||
pageSizeOptions={['24', '48', '96']}
|
||||
size="small"
|
||||
style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,16 @@ const ProtectedRoute = ({ children, requiredPermission }) => {
|
||||
|
||||
if (!initialized) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
<span style={{ color: '#8c8c8c', fontSize: '14px' }}>加载中...</span>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DesktopOutlined,
|
||||
UsbOutlined,
|
||||
MonitorOutlined,
|
||||
SettingOutlined
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import PortPanel from './PortPanel';
|
||||
import axios from 'axios';
|
||||
@@ -23,8 +23,8 @@ const designTokens = {
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b',
|
||||
metal: { light: '#9ca3af', DEFAULT: '#6b7280', dark: '#4b5563' },
|
||||
slot: { empty: '#d1d5db', occupied: '#3b82f6' }
|
||||
}
|
||||
slot: { empty: '#d1d5db', occupied: '#3b82f6' },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ const ServerBackplanePanel = ({
|
||||
cables,
|
||||
allDevices,
|
||||
onPortClick,
|
||||
onManageNetworkCards
|
||||
onManageNetworkCards,
|
||||
}) => {
|
||||
const [cards, setCards] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -82,9 +82,20 @@ const ServerBackplanePanel = ({
|
||||
const name = (card.name || '').toLowerCase();
|
||||
|
||||
// 判断网卡类型
|
||||
if (name.includes('idrac') || name.includes('ilo') || name.includes('bmc') || name.includes('mgmt') || name.includes('管理')) {
|
||||
if (
|
||||
name.includes('idrac') ||
|
||||
name.includes('ilo') ||
|
||||
name.includes('bmc') ||
|
||||
name.includes('mgmt') ||
|
||||
name.includes('管理')
|
||||
) {
|
||||
management.push({ ...card, type: 'management' });
|
||||
} else if (slotNum === 0 || name.includes('onboard') || name.includes('板载') || name.includes('内置')) {
|
||||
} else if (
|
||||
slotNum === 0 ||
|
||||
name.includes('onboard') ||
|
||||
name.includes('板载') ||
|
||||
name.includes('内置')
|
||||
) {
|
||||
onboard.push({ ...card, type: 'onboard' });
|
||||
} else {
|
||||
expansionSlots.push({ ...card, type: 'expansion', slotIndex: slotNum });
|
||||
@@ -115,7 +126,7 @@ const ServerBackplanePanel = ({
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
border: '2px solid #4b5563',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600 }}>MGMT</div>
|
||||
@@ -134,7 +145,7 @@ const ServerBackplanePanel = ({
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ fontSize: 16, color: '#10b981' }} />
|
||||
@@ -152,7 +163,7 @@ const ServerBackplanePanel = ({
|
||||
border: '2px dashed #6b7280',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<PlusOutlined style={{ fontSize: 14, color: '#6b7280' }} />
|
||||
@@ -160,11 +171,47 @@ const ServerBackplanePanel = ({
|
||||
)}
|
||||
|
||||
{/* 其他接口占位 */}
|
||||
<div style={{ width: '48px', height: '24px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
||||
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '22px' }}>VGA</Text>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '24px',
|
||||
background: '#1f2937',
|
||||
borderRadius: '2px',
|
||||
border: '1px solid #4b5563',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '8px',
|
||||
color: '#6b7280',
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
lineHeight: '22px',
|
||||
}}
|
||||
>
|
||||
VGA
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ width: '48px', height: '16px', background: '#1f2937', borderRadius: '2px', border: '1px solid #4b5563' }}>
|
||||
<Text style={{ fontSize: '8px', color: '#6b7280', display: 'block', textAlign: 'center', lineHeight: '14px' }}>USB</Text>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '16px',
|
||||
background: '#1f2937',
|
||||
borderRadius: '2px',
|
||||
border: '1px solid #4b5563',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '8px',
|
||||
color: '#6b7280',
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
lineHeight: '14px',
|
||||
}}
|
||||
>
|
||||
USB
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -182,11 +229,20 @@ const ServerBackplanePanel = ({
|
||||
borderRadius: '4px',
|
||||
padding: '12px',
|
||||
border: '2px solid #6b7280',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>板载网卡 (Onboard)</Text>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>
|
||||
板载网卡 (Onboard)
|
||||
</Text>
|
||||
{onboardCard && (
|
||||
<Badge
|
||||
count={onboardCard.ports?.length || 0}
|
||||
@@ -204,18 +260,21 @@ const ServerBackplanePanel = ({
|
||||
padding: '12px',
|
||||
border: `2px solid ${onboardCard.ports?.length > 0 ? designTokens.colors.primary.main : '#6b7280'}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{/* 4个RJ45端口布局 */}
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||
{[0, 1, 2, 3].map((idx) => {
|
||||
{[0, 1, 2, 3].map(idx => {
|
||||
const port = onboardCard.ports?.[idx];
|
||||
const hasPort = !!port;
|
||||
const isOccupied = hasPort && port.status === 'occupied';
|
||||
|
||||
return (
|
||||
<Tooltip key={idx} title={hasPort ? `${port.portName} - ${port.status}` : '未配置'}>
|
||||
<Tooltip
|
||||
key={idx}
|
||||
title={hasPort ? `${port.portName} - ${port.status}` : '未配置'}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '40px',
|
||||
@@ -224,15 +283,18 @@ const ServerBackplanePanel = ({
|
||||
? 'linear-gradient(180deg, #374151 0%, #1f2937 100%)'
|
||||
: '#374151',
|
||||
borderRadius: '4px',
|
||||
border: `2px solid ${hasPort
|
||||
? (isOccupied ? designTokens.colors.success : designTokens.colors.metal.light)
|
||||
: '#4b5563'
|
||||
border: `2px solid ${
|
||||
hasPort
|
||||
? isOccupied
|
||||
? designTokens.colors.success
|
||||
: designTokens.colors.metal.light
|
||||
: '#4b5563'
|
||||
}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative'
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* LED指示灯 */}
|
||||
@@ -241,13 +303,11 @@ const ServerBackplanePanel = ({
|
||||
width: '4px',
|
||||
height: '4px',
|
||||
borderRadius: '50%',
|
||||
background: hasPort
|
||||
? (isOccupied ? '#10b981' : '#6b7280')
|
||||
: '#374151',
|
||||
background: hasPort ? (isOccupied ? '#10b981' : '#6b7280') : '#374151',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
right: '2px',
|
||||
boxShadow: isOccupied ? '0 0 4px #10b981' : 'none'
|
||||
boxShadow: isOccupied ? '0 0 4px #10b981' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '8px', color: '#9ca3af' }}>⬡</span>
|
||||
@@ -273,7 +333,7 @@ const ServerBackplanePanel = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
gap: '8px'
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<PlusOutlined style={{ fontSize: 20, color: '#6b7280' }} />
|
||||
@@ -303,13 +363,23 @@ const ServerBackplanePanel = ({
|
||||
borderRadius: '4px',
|
||||
padding: '12px',
|
||||
border: '2px solid #6b7280',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)'
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '11px', color: '#d1d5db', fontWeight: 600 }}>PCIe 扩展插槽</Text>
|
||||
<Space size={4}>
|
||||
<Badge count={expansionSlots.length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
||||
<Badge
|
||||
count={expansionSlots.length}
|
||||
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||
/>
|
||||
<Text style={{ fontSize: '10px', color: '#9ca3af' }}>/{totalSlots}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -318,7 +388,9 @@ const ServerBackplanePanel = ({
|
||||
{slots.map(({ slotNumber, card }) => (
|
||||
<Tooltip
|
||||
key={slotNumber}
|
||||
title={card ? `${card.name} (${card.ports?.length || 0}口)` : `插槽 ${slotNumber} (空闲)`}
|
||||
title={
|
||||
card ? `${card.name} (${card.ports?.length || 0}口)` : `插槽 ${slotNumber} (空闲)`
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={() => card && setSelectedSlot(card)}
|
||||
@@ -337,15 +409,24 @@ const ServerBackplanePanel = ({
|
||||
padding: '6px',
|
||||
cursor: card ? 'pointer' : 'default',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: card ? '0 2px 8px rgba(59, 130, 246, 0.3)' : 'none'
|
||||
boxShadow: card ? '0 2px 8px rgba(59, 130, 246, 0.3)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '9px', color: '#6b7280', fontWeight: 600 }}>Slot {slotNumber}</Text>
|
||||
<Text style={{ fontSize: '9px', color: '#6b7280', fontWeight: 600 }}>
|
||||
Slot {slotNumber}
|
||||
</Text>
|
||||
|
||||
{card ? (
|
||||
<>
|
||||
<CloudServerOutlined style={{ fontSize: 20, color: '#3b82f6' }} />
|
||||
<div style={{ display: 'flex', gap: '2px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '2px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{card.ports?.slice(0, 4).map((port, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
@@ -354,19 +435,30 @@ const ServerBackplanePanel = ({
|
||||
height: '8px',
|
||||
borderRadius: '1px',
|
||||
background: port.status === 'occupied' ? '#10b981' : '#6b7280',
|
||||
boxShadow: port.status === 'occupied' ? '0 0 2px #10b981' : 'none'
|
||||
boxShadow: port.status === 'occupied' ? '0 0 2px #10b981' : 'none',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{card.ports?.length > 4 && (
|
||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>+{card.ports.length - 4}</Text>
|
||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>
|
||||
+{card.ports.length - 4}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>{card.ports?.length || 0}口</Text>
|
||||
<Text style={{ fontSize: '8px', color: '#9ca3af' }}>
|
||||
{card.ports?.length || 0}口
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ width: '40px', height: '40px', border: '2px dashed #4b5563', borderRadius: '4px' }} />
|
||||
<div
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
border: '2px dashed #4b5563',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
/>
|
||||
<Text style={{ fontSize: '8px', color: '#6b7280' }}>空闲</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -390,11 +482,13 @@ const ServerBackplanePanel = ({
|
||||
border: '2px solid #4b5563',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textAlign: 'center' }}>电源</Text>
|
||||
{[1, 2].map((psu) => (
|
||||
<Text style={{ fontSize: '10px', color: '#9ca3af', fontWeight: 600, textAlign: 'center' }}>
|
||||
电源
|
||||
</Text>
|
||||
{[1, 2].map(psu => (
|
||||
<div
|
||||
key={psu}
|
||||
style={{
|
||||
@@ -406,7 +500,7 @@ const ServerBackplanePanel = ({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px'
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<ThunderboltOutlined style={{ fontSize: 20, color: '#10b981' }} />
|
||||
@@ -417,7 +511,7 @@ const ServerBackplanePanel = ({
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
boxShadow: '0 0 6px #10b981'
|
||||
boxShadow: '0 0 6px #10b981',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -445,17 +539,24 @@ const ServerBackplanePanel = ({
|
||||
padding: '8px 12px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e2e8f0'
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Badge count={cards.filter(c => !c.isUngrouped).length} style={{ backgroundColor: designTokens.colors.primary.main }} />
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>个网卡</Text>
|
||||
<Badge
|
||||
count={cards.filter(c => !c.isUngrouped).length}
|
||||
style={{ backgroundColor: designTokens.colors.primary.main }}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||
个网卡
|
||||
</Text>
|
||||
<Badge
|
||||
count={cards.reduce((acc, card) => acc + (card.ports?.length || 0), 0)}
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>个端口</Text>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>
|
||||
个端口
|
||||
</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData}>
|
||||
@@ -480,7 +581,7 @@ const ServerBackplanePanel = ({
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
border: '3px solid #374151',
|
||||
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)'
|
||||
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
>
|
||||
{/* 服务器标识 */}
|
||||
@@ -492,7 +593,7 @@ const ServerBackplanePanel = ({
|
||||
marginBottom: '12px',
|
||||
padding: '6px 12px',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: '4px'
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<DesktopOutlined style={{ fontSize: 14, color: '#9ca3af', marginRight: 8 }} />
|
||||
@@ -536,16 +637,34 @@ const ServerBackplanePanel = ({
|
||||
>
|
||||
{selectedSlot && (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px', padding: '12px', background: '#f8fafc', borderRadius: '8px' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text type="secondary">类型: {selectedSlot.type === 'onboard' ? '板载网卡' : selectedSlot.type === 'management' ? '管理口' : '扩展网卡'}</Text>
|
||||
{selectedSlot.description && <Text type="secondary">描述: {selectedSlot.description}</Text>}
|
||||
<Text type="secondary">
|
||||
类型:{' '}
|
||||
{selectedSlot.type === 'onboard'
|
||||
? '板载网卡'
|
||||
: selectedSlot.type === 'management'
|
||||
? '管理口'
|
||||
: '扩展网卡'}
|
||||
</Text>
|
||||
{selectedSlot.description && (
|
||||
<Text type="secondary">描述: {selectedSlot.description}</Text>
|
||||
)}
|
||||
<div>
|
||||
<Text type="secondary">端口统计: </Text>
|
||||
<Space size={8}>
|
||||
<Tag color="success">空闲: {selectedSlot.stats?.free || 0}</Tag>
|
||||
<Tag color="processing">占用: {selectedSlot.stats?.occupied || 0}</Tag>
|
||||
{selectedSlot.stats?.fault > 0 && <Tag color="error">故障: {selectedSlot.stats.fault}</Tag>}
|
||||
{selectedSlot.stats?.fault > 0 && (
|
||||
<Tag color="error">故障: {selectedSlot.stats.fault}</Tag>
|
||||
)}
|
||||
<Tag color="blue">总计: {selectedSlot.ports?.length || 0}</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Empty, Spin, Badge, Typography, Space, Checkbox, Tooltip } from 'antd';
|
||||
import { DownOutlined, UpOutlined, EyeOutlined, EyeInvisibleOutlined, PlusOutlined, CloudServerOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DownOutlined,
|
||||
UpOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
PlusOutlined,
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ServerBackplanePanel from './ServerBackplanePanel';
|
||||
import PortPanel from './PortPanel';
|
||||
|
||||
@@ -9,7 +16,7 @@ const { Text } = Typography;
|
||||
/**
|
||||
* 虚拟设备列表组件
|
||||
* 用于优化大量设备面板的渲染性能
|
||||
*
|
||||
*
|
||||
* @param {Object[]} devices - 设备列表
|
||||
* @param {Object} groupedPorts - 按设备分组的端口数据
|
||||
* @param {Object[]} cables - 接线列表
|
||||
@@ -29,7 +36,7 @@ const VirtualDeviceList = ({
|
||||
onAddPort,
|
||||
onManageNetworkCards,
|
||||
initialVisibleCount = 5,
|
||||
loadMoreCount = 5
|
||||
loadMoreCount = 5,
|
||||
}) => {
|
||||
const [visibleCount, setVisibleCount] = useState(initialVisibleCount);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -54,10 +61,10 @@ const VirtualDeviceList = ({
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '100px',
|
||||
threshold: 0.1
|
||||
threshold: 0.1,
|
||||
};
|
||||
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
observerRef.current = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !loading && visibleCount < devices.length) {
|
||||
loadMore();
|
||||
@@ -79,7 +86,7 @@ const VirtualDeviceList = ({
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (loading || visibleCount >= devices.length) return;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
// 模拟异步加载,实际可以直接同步更新
|
||||
setTimeout(() => {
|
||||
@@ -110,10 +117,10 @@ const VirtualDeviceList = ({
|
||||
setShowAll(false);
|
||||
}, [devices]);
|
||||
|
||||
const toggleDeviceExpand = (deviceId) => {
|
||||
const toggleDeviceExpand = deviceId => {
|
||||
setExpandedDevices(prev => ({
|
||||
...prev,
|
||||
[deviceId]: !prev[deviceId]
|
||||
[deviceId]: !prev[deviceId],
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -121,42 +128,38 @@ const VirtualDeviceList = ({
|
||||
const hasMore = visibleCount < devices.length;
|
||||
|
||||
if (devices.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
description="暂无设备数据"
|
||||
style={{ padding: '60px 0' }}
|
||||
/>
|
||||
);
|
||||
return <Empty description="暂无设备数据" style={{ padding: '60px 0' }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{/* 控制栏 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<Space align="center">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>设备列表</Text>
|
||||
<Badge
|
||||
count={devices.length}
|
||||
style={{ backgroundColor: '#667eea' }}
|
||||
/>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
设备列表
|
||||
</Text>
|
||||
<Badge count={devices.length} style={{ backgroundColor: '#667eea' }} />
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
显示 {visibleDevices.length} / {devices.length}
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
<Button
|
||||
size="small"
|
||||
icon={showAll ? <UpOutlined /> : <DownOutlined />}
|
||||
onClick={showAll ? handleCollapseAll : handleShowAll}
|
||||
>
|
||||
@@ -166,7 +169,7 @@ const VirtualDeviceList = ({
|
||||
</div>
|
||||
|
||||
{/* 设备面板列表 */}
|
||||
{visibleDevices.map((device) => {
|
||||
{visibleDevices.map(device => {
|
||||
const deviceId = device.deviceId;
|
||||
const data = groupedPorts[deviceId] || { device, ports: [] };
|
||||
const isExpanded = expandedDevices[deviceId];
|
||||
@@ -174,14 +177,14 @@ const VirtualDeviceList = ({
|
||||
const occupiedCount = data.ports?.filter(p => p.status === 'occupied').length || 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
key={deviceId}
|
||||
style={{
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
transition: 'all 0.3s ease'
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{/* 设备标题栏 */}
|
||||
@@ -195,31 +198,37 @@ const VirtualDeviceList = ({
|
||||
background: isExpanded ? '#f1f5f9' : '#fff',
|
||||
cursor: 'pointer',
|
||||
borderBottom: isExpanded ? '1px solid #e2e8f0' : 'none',
|
||||
transition: 'background 0.2s'
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = '#f1f5f9';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
onMouseLeave={e => {
|
||||
if (!isExpanded) {
|
||||
e.currentTarget.style.background = '#fff';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px'
|
||||
}}>
|
||||
{device.type?.toLowerCase()?.includes('server') ? '🖥️' :
|
||||
device.type?.toLowerCase()?.includes('switch') ? '🔀' :
|
||||
device.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
|
||||
<div
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
}}
|
||||
>
|
||||
{device.type?.toLowerCase()?.includes('server')
|
||||
? '🖥️'
|
||||
: device.type?.toLowerCase()?.includes('switch')
|
||||
? '🔀'
|
||||
: device.type?.toLowerCase()?.includes('router')
|
||||
? '🌐'
|
||||
: '📦'}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '15px', color: '#1e293b' }}>
|
||||
@@ -233,16 +242,16 @@ const VirtualDeviceList = ({
|
||||
|
||||
<Space size="middle">
|
||||
<Space size="small">
|
||||
<Badge
|
||||
count={occupiedCount}
|
||||
<Badge
|
||||
count={occupiedCount}
|
||||
style={{ backgroundColor: '#3b82f6' }}
|
||||
overflowCount={999}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
已用
|
||||
</Text>
|
||||
<Badge
|
||||
count={portCount}
|
||||
<Badge
|
||||
count={portCount}
|
||||
style={{ backgroundColor: '#10b981' }}
|
||||
overflowCount={999}
|
||||
/>
|
||||
@@ -257,13 +266,13 @@ const VirtualDeviceList = ({
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<CloudServerOutlined />}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // 防止触发折叠
|
||||
onManageNetworkCards && onManageNetworkCards(device);
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
网卡管理
|
||||
@@ -275,13 +284,13 @@ const VirtualDeviceList = ({
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // 防止触发折叠
|
||||
onAddPort && onAddPort(device);
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
添加端口
|
||||
@@ -318,7 +327,9 @@ const VirtualDeviceList = ({
|
||||
cables={cables}
|
||||
allDevices={allDevices}
|
||||
onPortClick={onPortClick}
|
||||
onManageNetworkCards={() => onManageNetworkCards && onManageNetworkCards(device)}
|
||||
onManageNetworkCards={() =>
|
||||
onManageNetworkCards && onManageNetworkCards(device)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -329,20 +340,18 @@ const VirtualDeviceList = ({
|
||||
|
||||
{/* 加载更多触发器 */}
|
||||
{hasMore && !showAll && (
|
||||
<div
|
||||
<div
|
||||
id="load-more-trigger"
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#64748b'
|
||||
color: '#64748b',
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Spin size="small" tip="加载更多设备..." />
|
||||
) : (
|
||||
<Text type="secondary">
|
||||
向下滚动加载更多 ({devices.length - visibleCount} 个设备)
|
||||
</Text>
|
||||
<Text type="secondary">向下滚动加载更多 ({devices.length - visibleCount} 个设备)</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user