侧边栏壁纸
博主昵称
YunZheng

独处未必孤独,喜欢就是自由

Vue3 + ECharts 5 中国地图可视化大屏开发实战

2026年06月22日 27阅读 0评论 0点赞
创创云-专业云计算服务器提供商

前言

随着 AI 开发工具实现一键生成代码的能力,不少零基础新手小白都开始尝试 AI 开发。但在制作数据大屏页面时,单纯依靠 AI 生成成品不仅流程繁琐,实操效果也不尽如人意,基于这个痛点,便有了本文的分享。

在数据大屏、API管理系统、可视化后台、政务统计系统等场景中,中国地图可视化几乎是标配功能。但实际开发中,很多同学会遇到:

  • 找不到稳定完整的 GeoJson 数据源
  • ECharts 地图渲染空白、省份缺失
  • 大屏布局适配困难

这篇文章我会手把手带你实现一个完整的数据可视化大屏,效果包括:

  • 🗺️ 中国地图热力渲染 + 颜色渐变
  • 🏙️ 热门城市涟漪散点效果
  • 🖱️ 鼠标悬浮高亮 + Tooltip 提示
  • 📊 左右两侧数据面板(排名、城市、区域分布)
  • ⏰ 实时时间显示
  • 📱 窗口自适应 + 内存优化
本文带你从零搭建一个酷炫的数据可视化大屏,使用 Vue3 + ECharts 5 实现中国地图热力渲染、城市散点涟漪、交互拖拽缩放等效果,附带完整源码和踩坑指南。

最终效果

中国地图可视化大屏

整体布局为经典的三栏式数据大屏:左侧数据面板 + 中央地图 + 右侧数据面板,深色科技风格。

一、技术栈

技术说明
Vue 3组件化开发,Composition API
ECharts 5百度开源可视化库,地图渲染引擎
Vite极速前端构建工具
GeoJson阿里云 DataV 官方行政区边界数据

二、项目搭建

1. 创建 Vue3 项目

npm create vite@latest china-map -- --template vue
cd china-map
npm install

2. 安装 ECharts

npm install echarts --save

3. 下载中国地图 GeoJson

阿里云 DataV 提供了最新完整版中国地图数据,地址:

https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json

下载后保存为 public/map/china.json。使用 public 目录的好处是可以通过 fetch 异步加载,避免打包体积过大。

mkdir public/map
curl -o public/map/china.json https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json
为什么用 _full 版本? 精简版会丢失部分省份边界,完整版包含所有省级行政区坐标数据,渲染效果完整无缺失。

三、核心代码实现

3.1 项目结构

china-map/
├── public/
│   └── map/
│       └── china.json          # 中国地图 GeoJson 数据
├── src/
│   ├── components/
│   │   └── ChinaMap.vue        # 地图核心组件
│   ├── App.vue                 # 大屏主布局
│   ├── style.css               # 全局样式
│   └── main.js                 # 入口文件
├── index.html
└── package.json

3.2 地图核心组件 ChinaMap.vue

这是整个项目最核心的组件,负责 ECharts 地图的初始化、渲染和交互。

<script setup>
import { ref, onMounted, onUnmounted, shallowRef } from 'vue'
import * as echarts from 'echarts'

const mapRef = ref(null)
const myChart = shallowRef(null)
const isLoading = ref(true)

// 各省模拟数据
const mapData = [
  { name: '北京市', value: 2820 },
  { name: '上海市', value: 3150 },
  { name: '广东省', value: 4980 },
  { name: '江苏省', value: 3720 },
  { name: '浙江省', value: 3480 },
  { name: '山东省', value: 3210 },
  { name: '河南省', value: 2890 },
  { name: '四川省', value: 2740 },
  { name: '湖北省', value: 2420 },
  { name: '湖南省', value: 2180 },
  // ... 更多省份数据
]

// 热门城市散点数据(经纬度 + 数值)
const scatterData = [
  { name: '北京', value: [116.46, 39.92, 2820] },
  { name: '上海', value: [121.48, 31.22, 3150] },
  { name: '广州', value: [113.23, 23.16, 4980] },
  { name: '深圳', value: [114.07, 22.62, 3620] },
  { name: '成都', value: [104.06, 30.67, 2740] },
  { name: '杭州', value: [120.19, 30.26, 3480] },
  { name: '武汉', value: [114.31, 30.52, 2420] },
  { name: '南京', value: [118.78, 32.04, 3720] },
  { name: '重庆', value: [106.54, 29.59, 1850] },
  { name: '西安', value: [108.95, 34.27, 1960] },
]

