利用 Cesium 实现设备资产的三维模拟与可视化查看

在水利工程 AI 运行管理平台中,利用 Cesium 实现设备资产的三维模拟与可视化查看,是构建"数字孪生"大屏的核心环节。

下面我将为你提供一套完整的 Vue 3 + Cesium 实现方案,涵盖:场景初始化、设备资产(点位/3D模型)渲染、状态动态模拟(正常/告警)、以及点击交互查看详情。


一、 环境准备

在 Vue 3 项目中安装 Cesium 及其 Vite 插件(如果你用的是 Webpack,请使用 copy-webpack-plugin)。

bash 复制代码
npm install cesium vite-plugin-cesium

修改 vite.config.ts 以正确加载 Cesium 的静态资源(Workers, Assets, Widgets):

typescript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cesium from 'vite-plugin-cesium'

export default defineConfig({
  plugins: [vue(), cesium()],
})

二、 核心代码实现

我们将创建一个完整的组件 WaterProjectViewer.vue,它包含地图渲染、设备加载和交互逻辑。

WaterProjectViewer.vue
vue 复制代码
<template>
  <div class="cesium-container">
    <!-- Cesium 地图容器 -->
    <div ref="cesiumContainer" class="map-viewer"></div>

    <!-- 设备详情信息面板 (点击设备后显示) -->
    <transition name="slide-fade">
      <div v-if="selectedDevice" class="info-panel">
        <div class="panel-header">
          <h3>{{ selectedDevice.name }}</h3>
          <button @click="closePanel" class="close-btn">×</button>
        </div>
        <div class="panel-body">
          <div class="info-item">
            <span class="label">设备类型:</span>
            <span class="value">{{ getDeviceTypeName(selectedDevice.type) }}</span>
          </div>
          <div class="info-item">
            <span class="label">运行状态:</span>
            <span :class="['status-badge', selectedDevice.status === 'normal' ? 'status-normal' : 'status-alert']">
              {{ selectedDevice.status === 'normal' ? '正常运行' : '异常告警' }}
            </span>
          </div>
          <div class="info-item">
            <span class="label">实时数据:</span>
            <span class="value highlight">{{ selectedDevice.realtimeData }}</span>
          </div>
          <div class="info-item">
            <span class="label">经纬度:</span>
            <span class="value">{{ selectedDevice.longitude }}, {{ selectedDevice.latitude }}</span>
          </div>
          
          <!-- 模拟 AI 预测数据展示 -->
          <div class="ai-section" v-if="selectedDevice.aiPrediction">
            <h4>🤖 AI 智能分析</h4>
            <p>{{ selectedDevice.aiPrediction }}</p>
          </div>

          <button class="action-btn" @click="viewCamera(selectedDevice)">查看实时监控</button>
        </div>
      </div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'

// 1. 初始化 Cesium Token (请替换为你自己的 Cesium Ion Token)
Cesium.Ion.defaultAccessToken = 'YOUR_CESIUM_ION_TOKEN_HERE'

const cesiumContainer = ref<HTMLElement | null>(null)
let viewer: Cesium.Viewer | null = null
let handler: Cesium.ScreenSpaceEventHandler | null = null

// 选中的设备状态
const selectedDevice = ref<any>(null)

// 模拟从 Django 后端获取的设备资产数据
// 实际项目中应通过 axios.get('/api/devices/') 获取
const mockDevices = [
  {
    id: 'dev_001',
    name: '1号水库水位计',
    type: 'water_level',
    longitude: 116.397,
    latitude: 39.908,
    height: 50, // 海拔高度或相对高度
    status: 'normal',
    realtimeData: '当前水位: 45.2m (警戒线: 48.0m)',
    aiPrediction: 'AI预测未来2小时水位平稳,无超标风险。'
  },
  {
    id: 'dev_002',
    name: '溢洪道监控摄像头',
    type: 'camera',
    longitude: 116.399,
    latitude: 39.910,
    height: 55,
    status: 'alert', // 告警状态
    realtimeData: 'AI识别: 检测到人员靠近危险区域',
    aiPrediction: '已触发三级告警,建议立即广播驱离。'
  },
  {
    id: 'dev_003',
    name: '2号泵站流量计',
    type: 'flow_meter',
    longitude: 116.395,
    latitude: 39.906,
    height: 48,
    status: 'normal',
    realtimeData: '瞬时流量: 12.5 m³/s',
    aiPrediction: '设备运行效率 98%,状态良好。'
  }
]

// 设备类型映射
const getDeviceTypeName = (type: string) => {
  const map: Record<string, string> = {
    water_level: '水位监测计',
    camera: 'AI 视频监控',
    flow_meter: '流量监测计',
    rain_gauge: '雨量计'
  }
  return map[type] || '未知设备'
}

