利用 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_陈寒3 小时前
Java Stream并行流的坑:我花了3小时才找到的线程安全问题
前端·人工智能·后端
学术 学术 Fun3 小时前
2026 科研绘图工具横评:BioRender、Figdraw、SciDraw AI、PicDoc,我替你踩完坑了
人工智能
chsmiao3 小时前
深度学习之微积分
人工智能·深度学习
未来智慧谷4 小时前
【无标题】
人工智能·python·大模型·ai幻觉
Slow菜鸟4 小时前
AI开发-微信小程序(全流程提示词)
人工智能·微信小程序
东方佑4 小时前
状态范数崩溃:WDLM-60M 外推失效的根因分析与修复
人工智能
Bruce_Liuxiaowei4 小时前
2026年6月第1周网络安全形势周报
人工智能·安全·web安全·ai·智能体
水煮白菜王4 小时前
开源 AI 桌宠 Clawd on Desk:让 Claude Code 的状态从终端‘蹦‘到桌面
javascript·人工智能·开源
mit6.8244 小时前
Agent Memory Management
数据库·人工智能