1. 从零开始:搭建你的Vue3 + 高德地图项目

最近在做一个智慧城市的管理后台,需要把一堆枯燥的统计数据,比如人口密度、交通流量、区域经济指标,在地图上立体地“立”起来。平面地图看久了总觉得差点意思,领导也说想要那种一眼就能看出高低的“炫酷”效果。这不,我就把目光投向了高德地图的JS API,特别是它的3D能力,再配上Vue3的响应式开发,折腾了一番,效果还真不错。今天我就把自己从环境搭建到实现一个带立体高度的国家边界效果的全过程,掰开揉碎了分享给你,哪怕你之前没怎么接触过地图开发,跟着走一遍也能搞出来。

首先,咱们得把“舞台”搭好。我选择用Vue3,主要是看中了它的<script setup>语法和组合式API,写起来特别顺手,逻辑组织也清晰。如果你还没创建项目,打开终端,用Vite来初始化一个,速度飞快:

npm create vue@latest my-amap-3d-project

创建过程中,除了Vue,其他的像TypeScript、Router啥的,根据你项目需要来选就行,咱们这个演示核心是地图,先保持简单。项目创建好后,进入目录,安装两个最核心的依赖:

cd my-amap-3d-project
npm install
npm install @amap/amap-jsapi-loader --save

这里重点说一下@amap/amap-jsapi-loader。高德地图的JS API传统上是通过在index.html里直接引入<script>标签来加载的,但在Vue这种模块化工程里,那样用起来很别扭,管理异步加载和组件生命周期也麻烦。这个官方的Loader包就是为了解决这个问题的,它允许我们像引入一个普通npm模块一样,在代码中异步、按需地加载高德地图API,完美契合现代前端开发流程。

安装完依赖,别急着写代码,还有一件至关重要的事情:去高德开放平台申请密钥。这是使用所有高德API服务的通行证。打开高德开放平台官网,注册登录后,进入“控制台”,在“应用管理”里创建一个新应用,选择“Web端(JS API)”。创建成功后,你会得到一个key。为了安全起见,高德现在强制要求配置安全密钥securityJsCode,这个也在同一个应用的管理页面能找到。把这两个字符串记好,待会儿就要用。

项目基础准备好了,接下来我们创建一个专门用来装地图的Vue组件。我在src/components目录下新建了一个Amap3D.vue文件,后面的所有戏码,都将在这个文件里上演。

2. 核心初始化:安全加载与地图实例创建

环境齐备,让我们把目光移向代码。地图加载的第一步,是配置安全环境并初始化。这里有几个坑我提前帮你踩过了,一定要注意。

在你的Amap3D.vue组件的<script setup>部分,我们开始引入必要的工具:

<script setup>
import { ref, shallowRef, onMounted, onUnmounted } from 'vue'
import AMapLoader from '@amap/amap-jsapi-loader'

// 1. 安全配置必须放在加载器执行之前,且挂载到window对象
window._AMapSecurityConfig = {
  securityJsCode: '你申请的安全密钥', // 替换成你的securityJsCode
}

// 2. 使用 shallowRef 来存储地图实例
const map = shallowRef(null)
const mapZoom = ref(4.6) // 初始缩放级别
</script>

这里有两个关键点。第一,window._AMapSecurityConfig这个配置必须在调用AMapLoader.load()之前定义好,否则会报安全错误。第二,为什么用shallowRef而不是ref来存地图实例?因为高德地图的Map对象本身结构非常复杂、属性繁多。如果用ref,Vue会递归地对这个对象的所有属性进行响应式代理,这会带来不必要的性能开销,而且可能导致一些内部方法被代理后出现意外行为。shallowRef只对.value本身的变化进行响应,完美适合存储这种大型的第三方类实例。

接下来,我们编写核心的初始化函数initAMap。这个函数会完成加载API、创建地图、添加3D图层等一系列操作。

<script setup>
// ... 上述引入和变量定义

const initAMap = () => {
  AMapLoader.load({
    key: '你申请的Web端Key', // 替换成你的Key
    version: '2.0', // 特别注意版本!我们稍后解释
    plugins: [
      'AMap.DistrictSearch', // 行政区划搜索插件,用于获取国界数据
      // 注意:2.0版本插件名可能有变化,3D相关插件加载方式不同
    ],
  })
    .then((AMap) => {
      console.log('高德地图 API 加载成功')

      // 接下来所有的地图操作都基于这个 AMap 命名空间
      // 创建地图实例
      map.value = new AMap.Map('amapcontainer', {
        viewMode: '3D', // 开启3D视图模式
        zoom: mapZoom.value,
        center: [105, 38], // 初始中心点,大致在中国中部
        pitch: 45, // 地图俯仰角,给个角度才能看出3D效果
        mapStyle: 'amap://styles/dark', // 使用深色主题,数据可视化效果更突出
      })

      console.log('地图实例创建完成:', map.value)
    })
    .catch((e) => {
      console.error('高德地图 API 加载失败', e)
    })
}