const resizeHandler = () => {
  myChart.value?.resize()
}

onMounted(async () => {
  try {
    // 异步加载 GeoJson 数据
    const response = await fetch('/map/china.json')
    const chinaJson = await response.json()

    // 注册中国地图
    echarts.registerMap('china', chinaJson)
    myChart.value = echarts.init(mapRef.value)

    const option = {
      backgroundColor: 'transparent',
      tooltip: {
        trigger: 'item',
        backgroundColor: 'rgba(0, 0, 0, 0.75)',
        borderColor: 'rgba(59, 130, 246, 0.6)',
        borderWidth: 1,
        padding: [12, 16],
        textStyle: { color: '#e2e8f0', fontSize: 14 },
        formatter: (params) => {
          const val = params.value
            ? (Array.isArray(params.value)
              ? params.value[2].toLocaleString()
              : params.value.toLocaleString())
            : '暂无数据'
          return `<div style="font-weight:700">${params.name}</div>
                  <div>访问量:<span style="color:#60a5fa">${val}</span></div>`
        },
      },
      // 颜色渐变映射
      visualMap: {
        min: 0,
        max: 5000,
        left: 20,
        bottom: 20,
        text: ['高', '低'],
        textStyle: { color: '#94a3b8' },
        calculable: true,
        inRange: {
          color: ['#0c2d5a', '#0e4f8a', '#1a7fd4', '#3b9ff5', '#7cc4fa'],
        },
      },
      // 地理坐标系配置
      geo: {
        map: 'china',
        roam: true,           // 开启拖拽缩放
        zoom: 1.15,
        center: [104, 36],
        scaleLimit: { min: 0.8, max: 5 },
        itemStyle: {
          areaColor: '#0e2a47',
          borderColor: '#1a5291',
          borderWidth: 1,
          shadowColor: 'rgba(0, 0, 0, 0.3)',
          shadowBlur: 10,
        },
        emphasis: {
          itemStyle: {
            areaColor: '#1a7fd4',
            borderColor: '#60a5fa',
            borderWidth: 2,
          },
          label: {
            show: true,
            color: '#fff',
            fontSize: 13,
            fontWeight: 600,
          },
        },
      },
      series: [
        // 地图热力层
        {
          name: '访问量',
          type: 'map',
          map: 'china',
          geoIndex: 0,
          data: mapData,
        },
        // 城市散点涟漪层
        {
          name: '热门城市',
          type: 'effectScatter',
          coordinateSystem: 'geo',
          data: scatterData,
          symbolSize: (val) => Math.max(val[2] / 400, 6),
          showEffectOn: 'render',
          rippleEffect: {
            brushType: 'stroke',
            scale: 3,
            period: 4,
          },
          label: {
            show: true,
            position: 'right',
            formatter: '{b}',
            color: '#e2e8f0',
            fontSize: 11,
          },
          itemStyle: {
            color: '#f59e0b',
            shadowColor: 'rgba(245, 158, 11, 0.5)',
            shadowBlur: 10,
          },
        },
      ],
    }

    myChart.value.setOption(option)
    window.addEventListener('resize', resizeHandler)
  } catch (error) {
    console.error('地图加载失败:', error)
  } finally {
    isLoading.value = false
  }
})

onUnmounted(() => {
  window.removeEventListener('resize', resizeHandler)
  myChart.value?.dispose()
})
</script>

<template>
  <div class="map-wrapper">
    <div v-if="isLoading" class="map-loading">
      <div class="loading-spinner"></div>
      <span>地图数据加载中...</span>
    </div>
    <div ref="mapRef" class="map-chart"></div>
  </div>
</template>
关键点解析

1. 异步加载 GeoJson

const response = await fetch('/map/china.json')
const chinaJson = await response.json()
echarts.registerMap('china', chinaJson)

使用 fetchpublic 目录加载 GeoJson,而不是 import 静态引入,这样不会把 ~580KB 的 json 打包进 JS bundle,首屏加载更快。

2. 双层 series 叠加

  • type: 'map' — 地图热力填充层,根据数值深浅渐变
  • type: 'effectScatter' — 城市涟漪散点层,在 geo 坐标系上标注重点城市

通过 geoIndex: 0 让 map series 复用 geo 坐标系,两层叠加在同一个地图上。

3. 涟漪散点效果

rippleEffect: {
  brushType: 'stroke',  // 描边涟漪(比 fill 更精致)
  scale: 3,             // 涟漪扩散倍数
  period: 4,            // 动画周期(秒)
}

