import React, { useRef, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import type { ServerData, ServerIconConfig, RackConfig, ServerIconConfigs,DeviceStatusInfo } from './api.ts'; import { queryFns } from './api.ts'; interface UseServerHoverProps { scene: THREE.Scene; camera: THREE.Camera; containerRef: React.RefObject; tooltipRef: React.RefObject; } // 合并服务器基本信息和状态信息 interface ServerStatus extends ServerData, DeviceStatusInfo {} // 颜色常量定义 const COLORS = { // 背景色(调亮为 #002952) BACKGROUND: 0x002952, // 地板颜色(相应调亮) FLOOR: 0x1D5491, // 调亮地板颜色 FLOOR_GRID: 0x2E66A3, // 相应调亮网格线条 // 机柜颜色 RACK: { FRAME: 0x2E5483, // 机柜框架颜色(调亮) FRONT: 0x2E5483, // 机柜前面板 SIDE: 0x274D70, // 机柜侧面板(稍暗) }, // 服务器颜色 SERVER: { DEFAULT: 0x2E4D75, // 服务器默认颜色(调亮) FRONT: 0x355882, // 服务器前面板(调亮) STATUS: { ONLINE: 0x00FF00, // 在线状态指示灯(绿色) WARNING: 0xFFFF00, // 警告状态指示灯(黄色) OFFLINE: 0xFF0000 // 离线状态指示灯(红色) }, HOVER: { COLOR: 0x00DDFF, // 悬停发光颜色(青色) INTENSITY: 0.05 // 悬停发光强度 } }, // 灯光颜色 LIGHTS: { AMBIENT: 0xCCE0FF, // 环境光(偏蓝色冷光) DIRECT: 0xFFFFFF, // 直射光(白光) SPOT: 0x00DDFF, // 聚光灯(青色) } } as const; // UI样式常量 const UI_STYLES = { TOOLTIP: { BACKGROUND: '#001529', BORDER: '#00dff9', BORDER_INNER: '#15243A', SHADOW: '0 0 10px rgba(0,223,249,0.3)', TEXT: '#ffffff', PADDING: '12px 16px', BORDER_RADIUS: '4px', FONT_SIZE: '14px' } } as const; // 定义1U的高度(米) const U_HEIGHT = 0.04445; // 1U = 44.45mm = 0.04445m function useServerHover({ scene, camera, containerRef, tooltipRef }: UseServerHoverProps) { const HOVER_COLOR = new THREE.Color(COLORS.SERVER.HOVER.COLOR); const HOVER_INTENSITY = COLORS.SERVER.HOVER.INTENSITY; let INTERSECTED: THREE.Mesh | null = null; const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const setMaterialEmissive = (materials: THREE.Material | THREE.Material[], color: THREE.Color, intensity: number) => { if (Array.isArray(materials)) { materials.forEach(material => { if (material instanceof THREE.MeshPhongMaterial) { material.emissive.copy(color); material.emissiveIntensity = intensity; material.needsUpdate = true; } }); } else if (materials instanceof THREE.MeshPhongMaterial) { materials.emissive.copy(color); materials.emissiveIntensity = intensity; materials.needsUpdate = true; } }; const resetMaterialEmissive = (materials: THREE.Material | THREE.Material[]) => { if (Array.isArray(materials)) { materials.forEach(material => { if (material instanceof THREE.MeshPhongMaterial) { material.emissive.setHex(0x000000); material.emissiveIntensity = 0.2; material.needsUpdate = true; } }); } else if (materials instanceof THREE.MeshPhongMaterial) { materials.emissive.setHex(0x000000); materials.emissiveIntensity = 0.2; materials.needsUpdate = true; } }; const handleMouseMove = (event: MouseEvent) => { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); if (intersects.length > 0) { const found = intersects.find(item => item.object instanceof THREE.Mesh && item.object.userData.type === 'server' ); if (found) { const serverMesh = found.object as THREE.Mesh; if (INTERSECTED !== serverMesh) { if (INTERSECTED) { resetMaterialEmissive(INTERSECTED.material); } INTERSECTED = serverMesh; setMaterialEmissive(INTERSECTED.material, HOVER_COLOR, HOVER_INTENSITY); updateTooltip(event, INTERSECTED.userData.status); } } else { resetHoverState(); } } else { resetHoverState(); } }; const resetIntersected = () => { if (INTERSECTED) { resetMaterialEmissive(INTERSECTED.material); } }; const resetHoverState = () => { resetIntersected(); INTERSECTED = null; if (tooltipRef.current) { tooltipRef.current.style.display = 'none'; } }; const updateTooltip = (event: MouseEvent, serverStatus: ServerStatus) => { if (tooltipRef.current) { const containerRect = containerRef.current?.getBoundingClientRect(); if (!containerRect) return; const tooltipX = event.clientX - containerRect.left; const tooltipY = event.clientY - containerRect.top; const tooltipRect = tooltipRef.current.getBoundingClientRect(); const tooltipWidth = tooltipRect.width; const tooltipHeight = tooltipRect.height; const finalX = Math.min( tooltipX + 10, containerRect.width - tooltipWidth - 10 ); const finalY = Math.min( tooltipY + 10, containerRect.height - tooltipHeight - 10 ); tooltipRef.current.style.left = `${finalX}px`; tooltipRef.current.style.top = `${finalY}px`; tooltipRef.current.innerHTML = `
资产信息
名称: ${serverStatus.name}
IP地址: ${serverStatus.ip}
网络状态
状态:
${serverStatus.networkStatus === 1 ? '在线' : '离线'}
丢包率: ${serverStatus.packetLoss}%
配置信息
CPU: ${serverStatus.cpu || '-'}
内存: ${serverStatus.memory || '-'}
硬盘: ${serverStatus.disk || '-'}
`; tooltipRef.current.style.display = 'block'; } }; const cleanup = () => { resetHoverState(); }; return { handleMouseMove, cleanup }; } // 创建机柜模型 - 修改机柜的原点为底部中心 function createRack(position: THREE.Vector3): THREE.Mesh { // 创建机柜的六个面 const rackGeometry = new THREE.BoxGeometry(0.6, 2, 1); // 创建两种材质: // 1. 通用材质 - 用于除前面外的所有面,双面渲染 const commonMaterial = new THREE.MeshPhongMaterial({ color: COLORS.RACK.SIDE, side: THREE.DoubleSide, // 双面渲染 opacity: 0.9, }); // 2. 前面材质 - 半透明 const frontMaterial = new THREE.MeshPhongMaterial({ color: COLORS.RACK.FRONT, transparent: true, opacity: 0.1, // 前面设置为更透明 }); // 创建材质数组,按照几何体的面的顺序设置材质 // BoxGeometry的面顺序:右、左、上、下、前、后 const materials = [ commonMaterial, // 右面 - 双面渲染 commonMaterial, // 左面 - 双面渲染 commonMaterial, // 上面 - 双面渲染 commonMaterial, // 下面 - 双面渲染 frontMaterial, // 前面 - 半透明 commonMaterial, // 后面 - 双面渲染 ]; const rack = new THREE.Mesh(rackGeometry, materials); rack.position.copy(position); rackGeometry.translate(0, 1, 0); return rack; } // 创建服务器模型 function createServer( position: THREE.Vector3, serverData: ServerData, serverIconConfig: ServerIconConfig ): { server: THREE.Mesh } { const config = serverIconConfig; const U = serverData.u; const serverGeometry = new THREE.BoxGeometry( 0.483, // 19英寸 ≈ 0.483米 U * U_HEIGHT, // 将U数转换为实际高度 0.8 // 深度保持0.8米 ); serverGeometry.translate(0, U * U_HEIGHT/2, 0); // 创建基础材质(用于侧面、顶面、底面和后面) const baseMaterial = new THREE.MeshPhongMaterial({ color: config.color, shininess: 30, // 降低反光度 }); // 创建前面的材质(用于贴图) const frontMaterial = new THREE.MeshPhongMaterial({ color: config.color, shininess: 30, map: null, // 初始化时设为null,等待贴图加载 }); // 创建材质数组 const materials = [ baseMaterial, // 右面 baseMaterial, // 左面 baseMaterial, // 上面 baseMaterial, // 下面 frontMaterial, // 前面 - 用于贴图 baseMaterial, // 后面 ]; const server = new THREE.Mesh(serverGeometry, materials); server.position.copy(position); // 加载贴图(如果有) if (config.textureUrl) { const textureLoader = new THREE.TextureLoader(); textureLoader.load(config.textureUrl, (texture) => { // 设置贴图属性以提高清晰度 texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; texture.anisotropy = 16; // 增加各向异性过滤 // 调整贴图的重复和偏移 texture.repeat.set(1, 1); texture.offset.set(0, 0); // 更新材质 frontMaterial.map = texture; frontMaterial.needsUpdate = true; }); } return { server }; } // 添加创建地板的函数 function createFloor(): THREE.Mesh { const floorGeometry = new THREE.PlaneGeometry(10, 10); const floorMaterial = new THREE.MeshPhongMaterial({ color: COLORS.FLOOR, side: THREE.DoubleSide }); // 添加网格纹理 const gridHelper = new THREE.GridHelper(10, 20, COLORS.FLOOR_GRID, COLORS.FLOOR_GRID); gridHelper.rotation.x = Math.PI / 2; const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.add(gridHelper); return floor; } class ServerRack { rack: THREE.Mesh; servers: THREE.Mesh[] = []; statusLights: THREE.PointLight[] = []; constructor(scene: THREE.Scene, rackConfig: RackConfig, serverIconConfigs: ServerIconConfigs) { // 创建机柜 this.rack = createRack(rackConfig.position); scene.add(this.rack); const bottomSpace = 0.04445; // 底部预留空间1U const slotHeight = 0.04445; // 每个槽位高度1U // 使用配置中的服务器数据创建服务器 rackConfig.servers.forEach((serverData: ServerData) => { // 从底部开始计算高度 const currentHeight = bottomSpace + (slotHeight * (serverData.slot - 1)); const serverPosition = new THREE.Vector3( rackConfig.position.x, rackConfig.position.y + currentHeight, rackConfig.position.z ); const serverIconConfig = serverIconConfigs[serverData.type]; // console.log(serverIconConfig, serverIconConfigs); // console.log(serverData); // console.log(serverPosition); const { server } = createServer( serverPosition, serverData, serverIconConfig ); server.userData.type = 'server'; server.userData.status = getServerStatus(serverData); this.servers.push(server); scene.add(server); }); } // 更新服务器状态 updateServerStatus(serverId: string, status: ServerStatus) { const server = this.servers.find(s => s.userData.id === serverId); if (server) { server.userData.status = status; const index = this.servers.indexOf(server); if (index !== -1) { const light = this.statusLights[index]; light.color.setHex( status.status === 'online' ? COLORS.SERVER.STATUS.ONLINE : status.status === 'warning' ? COLORS.SERVER.STATUS.WARNING : COLORS.SERVER.STATUS.OFFLINE ); } } } } // 将服务器状态根据设备状态转换为对应的status格式 const getServerStatus = (serverData: ServerData): ServerStatus => { // 转换网络状态为UI显示状态 let status: 'online' | 'offline' | 'warning' = 'offline'; // 根据设备状态判断显示状态 if (serverData.deviceStatus === 0) { // 正常 status = 'online'; } else if (serverData.deviceStatus === 2) { // 故障 status = 'warning'; } else { // 其他情况(维护中、下线) status = 'offline'; } // 随机生成资源使用率用于展示 return { ...serverData, status, usage: { cpu: Math.floor(Math.random() * 100), memory: Math.floor(Math.random() * 100), disk: Math.floor(Math.random() * 100) } }; }; export function ThreeJSRoom() { const containerRef = useRef(null); const tooltipRef = useRef(null); const racksRef = useRef([]); const sceneRef = useRef<{ scene: THREE.Scene; camera: THREE.PerspectiveCamera; renderer: THREE.WebGLRenderer; controls: typeof OrbitControls; cleanup: () => void; } | null>(null); // 获取服务器类型图标 const { data: deviceIcons } = useQuery({ queryKey: ['deviceIcons'], queryFn: queryFns.fetchDeviceIcons }); // 获取机柜配置,添加自动刷新 const { data: rackConfigs } = useQuery({ queryKey: ['rackConfigs'], queryFn: queryFns.fetchRackConfigs, refetchInterval: 30000, refetchIntervalInBackground: true }); // 初始化3D场景 useEffect(() => { if (!containerRef.current) return; // 初始化场景 const scene = new THREE.Scene(); scene.background = new THREE.Color(COLORS.BACKGROUND); const camera = new THREE.PerspectiveCamera( 22, containerRef.current.clientWidth / containerRef.current.clientHeight, 0.1, 1000 ); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight); containerRef.current.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.maxPolarAngle = Math.PI / 2; controls.minDistance = 6; controls.maxDistance = 10; controls.enablePan = false; controls.target.set(0, 1, 0); controls.enabled = false; // 添加地板 const floor = createFloor(); floor.position.y = -0.01; scene.add(floor); // 设置场景光照 const ambientLight = new THREE.AmbientLight(COLORS.LIGHTS.AMBIENT, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(COLORS.LIGHTS.DIRECT, 0.8); directionalLight.position.set(5, 5, 5); scene.add(directionalLight); const spotLight = new THREE.SpotLight(COLORS.LIGHTS.SPOT, 0.8); spotLight.position.set(0, 5, 0); spotLight.angle = Math.PI / 4; spotLight.penumbra = 0.1; scene.add(spotLight); camera.position.set(0, 1, 1); camera.lookAt(0, 1, 0); const { handleMouseMove, cleanup } = useServerHover({ scene, camera, containerRef: containerRef as React.RefObject, tooltipRef: tooltipRef as React.RefObject }); containerRef.current.addEventListener('mousemove', handleMouseMove); // 动画循环 const animate = () => { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }; animate(); // 保存场景引用 sceneRef.current = { scene, camera, renderer, controls: controls as unknown as typeof OrbitControls, cleanup: () => { cleanup(); if (containerRef.current) { containerRef.current.removeEventListener('mousemove', handleMouseMove); containerRef.current.removeChild(renderer.domElement); } } }; // 使用 ResizeObserver 监听容器大小变化 const resizeObserver = new ResizeObserver(() => { if (!containerRef.current || !sceneRef.current) return; // console.log('resizeObserver', containerRef.current.clientWidth, containerRef.current.clientHeight); const { camera, renderer } = sceneRef.current; camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight); }); resizeObserver.observe(containerRef.current); // 清理 return () => { resizeObserver.disconnect(); sceneRef.current?.cleanup(); }; }, []); // 更新机柜数据 useEffect(() => { if (!sceneRef.current || !rackConfigs || !deviceIcons) return; // 清除现有机柜 racksRef.current.forEach(rack => { rack.servers.forEach(server => { sceneRef.current!.scene.remove(server); }); sceneRef.current!.scene.remove(rack.rack); }); racksRef.current = []; // 创建新机柜 racksRef.current = rackConfigs.map(config => { return new ServerRack(sceneRef.current!.scene, config, deviceIcons); }); }, [rackConfigs, deviceIcons]); return (
{/* 工具提示内容由useServerHover处理 */}
); }