// 在组件挂载时初始化地图
onMounted(() => {
  initAMap()
})

// 在组件销毁时,也销毁地图实例,避免内存泄漏
onUnmounted(() => {
  if (map.value) {
    map.value.destroy()
  }
})
</script>

模板部分非常简单,就是一个用于承载地图的DIV容器,注意其id需要和Map构造函数中的id参数对应。

<template>
  <div id="amapcontainer" class="w-full h-screen"></div>
</template>

<style scoped>
#amapcontainer {
  width: 100%;
  height: 100vh; /* 让地图占满整个视口 */
}
</style>

到这一步,如果你把组件放到App.vue里运行,应该已经能看到一个基础的、可以旋转和倾斜的3D高德地图了。但这只是个开始,我们的目标是让数据“立”起来。

3. 获取地理数据:绘制国家边界与高度面

平面地图有了,现在需要往上面“糊”数据。我们想做一个中国区域的3D凸起效果,第一步是拿到中国国界的精确坐标数据。高德地图提供了AMap.DistrictSearch行政区划搜索插件,正好可以干这个活儿。

在initAMap函数的.then回调里,创建完地图实例map.value之后,我们接着操作:

// 在 then 回调中,创建地图实例后...
const district = new AMap.DistrictSearch({
  subdistrict: 0, // 不获取下级行政区
  extensions: 'all', // 获取行政区边界坐标
  level: 'country', // 国家级别
})

// 搜索“中国”
district.search('中国', (status, result) => {
  if (status === 'complete' && result.info === 'OK') {
    const countryData = result.districtList[0]
    const boundaries = countryData.boundaries // 这是一个多边形坐标数组的数组

    if (!boundaries || boundaries.length === 0) {
      console.error('未获取到边界数据')
      return
    }

    console.log(`获取到 ${boundaries.length} 个边界多边形`)

    // 可选:先在地图上用Polyline描一下边,看看形状
    boundaries.forEach((boundaryPath) => {
      const polyline = new AMap.Polyline({
        path: boundaryPath,
        strokeColor: '#3388FF',
        strokeWeight: 2,
        strokeOpacity: 0.8,
        map: map.value,
      })
    })

    // 接下来,我们将利用 boundaries 数据创建3D立体面
    create3DHeightMap(boundaries, AMap)
  } else {
    console.error('行政区划搜索失败:', status, result)
  }
})

这段代码做了几件事:配置DistrictSearch只搜索国家级别、不获取子区域,并且要求返回边界坐标。搜索成功后,result.districtList[0].boundaries就是我们需要的国界坐标集合。它是一个数组,里面的每个元素又是一个坐标数组,代表一个独立的多边形(比如中国大陆主体、海南岛、台湾岛等)。我们先简单地用2D折线Polyline把这些边界画出来,确认数据获取正确。

重头戏来了——创建3D立体面。我定义了一个create3DHeightMap函数(需要在then回调外定义或直接写在内联函数里)。这里就遇到了版本兼容性这个大坑。

你提供的原始代码里,使用了AMap.Object3DLayer和AMap.Object3D.Wall。我必须提醒你,这套API是1.x版本的。在高德地图JS API 2.0版本中,3D渲染体系进行了重构,引入了更强大的Loca数据可视化库和新的Object3D模块,旧的Object3DLayer在2.0里已经不存在了。如果你在2.0版本下加载AMap.Object3DLayer插件,控制台会直接报错。

所以,我们必须根据版本选择不同的实现方案。为了内容的完整性,我先展示1.x版本的实现(对应原始代码),再详细讲解2.0版本更推荐的现代化做法。

3.1 方案一:使用JS API 1.x版本(兼容旧代码)

如果你为了兼容现有项目或特定需求,必须使用1.x版本,那么初始化加载器时,version应指定为'1.4.15'或类似的1.x最新版。插件列表需要包含AMap.Object3DLayer。