effectScatter 会自动在散点位置播放涟漪动画,非常适合标注重点城市。

4. shallowRef 管理 ECharts 实例

const myChart = shallowRef(null)

使用 shallowRef 而非 ref,避免 Vue 对 ECharts 实例进行深度响应式代理,提升性能。

3.3 大屏主布局 App.vue

大屏采用经典三栏布局,左右面板 + 中央地图:

<template>
  <div class="dashboard">
    <header class="dash-header">
      <h1>全国数据可视化大屏</h1>
      <span>{{ currentTime }}</span>
    </header>

    <main class="dash-body">
      <aside class="panel-left">
        <!-- 数据概览 + 省份排名 -->
      </aside>

      <section class="panel-center">
        <ChinaMap />
      </section>

      <aside class="panel-right">
        <!-- 热门城市 + 区域分布 -->
      </aside>
    </main>

    <footer class="dash-footer">...</footer>
  </div>
</template>

布局的核心 CSS:

.dash-body {
  display: flex;
  flex: 1;
  padding: 12px;
  gap: 12px;
}

.panel-left, .panel-right {
  width: 280px;
  flex-shrink: 0;
}

.panel-center {
  flex: 1;
  min-width: 0;
}

flex: 1 让地图区域自适应填满中间空间,左右面板固定宽度。

四、样式设计要点

4.1 深色科技风格

风格完全可以自定义,目前的风格是基于市面上的大部分系统的实例,吸取的颜色。

整体采用深蓝色背景(#0a1628),配合蓝色系渐变:

.panel-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, #3b82f6, transparent);
}

每个面板卡片顶部加一条渐变发光线,增加科技感。

4.2 数据面板

排名列表用进度条可视化:

.rank-bar {
  background: linear-gradient(90deg, #3b82f6, #60a5fa);
  border-radius: 4px;
  transition: width 0.6s ease;
}

4.3 标题渐变文字

.header-title {
  background: linear-gradient(90deg, #60a5fa, #34d399);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

五、踩坑记录

坑 1:地图渲染空白

原因:GeoJson 使用了精简版,缺少部分省份坐标。

解决:使用 _full 后缀的完整版数据:

https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json

坑 2:ECharts 实例被 Vue 深度代理

原因ref() 会对对象深度代理,ECharts 实例非常复杂,代理后性能差且可能报错。

解决:使用 shallowRef()

const myChart = shallowRef(null)

坑 3:窗口缩放地图变形

原因:容器大小变化后 ECharts 没有重绘。

解决:监听 resize 事件:

window.addEventListener('resize', () => myChart.resize())

坑 4:组件切换内存泄漏

原因:ECharts 实例和事件监听器未清理。

解决:在 onUnmounted 中清理:

onUnmounted(() => {
  window.removeEventListener('resize', resizeHandler)
  myChart.value?.dispose()
})

坑 5:GeoJson 打包体积过大

原因import china from './china.json' 会把 ~580KB 的数据打进 JS bundle。

解决:放在 public 目录,用 fetch 异步加载:

const res = await fetch('/map/china.json')
const data = await res.json()

六、进阶拓展

基于当前实现,可以继续拓展:

功能实现思路
省份下钻点击省份加载对应 adcode_full.json,重新 registerMap
飞线动画使用 type: 'lines' + effect 实现城市间飞线
自动轮播dispatchAction 循环高亮各省份
暗黑/浅色切换切换 option 中的配色方案
实时数据对接 WebSocket,setOption 动态更新数据

七、完整源码

项目已开源,完整源码可直接运行:

git clone <your-repo-url>
cd china-map
npm install
npm run dev

访问 http://localhost:5173 即可看到效果。

总结

本文实现了一个完整的 Vue3 + ECharts 5 中国地图可视化大屏,关键要点回顾:

  1. GeoJson 数据:使用阿里云 DataV 官方 _full 完整版,避免边界缺失
  2. 异步加载:用 fetch 加载 GeoJson,避免打包体积过大
  3. 双层 series:map 热力层 + effectScatter 涟漪层叠加
  4. 性能优化shallowRef 管理 ECharts 实例、组件卸载时清理资源
  5. 大屏布局:Flex 三栏布局,地图区域自适应

希望这篇教程对你有帮助,如果觉得不错,欢迎点赞分享!

0
打赏

—— 评论区 ——

博主关闭了所有页面的评论
人生倒计时
舔狗日记