鱿鱼公司业务,本文将详细解析如何使用 Vue3 和 ECharts 实现市级地图展示功能,并添加地图切换时的平滑过渡动画效果。
- 使用 ECharts 的
universalTransition
实现地图切换动画 - 通过 Canvas 动态处理图像实现区域高亮效果
- 设计响应式布局确保地图适应不同屏幕尺寸
- 实现层级导航和交互反馈提升用户体验
功能概述
本组件实现了一个具有以下特性的地图展示系统:
- 展示河南省周口市行政区划地图
- 点击地图区域可切换到区县级别视图
- 地图切换时带有平滑的过渡动画
- 自定义地图样式和交互效果
- 响应式布局适应不同屏幕尺寸
技术栈
- 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>
总结
轻点骂,代码些的不是很好