// 2. 初始化 Cesium 场景
const initCesium = () => {
  if (!cesiumContainer.value) return

  viewer = new Cesium.Viewer(cesiumContainer.value, {
    animation: false,      // 隐藏动画控件
    timeline: false,       // 隐藏时间轴
    baseLayerPicker: false,// 隐藏底图选择器
    geocoder: false,       // 隐藏地名查找
    homeButton: false,     // 隐藏Home按钮
    sceneModePicker: false,// 隐藏2D/3D切换
    navigationHelpButton: false,
    infoBox: false,        // 禁用默认的 InfoBox,使用我们自定义的 Vue 面板
    selectionIndicator: false, // 禁用默认的选中框
    terrainProvider: Cesium.createWorldTerrain() // 加载全球地形
  })

  // 隐藏 Cesium logo (仅供开发测试,生产环境请遵守 Cesium 许可协议)
  viewer.cesiumWidget.creditContainer.style.display = 'none'

  // 飞行定位到水利工程所在区域
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.397, 39.908, 2000), // 经度, 纬度, 高度(米)
    orientation: {
      heading: Cesium.Math.toRadians(0),
      pitch: Cesium.Math.toRadians(-45), // 俯视角度
      roll: 0.0
    },
    duration: 2 // 飞行时间(秒)
  })

  // (可选) 如果你有真实的 3D Tiles 模型 (如倾斜摄影、BIM模型),取消下方注释加载:
  // const tileset = await Cesium.Cesium3DTileset.fromUrl('YOUR_3D_TILES_URL')
  // viewer.scene.primitives.add(tileset)

  loadDeviceAssets()
  setupInteraction()
}

// 3. 加载设备资产 (使用 Entity API)
const loadDeviceAssets = () => {
  if (!viewer) return

  mockDevices.forEach(device => {
    // 根据设备状态决定颜色
    const color = device.status === 'normal' ? Cesium.Color.LIME : Cesium.Color.RED
    const iconUrl = device.type === 'camera' 
      ? 'https://cdn-icons-png.flaticon.com/512/2907/2907111.png' // 摄像头图标示例
      : 'https://cdn-icons-png.flaticon.com/512/2917/2917995.png' // 传感器图标示例

    const entity = viewer.entities.add({
      id: device.id,
      name: device.name,
      position: Cesium.Cartesian3.fromDegrees(device.longitude, device.latitude, device.height),
      
      // 使用 Billboard (广告牌/图标) 表示设备
      billboard: {
        image: iconUrl,
        width: 40,
        height: 40,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐到坐标点
        disableDepthTestDistance: Number.POSITIVE_INFINITY, // 始终显示在最上层,不被地形遮挡
      },
      
      // 添加标签
      label: {
        text: device.name,
        font: '14px sans-serif',
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 2,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        verticalOrigin: Cesium.VerticalOrigin.TOP,
        pixelOffset: new Cesium.Cartesian2(0, 10)
      },

      // 如果是告警状态,添加一个动态脉冲光圈效果 (模拟)
      ...(device.status === 'alert' ? {
        ellipse: {
          semiMinorAxis: 30.0,
          semiMajorAxis: 30.0,
          height: device.height,
          material: new Cesium.ColorMaterialProperty(color.withAlpha(0.3)),
          outline: true,
          outlineColor: color,
          outlineWidth: 2
        }
      } : {})
    })
  })
}

