Vue3 + ECharts 实现动态地图切换与平滑过渡动画

鱿鱼公司业务,本文将详细解析如何使用 Vue3 和 ECharts 实现市级地图展示功能,并添加地图切换时的平滑过渡动画效果。

  1. 使用 ECharts 的 universalTransition 实现地图切换动画
  2. 通过 Canvas 动态处理图像实现区域高亮效果
  3. 设计响应式布局确保地图适应不同屏幕尺寸
  4. 实现层级导航和交互反馈提升用户体验

功能概述

本组件实现了一个具有以下特性的地图展示系统:

  1. 展示河南省周口市行政区划地图
  2. 点击地图区域可切换到区县级别视图
  3. 地图切换时带有平滑的过渡动画
  4. 自定义地图样式和交互效果
  5. 响应式布局适应不同屏幕尺寸

技术栈

  • Vue3(Composition API)
  • ECharts 5
  • TypeScript
  • Canvas 图像处理

核心功能实现

1. 组件结构与依赖

html 复制代码
<template>
  <div id="mapChart" class="mt-4 w-full" ref="mapChartRef"></div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts';
import mapBgImgSrc from '@/assets/images/map-bg.png';
import geoData from '@/assets/jeo/jeoMap.json';
import { ref, onMounted, nextTick } from 'vue';

// 组件引用和数据
const mapChartRef = ref<HTMLElement | null>(null);
const selectDistrictVal = ref('zhoukou'); // 当前选中的地区
let myChart: echarts.ECharts | null = null; // ECharts实例

2. 动态创建遮罩背景图

typescript 复制代码
// 创建带遮罩的背景图像(用于高亮区域)
function createMaskedBgImg(src: string, maskColor = 'rgba(0,0,0,0.1)'): Promise<string> {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = src;
    const canvas = document.createElement('canvas');
    
    img.onload = () => {
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext('2d')!;
      
      // 绘制原始图像
      ctx.drawImage(img, 0, 0);
      
      // 添加遮罩层
      ctx.fillStyle = maskColor;
      ctx.fillRect(0, 0, img.width, img.height);
      
      resolve(canvas.toDataURL());
    };
    
    img.onerror = () => resolve(src); // 失败时返回原图
  });
}

3. 地图配置生成器

typescript 复制代码
// 生成ECharts地图配置
const getMapOption = (mapType = 'zhoukou', maskedBgImg = mapBgImgSrc) => {
  // 从GeoJSON数据中提取当前区域
  const feature = geoData.features.find(
    (f: any) => f.properties.name === mapType
  );
  
  const mapGeoData = mapType === 'zhoukou' 
    ? geoData 
    : { 
        type: 'FeatureCollection', 
        features: feature ? [feature] : [] 
      };
  
  // 注册地图数据
  echarts.registerMap('zhoukou', mapGeoData as any);
  
  return {
    // 动画配置
    animation: true,
    animationDuration: 800,
    animationEasing: 'cubicOut' as any,
    animationDurationUpdate: 800,
    animationEasingUpdate: 'cubicOut' as any,
    
    // 标题
    title: {
      top: 10,
      text: mapType === 'zhoukou' ? '河南省 - 周口市' : `周口市 - ${mapType}`,
      x: 'center',
      textStyle: {
        color: '#2564AD',
        fontWeight: 600,
        fontSize: 16
      }
    },
    
    // 地理坐标系配置
    geo: {
      map: 'zhoukou',
      aspectScale: 0.8,
      layoutCenter: ['50.6%', '51%'],
      layoutSize: '91.5%',
      roam: false,
      z: 0,
      itemStyle: {
        areaColor: '#2564AD',
        borderColor: '#2564AD',
        borderWidth: 1
      }
    },
    
    // 地图系列
    series: [{
      type: 'map',
      map: 'zhoukou',
      universalTransition: { 
        enabled: true, 
        divideShape: 'clone'  // 关键:启用形状分割动画
      },
      zoom: 1.2,
      itemStyle: {
        areaColor: { image: mapBgImgSrc, repeat: 'repeat' },
        borderColor: '#80AACC',
        borderWidth: 2
      },
      emphasis: { // 鼠标悬停样式
        itemStyle: {
          areaColor: { image: maskedBgImg, repeat: 'repeat' },
          borderColor: '#80AACC'
        },
        label: { color: '#409eff', fontWeight: 'bold' }
      },
      select: { // 选中区域样式
        itemStyle: {
          areaColor: { image: maskedBgImg, repeat: 'repeat' },
          borderColor: '#80AACC'
        },
        label: { color: '#409eff', fontWeight: 'bold' }
      },
      label: { // 区域标签
        show: true,
        color: '#80AACC',
        fontWeight: 'bold'
      }
    }]
  };
};

4. 地图初始化与交互

typescript 复制代码
// 初始化地图
const initMap = async () => {
  if (!mapChartRef.value) return;
  
  // 创建ECharts实例
  myChart = echarts.init(mapChartRef.value);
  
  // 创建遮罩背景图
  const maskedBgImg = await createMaskedBgImg(
    mapBgImgSrc, 
    'rgba(0,0,0,0.1)'
  );
  
  // 设置初始配置
  myChart.setOption(getMapOption('zhoukou', maskedBgImg), false);
  
  // 添加点击事件处理
  myChart.on('click', function (e: any) {
    if (!e.name || typeof e.name !== 'string') return;
    
    // 只有在地级市视图时才能点击进入区县
    if (selectDistrictVal.value === 'zhoukou') {
      myChart.setOption(getMapOption(e.name, maskedBgImg), false);
      selectDistrictVal.value = e.name;
    }
  });
};

