147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行

🧩 效果预览

👇 飞机从多个城市起飞并向其他城市飞行,动画流畅,地图可缩放拖拽:


📦 一、项目技术栈

技术 用途
Vue 3 现代前端框架
OpenLayers 地图底图渲染
ECharts + ol-echarts 飞机飞行动画渲染
ol-echarts 将 ECharts 图层嵌入到 OpenLayers

⚙️ 二、环境准备

1. 创建项目(如果你已有 Vue3 项目可跳过)

javascript 复制代码
npm init vue@latest vue-openlayers-echarts
cd vue-openlayers-echarts
npm install

选用 Vue 3 + TypeScriptVue 3 + JavaScript 皆可。

2. 安装必要依赖

javascript 复制代码
npm install ol echarts ol-echarts

如果你使用的是 Vite 构建工具,也可以添加 ECharts 按需引入优化:


🌍 三、准备地图 JSON 数据

我们需要一个 GeoJSON 格式的中国地图数据 作为 ECharts 的底图。你有两种方式下载:

✅ 方法一:使用阿里官方提供的地图数据

  1. 打开:https://geo.datav.aliyun.com/areas/bound/100000_full.json

  2. 将其放入你的项目 public/map/china.json

✅ 方法二:也可以使用世界地图 world.json(视觉更国际化)


🧱 四、完整代码实现(Composition API)

创建组件 OpenlayersPlane.vue,核心代码如下:

javascript 复制代码
<!--
 * @Author: 彭麒
 * @Date: 2025/7/8
 * @Email: 1062470959@qq.com
 * @Description: 此源码版权归吉檀迦俐所有,可供学习和借鉴或商用。
 -->
<template>
  <div class="container">
    <div class="w-full flex justify-center flex-wrap">
      <div class="font-bold text-[24px]">
        在 Vue3 中使用 OpenLayers 地图上Echarts模拟飞机循环飞行
      </div>
    </div>
    <div id="vue-openlayers"></div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import 'ol/ol.css'
import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import EChartsLayer from 'ol-echarts'
import * as echarts from 'echarts/core'
// 引入世界地图数据
import { registerMap } from 'echarts/core'

let map = null

onMounted(async () => {
  // 注册世界地图数据
  try {
    // 方法1:如果你已下载文件到项目中
    // import worldJson from '@/assets/geo/world.json'

    // 方法2:从公共目录获取
    const worldJson = await fetch('/map/china.json').then(res => res.json())
    //
    // // 注册世界地图数据
    registerMap('world', worldJson)
    initMap()
  } catch (error) {
    console.error('加载世界地图数据失败:', error)
  }
})

function initMap() {
  const osmLayer = new TileLayer({
    source: new OSM()
  })

  map = new Map({
    target: 'vue-openlayers',
    layers: [osmLayer],
    view: new View({
      projection: 'EPSG:4326',
      center: [116.53, 39.44],
      zoom: 7
    })
  })

  // 正确初始化 EChartsLayer
  const option = getOption()
  const echartslayer = new EChartsLayer(option, {
    hideOnMoving: false,
    hideOnZooming: false,
    forcedRerender: true, // 强制重新渲染
    coordinate: map.getView().getProjection().getCode()
  })

  echartslayer.appendTo(map)
}

