echarts实现伪3D地图+定位+移入弹窗+点击弹列表

5,259 阅读3分钟

效果图

  • 3d地图+定位 image.png

  • 移入弹窗 image.png

  • 点击弹列表

image.png

实现步骤

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>