效果图
-
3d地图+定位
-
移入弹窗
-
点击弹列表
实现步骤
1. 封装echarts容器
因为要多次调用echarts的init和setOption方法, 所以封装抽离出来方便复用
并且在init之前移除元素达到刷新效果
markRaw 标记一个对象,使其永远不会再成为响应式对象
import { markRaw } from 'vue';
import * as echarts from 'echarts';
const useEcharts = () => {
// echart 实例
const myChart = new Map();
/**
* 初始化echarts
*/
const initCharts = (el) => {
// 移除之前创建的实例并且重新创建一个Echarts实例达到刷新效果
el.removeAttribute('_echarts_instance_');
myChart.set(el.className, markRaw(echarts.init(el)))
};
/**
* 添加charts
*/
const addCharts = (el, option) => {
initCharts(el)
const chart = myChart.get(el.className)
chart.setOption(option);
};
return {
myChart,
initCharts,
addCharts,
}
}
export default useEcharts
2. 生成伪3D地图和定位图标
配置geo, 生成地图外轮廓, 通过阴影实现伪3D效果
还要在 series 配置区县的图层, 用来展示区县轮廓和移入高亮
地图数据需要调 echarts.registerMap 方法注册
定位采用scatter 散点图, 定位图片要用 'image://' + require() 的方法引入
为减少定位位置偏移太多, 加了 dots 图来添加鼠标移入事件
<!-- 地图 -->
<div :class="mainMap"></div>
onMounted(() => {
handleAddCharts()
})
/**
* 添加地图
* @param {type} 参数
*/
const handleAddCharts = () => {
var convertData = function (data) {
var res = [];
for (var i = 0; i < data.length; i++) {
res.push({
name: data[i].properties.name,
// value: data[i].properties.centroid.concat(data[i].properties.name),
value: data[i].properties.centroid,
});
}
return res;
};
const mapDate = convertData(jingzhou_county.features)
var img2 = 'image://' + require('@/assets/image/visualization/position.png');
// 注册地图
echarts.registerMap('jingzhouOutline', jingzhou_county);
echarts.registerMap('jingzhouCityOutline', jingzhou_city);
const option = {
// 荆州市轮廓阴影
geo: {
map: 'jingzhouCityOutline',
roam: false,
silent: true,
layoutCenter: ['50%', '45%'],
layoutSize: '120%',
itemStyle: {
normal: {
areaColor: '#f1f9fa',
shadowColor: '#97b8c7',
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 15,
borderColor: '#598982',
borderWidth: 1,
}
}
},
series: [
// 区县轮廓, 移入高亮
{
type: 'map',
roam: false,
layoutCenter: ['50%', '45%'],
layoutSize: '120%',
itemStyle: {
normal: {
borderColor: '#598982',
borderWidth: 1,
areaColor: '#f1f9fa',
borderType: 'dashed' // 'solid', 'dashed', 'dotted'
},
emphasis: {
areaColor: '#bdd4db',
borderColor: '#2ab8ff',
shadowColor: 'rgba(0, 255, 255, 0.7)',
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 1,
label: {
show: false,
},
},
},
map: 'jingzhouOutline',
},
// 定位图标底部圆圈
{
name: 'circle',
type: 'effectScatter',
coordinateSystem: 'geo',
rippleEffect: {
scale: 10,
brushType: 'stroke',
},
showEffectOn: 'render',
itemStyle: {
normal: {
shadowColor: '#0ff',
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 0,
color: '#5a9ea3',
},
},
label: {
normal: {
color: '#fff',
},
},
symbol: 'circle',
symbolSize: [10, 5],
data: mapDate,
zlevel: 1,
},
// 定位图标和区县名字
{
name: 'point',
type: 'scatter',
coordinateSystem: 'geo',
label: {
normal: {
show: true,
formatter: '{b}',
color: '#fff',
offset: [0, -15],
},
emphasis: {
show: true,
},
},
symbol: img2,
symbolSize: [45, 80],
symbolOffset: [0, -37],
z: 998,
data: mapDate,
},
// 用于显示目录的圆点
{
name: 'dots',
type: 'effectScatter',
coordinateSystem: 'geo',
symbolSize: 30,
symbolOffset: [0, -53],
itemStyle: {
color: 'transparent'
},
z: 999,
data: mapDate
},
],
};
addCharts(document.querySelector('.' + mainMap), option)
// 设置背景
// const backImg = ''
// myChart.value._dom.style.backgroundImage = "url('" + backImg + "')";
}
3. 移入定位图标显示目录
给地图添加鼠标移入事件, 判断图层, 通过div 定位的方法动态改弹窗位置
还要添加鼠标移出弹窗的事件
目录弹窗采用的是 circular 关系图改造
<!-- 目录饼图 -->
<div :class="mainCatalogue" v-show="showCatalogue" @mouseout="handleMouseout"></div>
/**
* 添加地图
* @param {type} 参数
*/
const handleAddCharts = () => {
......
myChart.get(mainMap).on('mouseover', (e) => {
if (e.seriesName === 'dots') {
showCatalogue.value = true
const el: any = document.querySelector('.' + mainCatalogue)
const transform = e.event.topTarget.transform
el.style.left = transform[transform.length-2] - 100 + 'px'
el.style.top = transform[transform.length-1] - 100 + 'px'
// 打开目录环
openCatalogue(el)
}
});
}
/**
* 鼠标移出目录
* @param {type} 参数
* @returns {type} 返回值
*/
const handleMouseout = (e) => {
// myChart.get(mainCatalogue).dispose()
showCatalogue.value = false
}
/**
* 打开目录环
* @param {type} 参数
* @returns {type} 返回值
*/
const openCatalogue = (el) => {
const option = {
series: [
// 饼图底色底图
{
name: 'baseImg',
type: 'pie',
itemStyle: {
normal: {
label: {
show: false
},
color: '#388A9033'
}
},
radius: ['30%', '65%'],
silent: true,
data: [
{ value: 1, name: '' }
]
},
// 关系图--6个目录球
{
'animation': true,
'animationDuration': 1000,
'animationEasing': 'cubicOut', // elasticOut
name: 'catalogue',
type: 'graph',
layout: 'circular',
width: '65%',
height: '65%',
symbolSize: 55,
label: {
show: true,
color: '#fff',
fontSize: 12,
rich: {
a: {
color: '#fff',
lineHeight: 25
},
b: {
color: '#fff',
align: 'center'
}
},
formatter: function (e) {
let name = e.name
if (e.name.length > 4) {
// 5个字
name = `{a|${name.slice(0, 3)}}\n{b|${name.slice(3)}}`
} else {
// 4个字
name = `{a|${name.slice(0, 2)}}\n{b|${name.slice(2)}}`
}
return name;
},
},
itemStyle: {
color: '#5a9fa4',
borderColor: '#adcfd2',
borderWidth: 3,
},
emphasis: {
label: {
fontSize: 14,
color: '#1D2129',
},
itemStyle: {
color: '#bfdddd',
borderColor: '#0FF8F880',
},
},
data: [
{ value: 50, name: '业务类型' },
{ value: 50, name: '受保护光缆' },
{ value: 50, name: '建设目标' },
{ value: 50, name: '纤芯利用率' },
{ value: 50, name: '光缆类型' },
{ value: 50, name: '业务分级' },
],
}
]
};
addCharts(el, option)
myChart.get(mainCatalogue).on('click', (e) => {
if (e.seriesName === 'catalogue') {
showPanel.value = true
const name = e.name
const el: any = document.querySelector('.' + mainPanel)
el.style.left = e.event.event.clientX + 30 + 'px'
el.style.top = ((e.event.event.clientY < 200) ? e.event.event.clientY : (e.event.event.clientY - 220)) + 'px'
}
});
}
4. 点击目录打开列表弹窗
列表弹窗直接用 div 加定位的方式实现
<!-- 弹窗面板 -->
<div :class="mainPanel" v-show="showPanel">
<div class="panel-close" @click="closePanel"><el-icon>
<Close />
</el-icon></div>
<div class="panel-title">{{ panelData.title }}</div>
<div class="panel-content">
<div class="content-item" v-for="v in panelData.data">
<div class="item-name">{{ v.name }}</div>
<div class="item-value">{{ v.value }}</div>
</div>
</div>
</div>
// 面板数据
const panelData = ref({
title: '沙市区 - 光缆类型统计',
data: [
{ name: 'OPGW(km)', value: '2,374' },
{ name: 'ADSS(km)', value: '2,374' },
{ name: '普通光缆(km)', value: '2,374' },
]
})
/**
* 关闭面板
* @param {type} 参数
* @returns {type} 返回值
*/
const closePanel = () => {
showPanel.value = false
}
/**
* 打开目录环
* @param {type} 参数
* @returns {type} 返回值
*/
const openCatalogue = (el) => {
......
myChart.get(mainCatalogue).on('click', (e) => {
if (e.seriesName === 'catalogue') {
showPanel.value = true
const name = e.name
const el: any = document.querySelector('.' + mainPanel)
el.style.left = e.event.event.clientX + 30 + 'px'
el.style.top = ((e.event.event.clientY < 200) ? e.event.event.clientY : (e.event.event.clientY - 220)) + 'px'
}
});
}
完整代码
<template>
<div class="visualization">
<!-- 内容 -->
<div class="visualization-main">
<!-- 地图 -->
<div :class="mainMap"></div>
<!-- 目录饼图 -->
<div :class="mainCatalogue" v-show="showCatalogue" @mouseout="handleMouseout"></div>
<!-- 弹窗面板 -->
<div :class="mainPanel" v-show="showPanel">
<div class="panel-close" @click="closePanel"><el-icon>
<Close />
</el-icon></div>
<div class="panel-title">{{ panelData.title }}</div>
<div class="panel-content">
<div class="content-item" v-for="v in panelData.data">
<div class="item-name">{{ v.name }}</div>
<div class="item-value">{{ v.value }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import useEcharts from '../gridOverview/useEcharts'
import { ref, onMounted } from 'vue';
import jingzhou_county from '../gridOverview/geojson-county.json'
import jingzhou_city from '../gridOverview/geojson-city.json'
import * as echarts from 'echarts';
const { myChart, addCharts } = useEcharts()
const mainCatalogue = 'main-catalogue'
const mainMap = 'main-map'
const mainPanel = 'main-panel'
const showPanel = ref(false) // 显示面板
const showCatalogue = ref(false) // 显示目录
// 面板数据
const panelData = ref({
title: '沙市区 - 光缆类型统计',
data: [
{ name: 'OPGW(km)', value: '2,374' },
{ name: 'ADSS(km)', value: '2,374' },
{ name: '普通光缆(km)', value: '2,374' },
]
})
onMounted(() => {
handleAddCharts()
})
/**
* 关闭面板
* @param {type} 参数
* @returns {type} 返回值
*/
const closePanel = () => {
showPanel.value = false
}
/**
* 添加环形图
* @param {type} 参数
*/
const handleAddCharts = () => {
var convertData = function (data) {
var res = [];
for (var i = 0; i < data.length; i++) {
res.push({
name: data[i].properties.name,
// value: data[i].properties.centroid.concat(data[i].properties.name),
value: data[i].properties.centroid,
});
}
return res;
};
const mapDate = convertData(jingzhou_county.features)
var img2 = 'image://' + require('@/assets/image/visualization/position.png');
// 注册地图
echarts.registerMap('jingzhouOutline', jingzhou_county);
echarts.registerMap('jingzhouCityOutline', jingzhou_city);
const option = {
// 荆州市轮廓阴影
geo: {
map: 'jingzhouCityOutline',
roam: false,
silent: true,
layoutCenter: ['50%', '45%'],
layoutSize: '120%',
itemStyle: {
normal: {
areaColor: '#f1f9fa',
shadowColor: '#97b8c7',
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 15,
borderColor: '#598982',
borderWidth: 1,
}
}
},
series: [
// 区县轮廓, 移入高亮
{
type: 'map',
roam: false,
layoutCenter: ['50%', '45%'],
layoutSize: '120%',
itemStyle: {
normal: {
borderColor: '#598982',
borderWidth: 1,
areaColor: '#f1f9fa',
borderType: 'dashed' // 'solid', 'dashed', 'dotted'
},
emphasis: {
areaColor: '#bdd4db',
borderColor: '#2ab8ff',
shadowColor: 'rgba(0, 255, 255, 0.7)',
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 1,
label: {
show: false,
},
},
},
map: 'jingzhouOutline',
},
// 定位图标底部圆圈
{
name: 'circle',
type: 'effectScatter',
coordinateSystem: 'geo',
rippleEffect: {
scale: 10,
brushType: 'stroke',
},
showEffectOn: 'render',
itemStyle: {
normal: {
shadowColor: '#0ff',
shadowBlur: 10,
shadowOffsetX: 0,
shadowOffsetY: 0,
color: '#5a9ea3',
},
},
label: {
normal: {
color: '#fff',
},
},
symbol: 'circle',
symbolSize: [10, 5],
data: mapDate,
zlevel: 1,
},
// 定位图标和区县名字
{
name: 'point',
type: 'scatter',
coordinateSystem: 'geo',
label: {
normal: {
show: true,
formatter: '{b}',
color: '#fff',
offset: [0, -15],
},
emphasis: {
show: true,
},
},
symbol: img2,
symbolSize: [45, 80],
symbolOffset: [0, -37],
z: 998,
data: mapDate,
},
// 用于显示目录的圆点
{
name: 'dots',
type: 'effectScatter',
coordinateSystem: 'geo',
symbolSize: 30,
symbolOffset: [0, -53],
itemStyle: {
color: 'transparent'
},
z: 999,
data: mapDate
},
],
};
addCharts(document.querySelector('.' + mainMap), option)
myChart.get(mainMap).on('mouseover', (e) => {
if (e.seriesName === 'dots') {
showCatalogue.value = true
const el: any = document.querySelector('.' + mainCatalogue)
const transform = e.event.topTarget.transform
el.style.left = transform[transform.length - 2] - 100 + 'px'
el.style.top = transform[transform.length - 1] - 100 + 'px'
// 打开目录环
openCatalogue(el)
}
});
// 设置背景
// const backImg = ''
// myChart.value._dom.style.backgroundImage = "url('" + backImg + "')";
}
/**
* 鼠标移除目录
* @param {type} 参数
* @returns {type} 返回值
*/
const handleMouseout = (e) => {
// myChart.get(mainCatalogue).dispose()
showCatalogue.value = false
}
/**
* 打开目录环
* @param {type} 参数
* @returns {type} 返回值
*/
const openCatalogue = (el) => {
const option = {
series: [
// 饼图底色底图
{
name: 'baseImg',
type: 'pie',
itemStyle: {
normal: {
label: {
show: false
},
color: '#388A9033'
}
},
radius: ['30%', '65%'],
silent: true,
data: [
{ value: 1, name: '' }
]
},
// 关系图--6个目录球
{
'animation': true,
'animationDuration': 1000,
'animationEasing': 'cubicOut', // elasticOut
name: 'catalogue',
type: 'graph',
layout: 'circular',
width: '65%',
height: '65%',
symbolSize: 55,
label: {
show: true,
color: '#fff',
fontSize: 12,
rich: {
a: {
color: '#fff',
lineHeight: 25
},
b: {
color: '#fff',
align: 'center'
}
},
formatter: function (e) {
let name = e.name
if (e.name.length > 4) {
// 5个字
name = `{a|${name.slice(0, 3)}}\n{b|${name.slice(3)}}`
} else {
// 4个字
name = `{a|${name.slice(0, 2)}}\n{b|${name.slice(2)}}`
}
return name;
},
},
itemStyle: {
color: '#5a9fa4',
borderColor: '#adcfd2',
borderWidth: 3,
},
emphasis: {
label: {
fontSize: 14,
color: '#1D2129',
},
itemStyle: {
color: '#bfdddd',
borderColor: '#0FF8F880',
},
},
data: [
{ value: 50, name: '业务类型' },
{ value: 50, name: '受保护光缆' },
{ value: 50, name: '建设目标' },
{ value: 50, name: '纤芯利用率' },
{ value: 50, name: '光缆类型' },
{ value: 50, name: '业务分级' },
],
}
]
};
addCharts(el, option)
myChart.get(mainCatalogue).on('click', (e) => {
if (e.seriesName === 'catalogue') {
showPanel.value = true
const name = e.name
const el: any = document.querySelector('.' + mainPanel)
el.style.left = e.event.event.clientX + 30 + 'px'
el.style.top = ((e.event.event.clientY < 200) ? e.event.event.clientY : (e.event.event.clientY - 220)) + 'px'
}
});
}
</script>
<style lang='scss' scoped>
.visualization {
.visualization-main {
margin-top: vh(-16);
position: relative;
// height: calc(100vh - 74px);
height: vh(1008);
.main-map {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.main-catalogue {
position: absolute;
top: 0;
left: -1000px;
width: 200px;
height: 200px;
}
.main-panel {
position: absolute;
left: -1000px;
top: 0;
padding: 20px;
width: 300px;
height: 122px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 4px;
background: rgba(225, 243, 244, 0.9);
box-sizing: border-box;
z-index: 9;
.panel-close {
position: absolute;
right: 15px;
top: 15px;
cursor: pointer;
}
.panel-title {
font-weight: 550;
margin-bottom: 18px;
}
.panel-content {
@include flex(4);
.content-item {
.item-name {
margin-bottom: 10px;
font-size: 12px;
color: #516883;
}
.item-value {
font-weight: 550;
font-size: 16px;
}
}
}
}
.main-left {
position: absolute;
top: 0;
left: vw(24);
}
.main-center {
position: absolute;
bottom: vh(24);
left: 50%;
transform: translateX(-50%);
}
.main-right {
position: absolute;
top: 0;
right: vw(24);
}
}
}
</style>