function getOption() {
  const geoCoordMap = {
    '北京': [116.4551, 40.2539],
    '上海': [121.4648, 31.2891],
    '广州': [113.5107, 23.2196],
    '大连': [122.2229, 39.4409],
    '南宁': [108.479, 23.1152],
    '南昌': [116.0046, 28.6633],
    '拉萨': [91.1865, 30.1465],
    '长春': [125.8154, 44.2584],
    '包头': [110.3467, 41.4899],
    '重庆': [107.7539, 30.1904],
    '常州': [119.4543, 31.5582],
    '昆明': [102.9199, 25.4663],
    '郑州': [113.4668, 34.6234],
    '长沙': [113.0823, 28.2568],
    '丹东': [124.541, 40.4242]
  }

  const BJData = [
    [{ name: '北京' }, { name: '上海', value: 95 }],
    [{ name: '北京' }, { name: '广州', value: 90 }],
    [{ name: '北京' }, { name: '大连', value: 80 }],
    [{ name: '北京' }, { name: '南宁', value: 70 }],
    [{ name: '北京' }, { name: '南昌', value: 60 }],
    [{ name: '北京' }, { name: '拉萨', value: 50 }],
    [{ name: '北京' }, { name: '长春', value: 40 }],
    [{ name: '北京' }, { name: '包头', value: 30 }],
    [{ name: '北京' }, { name: '重庆', value: 20 }],
    [{ name: '北京' }, { name: '常州', value: 10 }]
  ]

  const SHData = [
    [{ name: '上海' }, { name: '包头', value: 95 }],
    [{ name: '上海' }, { name: '昆明', value: 90 }],
    [{ name: '上海' }, { name: '广州', value: 80 }],
    [{ name: '上海' }, { name: '郑州', value: 70 }],
    [{ name: '上海' }, { name: '长春', value: 60 }],
    [{ name: '上海' }, { name: '重庆', value: 50 }],
    [{ name: '上海' }, { name: '长沙', value: 40 }],
    [{ name: '上海' }, { name: '北京', value: 30 }],
    [{ name: '上海' }, { name: '丹东', value: 20 }],
    [{ name: '上海' }, { name: '大连', value: 10 }]
  ]

  const planePath =
    'path://M1705.06,1318.313v-89.254l-319.9-221.799l0.073-208.063c0.521-84.662-26.629-121.796-63.961-121.491c-37.332-0.305-64.482,36.829-63.961,121.491l0.073,208.063l-319.9,221.799v89.254l330.343-157.288l12.238,241.308l-134.449,92.931l0.531,42.034l175.125-42.917l175.125,42.917l0.531-42.034l-134.449-92.931l12.238-241.308L1705.06,1318.313z';

  function convertData(data) {
    const res = []
    for (let i = 0; i < data.length; i++) {
      const fromCoord = geoCoordMap[data[i][0].name]
      const toCoord = geoCoordMap[data[i][1].name]
      if (fromCoord && toCoord) {
        res.push({
          fromName: data[i][0].name,
          toName: data[i][1].name,
          coords: [fromCoord, toCoord]
        })
      }
    }
    return res
  }

  const color = ['#f00', '#0000ff']
  const series = []

  // 修复数组格式和括号对齐问题
  const dataList = [
    ['北京', BJData],
    ['上海', SHData]
  ]

  dataList.forEach((item, i) => {
    series.push(
      {
        name: item[0] + ' Top10',
        type: 'lines',
        coordinateSystem: 'geo', // 添加坐标系统
        zlevel: 1,
        effect: {
          show: true,
          period: 6,
          trailLength: 0.7,
          color: '#fff',
          symbolSize: 3
        },
        lineStyle: {
          normal: {
            color: color[i],
            width: 0,
            curveness: 0.2
          }
        },
        data: convertData(item[1])
      },
      {
        name: item[0] + ' Top10',
        type: 'lines',
        coordinateSystem: 'geo', // 添加坐标系统
        zlevel: 2,
        effect: {
          show: true,
          period: 6,
          trailLength: 0,
          symbol: planePath,
          symbolSize: 15
        },
        lineStyle: {
          normal: {
            color: color[i],
            width: 1,
            opacity: 0.4,
            curveness: 0.2
          }
        },
        data: convertData(item[1])
      },
      {
        name: item[0] + ' Top10',
        type: 'effectScatter',
        coordinateSystem: 'geo',
        zlevel: 2,
        rippleEffect: {
          brushType: 'stroke'
        },
        label: {
          normal: {
            show: true,
            position: 'right',
            formatter: '{b}'
          }
        },
        symbolSize(val) {
          return val[2] / 8
        },
        itemStyle: {
          normal: {
            color: color[i]
          }
        },
        data: item[1].map(dataItem => ({
          name: dataItem[1].name,
          value: geoCoordMap[dataItem[1].name].concat([dataItem[1].value])
        }))
      }
    )
  })

  return {
    tooltip: {
      trigger: 'item'
    },
    geo: {
      map: 'world',
      roam: true,
      silent: true,
      itemStyle: {
        normal: {
          borderColor: 'rgba(0, 0, 0, 0.2)'
        }
      }
    },
    series
  }
}
</script>

<style scoped>
.container {
  width: 840px;
  height: 570px;
  margin: 50px auto;
  border: 1px solid #42B983;
  position: relative;
}

#vue-openlayers {
  width: 800px;
  height: 450px;
  margin: 0 auto;
  border: 1px solid #42B983;
  position: relative;
}
</style>

🔍 五、关键说明

✅ 为什么用 ol-echarts

  • 官方维护,轻量集成 ECharts 图层到 OpenLayers

  • 支持地图缩放拖拽不丢失动画

  • 自动将 ECharts 转换为地图坐标系

✅ 为什么要注册地图数据?

ECharts 默认不包含地图底图,注册 worldchina 是必须的。

✅ 常见问题:

  • 地图加载失败 :检查 /map/china.json 路径是否正确

  • 飞机不动 :确认 symbol 使用了合法的 SVG path

  • 加载慢:地图数据尽量缓存到本地,避免 CDN 延迟


🔚 六、结语与拓展建议

这只是地理可视化的一个小案例,你还可以尝试:

  • 🌟 动态航线实时更新(结合 WebSocket)

  • 🧭 飞机移动+转向效果(结合 Cesium)

  • 📡 与后端数据库联动(显示实时航班位置)

  • 🎯 鼠标交互事件,如点击城市展示信息面板


📁 七、项目源码 & 参考


如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、关注我获取更多前端 + 地图可视化实战教程!

相关推荐
前端 贾公子1 小时前
pnpm 的 resolution-mode 配置 ( pnpm 的版本解析)
前端
伍哥的传说2 小时前
React 自定义Hook——页面或元素滚动到底部监听 Hook
前端·react.js·前端框架
麦兜*3 小时前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven
Jinkxs3 小时前
JavaScript性能优化实战技术
开发语言·javascript·性能优化
知了一笑4 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
UI前端开发工作室4 小时前
数字孪生技术为UI前端提供新视角:产品性能的实时模拟与预测
大数据·前端
Sapphire~4 小时前
重学前端004 --- html 表单
前端·html
TE-茶叶蛋5 小时前
Flutter、Vue 3 和 React 在 UI 布局比较
vue.js·flutter·react.js
Maybyy5 小时前
力扣242.有效的字母异位词
java·javascript·leetcode
遇到困难睡大觉哈哈5 小时前
CSS中的Element语法
前端·css