返回笔记首页

3D机房可视化监控系统

主题配置
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>3D机房可视化监控系统</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.156.1/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.156.1/examples/js/controls/OrbitControls.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #1a1a2e;
      color: white;
      overflow: hidden;
    }
    #app {
      width: 100vw;
      height: 100vh;
      display: flex;
    }
    .sidebar {
      width: 350px;
      background: #16213e;
      border-right: 2px solid #0f3460;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .sidebar-header {
      padding: 25px;
      background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
      border-bottom: 2px solid #00ff88;
    }
    .sidebar-header h1 {
      font-size: 20px;
      color: #00ff88;
      margin-bottom: 8px;
    }
    .sidebar-header p {
      font-size: 13px;
      color: rgba(255,255,255,0.6);
    }
    .device-list {
      flex: 1;
      overflow-y: auto;
      padding: 15px;
    }
    .device-category {
      margin-bottom: 20px;
    }
    .category-title {
      font-size: 14px;
      color: #00ff88;
      padding: 10px 12px;
      background: rgba(0,255,136,0.1);
      border-left: 3px solid #00ff88;
      margin-bottom: 10px;
    }
    .device-item {
      padding: 15px;
      margin-bottom: 8px;
      background: rgba(15,52,96,0.5);
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.3s;
      border-left: 3px solid transparent;
    }
    .device-item:hover {
      background: rgba(15,52,96,0.8);
      transform: translateX(5px);
    }
    .device-item.selected {
      background: rgba(0,255,136,0.2);
      border-left-color: #00ff88;
    }
    .device-item.status-warning {
      border-left-color: #ffd93d;
    }
    .device-item.status-error {
      border-left-color: #ff6b6b;
    }
    .device-item.status-normal {
      border-left-color: #00ff88;
    }
    .device-name {
      font-size: 14px;
      font-weight: 600;
      margin-bottom: 8px;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .device-status {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      animation: pulse 2s ease-in-out infinite;
    }
    .device-status.normal { background: #00ff88; box-shadow: 0 0 8px #00ff88; }
    .device-status.warning { background: #ffd93d; box-shadow: 0 0 8px #ffd93d; }
    .device-status.error { background: #ff6b6b; box-shadow: 0 0 8px #ff6b6b; }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
    .device-info {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
      line-height: 1.6;
    }
    .main-view {
      flex: 1;
      position: relative;
      display: flex;
      flex-direction: column;
    }
    .top-bar {
      padding: 20px 30px;
      background: linear-gradient(180deg, rgba(22,33,62,0.95) 0%, rgba(22,33,62,0) 100%);
      display: flex;
      justify-content: space-between;
      align-items: center;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      z-index: 10;
    }
    .stats-bar {
      display: flex;
      gap: 30px;
    }
    .stat-item {
      text-align: center;
    }
    .stat-value {
      font-size: 24px;
      font-weight: 700;
      color: #00ff88;
      display: block;
    }
    .stat-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
      margin-top: 5px;
    }
    .control-buttons {
      display: flex;
      gap: 10px;
    }
    .btn {
      padding: 10px 20px;
      background: rgba(0,255,136,0.1);
      border: 1px solid rgba(0,255,136,0.5);
      border-radius: 6px;
      color: #00ff88;
      font-size: 13px;
      cursor: pointer;
      transition: all 0.3s;
    }
    .btn:hover {
      background: rgba(0,255,136,0.2);
      transform: translateY(-2px);
    }
    .btn.active {
      background: rgba(0,255,136,0.3);
      border-color: #00ff88;
    }
    #canvas-container {
      flex: 1;
    }
    .detail-panel {
      position: absolute;
      top: 50%;
      right: 20px;
      transform: translateY(-50%);
      width: 320px;
      background: rgba(22,33,62,0.95);
      border: 2px solid #00ff88;
      border-radius: 12px;
      padding: 20px;
      backdrop-filter: blur(10px);
      box-shadow: 0 10px 40px rgba(0,0,0,0.5);
      max-height: 70vh;
      overflow-y: auto;
    }
    .detail-panel h3 {
      font-size: 18px;
      color: #00ff88;
      margin-bottom: 20px;
      padding-bottom: 15px;
      border-bottom: 2px solid rgba(0,255,136,0.3);
    }
    .detail-row {
      display: flex;
      justify-content: space-between;
      padding: 12px 0;
      border-bottom: 1px solid rgba(255,255,255,0.1);
    }
    .detail-row:last-child {
      border-bottom: none;
    }
    .detail-label {
      color: rgba(255,255,255,0.7);
      font-size: 13px;
    }
    .detail-value {
      color: white;
      font-weight: 600;
      font-size: 14px;
    }
    .detail-value.warning {
      color: #ffd93d;
    }
    .detail-value.error {
      color: #ff6b6b;
    }
    .progress-bar {
      width: 100%;
      height: 8px;
      background: rgba(255,255,255,0.1);
      border-radius: 4px;
      overflow: hidden;
      margin-top: 8px;
    }
    .progress-fill {
      height: 100%;
      background: linear-gradient(90deg, #00ff88, #00d4ff);
      transition: width 0.3s;
      border-radius: 4px;
    }
    .progress-fill.warning {
      background: linear-gradient(90deg, #ffd93d, #ff9f43);
    }
    .progress-fill.error {
      background: linear-gradient(90deg, #ff6b6b, #ee5a6f);
    }
    ::-webkit-scrollbar {
      width: 6px;
    }
    ::-webkit-scrollbar-track {
      background: rgba(255,255,255,0.05);
    }
    ::-webkit-scrollbar-thumb {
      background: rgba(0,255,136,0.3);
      border-radius: 3px;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="sidebar">
      <div class="sidebar-header">
        <h1>机房设备列表</h1>
        <p>实时监控 {{ totalDevices }} 台设备</p>
      </div>
      <div class="device-list">
        <div v-for="category in deviceCategories" :key="category.name" class="device-category">
          <div class="category-title">{{ category.name }} ({{ category.devices.length }})</div>
          <div
            v-for="device in category.devices"
            :key="device.id"
            class="device-item"
            :class="[
              'status-' + device.status,
              { selected: selectedDevice && selectedDevice.id === device.id }
            ]"
            @click="selectDevice(device)"
          >
            <div class="device-name">
              <span class="device-status" :class="device.status"></span>
              {{ device.name }}
            </div>
            <div class="device-info">
              {{ device.location }} | 机柜{{ device.rack }}
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="main-view">
      <div class="top-bar">
        <div class="stats-bar">
          <div class="stat-item">
            <span class="stat-value">{{ onlineDevices }}</span>
            <span class="stat-label">在线设备</span>
          </div>
          <div class="stat-item">
            <span class="stat-value" style="color: #ffd93d">{{ warningDevices }}</span>
            <span class="stat-label">预警设备</span>
          </div>
          <div class="stat-item">
            <span class="stat-value" style="color: #ff6b6b">{{ errorDevices }}</span>
            <span class="stat-label">故障设备</span>
          </div>
        </div>
        <div class="control-buttons">
          <button class="btn" :class="{ active: viewMode === 'overview' }" @click="setViewMode('overview')">
            全景视图
          </button>
          <button class="btn" :class="{ active: viewMode === 'detail' }" @click="setViewMode('detail')">
            细节视图
          </button>
          <button class="btn" @click="resetView">重置视角</button>
        </div>
      </div>

      <div id="canvas-container"></div>

      <div class="detail-panel" v-if="selectedDevice">
        <h3>{{ selectedDevice.name }}</h3>
        <div class="detail-row">
          <span class="detail-label">设备状态</span>
          <span class="detail-value" :class="selectedDevice.status">
            {{ statusText[selectedDevice.status] }}
          </span>
        </div>
        <div class="detail-row">
          <span class="detail-label">设备位置</span>
          <span class="detail-value">{{ selectedDevice.location }} - 机柜{{ selectedDevice.rack }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">IP地址</span>
          <span class="detail-value">{{ selectedDevice.ip }}</span>
        </div>
        <div class="detail-row">
          <span class="detail-label">CPU使用率</span>
          <span class="detail-value" :class="getCpuClass(selectedDevice.cpu)">
            {{ selectedDevice.cpu }}%
          </span>
        </div>
        <div class="progress-bar">
          <div
            class="progress-fill"
            :class="getCpuClass(selectedDevice.cpu)"
            :style="{ width: selectedDevice.cpu + '%' }"
          ></div>
        </div>
        <div class="detail-row">
          <span class="detail-label">内存使用率</span>
          <span class="detail-value" :class="getMemoryClass(selectedDevice.memory)">
            {{ selectedDevice.memory }}%
          </span>
        </div>
        <div class="progress-bar">
          <div
            class="progress-fill"
            :class="getMemoryClass(selectedDevice.memory)"
            :style="{ width: selectedDevice.memory + '%' }"
          ></div>
        </div>
        <div class="detail-row">
          <span class="detail-label">温度</span>
          <span class="detail-value" :class="getTempClass(selectedDevice.temperature)">
            {{ selectedDevice.temperature }}°C
          </span>
        </div>
        <div class="detail-row">
          <span class="detail-label">运行时长</span>
          <span class="detail-value">{{ selectedDevice.uptime }}</span>
        </div>
      </div>
    </div>
  </div>

  <script>
    const { createApp } = Vue;
    const { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight, PointLight,
            BoxGeometry, MeshStandardMaterial, Mesh, Group, Color, Raycaster, Vector2 } = THREE;

    createApp({
      data() {
        return {
          scene: null,
          camera: null,
          renderer: null,
          controls: null,
          raycaster: null,
          mouse: null,
          animationId: null,
          rackMeshes: [],
          deviceMeshes: [],

          viewMode: 'overview',
          selectedDevice: null,

          statusText: {
            normal: '正常运行',
            warning: '预警状态',
            error: '故障离线'
          },

          deviceCategories: [
            {
              name: '服务器',
              devices: [
                { id: 1, name: 'Web-Server-01', location: 'A区', rack: 'R1', status: 'normal', ip: '192.168.1.101', cpu: 45, memory: 62, temperature: 42, uptime: '156天', position: { row: 0, col: 0, level: 0 } },
                { id: 2, name: 'Web-Server-02', location: 'A区', rack: 'R1', status: 'normal', ip: '192.168.1.102', cpu: 38, memory: 58, temperature: 41, uptime: '156天', position: { row: 0, col: 0, level: 1 } },
                { id: 3, name: 'DB-Server-01', location: 'A区', rack: 'R2', status: 'warning', ip: '192.168.1.201', cpu: 82, memory: 89, temperature: 58, uptime: '89天', position: { row: 0, col: 1, level: 0 } },
                { id: 4, name: 'DB-Server-02', location: 'A区', rack: 'R2', status: 'error', ip: '192.168.1.202', cpu: 0, memory: 0, temperature: 0, uptime: '0天', position: { row: 0, col: 1, level: 1 } }
              ]
            },
            {
              name: '网络设备',
              devices: [
                { id: 5, name: 'Core-Switch-01', location: 'B区', rack: 'R3', status: 'normal', ip: '192.168.1.1', cpu: 25, memory: 45, temperature: 38, uptime: '298天', position: { row: 1, col: 0, level: 0 } },
                { id: 6, name: 'Core-Switch-02', location: 'B区', rack: 'R3', status: 'normal', ip: '192.168.1.2', cpu: 28, memory: 42, temperature: 39, uptime: '298天', position: { row: 1, col: 0, level: 1 } },
                { id: 7, name: 'Firewall-01', location: 'B区', rack: 'R4', status: 'warning', ip: '192.168.1.254', cpu: 76, memory: 71, temperature: 52, uptime: '178天', position: { row: 1, col: 1, level: 0 } }
              ]
            },
            {
              name: '存储设备',
              devices: [
                { id: 8, name: 'Storage-Array-01', location: 'C区', rack: 'R5', status: 'normal', ip: '192.168.2.100', cpu: 35, memory: 68, temperature: 44, uptime: '234天', position: { row: 2, col: 0, level: 0 } },
                { id: 9, name: 'Backup-Server-01', location: 'C区', rack: 'R6', status: 'normal', ip: '192.168.2.200', cpu: 22, memory: 38, temperature: 36, uptime: '267天', position: { row: 2, col: 1, level: 0 } }
              ]
            }
          ],

          dataUpdateInterval: null
        };
      },

      computed: {
        totalDevices() {
          return this.deviceCategories.reduce((sum, cat) => sum + cat.devices.length, 0);
        },
        onlineDevices() {
          let count = 0;
          this.deviceCategories.forEach(cat => {
            cat.devices.forEach(dev => {
              if (dev.status !== 'error') count++;
            });
          });
          return count;
        },
        warningDevices() {
          let count = 0;
          this.deviceCategories.forEach(cat => {
            cat.devices.forEach(dev => {
              if (dev.status === 'warning') count++;
            });
          });
          return count;
        },
        errorDevices() {
          let count = 0;
          this.deviceCategories.forEach(cat => {
            cat.devices.forEach(dev => {
              if (dev.status === 'error') count++;
            });
          });
          return count;
        }
      },

      mounted() {
        this.initScene();
        this.createMachineRoom();
        this.animate();
        this.startDataUpdate();
        window.addEventListener('resize', this.onWindowResize);
        window.addEventListener('click', this.onMouseClick);
      },

      beforeUnmount() {
        window.removeEventListener('resize', this.onWindowResize);
        window.removeEventListener('click', this.onMouseClick);
        if (this.animationId) {
          cancelAnimationFrame(this.animationId);
        }
        if (this.dataUpdateInterval) {
          clearInterval(this.dataUpdateInterval);
        }
        this.dispose();
      },

      methods: {
        initScene() {
          const container = document.getElementById('canvas-container');
          const width = container.clientWidth;
          const height = container.clientHeight;

          this.scene = new Scene();
          this.scene.background = new Color(0x1a1a2e);

          this.camera = new PerspectiveCamera(60, width / height, 0.1, 1000);
          this.camera.position.set(10, 8, 10);

          this.renderer = new WebGLRenderer({ antialias: true });
          this.renderer.setSize(width, height);
          this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
          this.renderer.shadowMap.enabled = true;
          container.appendChild(this.renderer.domElement);

          this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
          this.controls.enableDamping = true;
          this.controls.dampingFactor = 0.05;
          this.controls.maxPolarAngle = Math.PI / 2.1;

          this.raycaster = new Raycaster();
          this.mouse = new Vector2();

          const ambientLight = new AmbientLight(0xffffff, 0.5);
          this.scene.add(ambientLight);

          const pointLight1 = new PointLight(0xffffff, 0.8, 100);
          pointLight1.position.set(5, 10, 5);
          pointLight1.castShadow = true;
          this.scene.add(pointLight1);

          const pointLight2 = new PointLight(0xffffff, 0.5, 100);
          pointLight2.position.set(-5, 10, -5);
          this.scene.add(pointLight2);

          // 地面
          const groundGeometry = new BoxGeometry(20, 0.2, 15);
          const groundMaterial = new MeshStandardMaterial({
            color: 0x2a2a3e,
            roughness: 0.8,
            metalness: 0.2
          });
          const ground = new Mesh(groundGeometry, groundMaterial);
          ground.receiveShadow = true;
          ground.position.y = -0.1;
          this.scene.add(ground);
        },

        createMachineRoom() {
          // 创建机柜
          for (let row = 0; row < 3; row++) {
            for (let col = 0; col < 2; col++) {
              const rack = this.createRack(row, col);
              this.rackMeshes.push(rack);
              this.scene.add(rack);
            }
          }

          // 创建设备
          this.deviceCategories.forEach(category => {
            category.devices.forEach(device => {
              const deviceMesh = this.createDevice(device);
              deviceMesh.userData = device;
              this.deviceMeshes.push(deviceMesh);
              this.scene.add(deviceMesh);
            });
          });
        },

        createRack(row, col) {
          const group = new Group();

          // 机柜框架
          const frameGeometry = new BoxGeometry(1.8, 4, 1);
          const frameMaterial = new MeshStandardMaterial({
            color: 0x3a3a4e,
            metalness: 0.6,
            roughness: 0.4
          });
          const frame = new Mesh(frameGeometry, frameMaterial);
          frame.castShadow = true;
          frame.receiveShadow = true;
          frame.position.y = 2;
          group.add(frame);

          // 机柜门
          const doorGeometry = new BoxGeometry(1.82, 3.8, 0.05);
          const doorMaterial = new MeshStandardMaterial({
            color: 0x2a2a3e,
            metalness: 0.8,
            roughness: 0.3,
            transparent: true,
            opacity: 0.9
          });
          const door = new Mesh(doorGeometry, doorMaterial);
          door.position.set(0, 2, 0.52);
          group.add(door);

          group.position.set(col * 3 - 1.5, 0, row * 4 - 4);
          return group;
        },

        createDevice(data) {
          const geometry = new BoxGeometry(1.6, 0.3, 0.8);
          const material = new MeshStandardMaterial({
            color: this.getDeviceColor(data.status),
            metalness: 0.7,
            roughness: 0.3,
            emissive: this.getDeviceColor(data.status),
            emissiveIntensity: 0.3
          });
          const mesh = new Mesh(geometry, material);
          mesh.castShadow = true;

          const { row, col, level } = data.position;
          const x = col * 3 - 1.5;
          const y = 0.5 + level * 0.5;
          const z = row * 4 - 4;

          mesh.position.set(x, y, z);
          mesh.userData.material = material;
          mesh.userData.originalColor = material.color.getHex();

          return mesh;
        },

        getDeviceColor(status) {
          const colors = {
            normal: 0x00ff88,
            warning: 0xffd93d,
            error: 0xff6b6b
          };
          return colors[status] || colors.normal;
        },

        selectDevice(device) {
          // 重置之前选中的设备
          this.deviceMeshes.forEach(mesh => {
            if (mesh.userData.material) {
              mesh.userData.material.emissiveIntensity = 0.3;
              mesh.userData.material.color.setHex(mesh.userData.originalColor);
            }
          });

          this.selectedDevice = device;

          // 高亮选中的设备
          const selectedMesh = this.deviceMeshes.find(m => m.userData.id === device.id);
          if (selectedMesh && selectedMesh.userData.material) {
            selectedMesh.userData.material.emissiveIntensity = 0.8;
            selectedMesh.userData.material.color.setHex(0x00d4ff);
          }

          // 聚焦到设备
          if (this.controls && selectedMesh) {
            this.controls.target.copy(selectedMesh.position);
            this.controls.update();
          }
        },

        setViewMode(mode) {
          this.viewMode = mode;
          if (mode === 'overview') {
            this.camera.position.set(10, 8, 10);
            this.controls.target.set(0, 2, 0);
          } else {
            this.camera.position.set(5, 4, 5);
            this.controls.target.set(0, 2, 0);
          }
          this.controls.update();
        },

        resetView() {
          this.camera.position.set(10, 8, 10);
          this.controls.target.set(0, 2, 0);
          this.controls.update();
        },

        getCpuClass(cpu) {
          if (cpu > 80) return 'error';
          if (cpu > 60) return 'warning';
          return '';
        },

        getMemoryClass(memory) {
          if (memory > 85) return 'error';
          if (memory > 70) return 'warning';
          return '';
        },

        getTempClass(temp) {
          if (temp > 55) return 'error';
          if (temp > 45) return 'warning';
          return '';
        },

        startDataUpdate() {
          this.dataUpdateInterval = setInterval(() => {
            this.deviceCategories.forEach(category => {
              category.devices.forEach(device => {
                if (device.status !== 'error') {
                  device.cpu += Math.floor((Math.random() - 0.5) * 10);
                  device.cpu = Math.max(10, Math.min(device.cpu, 95));
                  device.memory += Math.floor((Math.random() - 0.5) * 5);
                  device.memory = Math.max(20, Math.min(device.memory, 95));
                  device.temperature += Math.floor((Math.random() - 0.5) * 3);
                  device.temperature = Math.max(30, Math.min(device.temperature, 65));
                }
              });
            });
          }, 3000);
        },

        onMouseClick(event) {
          const container = document.getElementById('canvas-container');
          const rect = container.getBoundingClientRect();

          this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
          this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

          this.raycaster.setFromCamera(this.mouse, this.camera);
          const intersects = this.raycaster.intersectObjects(this.deviceMeshes);

          if (intersects.length > 0) {
            const device = intersects[0].object.userData;
            if (device && device.id) {
              this.selectDevice(device);
            }
          }
        },

        animate() {
          this.animationId = requestAnimationFrame(this.animate);

          if (this.controls) {
            this.controls.update();
          }

          // 设备脉冲动画
          const time = Date.now() * 0.001;
          this.deviceMeshes.forEach((mesh, i) => {
            if (mesh.userData.material && mesh.userData.status !== 'error') {
              mesh.userData.material.emissiveIntensity =
                0.3 + Math.sin(time * 3 + i * 0.5) * 0.1;
            }
          });

          if (this.renderer && this.scene && this.camera) {
            this.renderer.render(this.scene, this.camera);
          }
        },

        onWindowResize() {
          const container = document.getElementById('canvas-container');
          if (!container) return;

          const width = container.clientWidth;
          const height = container.clientHeight;

          if (this.camera) {
            this.camera.aspect = width / height;
            this.camera.updateProjectionMatrix();
          }

          if (this.renderer) {
            this.renderer.setSize(width, height);
          }
        },

        dispose() {
          if (this.scene) {
            this.scene.traverse((object) => {
              if (object.geometry) object.geometry.dispose();
              if (object.material) {
                if (Array.isArray(object.material)) {
                  object.material.forEach(m => m.dispose());
                } else {
                  object.material.dispose();
                }
              }
            });
          }
          if (this.renderer) this.renderer.dispose();
          if (this.controls) this.controls.dispose();
        }
      }
    }).mount('#app');
  </script>
</body>
</html>