前言

在 Web 端实现 3D 地图可视化,Cesium 无疑是目前最强大的开源解决方案之一。结合 Vue 框架,我们可以快速构建出交互丰富、性能优秀的 3D 地理信息应用。本文将带你从零开始,手把手实现一个完整的 Vue + Cesium 3D 地图项目。

效果预览:

  • 加载全球 3D 地形

  • 展示 3D 建筑模型

  • 添加标记点和信息弹窗

  • 支持自由漫游和视角控制


一、环境准备

1.1 创建 Vue 项目

# 使用 Vite 创建 Vue 3 项目
npm create vite@latest vue-cesium-map -- --template vue
cd vue-cesium-map
​
# 安装依赖
npm install

1.2 安装 Cesium

# 安装 cesium 和 vite-plugin-cesium
npm install cesium
npm install vite-plugin-cesium -D

1.3 配置 Vite

修改 vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cesium from 'vite-plugin-cesium'
​
export default defineConfig({
  plugins: [
    vue(),
    cesium()
  ],
  resolve: {
    alias: {
      '@': '/src'
    }
  }
})

二、项目结构

vue-cesium-map/
├── src/
│   ├── components/
│   │   └── CesiumMap.vue      # 地图组件
│   ├── assets/
│   │   └── cesium/            # Cesium 静态资源
│   ├── App.vue
│   └── main.js
├── public/
│   └── CesiumAssets/          # 地形、模型等资源
└── vite.config.js

三、核心代码实现

3.1 创建地图组件

src/components/CesiumMap.vue

<template>
  <div class="cesium-container">
    <div ref="cesiumContainer" class="cesium-viewer"></div>
    
    <!-- 控制面板 -->
    <div class="control-panel">
      <h3>🎮 控制面板</h3>
      <div class="control-item">
        <label>视角位置:</label>
        <select v-model="currentView" @change="flyToView">
          <option value="beijing">北京</option>
          <option value="shanghai">上海</option>
          <option value="guangzhou">广州</option>
          <option value="global">全球视图</option>
        </select>
      </div>
      <div class="control-item">
        <button @click="toggleLayer('terrain')">🏔️ 地形</button>
        <button @click="toggleLayer('imagery')">🛰️ 影像</button>
        <button @click="toggleLayer('building')">🏢 建筑</button>
      </div>
    </div>
    
    <!-- 信息弹窗 -->
    <div v-if="showInfo" class="info-popup" :style="popupStyle">
      <h4>{{ popupData.title }}</h4>
      <p>{{ popupData.content }}</p>
      <button @click="showInfo = false">关闭</button>
    </div>
  </div>
