components_three.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. import React, { useRef, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import * as THREE from 'three';
  4. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  5. import type { ServerData, ServerIconConfig, RackConfig, ServerIconConfigs,DeviceStatusInfo } from './api.ts';
  6. import { queryFns } from './api.ts';
  7. interface UseServerHoverProps {
  8. scene: THREE.Scene;
  9. camera: THREE.Camera;
  10. containerRef: React.RefObject<HTMLDivElement>;
  11. tooltipRef: React.RefObject<HTMLDivElement>;
  12. }
  13. // 合并服务器基本信息和状态信息
  14. interface ServerStatus extends ServerData, DeviceStatusInfo {}
  15. // 颜色常量定义
  16. const COLORS = {
  17. // 背景色(调亮为 #002952)
  18. BACKGROUND: 0x002952,
  19. // 地板颜色(相应调亮)
  20. FLOOR: 0x1D5491, // 调亮地板颜色
  21. FLOOR_GRID: 0x2E66A3, // 相应调亮网格线条
  22. // 机柜颜色
  23. RACK: {
  24. FRAME: 0x2E5483, // 机柜框架颜色(调亮)
  25. FRONT: 0x2E5483, // 机柜前面板
  26. SIDE: 0x274D70, // 机柜侧面板(稍暗)
  27. },
  28. // 服务器颜色
  29. SERVER: {
  30. DEFAULT: 0x2E4D75, // 服务器默认颜色(调亮)
  31. FRONT: 0x355882, // 服务器前面板(调亮)
  32. STATUS: {
  33. ONLINE: 0x00FF00, // 在线状态指示灯(绿色)
  34. WARNING: 0xFFFF00, // 警告状态指示灯(黄色)
  35. OFFLINE: 0xFF0000 // 离线状态指示灯(红色)
  36. },
  37. HOVER: {
  38. COLOR: 0x00DDFF, // 悬停发光颜色(青色)
  39. INTENSITY: 0.05 // 悬停发光强度
  40. }
  41. },
  42. // 灯光颜色
  43. LIGHTS: {
  44. AMBIENT: 0xCCE0FF, // 环境光(偏蓝色冷光)
  45. DIRECT: 0xFFFFFF, // 直射光(白光)
  46. SPOT: 0x00DDFF, // 聚光灯(青色)
  47. }
  48. } as const;
  49. // UI样式常量
  50. const UI_STYLES = {
  51. TOOLTIP: {
  52. BACKGROUND: '#001529',
  53. BORDER: '#00dff9',
  54. BORDER_INNER: '#15243A',
  55. SHADOW: '0 0 10px rgba(0,223,249,0.3)',
  56. TEXT: '#ffffff',
  57. PADDING: '12px 16px',
  58. BORDER_RADIUS: '4px',
  59. FONT_SIZE: '14px'
  60. }
  61. } as const;
  62. // 定义1U的高度(米)
  63. const U_HEIGHT = 0.04445; // 1U = 44.45mm = 0.04445m
  64. function useServerHover({ scene, camera, containerRef, tooltipRef }: UseServerHoverProps) {
  65. const HOVER_COLOR = new THREE.Color(COLORS.SERVER.HOVER.COLOR);
  66. const HOVER_INTENSITY = COLORS.SERVER.HOVER.INTENSITY;
  67. let INTERSECTED: THREE.Mesh | null = null;
  68. const raycaster = new THREE.Raycaster();
  69. const mouse = new THREE.Vector2();
  70. const setMaterialEmissive = (materials: THREE.Material | THREE.Material[], color: THREE.Color, intensity: number) => {
  71. if (Array.isArray(materials)) {
  72. materials.forEach(material => {
  73. if (material instanceof THREE.MeshPhongMaterial) {
  74. material.emissive.copy(color);
  75. material.emissiveIntensity = intensity;
  76. material.needsUpdate = true;
  77. }
  78. });
  79. } else if (materials instanceof THREE.MeshPhongMaterial) {
  80. materials.emissive.copy(color);
  81. materials.emissiveIntensity = intensity;
  82. materials.needsUpdate = true;
  83. }
  84. };
  85. const resetMaterialEmissive = (materials: THREE.Material | THREE.Material[]) => {
  86. if (Array.isArray(materials)) {
  87. materials.forEach(material => {
  88. if (material instanceof THREE.MeshPhongMaterial) {
  89. material.emissive.setHex(0x000000);
  90. material.emissiveIntensity = 0.2;
  91. material.needsUpdate = true;
  92. }
  93. });
  94. } else if (materials instanceof THREE.MeshPhongMaterial) {
  95. materials.emissive.setHex(0x000000);
  96. materials.emissiveIntensity = 0.2;
  97. materials.needsUpdate = true;
  98. }
  99. };
  100. const handleMouseMove = (event: MouseEvent) => {
  101. const rect = containerRef.current?.getBoundingClientRect();
  102. if (!rect) return;
  103. mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  104. mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
  105. raycaster.setFromCamera(mouse, camera);
  106. const intersects = raycaster.intersectObjects(scene.children, true);
  107. if (intersects.length > 0) {
  108. const found = intersects.find(item =>
  109. item.object instanceof THREE.Mesh &&
  110. item.object.userData.type === 'server'
  111. );
  112. if (found) {
  113. const serverMesh = found.object as THREE.Mesh;
  114. if (INTERSECTED !== serverMesh) {
  115. if (INTERSECTED) {
  116. resetMaterialEmissive(INTERSECTED.material);
  117. }
  118. INTERSECTED = serverMesh;
  119. setMaterialEmissive(INTERSECTED.material, HOVER_COLOR, HOVER_INTENSITY);
  120. updateTooltip(event, INTERSECTED.userData.status);
  121. }
  122. } else {
  123. resetHoverState();
  124. }
  125. } else {
  126. resetHoverState();
  127. }
  128. };
  129. const resetIntersected = () => {
  130. if (INTERSECTED) {
  131. resetMaterialEmissive(INTERSECTED.material);
  132. }
  133. };
  134. const resetHoverState = () => {
  135. resetIntersected();
  136. INTERSECTED = null;
  137. if (tooltipRef.current) {
  138. tooltipRef.current.style.display = 'none';
  139. }
  140. };
  141. const updateTooltip = (event: MouseEvent, serverStatus: ServerStatus) => {
  142. if (tooltipRef.current) {
  143. const containerRect = containerRef.current?.getBoundingClientRect();
  144. if (!containerRect) return;
  145. const tooltipX = event.clientX - containerRect.left;
  146. const tooltipY = event.clientY - containerRect.top;
  147. const tooltipRect = tooltipRef.current.getBoundingClientRect();
  148. const tooltipWidth = tooltipRect.width;
  149. const tooltipHeight = tooltipRect.height;
  150. const finalX = Math.min(
  151. tooltipX + 10,
  152. containerRect.width - tooltipWidth - 10
  153. );
  154. const finalY = Math.min(
  155. tooltipY + 10,
  156. containerRect.height - tooltipHeight - 10
  157. );
  158. tooltipRef.current.style.left = `${finalX}px`;
  159. tooltipRef.current.style.top = `${finalY}px`;
  160. tooltipRef.current.innerHTML = `
  161. <div class="bg-[${UI_STYLES.TOOLTIP.BACKGROUND}] border border-[${UI_STYLES.TOOLTIP.BORDER}]
  162. shadow-[${UI_STYLES.TOOLTIP.SHADOW}] p-3 px-4 rounded text-[${UI_STYLES.TOOLTIP.TEXT}] text-sm">
  163. <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
  164. <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
  165. <path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM5 19V5H19V19H5Z" fill="currentColor"/>
  166. <path d="M12 17H17V15H12V17ZM7 13H17V11H7V13ZM7 9H17V7H7V9Z" fill="currentColor"/>
  167. </svg>
  168. <span class="font-medium">资产信息</span>
  169. </div>
  170. <div class="grid gap-0.5 ml-1 mb-2">
  171. <div>名称: ${serverStatus.name}</div>
  172. <div>IP地址: ${serverStatus.ip}</div>
  173. </div>
  174. <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
  175. <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
  176. <path d="M15.9 4.99999C15.9 4.99999 15.9 4.89999 15.8 4.89999C15.4 4.39999 14.8 3.99999 14.2 3.69999L12.2 6.29999L15.9 4.99999Z" fill="currentColor"/>
  177. <path d="M18.2 7.3L15.9 5C15.9 5 15.9 4.9 15.8 4.9C16.8 5.7 17.6 6.4 18.2 7.3Z" fill="currentColor"/>
  178. <path d="M21 16V8C21 6.7 20.2 5.6 19 5.2C18.3 4.1 17.3 3.2 16.2 2.5C14.7 1.6 13 1 11.2 1C6.1 1 2 5.1 2 10.2C2 13.6 3.9 16.5 6.5 18.1C7.8 18.9 9 20.2 9 21.7V23H15V21.7C15 20.2 16.2 18.9 17.5 18.1C19.4 17 20.7 15.1 21 16ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z" fill="currentColor"/>
  179. </svg>
  180. <span class="font-medium">网络状态</span>
  181. </div>
  182. <div class="grid gap-0.5 ml-1 mb-2">
  183. <div class="flex justify-between">
  184. <span>状态:</span>
  185. <div class="flex items-center gap-1.5">
  186. <span>${serverStatus.networkStatus === 1 ? '在线' : '离线'}</span>
  187. <span class="w-2 h-2 rounded-full ${serverStatus.networkStatus === 1 ? 'bg-green-500' : 'bg-red-500'}"></span>
  188. </div>
  189. </div>
  190. <div>丢包率: ${serverStatus.packetLoss}%</div>
  191. </div>
  192. <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
  193. <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
  194. <path d="M4 6H20V8H4V6Z" fill="currentColor"/>
  195. <path d="M4 11H20V13H4V11Z" fill="currentColor"/>
  196. <path d="M4 16H20V18H4V16Z" fill="currentColor"/>
  197. <path d="M2 4.5C2 3.67157 2.67157 3 3.5 3H20.5C21.3284 3 22 3.67157 22 4.5V19.5C22 20.3284 21.3284 21 20.5 21H3.5C2.67157 21 2 20.3284 2 19.5V4.5Z" stroke="currentColor" stroke-width="2"/>
  198. </svg>
  199. <span class="font-medium">配置信息</span>
  200. </div>
  201. <div class="grid gap-0.5 ml-1">
  202. <div>CPU: ${serverStatus.cpu || '-'}</div>
  203. <div>内存: ${serverStatus.memory || '-'}</div>
  204. <div>硬盘: ${serverStatus.disk || '-'}</div>
  205. </div>
  206. </div>
  207. `;
  208. tooltipRef.current.style.display = 'block';
  209. }
  210. };
  211. const cleanup = () => {
  212. resetHoverState();
  213. };
  214. return {
  215. handleMouseMove,
  216. cleanup
  217. };
  218. }
  219. // 创建机柜模型 - 修改机柜的原点为底部中心
  220. function createRack(position: THREE.Vector3): THREE.Mesh {
  221. // 创建机柜的六个面
  222. const rackGeometry = new THREE.BoxGeometry(0.6, 2, 1);
  223. // 创建两种材质:
  224. // 1. 通用材质 - 用于除前面外的所有面,双面渲染
  225. const commonMaterial = new THREE.MeshPhongMaterial({
  226. color: COLORS.RACK.SIDE,
  227. side: THREE.DoubleSide, // 双面渲染
  228. opacity: 0.9,
  229. });
  230. // 2. 前面材质 - 半透明
  231. const frontMaterial = new THREE.MeshPhongMaterial({
  232. color: COLORS.RACK.FRONT,
  233. transparent: true,
  234. opacity: 0.1, // 前面设置为更透明
  235. });
  236. // 创建材质数组,按照几何体的面的顺序设置材质
  237. // BoxGeometry的面顺序:右、左、上、下、前、后
  238. const materials = [
  239. commonMaterial, // 右面 - 双面渲染
  240. commonMaterial, // 左面 - 双面渲染
  241. commonMaterial, // 上面 - 双面渲染
  242. commonMaterial, // 下面 - 双面渲染
  243. frontMaterial, // 前面 - 半透明
  244. commonMaterial, // 后面 - 双面渲染
  245. ];
  246. const rack = new THREE.Mesh(rackGeometry, materials);
  247. rack.position.copy(position);
  248. rackGeometry.translate(0, 1, 0);
  249. return rack;
  250. }
  251. // 创建服务器模型
  252. function createServer(
  253. position: THREE.Vector3,
  254. serverData: ServerData,
  255. serverIconConfig: ServerIconConfig
  256. ): { server: THREE.Mesh } {
  257. const config = serverIconConfig;
  258. const U = serverData.u;
  259. const serverGeometry = new THREE.BoxGeometry(
  260. 0.483, // 19英寸 ≈ 0.483米
  261. U * U_HEIGHT, // 将U数转换为实际高度
  262. 0.8 // 深度保持0.8米
  263. );
  264. serverGeometry.translate(0, U * U_HEIGHT/2, 0);
  265. // 创建基础材质(用于侧面、顶面、底面和后面)
  266. const baseMaterial = new THREE.MeshPhongMaterial({
  267. color: config.color,
  268. shininess: 30, // 降低反光度
  269. });
  270. // 创建前面的材质(用于贴图)
  271. const frontMaterial = new THREE.MeshPhongMaterial({
  272. color: config.color,
  273. shininess: 30,
  274. map: null, // 初始化时设为null,等待贴图加载
  275. });
  276. // 创建材质数组
  277. const materials = [
  278. baseMaterial, // 右面
  279. baseMaterial, // 左面
  280. baseMaterial, // 上面
  281. baseMaterial, // 下面
  282. frontMaterial, // 前面 - 用于贴图
  283. baseMaterial, // 后面
  284. ];
  285. const server = new THREE.Mesh(serverGeometry, materials);
  286. server.position.copy(position);
  287. // 加载贴图(如果有)
  288. if (config.textureUrl) {
  289. const textureLoader = new THREE.TextureLoader();
  290. textureLoader.load(config.textureUrl, (texture) => {
  291. // 设置贴图属性以提高清晰度
  292. texture.minFilter = THREE.LinearFilter;
  293. texture.magFilter = THREE.LinearFilter;
  294. texture.anisotropy = 16; // 增加各向异性过滤
  295. // 调整贴图的重复和偏移
  296. texture.repeat.set(1, 1);
  297. texture.offset.set(0, 0);
  298. // 更新材质
  299. frontMaterial.map = texture;
  300. frontMaterial.needsUpdate = true;
  301. });
  302. }
  303. return { server };
  304. }
  305. // 添加创建地板的函数
  306. function createFloor(): THREE.Mesh {
  307. const floorGeometry = new THREE.PlaneGeometry(10, 10);
  308. const floorMaterial = new THREE.MeshPhongMaterial({
  309. color: COLORS.FLOOR,
  310. side: THREE.DoubleSide
  311. });
  312. // 添加网格纹理
  313. const gridHelper = new THREE.GridHelper(10, 20, COLORS.FLOOR_GRID, COLORS.FLOOR_GRID);
  314. gridHelper.rotation.x = Math.PI / 2;
  315. const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  316. floor.rotation.x = -Math.PI / 2;
  317. floor.add(gridHelper);
  318. return floor;
  319. }
  320. class ServerRack {
  321. rack: THREE.Mesh;
  322. servers: THREE.Mesh[] = [];
  323. statusLights: THREE.PointLight[] = [];
  324. constructor(scene: THREE.Scene, rackConfig: RackConfig, serverIconConfigs: ServerIconConfigs) {
  325. // 创建机柜
  326. this.rack = createRack(rackConfig.position);
  327. scene.add(this.rack);
  328. const bottomSpace = 0.04445; // 底部预留空间1U
  329. const slotHeight = 0.04445; // 每个槽位高度1U
  330. // 使用配置中的服务器数据创建服务器
  331. rackConfig.servers.forEach((serverData: ServerData) => {
  332. // 从底部开始计算高度
  333. const currentHeight = bottomSpace + (slotHeight * (serverData.slot - 1));
  334. const serverPosition = new THREE.Vector3(
  335. rackConfig.position.x,
  336. rackConfig.position.y + currentHeight,
  337. rackConfig.position.z
  338. );
  339. const serverIconConfig = serverIconConfigs[serverData.type];
  340. // console.log(serverIconConfig, serverIconConfigs);
  341. // console.log(serverData);
  342. // console.log(serverPosition);
  343. const { server } = createServer(
  344. serverPosition,
  345. serverData,
  346. serverIconConfig
  347. );
  348. server.userData.type = 'server';
  349. server.userData.status = getServerStatus(serverData);
  350. this.servers.push(server);
  351. scene.add(server);
  352. });
  353. }
  354. // 更新服务器状态
  355. updateServerStatus(serverId: string, status: ServerStatus) {
  356. const server = this.servers.find(s => s.userData.id === serverId);
  357. if (server) {
  358. server.userData.status = status;
  359. const index = this.servers.indexOf(server);
  360. if (index !== -1) {
  361. const light = this.statusLights[index];
  362. light.color.setHex(
  363. status.status === 'online' ? COLORS.SERVER.STATUS.ONLINE :
  364. status.status === 'warning' ? COLORS.SERVER.STATUS.WARNING :
  365. COLORS.SERVER.STATUS.OFFLINE
  366. );
  367. }
  368. }
  369. }
  370. }
  371. // 将服务器状态根据设备状态转换为对应的status格式
  372. const getServerStatus = (serverData: ServerData): ServerStatus => {
  373. // 转换网络状态为UI显示状态
  374. let status: 'online' | 'offline' | 'warning' = 'offline';
  375. // 根据设备状态判断显示状态
  376. if (serverData.deviceStatus === 0) { // 正常
  377. status = 'online';
  378. } else if (serverData.deviceStatus === 2) { // 故障
  379. status = 'warning';
  380. } else { // 其他情况(维护中、下线)
  381. status = 'offline';
  382. }
  383. // 随机生成资源使用率用于展示
  384. return {
  385. ...serverData,
  386. status,
  387. usage: {
  388. cpu: Math.floor(Math.random() * 100),
  389. memory: Math.floor(Math.random() * 100),
  390. disk: Math.floor(Math.random() * 100)
  391. }
  392. };
  393. };
  394. export function ThreeJSRoom() {
  395. const containerRef = useRef<HTMLDivElement>(null);
  396. const tooltipRef = useRef<HTMLDivElement>(null);
  397. const racksRef = useRef<ServerRack[]>([]);
  398. const sceneRef = useRef<{
  399. scene: THREE.Scene;
  400. camera: THREE.PerspectiveCamera;
  401. renderer: THREE.WebGLRenderer;
  402. controls: typeof OrbitControls;
  403. cleanup: () => void;
  404. } | null>(null);
  405. // 获取服务器类型图标
  406. const { data: deviceIcons } = useQuery({
  407. queryKey: ['deviceIcons'],
  408. queryFn: queryFns.fetchDeviceIcons
  409. });
  410. // 获取机柜配置,添加自动刷新
  411. const { data: rackConfigs } = useQuery({
  412. queryKey: ['rackConfigs'],
  413. queryFn: queryFns.fetchRackConfigs,
  414. refetchInterval: 30000,
  415. refetchIntervalInBackground: true
  416. });
  417. // 初始化3D场景
  418. useEffect(() => {
  419. if (!containerRef.current) return;
  420. // 初始化场景
  421. const scene = new THREE.Scene();
  422. scene.background = new THREE.Color(COLORS.BACKGROUND);
  423. const camera = new THREE.PerspectiveCamera(
  424. 22,
  425. containerRef.current.clientWidth / containerRef.current.clientHeight,
  426. 0.1,
  427. 1000
  428. );
  429. const renderer = new THREE.WebGLRenderer({ antialias: true });
  430. renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
  431. containerRef.current.appendChild(renderer.domElement);
  432. const controls = new OrbitControls(camera, renderer.domElement);
  433. controls.enableDamping = true;
  434. controls.maxPolarAngle = Math.PI / 2;
  435. controls.minDistance = 6;
  436. controls.maxDistance = 10;
  437. controls.enablePan = false;
  438. controls.target.set(0, 1, 0);
  439. controls.enabled = false;
  440. // 添加地板
  441. const floor = createFloor();
  442. floor.position.y = -0.01;
  443. scene.add(floor);
  444. // 设置场景光照
  445. const ambientLight = new THREE.AmbientLight(COLORS.LIGHTS.AMBIENT, 0.6);
  446. scene.add(ambientLight);
  447. const directionalLight = new THREE.DirectionalLight(COLORS.LIGHTS.DIRECT, 0.8);
  448. directionalLight.position.set(5, 5, 5);
  449. scene.add(directionalLight);
  450. const spotLight = new THREE.SpotLight(COLORS.LIGHTS.SPOT, 0.8);
  451. spotLight.position.set(0, 5, 0);
  452. spotLight.angle = Math.PI / 4;
  453. spotLight.penumbra = 0.1;
  454. scene.add(spotLight);
  455. camera.position.set(0, 1, 1);
  456. camera.lookAt(0, 1, 0);
  457. const { handleMouseMove, cleanup } = useServerHover({
  458. scene,
  459. camera,
  460. containerRef: containerRef as React.RefObject<HTMLDivElement>,
  461. tooltipRef: tooltipRef as React.RefObject<HTMLDivElement>
  462. });
  463. containerRef.current.addEventListener('mousemove', handleMouseMove);
  464. // 动画循环
  465. const animate = () => {
  466. requestAnimationFrame(animate);
  467. controls.update();
  468. renderer.render(scene, camera);
  469. };
  470. animate();
  471. // 保存场景引用
  472. sceneRef.current = {
  473. scene,
  474. camera,
  475. renderer,
  476. controls: controls as unknown as typeof OrbitControls,
  477. cleanup: () => {
  478. cleanup();
  479. if (containerRef.current) {
  480. containerRef.current.removeEventListener('mousemove', handleMouseMove);
  481. containerRef.current.removeChild(renderer.domElement);
  482. }
  483. }
  484. };
  485. // 使用 ResizeObserver 监听容器大小变化
  486. const resizeObserver = new ResizeObserver(() => {
  487. if (!containerRef.current || !sceneRef.current) return;
  488. // console.log('resizeObserver', containerRef.current.clientWidth, containerRef.current.clientHeight);
  489. const { camera, renderer } = sceneRef.current;
  490. camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight;
  491. camera.updateProjectionMatrix();
  492. renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
  493. });
  494. resizeObserver.observe(containerRef.current);
  495. // 清理
  496. return () => {
  497. resizeObserver.disconnect();
  498. sceneRef.current?.cleanup();
  499. };
  500. }, []);
  501. // 更新机柜数据
  502. useEffect(() => {
  503. if (!sceneRef.current || !rackConfigs || !deviceIcons) return;
  504. // 清除现有机柜
  505. racksRef.current.forEach(rack => {
  506. rack.servers.forEach(server => {
  507. sceneRef.current!.scene.remove(server);
  508. });
  509. sceneRef.current!.scene.remove(rack.rack);
  510. });
  511. racksRef.current = [];
  512. // 创建新机柜
  513. racksRef.current = rackConfigs.map(config => {
  514. return new ServerRack(sceneRef.current!.scene, config, deviceIcons);
  515. });
  516. }, [rackConfigs, deviceIcons]);
  517. return (
  518. <div className="relative w-full h-full">
  519. <div ref={containerRef} className="w-full h-full" />
  520. <div
  521. ref={tooltipRef}
  522. className="absolute hidden z-10"
  523. style={{
  524. pointerEvents: 'none',
  525. position: 'absolute',
  526. transform: 'translate3d(0, 0, 0)'
  527. }}
  528. >
  529. <div className="bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] p-2 rounded">
  530. {/* 工具提示内容由useServerHover处理 */}
  531. </div>
  532. </div>
  533. </div>
  534. );
  535. }