// 在 initAMap 函数中
AMapLoader.load({
  key: '你的Key',
  version: '1.4.15', // 指定1.x版本
  plugins: [
    'AMap.DistrictSearch',
    'AMap.Object3DLayer', // 1.x 3D图层插件
    'AMap.Object3D', // 1.x 3D对象基类
  ],
}).then((AMap) => {
  // ... 获取 boundaries 数据后

  // 创建3D图层
  const object3Dlayer = new AMap.Object3DLayer({ zIndex: 1 })
  map.value.add(object3Dlayer)

  // 创建立体墙面
  const height = -1300000 // 负值表示向下凹陷,正值表示向上凸起。这个值很大,是1.x坐标系下的特点。
  const color = 'rgba(0, 136, 255, 0.8)' // 颜色与透明度

  const wall = new AMap.Object3D.Wall({
    path: boundaries, // 注意这里传入的是整个 boundaries 数组
    height: height,
    color: color,
  })
  wall.transparent = true // 开启透明
  object3Dlayer.add(wall)

  // 添加边界描边(在3D面上方,更清晰)
  boundaries.forEach((path) => {
    new AMap.Polyline({
      path: path,
      strokeColor: '#8EECFF',
      strokeWeight: 4,
      map: map.value,
    })
  })
})

在1.x版本里,height值的单位是“地图单位”,数值非常大,需要反复调整才能达到合适的视觉凸起或凹陷效果。这种方式的优点是代码相对直接,但灵活性和性能不如2.0的新方案。

3.2 方案二:使用JS API 2.0 + Loca(推荐新项目)

对于新项目,我强烈建议直接上2.0版本,并使用Loca库。Loca是高德专为大数据量、高性能可视化设计的库,对3D的支持更原生、更强大。

首先,加载方式变了。我们需要同时加载核心的AMap和Loca库。

const initAMap = () => {
  // 2.0版本加载核心库
  AMapLoader.load({
    key: '你的Key',
    version: '2.0',
    plugins: ['AMap.DistrictSearch'], // 先只加载必要插件
  }).then((AMap) => {
    // 动态加载 Loca 库
    const locaScript = document.createElement('script')
    locaScript.src = `https://webapi.amap.com/loca?v=2.0.0&key=你的Key`
    document.head.appendChild(locaScript)

    locaScript.onload = () => {
      // Loca 库加载完毕
      console.log('Loca 库加载成功')
      // 此时全局有了 Loca 变量
      createMapWithLoca(AMap, Loca)
    }
  }).catch(console.error)
}

const createMapWithLoca = (AMap, Loca) => {
  // 创建地图实例(与之前类似)
  map.value = new AMap.Map('amapcontainer', {
    viewMode: '3D',
    zoom: 4,
    center: [105, 38],
    pitch: 60, // 俯角可以大一些,3D效果更明显
    mapStyle: 'amap://styles/dark',
  })

  // 创建 Loca 容器
  const loca = new Loca.Container({
    map: map.value,
  })

  // 获取行政区划边界(同上)
  const district = new AMap.DistrictSearch({ subdistrict: 0, extensions: 'all', level: 'country' })
  district.search('中国', (status, result) => {
    if (status !== 'complete') return
    const boundaries = result.districtList[0].boundaries

    // 使用 Loca.PolygonLayer 绘制3D面
    const layer = new Loca.PolygonLayer({
      loca: loca,
      zIndex: 10,
    })

    // 准备数据:Loca需要特定格式的GeoJSON
    const features = boundaries.map((path, index) => {
      return {
        type: 'Feature',
        properties: { height: 500000, color: '#0088ff' }, // 属性:高度和颜色
        geometry: {
          type: 'Polygon',
          coordinates: [path.map(p => [p.lng, p.lat])], // 坐标转换
        },
      }
    })

    const geoJsonData = { type: 'FeatureCollection', features: features }

    layer.setData(geoJsonData, {
      lnglat: 'coords', // 指定坐标字段
      type: 'json',
    })

    // 设置样式:使用3D挤出效果
    layer.setOptions({
      style: {
        topColor: function (index, feat) {
          return feat.properties.color + 'CC' // 顶部颜色,带透明度
        },
        sideTopColor: function (index, feat) {
          return feat.properties.color + '99' // 侧面顶部颜色
        },
        sideBottomColor: function (index, feat) {
          return feat.properties.color + '66' // 侧面底部颜色
        },
        altitude: function (index, feat) {
          return feat.properties.height // 挤出高度
        },
      },
    })

    layer.render() // 渲染图层
    loca.add(layer)
    loca.animate.start()

    // 也可以继续用AMap.Polyline描边
    boundaries.forEach(path => {
      new AMap.Polyline({
        path: path,
        strokeColor: '#8EECFF',
        strokeWeight: 2,
        map: map.value,
      })
    })
  })
}