</template>
​
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'
​
// 配置 Cesium Ion Token(需要去 cesium.com 注册获取)
Cesium.Ion.defaultAccessToken = 'YOUR_CESIUM_ION_TOKEN'
​
const cesiumContainer = ref(null)
const viewer = ref(null)
const currentView = ref('beijing')
const showInfo = ref(false)
const popupData = ref({ title: '', content: '' })
const popupStyle = ref({})
​
// 城市坐标
const cityViews = {
  beijing: { lon: 116.4074, lat: 39.9042, height: 1000 },
  shanghai: { lon: 121.4737, lat: 31.2304, height: 1000 },
  guangzhou: { lon: 113.2644, lat: 23.1291, height: 1000 },
  global: { lon: 104.1954, lat: 35.8617, height: 10000000 }
}
​
// 初始化地图
onMounted(() => {
  initCesium()
  addMarkers()
})
​
onUnmounted(() => {
  if (viewer.value) {
    viewer.value.destroy()
  }
})
​
function initCesium() {
  viewer.value = new Cesium.Viewer(cesiumContainer.value, {
    // 基础配置
    animation: false,           // 隐藏动画控件
    timeline: false,            // 隐藏时间轴
    fullscreenButton: false,    // 隐藏全屏按钮
    homeButton: false,          // 隐藏 Home 按钮
    navigationHelpButton: false, // 隐藏帮助按钮
    sceneModePicker: false,     // 隐藏 2D/3D 切换
    
    // 影像图层
    imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
      url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
    }),
    
    // 地形配置
    terrainProvider: Cesium.createWorldTerrain({
      requestWaterMask: true,
      requestVertexNormals: true
    }),
    
    // 其他配置
    baseLayerPicker: true,      // 显示图层选择器
    geocoder: true,             // 显示地名搜索
    infoBox: true,              // 启用信息框
    selectionIndicator: true,   // 显示选择指示器
  })
  
  // 隐藏 Cesium logo(仅用于学习,生产环境请保留)
  viewer.value.cesiumWidget.creditContainer.style.display = 'none'
  
  // 设置初始视角
  flyToView()
}
​
// 添加标记点
function addMarkers() {
  const markers = [
    { lon: 116.4074, lat: 39.9042, title: '北京', content: '中国首都,政治文化中心' },
    { lon: 121.4737, lat: 31.2304, title: '上海', content: '中国经济中心,国际大都市' },
    { lon: 113.2644, lat: 23.1291, title: '广州', content: '华南中心城市,千年商都' },
  ]
  
  markers.forEach(marker => {
    const entity = viewer.value.entities.add({
      position: Cesium.Cartesian3.fromDegrees(marker.lon, marker.lat),
      point: {
        pixelSize: 15,
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 3
      },
      label: {
        text: marker.title,
        font: '16px sans-serif',
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 2,
        pixelOffset: new Cesium.Cartesian2(0, -30)
      }
    })
    
    // 添加点击事件
    entity._markerData = marker
  })
  
  // 处理点击事件
  viewer.value.screenSpaceEventHandler.setInputAction((click) => {
    const pickedObject = viewer.value.scene.pick(click.position)
    if (Cesium.defined(pickedObject) && pickedObject.id) {
      const entity = pickedObject.id
      if (entity._markerData) {
        popupData.value = entity._markerData
        popupStyle.value = {
          left: click.position.x + 10 + 'px',
          top: click.position.y + 10 + 'px'
        }
        showInfo.value = true
      }
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}
​
// 切换视角
function flyToView() {
  const view = cityViews[currentView.value]
  viewer.value.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(
      view.lon,
      view.lat,
      view.height
    ),
    duration: 2
  })
}
​
// 切换图层
function toggleLayer(type) {
  switch(type) {
    case 'terrain':
      viewer.value.terrainProvider = viewer.value.terrainProvider ? 
        new Cesium.EllipsoidTerrainProvider() : 
        Cesium.createWorldTerrain()
      break
    case 'imagery':
      viewer.value.imageryLayers.removeAll()
      viewer.value.imageryLayers.addImageryProvider(
        new Cesium.ArcGisMapServerImageryProvider({
          url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
        })
      )
      break
    case 'building':
      // 3D 建筑需要加载 3D Tiles
      console.log('3D 建筑功能需要配置 3D Tiles 数据源')
      break
  }
}
</script>
​
<style scoped>
.cesium-container {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
​
.cesium-viewer {
  width: 100%;
  height: 100%;
}
​
.control-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 20px;
  border-radius: 8px;
  z-index: 100;
  min-width: 200px;
}
​
.control-panel h3 {
  margin: 0 0 15px 0;
  font-size: 16px;
}
​
.control-item {
  margin-bottom: 15px;
}
​
.control-item select {
  width: 100%;
  padding: 8px;
  border-radius: 4px;
  border: none;
  margin-top: 5px;
}
​
.control-item button {
  margin: 5px;
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  background: #4CAF50;
  color: white;
  cursor: pointer;
  transition: background 0.3s;
}
​
.control-item button:hover {
  background: #45a049;
}
​
.info-popup {
  position: absolute;
  background: white;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  z-index: 101;
  max-width: 250px;
}
​
.info-popup h4 {
  margin: 0 0 10px 0;
  color: #333;
}
​
.info-popup p {
  margin: 0 0 10px 0;
  color: #666;
  font-size: 14px;
}
​
.info-popup button {
  padding: 5px 15px;
  background: #2196F3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

3.2 修改主应用

src/App.vue

<template>
  <CesiumMap />
</template>
​
<script setup>
import CesiumMap from './components/CesiumMap.vue'
</script>
​
<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
​
html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

四、高级功能扩展

4.1 加载 3D 建筑模型

// 在 initCesium 函数中添加
async function load3DBuildings() {
  const tileset = await Cesium.Cesium3DTileset.fromUrl(
    'https://assets.cesium.com/your-3d-tiles-url'
  )
  viewer.value.scene.primitives.add(tileset)
  
  // 自动调整视角到模型
  viewer.value.zoomTo(tileset)
}

4.2 添加动态轨迹线

function drawFlightPath() {
  const positions = [
    Cesium.Cartesian3.fromDegrees(116.4074, 39.9042, 100),
    Cesium.Cartesian3.fromDegrees(121.4737, 31.2304, 100),
    Cesium.Cartesian3.fromDegrees(113.2644, 23.1291, 100)
  ]
  
  viewer.value.entities.add({
    polyline: {
      positions: positions,
      width: 5,
      material: new Cesium.PolylineGlowMaterialProperty({
        color: Cesium.Color.CYAN,
        glowPower: 0.3
      })
    }
  })
}

4.3 实现时间轴动画

function setupTimeAnimation() {
  viewer.value.timeline.zoomTo(
    Cesium.JulianDate.fromIso8601('2026-01-01'),
    Cesium.JulianDate.fromIso8601('2026-12-31')
  )
  
  viewer.value.clock.shouldAnimate = true
  viewer.value.clock.multiplier = 3600 // 1 小时/秒
}

五、性能优化建议

5.1 按需加载资源

// 使用 LOD(Level of Detail)技术
viewer.value.scene.screenSpaceCameraController.enableTilt = true
viewer.value.scene.screenSpaceCameraController.minimumZoomDistance = 100
viewer.value.scene.screenSpaceCameraController.maximumZoomDistance = 50000000

5.2 控制渲染帧率

// 非激活状态降低帧率
viewer.value.scene.requestRenderMode = true
viewer.value.scene.maximumRenderTimeChange = Infinity

5.3 合理使用 Entity 和 Primitive

  • Entity API:适合少量动态对象,开发便捷

  • Primitive API:适合大量静态对象,性能更优


六、常见问题解决

❓ 问题 1:Cesium 加载缓慢

解决方案:

  • 使用国内 CDN 镜像

  • 开启地形缓存

  • 按需加载图层

❓ 问题 2:模型显示黑色

解决方案:

viewer.value.scene.globe.enableLighting = true
viewer.value.scene.globe.dayNightTint = new Cesium.Color(0, 0, 0, 0.5)

❓ 问题 3:移动端性能差

解决方案:

  • 降低地形精度

  • 减少同时显示的 Entity 数量

  • 使用 WebGL 性能检测

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