返回笔记首页

智慧园区数字孪生系统

主题配置
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>智慧园区数字孪生系统</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: #0a0e27;
      color: white;
      overflow: hidden;
    }
    #app {
      width: 100vw;
      height: 100vh;
      position: relative;
    }
    #canvas-container {
      width: 100%;
      height: 100%;
    }
    .overlay {
      position: absolute;
      pointer-events: none;
    }
    .header {
      top: 0;
      left: 0;
      right: 0;
      padding: 20px 30px;
      background: linear-gradient(180deg, rgba(10,14,39,0.95) 0%, rgba(10,14,39,0) 100%);
      pointer-events: auto;
    }
    .header h1 {
      font-size: 28px;
      font-weight: 700;
      color: #00d4ff;
      margin-bottom: 5px;
      text-shadow: 0 0 20px rgba(0,212,255,0.5);
    }
    .header p {
      font-size: 14px;
      color: rgba(255,255,255,0.7);
    }
    .data-panels {
      position: absolute;
      top: 100px;
      right: 20px;
      width: 320px;
      display: flex;
      flex-direction: column;
      gap: 15px;
      pointer-events: auto;
    }
    .data-card {
      background: rgba(10,30,60,0.85);
      border: 1px solid rgba(0,212,255,0.3);
      border-radius: 12px;
      padding: 20px;
      backdrop-filter: blur(10px);
      box-shadow: 0 8px 32px rgba(0,0,0,0.3);
    }
    .data-card h3 {
      font-size: 16px;
      color: #00d4ff;
      margin-bottom: 15px;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .data-card h3::before {
      content: '';
      width: 4px;
      height: 16px;
      background: linear-gradient(180deg, #00d4ff, #0066ff);
      border-radius: 2px;
    }
    .data-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px 0;
      border-bottom: 1px solid rgba(255,255,255,0.1);
    }
    .data-item:last-child {
      border-bottom: none;
    }
    .data-label {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    .data-value {
      font-size: 18px;
      font-weight: 700;
      color: #00d4ff;
    }
    .data-value.warning {
      color: #ffd700;
    }
    .data-value.danger {
      color: #ff4757;
    }
    .status-indicator {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      margin-left: 8px;
      animation: pulse 2s ease-in-out infinite;
    }
    .status-indicator.normal {
      background: #2ecc71;
      box-shadow: 0 0 10px #2ecc71;
    }
    .status-indicator.warning {
      background: #ffd700;
      box-shadow: 0 0 10px #ffd700;
    }
    .status-indicator.danger {
      background: #ff4757;
      box-shadow: 0 0 10px #ff4757;
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
    .building-list {
      max-height: 250px;
      overflow-y: auto;
    }
    .building-item {
      padding: 12px;
      margin-bottom: 8px;
      background: rgba(0,212,255,0.05);
      border-left: 3px solid #00d4ff;
      border-radius: 4px;
      cursor: pointer;
      transition: all 0.3s;
    }
    .building-item:hover {
      background: rgba(0,212,255,0.15);
      transform: translateX(5px);
    }
    .building-item.selected {
      background: rgba(0,212,255,0.2);
      border-left-color: #ffd700;
    }
    .building-name {
      font-size: 14px;
      font-weight: 600;
      margin-bottom: 5px;
    }
    .building-info {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    .controls {
      position: absolute;
      bottom: 30px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      gap: 15px;
      pointer-events: auto;
    }
    .btn {
      padding: 12px 24px;
      background: rgba(0,212,255,0.15);
      border: 1px solid rgba(0,212,255,0.5);
      border-radius: 8px;
      color: #00d4ff;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
      backdrop-filter: blur(10px);
    }
    .btn:hover {
      background: rgba(0,212,255,0.3);
      transform: translateY(-2px);
      box-shadow: 0 5px 20px rgba(0,212,255,0.3);
    }
    .btn.active {
      background: rgba(0,212,255,0.4);
      border-color: #00d4ff;
    }
    .legend {
      position: absolute;
      bottom: 30px;
      right: 20px;
      background: rgba(10,30,60,0.85);
      border: 1px solid rgba(0,212,255,0.3);
      border-radius: 12px;
      padding: 15px;
      backdrop-filter: blur(10px);
      pointer-events: auto;
    }
    .legend h4 {
      font-size: 14px;
      color: #00d4ff;
      margin-bottom: 10px;
    }
    .legend-item {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 8px;
      font-size: 12px;
    }
    .legend-color {
      width: 20px;
      height: 20px;
      border-radius: 4px;
    }
    ::-webkit-scrollbar {
      width: 6px;
    }
    ::-webkit-scrollbar-track {
      background: rgba(255,255,255,0.05);
      border-radius: 3px;
    }
    ::-webkit-scrollbar-thumb {
      background: rgba(0,212,255,0.3);
      border-radius: 3px;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: rgba(0,212,255,0.5);
    }
  </style>
</head>
<body>
  <div id="app">
    <div id="canvas-container"></div>

    <div class="overlay header">
      <h1>智慧园区数字孪生系统</h1>
      <p>实时监控园区运行状态 | 数据更新频率: 实时</p>
    </div>

    <div class="data-panels">
      <div class="data-card">
        <h3>实时数据总览</h3>
        <div class="data-item">
          <span class="data-label">在线人数</span>
          <span class="data-value">{{ realtimeData.people }}</span>
        </div>
        <div class="data-item">
          <span class="data-label">能耗 (kW)</span>
          <span class="data-value" :class="energyStatus">{{ realtimeData.energy }}</span>
        </div>
        <div class="data-item">
          <span class="data-label">设备在线率</span>
          <span class="data-value">{{ realtimeData.deviceOnline }}%</span>
        </div>
        <div class="data-item">
          <span class="data-label">告警数量</span>
          <span class="data-value danger">{{ realtimeData.alarms }}</span>
        </div>
      </div>

      <div class="data-card">
        <h3>建筑列表</h3>
        <div class="building-list">
          <div
            v-for="building in buildings"
            :key="building.id"
            class="building-item"
            :class="{ selected: selectedBuilding === building.id }"
            @click="selectBuilding(building)"
          >
            <div class="building-name">
              {{ building.name }}
              <span
                class="status-indicator"
                :class="building.status"
              ></span>
            </div>
            <div class="building-info">
              人数: {{ building.people }} | 能耗: {{ building.energy }}kW
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="controls">
      <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" :class="{ active: showHeatmap }" @click="toggleHeatmap">
        {{ showHeatmap ? '关闭' : '显示' }}热力图
      </button>
    </div>

    <div class="legend" v-if="showHeatmap">
      <h4>能耗热力图</h4>
      <div class="legend-item">
        <div class="legend-color" style="background: #2ecc71"></div>
        <span>正常 (0-60kW)</span>
      </div>
      <div class="legend-item">
        <div class="legend-color" style="background: #ffd700"></div>
        <span>偏高 (60-90kW)</span>
      </div>
      <div class="legend-item">
        <div class="legend-color" style="background: #ff4757"></div>
        <span>过高 (90kW+)</span>
      </div>
    </div>
  </div>

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

    createApp({
      data() {
        return {
          scene: null,
          camera: null,
          renderer: null,
          controls: null,
          animationId: null,
          buildingMeshes: [],

          viewMode: 'overview',
          showHeatmap: false,
          selectedBuilding: null,

          realtimeData: {
            people: 2847,
            energy: 1256,
            deviceOnline: 98.5,
            alarms: 3
          },

          buildings: [
            { id: 1, name: 'A栋办公楼', people: 856, energy: 342, status: 'normal', position: { x: -6, z: -6 } },
            { id: 2, name: 'B栋研发楼', people: 632, energy: 278, status: 'normal', position: { x: -6, z: 0 } },
            { id: 3, name: 'C栋实验楼', people: 421, energy: 456, status: 'warning', position: { x: -6, z: 6 } },
            { id: 4, name: 'D栋生产车间', people: 538, energy: 1089, status: 'danger', position: { x: 0, z: -6 } },
            { id: 5, name: 'E栋仓储楼', people: 145, energy: 123, status: 'normal', position: { x: 0, z: 0 } },
            { id: 6, name: 'F栋展示中心', people: 255, energy: 267, status: 'normal', position: { x: 0, z: 6 } }
          ],

          dataUpdateInterval: null
        };
      },

      computed: {
        energyStatus() {
          const energy = this.realtimeData.energy;
          if (energy > 1500) return 'danger';
          if (energy > 1200) return 'warning';
          return '';
        }
      },

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

      beforeUnmount() {
        window.removeEventListener('resize', this.onWindowResize);
        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(0x0a0e27);
          this.scene.fog = new Fog(0x0a0e27, 20, 50);

          this.camera = new PerspectiveCamera(60, width / height, 0.1, 1000);
          this.camera.position.set(0, 25, 25);
          this.camera.lookAt(0, 0, 0);

          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.2;
          this.controls.minDistance = 10;
          this.controls.maxDistance = 60;

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

          const dirLight = new DirectionalLight(0xffffff, 0.8);
          dirLight.position.set(10, 20, 10);
          dirLight.castShadow = true;
          dirLight.shadow.camera.left = -30;
          dirLight.shadow.camera.right = 30;
          dirLight.shadow.camera.top = 30;
          dirLight.shadow.camera.bottom = -30;
          this.scene.add(dirLight);

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

          // 添加网格线
          this.createGrid();
        },

        createGrid() {
          const gridSize = 40;
          const divisions = 20;
          const gridHelper = new THREE.GridHelper(gridSize, divisions, 0x00d4ff, 0x1a3a52);
          gridHelper.material.transparent = true;
          gridHelper.material.opacity = 0.3;
          this.scene.add(gridHelper);
        },

        createPark() {
          this.buildings.forEach(building => {
            const buildingMesh = this.createBuilding(building);
            buildingMesh.userData = building;
            this.buildingMeshes.push(buildingMesh);
            this.scene.add(buildingMesh);
          });
        },

        createBuilding(data) {
          const group = new Group();

          // 建筑高度根据能耗决定
          const height = 3 + (data.energy / 200);

          // 主体建筑
          const geometry = new BoxGeometry(4, height, 4);
          const material = new MeshStandardMaterial({
            color: this.getBuildingColor(data.status),
            roughness: 0.7,
            metalness: 0.3,
            emissive: this.getBuildingColor(data.status),
            emissiveIntensity: 0.2
          });
          const mesh = new Mesh(geometry, material);
          mesh.castShadow = true;
          mesh.receiveShadow = true;
          mesh.position.y = height / 2;
          group.add(mesh);

          // 顶部标识
          const topGeometry = new BoxGeometry(4.2, 0.2, 4.2);
          const topMaterial = new MeshStandardMaterial({
            color: 0x00d4ff,
            emissive: 0x00d4ff,
            emissiveIntensity: 0.5
          });
          const top = new Mesh(topGeometry, topMaterial);
          top.position.y = height + 0.1;
          group.add(top);

          group.position.set(data.position.x, 0, data.position.z);
          group.userData.material = material;
          group.userData.originalColor = material.color.getHex();

          return group;
        },

        getBuildingColor(status) {
          const colors = {
            normal: 0x2ecc71,
            warning: 0xffd700,
            danger: 0xff4757
          };
          return colors[status] || colors.normal;
        },

        selectBuilding(building) {
          // 重置之前选中的建筑
          this.buildingMeshes.forEach(mesh => {
            if (mesh.userData.material) {
              mesh.userData.material.emissiveIntensity = 0.2;
              mesh.userData.material.color.setHex(mesh.userData.originalColor);
            }
          });

          // 高亮选中的建筑
          this.selectedBuilding = building.id;
          const selectedMesh = this.buildingMeshes.find(m => m.userData.id === building.id);
          if (selectedMesh && selectedMesh.userData.material) {
            selectedMesh.userData.material.emissiveIntensity = 0.6;
            selectedMesh.userData.material.color.setHex(0x00d4ff);
          }

          // 相机聚焦到选中的建筑
          if (this.controls) {
            this.controls.target.set(building.position.x, 0, building.position.z);
            this.controls.update();
          }
        },

        setViewMode(mode) {
          this.viewMode = mode;
          if (mode === 'overview') {
            this.camera.position.set(0, 25, 25);
            this.controls.target.set(0, 0, 0);
          } else {
            this.camera.position.set(0, 15, 15);
            this.controls.target.set(0, 0, 0);
          }
          this.controls.update();
        },

        toggleHeatmap() {
          this.showHeatmap = !this.showHeatmap;
          this.buildingMeshes.forEach(mesh => {
            if (mesh.userData.material) {
              if (this.showHeatmap) {
                const energy = mesh.userData.energy;
                let color;
                if (energy > 900) color = 0xff4757;
                else if (energy > 600) color = 0xffd700;
                else color = 0x2ecc71;
                mesh.userData.material.color.setHex(color);
                mesh.userData.material.emissiveIntensity = 0.4;
              } else {
                mesh.userData.material.color.setHex(mesh.userData.originalColor);
                mesh.userData.material.emissiveIntensity = 0.2;
              }
            }
          });
        },

        startDataUpdate() {
          this.dataUpdateInterval = setInterval(() => {
            // 模拟实时数据更新
            this.realtimeData.people = 2800 + Math.floor(Math.random() * 100);
            this.realtimeData.energy = 1200 + Math.floor(Math.random() * 200);
            this.realtimeData.deviceOnline = 98 + Math.random() * 2;

            // 更新建筑数据
            this.buildings.forEach(building => {
              building.people += Math.floor((Math.random() - 0.5) * 20);
              building.people = Math.max(50, Math.min(building.people, 1000));
              building.energy += Math.floor((Math.random() - 0.5) * 50);
              building.energy = Math.max(50, Math.min(building.energy, 1500));
            });
          }, 3000);
        },

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

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

          // 建筑脉冲动画
          this.buildingMeshes.forEach((mesh, i) => {
            const time = Date.now() * 0.001;
            if (mesh.userData.material) {
              mesh.userData.material.emissiveIntensity =
                0.2 + Math.sin(time * 2 + 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>