2.0 + Loca的方案看起来代码量多一些,但优势明显:数据格式标准化(GeoJSON),样式配置更灵活(可以通过属性函数动态计算颜色、高度),性能更好,而且能与Loca的其他可视化类型(如点云、热力、航线)无缝结合。altitude属性控制挤出高度,单位更直观,调参更容易。

4. 调优与实战技巧:让可视化地图真正可用

代码能跑起来只是第一步,要让这个3D可视化地图在实际项目中好用、好看,还得下不少功夫。我把自己调试过程中积累的几个关键技巧分享给你。

首先是视觉效果的调优。 颜色和高度直接决定了地图的“颜值”和信息传达效率。不要用死板的固定值。在Loca的方案中,我们可以根据数据属性动态计算样式。假设我们的features数据里每个区域都有一个value属性代表某种指标(如GDP),我们可以这样设置高度和颜色:

layer.setOptions({
  style: {
    altitude: function (index, feat) {
      // 假设 value 范围在 0-100, 将高度映射到 100000 - 1000000 之间
      const minAlt = 100000
      const maxAlt = 1000000
      const value = feat.properties.value || 0
      return minAlt + (value / 100) * (maxAlt - minAlt)
    },
    topColor: function (index, feat) {
      const value = feat.properties.value || 0
      // 使用颜色插值,从蓝色(低值)到红色(高值)
      const r = Math.floor((value / 100) * 255)
      const b = Math.floor(255 - (value / 100) * 255)
      return `rgba(${r}, 100, ${b}, 0.8)`
    },
  },
})

其次是交互体验。 一个不能交互的地图只是个图片。我们需要添加信息窗、点击高亮等功能。在高德地图中,这通常通过给图层绑定事件来实现。对于Loca的图层:

// 为图层添加点击事件
layer.on('click', function (ev) {
  const feature = ev.feature // 点击到的要素
  const properties = feature.properties
  const geometry = feature.geometry

  // 1. 创建信息窗内容
  const content = `
    <div class="info-window">
      <h4>区域数据</h4>
      <p>指标值: <strong>${properties.value}</strong></p>
      <p>其他信息...</p>
    </div>
  `

  // 2. 计算点击区域的大致中心点(简单取第一个坐标)
  const coords = geometry.coordinates[0][0]
  const center = new AMap.LngLat(coords[0], coords[1])

  // 3. 创建并打开信息窗
  if (map.value) {
    const infoWindow = new AMap.InfoWindow({
      content: content,
      offset: new AMap.Pixel(0, -30),
    })
    infoWindow.open(map.value, center)
  }

  // 4. 高亮被点击的区域(例如临时改变其颜色)
  // 可以通过更新该要素的properties,然后重新setData或使用highlight方法实现
  console.log('点击了要素:', properties)
})

性能优化也是一个绕不开的话题。 当边界数据非常复杂(比如绘制省级甚至市级边界)时,渲染成千上万个顶点可能会卡顿。有几种策略:第一,使用Loca库,它本身针对大数据量做了优化。第二,对数据进行简化,在保证轮廓不失真的前提下,减少多边形点数。可以使用地图工具库(如Turf.js)的simplify功能。第三,按需渲染,比如初始只显示国家层级, zoom in 到一定级别再加载更详细的省级数据。

最后是版本管理和错误处理。 一定要在项目文档中明确记录所使用的JS API版本(如2.0.0)和Loca版本(如2.0.0)。在AMapLoader.load的.catch中做好错误处理,特别是网络加载失败的情况,给用户友好的提示。对于DistrictSearch可能因为网络或密钥问题失败的情况,也要有降级方案(比如使用本地缓存的简化边界数据)。

我在实际项目里把这些都走通之后,发现最大的成就感不是功能实现,而是当产品和运营同事看着屏幕上那个随着数据起伏的、流光溢彩的立体中国地图发出“哇”的一声的时候。技术最终要服务于业务表达,而3D数据可视化地图,正是将冰冷数字转化为直观洞察的一座绝佳桥梁。希望这篇长文能帮你绕过我踩过的那些坑,顺利搭起你自己的那座桥。如果遇到问题,不妨回头仔细核对一下API版本,这往往是大多数奇怪错误的根源。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