| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632 |
- 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<HTMLDivElement>;
- tooltipRef: React.RefObject<HTMLDivElement>;
- }
- // 合并服务器基本信息和状态信息
- 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 = `
- <div class="bg-[${UI_STYLES.TOOLTIP.BACKGROUND}] border border-[${UI_STYLES.TOOLTIP.BORDER}]
- shadow-[${UI_STYLES.TOOLTIP.SHADOW}] p-3 px-4 rounded text-[${UI_STYLES.TOOLTIP.TEXT}] text-sm">
- <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
- <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"/>
- <path d="M12 17H17V15H12V17ZM7 13H17V11H7V13ZM7 9H17V7H7V9Z" fill="currentColor"/>
- </svg>
- <span class="font-medium">资产信息</span>
- </div>
- <div class="grid gap-0.5 ml-1 mb-2">
- <div>名称: ${serverStatus.name}</div>
- <div>IP地址: ${serverStatus.ip}</div>
- </div>
-
- <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
- <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"/>
- <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"/>
- <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"/>
- </svg>
- <span class="font-medium">网络状态</span>
- </div>
- <div class="grid gap-0.5 ml-1 mb-2">
- <div class="flex justify-between">
- <span>状态:</span>
- <div class="flex items-center gap-1.5">
- <span>${serverStatus.networkStatus === 1 ? '在线' : '离线'}</span>
- <span class="w-2 h-2 rounded-full ${serverStatus.networkStatus === 1 ? 'bg-green-500' : 'bg-red-500'}"></span>
- </div>
- </div>
- <div>丢包率: ${serverStatus.packetLoss}%</div>
- </div>
- <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
- <path d="M4 6H20V8H4V6Z" fill="currentColor"/>
- <path d="M4 11H20V13H4V11Z" fill="currentColor"/>
- <path d="M4 16H20V18H4V16Z" fill="currentColor"/>
- <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"/>
- </svg>
- <span class="font-medium">配置信息</span>
- </div>
- <div class="grid gap-0.5 ml-1">
- <div>CPU: ${serverStatus.cpu || '-'}</div>
- <div>内存: ${serverStatus.memory || '-'}</div>
- <div>硬盘: ${serverStatus.disk || '-'}</div>
- </div>
- </div>
- `;
- 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<HTMLDivElement>(null);
- const tooltipRef = useRef<HTMLDivElement>(null);
- const racksRef = useRef<ServerRack[]>([]);
- 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<HTMLDivElement>,
- tooltipRef: tooltipRef as React.RefObject<HTMLDivElement>
- });
- 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 (
- <div className="relative w-full h-full">
- <div ref={containerRef} className="w-full h-full" />
- <div
- ref={tooltipRef}
- className="absolute hidden z-10"
- style={{
- pointerEvents: 'none',
- position: 'absolute',
- transform: 'translate3d(0, 0, 0)'
- }}
- >
- <div className="bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] p-2 rounded">
- {/* 工具提示内容由useServerHover处理 */}
- </div>
- </div>
- </div>
- );
- }
|