简介:这是一套基于 Vue 与 ECharts 的可视化地图实战源码,面向需要在大屏或后台系统中实现地理数据展示的前端开发者,覆盖从入门到进阶的常见地图需求。资源围绕中国地图展开,支持 3D 地图渲染、省—市—县三级下钻与回钻、南海诸岛小窗化显示,并附带点位飞线图与点位名称弹窗轮播效果,同时整理了开发中容易遇到的常见问题,解压后即可直接运行调试。压缩包共 28 个文件,约 494KB,以 9 个 vue 组件、7 个 js 逻辑文件、3 个 json 配置为主,另含 png、jpg 图片素材、ttf 字体、css 样式与 html 入口,目录按 components、views、router、util 等模块划分,结构清晰便于二次开发。目前已有 5554 人学习下载,适合希望快速掌握 ECharts 地图下钻、飞线动画与 3D 效果实现思路的前端工程师参考借鉴。
1. 从一张“点不动”的地图说起:这套 Vue + ECharts 方案到底能扛什么活
去年帮一个做园区管理的朋友看大屏,需求很朴素:一张中国地图,点省份进去看城市,点城市进去看区县,再点一下能退回来,顺便把几个重点项目的点位用飞线连起来,鼠标悬停弹个名字,最好还能自动轮播。他找的外包第一版交付后,地图是画出来了,但点进去就回不来,南海诸岛那块要么缺一块要么糊成一团,飞线一动整个页面卡成幻灯片。这不是 ECharts 不行,是下钻和回钻的状态管理没做对,3D 和 2D 混用的时候坐标系没统一。
这套资源解决的就是这类“看着简单、做起来全是细节”的地图可视化需求。核心是 Vue 做组件化封装,ECharts 负责渲染,覆盖中国地图、3D 地图、省/市/县三级下钻与回钻、南海诸岛小窗化显示、点位飞线图、点位名称弹窗轮播这几个高频场景。适合谁?适合手里有大屏项目、需要快速搭出可交互地图、又不想从零啃 GeoJSON 和坐标系转换的前端。下面我按“能跑起来 → 能改参数 → 能避坑”的顺序拆一遍。
2. 环境搭起来:Vue 项目里把 ECharts 和地图数据接上
2.1 依赖选型:echarts 5 与 vue-echarts 的取舍
ECharts 5 对地图系列的支持比 4 稳定不少,尤其是geo组件和series-map的联动。Vue 这边有两种接法:直接用echarts.init手动管理实例,或者用vue-echarts封装组件。我一般推荐手动管理,原因很实际——下钻场景里地图实例需要频繁setOption和dispose,封装组件反而多一层响应式干扰,容易出现“数据变了图没变”的玄学问题。
安装依赖就三条命令,注意echarts和地图数据是分开的,地图 JSON 不打包进库:
npm install echarts@5 --save npm install vue@3 --save # 地图 GeoJSON 数据单独放 public 或 assets,不要塞进 node_modules逻辑说明:ECharts 本体只提供渲染引擎,中国及各省市的边界数据需要单独引入。参数上,echarts@5是当前下钻方案兼容性最好的大版本,4.x 在registerMap重复注册时会有内存泄漏。地图数据建议放public/map/下按china.json、province_110000.json这种命名,方便按 adcode 动态加载。
2.2 地图数据注册:registerMap 的正确调用姿势
很多人第一次做下钻,卡在“点了省份没反应”,八成是registerMap没调对。ECharts 注册地图是全局的,同名地图重复注册会覆盖,但如果你在组件销毁时没清理,切换路由后再进来就会拿到旧数据。
import * as echarts from 'echarts' import chinaJson from '@/assets/map/china.json' // 注册中国地图,'china' 是后续 series.map 引用的名字 echarts.registerMap('china', chinaJson) // 下钻时动态注册省份,adcode 用国家行政区划代码 async function registerProvinceMap(adcode) { const res = await fetch(`/map/province_${adcode}.json`) const geoJson = await res.json() // 关键:注册名用 adcode,避免和 'china' 冲突 echarts.registerMap(`province_${adcode}`, geoJson) return `province_${adcode}` }逻辑说明:registerMap第一个参数是地图名,第二个是 GeoJSON 对象。参数上,adcode用六位行政区划代码,比如北京是 110000,这样省市县三级可以统一用代码做 key,回钻时只需把地图名切回上一级。注意 fetch 路径要和你实际存放目录一致,Vue CLI 项目放public下用绝对路径,Vite 项目放public同样用/开头。
2.3 初始化一个最小可用的中国地图
先把最基础的渲染跑通,再叠加下钻和飞线。下面这段是mounted里的初始化骨架:
const chart = echarts.init(document.getElementById('map-container')) const option = { geo: { map: 'china', roam: true, // 允许缩放拖拽 zoom: 1.2, // 初始缩放,1.2 能看清南海小窗 label: { show: false }, itemStyle: { areaColor: '#0b1c3a', borderColor: '#2a5caa' } }, series: [] } chart.setOption(option)逻辑说明:geo组件负责底图,series后续挂飞线和散点。参数上roam: true开启交互,zoom控制初始视野,做南海小窗时这个值要配合layoutCenter和layoutSize调。注意容器必须有明确宽高,否则init出来是 0×0 的白板,这是新手最常见的翻车点。
3. 三级下钻与回钻:状态机比递归更靠谱
3.1 下钻的本质:换地图 + 换数据 + 记路径
下钻不是“把地图放大”,而是把当前地图替换成下一级地图,同时把 series 里的数据换成下一级的点位。很多人写成递归调用,结果回钻时找不到上一级。正确做法是维护一个levelStack数组,每次下钻 push 当前层级,回钻 pop。
const levelStack = [] // 记录路径:['china', 'province_110000', ...] async function drillDown(adcode, level) { // 记录当前层级,回钻时用 levelStack.push({ map: currentMap, adcode: currentAdcode }) const mapName = await registerProvinceMap(adcode) currentMap = mapName currentAdcode = adcode chart.setOption({ geo: { map: mapName, zoom: 1.1 }, series: buildSeries(level, adcode) // 按层级重建飞线和散点 }) } function drillUp() { if (levelStack.length === 0) return // 已经在顶层,不回 const prev = levelStack.pop() currentMap = prev.map currentAdcode = prev.adcode chart.setOption({ geo: { map: prev.map, zoom: prev.map === 'china' ? 1.2 : 1.1 }, series: buildSeries(prev.level, prev.adcode) }) }逻辑说明:levelStack是回钻的“后悔药”,每次下钻前把当前状态存进去。参数上level用来区分省、市、县,决定 series 里散点用哪套坐标。注意drillUp里要判断栈空,否则在顶层点回钻会报错。常见做法是给地图容器绑chart.on('click', params => ...),从params.name反查 adcode 再触发下钻。
3.2 回钻的触发方式:双击、按钮还是右键
回钻交互有三种常见设计:双击地图空白处、右上角放返回按钮、右键菜单。大屏项目我建议用返回按钮,因为双击在触屏上容易误触,右键在 kiosk 模式可能被禁用。按钮方案最简单:
<button class="back-btn" @click="drillUp" v-show="levelStack.length > 0"> 返回上一级 </button>逻辑说明:v-show比v-if好,因为按钮频繁切换,v-show只改 display 不销毁 DOM。参数上levelStack.length > 0控制显隐,顶层不显示。注意按钮要绝对定位在地图容器上层,z-index 给够,否则被 canvas 盖住点不到。
3.3 南海诸岛小窗化:layoutCenter 与 boundingCoords 的配合
南海诸岛默认在中国地图右下角,但 ECharts 的geo组件不会自动把它做成独立小窗。常见做法是用layoutCenter和layoutSize把主图缩小,再单独放一个geo实例渲染南海区域。更省事的方案是直接用带南海小窗的 GeoJSON,然后在 option 里调boundingCoords。
geo: { map: 'china', layoutCenter: ['50%', '50%'], layoutSize: '95%', // 南海小窗通过 GeoJSON 自带的多边形实现,这里控制主图不遮挡 boundingCoords: [ [73, 54], // 左上角经纬度 [135, 18] // 右下角经纬度 ] }逻辑说明:boundingCoords定义地图显示的经纬度范围,把南海区域包含进来但又不让它撑大主图。参数上左上角取中国最西北,右下角取最东南,这样南海自然落在右下角。注意如果你的 GeoJSON 不含南海诸岛,需要换一份完整数据,这是数据层面的问题,调参数解决不了。
4. 飞线图与弹窗轮播:让点位“活”起来
4.1 飞线图的 lines 系列:起点终点与动效参数
飞线本质是series-lines,每条线给起点和终点的经纬度。动效靠effect配置,symbol用箭头或圆点,trailLength控制拖尾长度。
{ type: 'lines', coordinateSystem: 'geo', data: [ { coords: [[116.4, 39.9], [121.47, 31.23]] }, // 北京到上海 { coords: [[116.4, 39.9], [113.26, 23.13]] } // 北京到广州 ], effect: { show: true, period: 4, // 动画周期秒数,越小越快 trailLength: 0.3, // 拖尾长度 0~1 symbol: 'arrow', symbolSize: 6 }, lineStyle: { color: '#4fc3f7', width: 1.5, curveness: 0.2 // 弯曲度,0 是直线 } }逻辑说明:coords是[起点经纬度, 终点经纬度],顺序不能反。参数上period控制飞线速度,大屏一般 3~5 秒;curveness给 0.2 左右能避免多条线重叠。注意飞线数据要跟着下钻层级变,省级下钻后起点终点应该换成省内城市坐标,否则线会飞到地图外面。
4.2 点位名称弹窗轮播:dispatchAction 的 showTip
轮播弹窗不是 CSS 动画,而是 ECharts 的dispatchAction定时触发showTip。这样弹窗位置会自动跟随点位,不用自己算坐标。
let tipIndex = 0 const points = [ { name: '项目A', value: [116.4, 39.9] }, { name: '项目B', value: [121.47, 31.23] } ] setInterval(() => { chart.dispatchAction({ type: 'showTip', seriesIndex: 0, // 对应散点系列的索引 dataIndex: tipIndex }) tipIndex = (tipIndex + 1) % points.length }, 3000)逻辑说明:showTip需要指定seriesIndex和dataIndex,所以散点系列必须排在 series 数组的固定位置。参数上 3000 是轮播间隔毫秒,和飞线period错开能避免视觉打架。注意组件销毁时要clearInterval,否则切路由后定时器还在跑,控制台会报实例已销毁的错。
4.3 3D 地图的接入:geo3D 与 map3D 的差异
ECharts 本身不直接支持 3D 地图,需要echarts-gl扩展。geo3D和map3D两个组件容易混:geo3D是三维地理坐标系,可以叠加scatter3D和lines3D;map3D是三维地图本身,适合做区域高度映射。
import 'echarts-gl' option = { geo3D: { map: 'china', boxWidth: 100, boxDepth: 80, regionHeight: 3, // 区域挤出高度 viewControl: { alpha: 40, // 俯仰角 beta: 0, // 旋转角 distance: 120 // 视距 }, itemStyle: { color: '#1a3a6b', opacity: 0.9 } } }逻辑说明:echarts-gl必须单独 import,否则geo3D不生效。参数上regionHeight控制地图厚度,viewControl.alpha调俯仰,distance调远近。注意 3D 模式下roam行为不同,下钻时setOption要重新指定geo3D.map,且 3D 和 2D 的 series 不能混用,切换时要清空 series。
5. 避坑排查:那些让我加班到凌晨的地图问题
5.1 地图空白或只显示一个方块
现象:init后容器里只有背景色,地图轮廓不出现。原因通常是 GeoJSON 没注册成功,或者series.map引用的名字和registerMap不一致。解决:在setOption前打印echarts.getMap('china'),返回 undefined 就是没注册上;检查 import 路径是否被 tree-shaking 干掉,改用require或把 JSON 放 public 用 fetch。
5.2 下钻后回钻,地图数据错乱
现象:点进省份再返回,中国地图上出现了省份的点位。原因是setOption默认合并,旧的 series 没被清掉。解决:setOption(option, true)第二个参数传true表示不合并,或者每次下钻前chart.clear()。我一般用notMerge: true,但要注意geo的 roam 状态会重置,需要手动恢复缩放级别。
5.3 飞线动画卡顿,帧率掉到个位数
现象:飞线一多,整个大屏拖动都卡。原因是effect的trailLength太大或者线数量超过 50 条,canvas 每帧重绘压力大。解决:trailLength降到 0.2 以下,线数量控制在 30 条以内,或者改用lines的polyline: false减少路径计算。如果还卡,把effect.period调大,牺牲流畅度换帧率。
5.4 南海小窗被主图盖住或错位
现象:南海诸岛显示在主图右下角但被省份色块遮挡。原因是geo的zlevel层级没设对,或者layoutCenter把主图偏移了。解决:给南海单独一个geo实例,设zlevel: 2高于主图,layoutCenter定位到右下角,layoutSize给 15% 左右。注意两个 geo 实例的map不能同名,南海用单独的 GeoJSON。
5.5 弹窗轮播和鼠标悬停冲突
现象:轮播到某个点位时鼠标正好移上去,弹窗闪烁或卡住。原因是showTip和mouseover同时触发。解决:监听mouseover时暂停轮播定时器,mouseout后重启。用一个isHovering标志位控制,简单有效。
6. 进阶技巧:把下钻做成可配置的组件
6.1 用 props 驱动层级和点位数据
把地图封装成 Vue 组件,props接收level、adcode、points、lines,组件内部只负责渲染和事件派发。这样换一个项目只需改数据,不用动渲染逻辑。
props: { level: { type: String, default: 'country' }, // country/province/city adcode: { type: String, default: '100000' }, points: { type: Array, default: () => [] }, lines: { type: Array, default: () => [] }, autoPlay: { type: Boolean, default: true } }逻辑说明:level决定加载哪一级地图数据,adcode是当前区域代码,points和lines是业务数据。参数上autoPlay控制弹窗轮播开关。注意points的value必须是[lng, lat]格式,否则散点会飞到 (0,0)。
6.2 用 watch 监听 adcode 变化触发重绘
下钻和回钻本质是adcode变化,用watch监听比手动调方法更符合 Vue 的数据驱动。
watch: { adcode: { immediate: true, async handler(newVal) { await this.loadMap(newVal) this.renderChart() } } }逻辑说明:immediate: true让组件初始化时就加载一次。参数上loadMap负责注册地图,renderChart负责setOption。注意watch里要处理异步竞态,快速连点下钻时后发的请求可能先返回,加一个requestId比对。
6.3 验证清单:上线前必过的五项检查
| 检查项 | 验证方法 | 通过标准 |
|---|---|---|
| 地图注册 | 控制台echarts.getMap('china') | 返回对象非 undefined |
| 下钻回钻 | 连点三级再连回三级 | 地图和数据同步切换,无残留 |
| 南海显示 | 缩放到最小看右下角 | 南海诸岛完整,不被遮挡 |
| 飞线动效 | 观察 10 秒 | 无卡顿,线不飞出地图 |
| 弹窗轮播 | 挂 5 分钟 | 弹窗循环,无内存增长 |
这张表我每次交付前都跑一遍,尤其是内存增长,用 Performance 面板录一段就能看出来。从那以后我每次做地图大屏,都强制走一遍这个清单,宁可多花十分钟,也不想半夜被电话叫起来改 bug。希望帮到你。
本文还有配套的精品资源,点击获取