// 4. 设置交互事件 (点击设备)
const setupInteraction = () => {
  if (!viewer) return

  handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  
  handler.setInputAction((click: any) => {
    // 拾取点击位置的实体
    const pickedObject = viewer.scene.pick(click.position)
    
    if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
      const entityId = pickedObject.id.id
      // 从数据源中找到对应的设备信息
      const device = mockDevices.find(d => d.id === entityId)
      
      if (device) {
        selectedDevice.value = device
        
        // 相机平滑飞向选中的设备
        viewer.camera.flyTo({
          destination: Cesium.Cartesian3.fromDegrees(device.longitude, device.latitude, 500),
          orientation: {
            heading: Cesium.Math.toRadians(0),
            pitch: Cesium.Math.toRadians(-30),
            roll: 0.0
          },
          duration: 1.5
        })
      }
    } else {
      // 点击空白处关闭面板
      closePanel()
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

const closePanel = () => {
  selectedDevice.value = null
  if (viewer) {
    // 取消选中状态
    viewer.selectedEntity = undefined
  }
}

const viewCamera = (device: any) => {
  alert(`正在调取设备 [${device.name}] 的实时 RTSP 视频流...\n(此处可对接 Vue 视频播放组件如 flv.js 或 WebRTC)`);
}

onMounted(() => {
  initCesium()
})

onUnmounted(() => {
  if (handler) {
    handler.destroy()
  }
  if (viewer) {
    viewer.destroy()
  }
})
</script>

<style scoped>
.cesium-container {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
}

.map-viewer {
  width: 100%;
  height: 100%;
}

/* 自定义信息面板样式 */
.info-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 320px;
  background: rgba(15, 23, 42, 0.85); /* 深色半透明背景,适合大屏 */
  backdrop-filter: blur(10px);
  border: 1px solid rgba(56, 189, 248, 0.3);
  border-radius: 8px;
  color: #fff;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  z-index: 100;
}

.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.panel-header h3 {
  margin: 0;
  font-size: 16px;
  color: #38bdf8;
}

.close-btn {
  background: none;
  border: none;
  color: #fff;
  font-size: 24px;
  cursor: pointer;
  line-height: 1;
}

.panel-body {
  padding: 15px;
}

.info-item {
  display: flex;
  margin-bottom: 12px;
  font-size: 14px;
}

.info-item .label {
  color: #94a3b8;
  width: 80px;
  flex-shrink: 0;
}

.info-item .value {
  color: #e2e8f0;
  flex: 1;
}

.info-item .value.highlight {
  color: #38bdf8;
  font-weight: bold;
}

.status-badge {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
}

.status-normal {
  background: rgba(34, 197, 94, 0.2);
  color: #4ade80;
  border: 1px solid #4ade80;
}

.status-alert {
  background: rgba(239, 68, 68, 0.2);
  color: #f87171;
  border: 1px solid #f87171;
  animation: pulse 1.5s infinite;
}

.ai-section {
  margin-top: 15px;
  padding: 10px;
  background: rgba(56, 189, 248, 0.1);
  border-left: 3px solid #38bdf8;
  border-radius: 4px;
}

.ai-section h4 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #38bdf8;
}

.ai-section p {
  margin: 0;
  font-size: 13px;
  color: #cbd5e1;
  line-height: 1.5;
}

.action-btn {
  width: 100%;
  margin-top: 15px;
  padding: 10px;
  background: #0ea5e9;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: bold;
  transition: background 0.3s;
}

.action-btn:hover {
  background: #0284c7;
}

/* 动画效果 */
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
  70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
  100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}

.slide-fade-enter-active, .slide-fade-leave-active {
  transition: all 0.3s ease;
}
.slide-fade-enter-from, .slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

三、 进阶优化与真实生产环境建议

  1. 替换为真实的 3D 资产 (3D Tiles)

    上面的代码使用了 2D 图标 (Billboard) 来模拟设备。在真实的数字孪生平台中,你应该:

    • 使用 Blender 或 SketchUp 建立水闸、泵站、水位计的精细 3D 模型。
    • 导出为 glTF/glb 格式,Cesium 可以直接通过 Cesium.Model.fromGltfAsync 加载单个设备模型。
    • 对于整个水利枢纽,使用无人机倾斜摄影生成 3D Tiles ,通过 Cesium.Cesium3DTileset.fromUrl 加载作为底图,设备作为 Entity 叠加在上面。
  2. 动态数据驱动 (WebSocket)

    目前的 mockDevices 是静态的。在生产环境中,应建立 WebSocket 连接:

    javascript 复制代码
    const ws = new WebSocket('ws://your-django-backend/ws/device-status/')
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      // 根据 data.device_id 找到对应的 Entity
      const entity = viewer.entities.getById(data.device_id)
      if (entity) {
        // 动态更新状态和颜色
        if (data.status === 'alert') {
          entity.billboard.color = Cesium.Color.RED
          // 动态添加光圈
          entity.ellipse = { /* ... */ }
        }
      }
    }
  3. 性能优化 (Primitive API)

    如果你的水利工程有 成百上千个 传感器设备,使用 Entity API 会导致性能下降。此时应改用底层的 Primitive APICesium.PointPrimitiveCollection 来批量渲染点位,可轻松支撑 10 万+ 点位的流畅渲染。

  4. 与 YOLO 告警联动

    当 Django 接收到前文提到的 YOLO 视频告警 Webhook 时,可以通过 WebSocket 推送给前端。前端收到后,不仅弹出系统通知,还可以让 Cesium 相机自动飞行 (flyTo) 到该告警摄像头的位置,并高亮闪烁,实现真正的"AI 驱动三维联动"。

相关推荐
IT_陈寒13 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷14 小时前
Node给AI接口做SSE代理与鉴权
人工智能
redreamSo14 小时前
大模型是不是到顶了?瓶颈到底在哪
人工智能·openai
Oo92014 小时前
Tool Use 背后的技术逻辑
人工智能
姗姗来迟了14 小时前
Vue3封装AI流式对话组件踩坑实录
人工智能
码上天下15 小时前
用Pinia管理AI多会话状态
人工智能
用户0543243297016 小时前
Next.js接大模型流式SSE实操踩坑
人工智能
Assby16 小时前
从 Function Calling 到 MCP:理解 Agent 工具调用的底层通信机制
人工智能·后端
小星AI16 小时前
Claude Code 从入门到精通,一步到位
人工智能
后端小肥肠16 小时前
Codex + Obsidian 做人生副本视频:输入主题文案,直通剪映草稿
人工智能·aigc·agent