// 响应式调整地图尺寸
const setMapView = async () => {
  await nextTick();
  if (mapChartRef.value?.parentNode) {
    const parentHeight = mapChartRef.value.parentNode.offsetHeight;
    const siblingHeight = mapChartRef.value.previousSibling?.offsetHeight || 0;
    mapChartRef.value.style.height = `${parentHeight - siblingHeight - 16}px`;
  }
  
  // 窗口大小变化时重绘
  window.addEventListener('resize', () => {
    myChart?.resize();
  });
};

// 组件挂载时初始化
onMounted(async () => {
  await setMapView();
  initMap();
});

// 暴露外部控制方法
defineExpose({
  getCurrentDistrict: () => selectDistrictVal.value,
  
  setDistrict: async (district: string) => {
    selectDistrictVal.value = district;
    const maskedBgImg = await createMaskedBgImg(
      mapBgImgSrc, 
      'rgba(0,0,0,0.1)'
    );
    myChart?.setOption(getMapOption(district, maskedBgImg), false);
  }
});
</script>

关键技术与优化点

1. 平滑过渡动画实现

通过以下配置实现地图切换时的平滑动画效果:

javascript 复制代码
// 关键动画配置
universalTransition: { 
  enabled: true, 
  divideShape: 'clone'  // 形状分割动画
},
animationDuration: 800,
animationEasing: 'cubicOut',
animationDurationUpdate: 800,
animationEasingUpdate: 'cubicOut'

universalTransition 是 ECharts 5 引入的强大功能,它允许在数据更新时自动生成过渡动画,特别适合地理区域的切换。

2. Canvas 动态图像处理

使用 Canvas 动态生成带遮罩的背景图,实现区域高亮效果:

typescript 复制代码
// 创建带遮罩的背景图
ctx.drawImage(img, 0, 0); // 绘制原图
ctx.fillStyle = maskColor; // 设置遮罩颜色
ctx.fillRect(0, 0, img.width, img.height); // 绘制遮罩层

3. 响应式布局处理

typescript 复制代码
// 动态计算地图容器高度
const parentHeight = mapChartRef.value.parentNode.offsetHeight;
const siblingHeight = mapChartRef.value.previousSibling?.offsetHeight || 0;
mapChartRef.value.style.height = `${parentHeight - siblingHeight - 16}px`;

// 监听窗口大小变化
window.addEventListener('resize', () => {
  myChart?.resize();
});

4. 交互体验优化

  • 层级导航:只能从市级视图进入区县视图,防止无限深入
  • 视觉反馈:悬停和选中状态有明显样式变化
  • 标题动态更新:根据当前视图级别显示不同标题
  • 性能优化 :使用 false 参数避免不必要的重绘
javascript 复制代码
myChart.setOption(getMapOption(e.name, maskedBgImg), false);

使用示例

在父组件中使用地图组件

html 复制代码
<template>
  <div class="container">
    <h1>河南省行政区划地图</h1>
    <div class="controls">
      <button @click="backToCity">返回市级视图</button>
      <select v-model="selectedDistrict" @change="changeDistrict">
        <option value="zhoukou">周口市</option>
        <option value="taikang">太康县</option>
        <option value="huaiyang">淮阳县</option>
        <!-- 其他区县选项 -->
      </select>
    </div>
    <MapComponent ref="mapRef" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import MapComponent from './MapComponent.vue';

const mapRef = ref();
const selectedDistrict = ref('zhoukou');

// 切换区县
const changeDistrict = () => {
  mapRef.value.setDistrict(selectedDistrict.value);
};

// 返回市级视图
const backToCity = () => {
  selectedDistrict.value = 'zhoukou';
  mapRef.value.setDistrict('zhoukou');
};
</script>

总结

轻点骂,代码些的不是很好

相关推荐
黑幕困兽13 分钟前
vue 项目给输入框增加trim()方法
vue.js
Goboy39 分钟前
秋招季,我用Trae制作在线简历,惊呆鹅厂面试官,当场录用
trae
龙国浪子1 小时前
从零构建桌面写作软件的书籍管理系统:Electron + Vue 3 实战指南
vue.js·electron
王者鳜錸2 小时前
VUE+SPRINGBOOT从0-1打造前后端-前后台系统-文章详情、评论、点赞
前端·vue.js·spring boot
一大树2 小时前
Vue 3 中 `ref` 的“浅监听”行为解析:是误解还是真相?
前端·vue.js
海天胜景2 小时前
vue3 el-select 加载内容后 触发事件
前端·javascript·vue.js
掘金012 小时前
Vue3+Element Plus实现动态条件字段联动校验
前端·vue.js·前端框架
蒙面人2 小时前
拖动组件 vue-draggable-next 跨组件和clone问题
vue.js
蓝爱人3 小时前
vue3接收SSE流数据进行实时渲染日志
前端·javascript·vue.js
梦鱼3 小时前
🔥 用 Vue2 + PDF.js 手撸一个「PDF 连续预览器」,自适应屏幕、支持缩放,直接拿来用!
前端·javascript·vue.js