最近在工作项目中遇到一个绘制地图轮廓,并且在地图上根据需求里的学校位置打点,从最初的无从下手到最后完成,中间是边看echarts([echarts官网文档][1])文档边尝试,开了一个好头后面越写越顺利,因此谨以此篇文章记录下我本次学习的过程。

首先上图看效果

然后说说地图的实现思路:

  1. 首先需要绘制出地图轮廓;
  2. 请求接口数据,然后在地图上打点;
  3. 地图上的学校点击时异步加载学校数据

现在开始说正经的:


第一步:绘制地图轮廓

在绘制地图的准备工作中,需要事先准备好需要绘制的地区的json文件(主要是区域轮廓图的一些重要拐点经纬度,地图就是依靠它一个点一个点连线绘制出来滴),大概长这样:

然后在代码导入并中注册:

import echarts from 'echarts'
var config = require('../../../../static/mock/conf')
var dingnan = require(`../../../../static/mock/${config.map}`)
echarts.registerMap('dingnan', dingnan)

前两句代码基本上可以合并成一句,其意思是说给这个地图Json起了个名字,引入的时候要对号入座(还有其他地域地图)。。。
这一步完成之后,相当于地图已经注册了,但是需要一个载体来呈现它,这时候echarts就出场了:

各种数据初始化准备:

另外我封装了一个方法makeMapOption用来装载echarts图表所需的各种参数(主要为了方便后面接口请求数据后传到这个方法中地图打点):

当程序解析到

 mounted() {
        this.mapChart = this.$refs.map.chart
        this.mapOption = makeMapOption()
    },

的时候,地图轮廓已经加载出来了:


第二步:地图打点

这里就需要异步请求接口了,根据后台返回的数据看看该在哪里打点,打点最重要的两个参数就是经度和纬度啦,接口返回的数据大概长这样:

这个latitude和longitude就是每个学校的经纬度了,而schoolCode则是我们在第三步需要用到的重要参数,接下来就是拿着这些数据封装成我们地图中所需要的结构:

分别是打点的学校的名字,学校id,经纬度,学校类别...
之后传到刚才说的方法中去,echaerts图表参数主要部分series中其中一个项大概就是这样滴(这个截图对应的是文章开头第一张图中的黄色点学校,该地区高中中职在我们系统中的就这两...,同理小学初中,完全中学,教育局也是一样滴,不同就是经纬度和颜色啦,看到这里如果懵了的话,不要捉急,代码被我截图拆的四零八散,最后我会附上完整代码滴~~~~):

到这地图打点算是完成了,效果就是图一的样子,但是只有地图和点,没有其他信息,看官也不知道这些点到底是干嘛滴不是吗,所以再看


第三步:异步获取地图上某个点的信息

其实本来是很简单的一个echarts图表中的tooltip,就是像echarts官网中的许多例子一样,鼠标放上去出现一个小tips,有什么系统名啊,本item的值啊,百分比啊云云,but我们的需求不按常理出牌啊,tips上的文字是自定义滴,需要展示的数据还是需要请求滴,一开始我还是不会滴...


就是图上红框框里那个玩意儿~~
于是我又去看了看echarts官网关于formatter链接描述的部分,


划重点aaaaaa
第二个参数 ticket 是异步回调标识,配合第三个参数 callback 使用。 第三个参数 callback 是异步回调,在提示框浮层内容是异步获取的时候,可以通过 callback 传入上述的 ticket 和 html 更新提示框浮层内容。
(感谢官网大大)
然后怎么办?--硬着头皮写啊,异步请求啊,连接字符串啊,显示啊,上代码:

tooltip: {
    show: true,
    triggerOn: 'click',
    backgroundColor: 'rgba(10, 17, 64, 0.8)',
    formatter(value, ticket, callback) {
        let _this = this
        fetchSchoolDeatail({ unitCode: value.data.schoolCode }).then(({ data, headers }) => {
           let info = data.data
           let str = `<div style="padding: 10px;">
                <h4>${value.name}</h4>
                <p>${info.bxTypeName}</p>
                <p style="display: flex; line-height: 200%">
                    <span style="flex: 1 1 50%;margin-right: 30px;">在册学生<br/><b style="color: #62b2e2; font-size: 200%">${info.studentNum}</b>人 </span>
                    <span style="flex: 1 1 50%;">在册职工数<br/><b style="color: #65db8a; font-size: 200%">${info.teacherNum}</b>人</span>
                </p>
                <p style="display: flex; line-height: 200%">
                    <span style="flex: 1 1 50%;margin-right: 30px;">今日到校学生数<br/><b style="color: #62b2e2; font-size: 200%;">${info.studentArriveSchoolNum}</b> 人</span>
                    <span style="flex: 1 1 50%;">今日到校职工数<br/><b style="color: #65db8a; font-size: 200%">${info.teacherArriveSchoolNum}</b>人</span>
                </p>
            </div>`
            callback(ticket, str)
       }).catch(err => {
             console.dir(err)
             _this.$message.error(err.message || '获取学校信息失败,请稍后再试')
       }).finally(() => {
             console.log(1)
       })
          return '加载中...'
        }
    },

有几点说明:

  1. 为了避免鼠标划过地图点就请求接口导致接口卡顿,触发tooltip的方法改成了click,也就是点击一下学校点才会加载学校信息接口数据;
  2. 由于接口请求发送到成功返回需要一点时间,刚一点上去可能是空白或者tips没出来,所以点击那一瞬间tips显示的是“加载中...”,这就是最外层的return;
  3. 当数据加载完成之后必须要写callback(ticket, str),这是人家formatter回调函数规定滴,ticket异步标识不能改,否则不返回任何东西。

至此呢,整个地图绘制,打点,加载点信息就完成了,下面补充几个我在写代码时的注意项:

  1. 绘制图表的点的样式可以是一般的ciecle圆点或者rect方形,也可以自定义,比如我地图中的教育局图表,就是引入了一张图片

const starImg = require('../../../assets/images/secondBg/star.png')
引入以后vue自己转化成了base64的编码,然后我们在需要使用的地方是“image://http://xxx.xxx.xxx/a/b.png”这种格式,我们自己需要加上'image://'这段字符,

icon: `image://${starImg}`
  1. 为了方便操作地图上的点,不和页面上其他部分重叠,我设置了地图鼠标缩放和平移漫游(geo坐标系中的roam),也就是鼠标在地图区域内的时候可以拖拽地图移动位置,也可以通过滑轮放大缩小地图面积,有个小瑕疵是有时候拖拽完之后鼠标已经没有左键右键控制了,但是鼠标一挪动,地图还是会跟着走,只有在地图之外的空白处点一下才会丢下拖拽。。。
  2. 本地图series中共有4个不同类型的数据组合,有的点经纬度离得近,地图缩小时会看上去重合,这时候就需要设置好每个类型的zlevel值,这是为了给每个类型绘制canvas时分层,不至于累在一起看不到别的点

最后附上我的全部代码

<template>
    <div class="map-wrapper">
        <v-chart :option="mapOption" ref="map" style="height: 100% !important;" auto-resize></v-chart>
    </div>
</template>
<script>
import { allSchools, fetchSchoolDeatail } from '@/services/showView'
import echarts from 'echarts'
var config = require('../../../../static/mock/conf')
var dingnan = require(`../../../../static/mock/${config.map}`)
const starImg = require('../../../assets/images/secondBg/star.png')
echarts.registerMap('dingnan', dingnan)

const makeMapOption = (data) => {
    return {
        legend: {
            show: true,
            orient: 'vertical',
            left: '22%',
            top: '15%',
            symbolKeepAspect: false,
            itemWidth: 15,
            itemHeight: 15,
            data: [
                {
                    name: '小学初中',
                    icon: 'circle',
                    textStyle: {
                        color: '#75eef5'
                    }
                },
                {
                    name: '高中中职',
                    icon: 'circle',
                    textStyle: {
                        color: '#f0ea75'
                    }
                },
                {
                    name: '完全中学',
                    icon: 'circle',
                    textStyle: {
                        color: '#887bff'
                    }
                },
                {
                    name: '县教育局',
                    icon: `image://${starImg}`,
                    textStyle: {
                        color: '#da4800'
                    }
                }
            ]
        },
        tooltip: {
            show: true,
            triggerOn: 'click',
            backgroundColor: 'rgba(10, 17, 64, 0.8)',
            formatter(value, ticket, callback) {
                let _this = this
                fetchSchoolDeatail({ unitCode: value.data.schoolCode }).then(({ data, headers }) => {
                    let info = data.data
                    let str = `<div style="padding: 10px;">
                    <h4>${value.name}</h4>
                    <p>${info.bxTypeName}</p>
                    <p style="display: flex; line-height: 200%">
                        <span style="flex: 1 1 50%;margin-right: 30px;">在册学生<br/><b style="color: #62b2e2; font-size: 200%">${info.studentNum}</b>人 </span>
                        <span style="flex: 1 1 50%;">在册职工数<br/><b style="color: #65db8a; font-size: 200%">${info.teacherNum}</b>人</span>
                    </p>
                    <p style="display: flex; line-height: 200%">
                        <span style="flex: 1 1 50%;margin-right: 30px;">今日到校学生数<br/><b style="color: #62b2e2; font-size: 200%;">${info.studentArriveSchoolNum}</b> 人</span>
                        <span style="flex: 1 1 50%;">今日到校职工数<br/><b style="color: #65db8a; font-size: 200%">${info.teacherArriveSchoolNum}</b>人</span>
                    </p>
                </div>`
                    callback(ticket, str)
                }).catch(err => {
                    console.dir(err)
                    _this.$message.error(err.message || '获取学校信息失败,请稍后再试')
                }).finally(() => {
                    console.log(1)
                })
                return '加载中...'
            }
        },
        geo: {
            map: 'dingnan',
            label: {
                emphasis: {
                    show: false
                }
            },
            roam: true,
            itemStyle: {
                normal: {
                    color: 'rgba(43,58,113, 0.2)',
                    borderColor: '#0c6cd2'
                },
                emphasis: {
                    color: 'rgba(43,58,113, 0.2)'
                }
            }
        },
        series: [{
            name: '县教育局', // 地图中心点
            type: 'scatter',
            coordinateSystem: 'geo',
            zlevel: 10,
            symbol: `image://${starImg}`,
            data: [{
                name: '县教育局',
                value: data[3]
            }],
            symbolSize: [20, 20],
            label: {
                normal: {
                    formatter: '{b}',
                    position: 'top',
                    show: false
                },
                emphasis: {
                    show: false
                }
            },
            itemStyle: {
                normal: {
                    color: '#96edfe'
                },
                emphasis: {
                    show: false
                }
            }
        },
        {
            name: '高中中职',
            type: 'effectScatter', // 涟漪
            coordinateSystem: 'geo',
            zlevel: 2,
            rippleEffect: {
                brushType: 'stroke',
                scale: 3
            },
            label: {
                normal: {
                    fontSize: 16,
                    offset: [12, 0],
                    show: false,
                    position: 'right',
                    formatter: '{b}'
                },
                emphasis: {
                    show: false
                }
            },
            symbolSize: 12,
            itemStyle: {
                normal: {
                    color: '#f0ea75'
                }
            },
            data: data[1]
        },
        {
            name: '小学初中',
            type: 'effectScatter', //带有涟漪特效动画的散点
            coordinateSystem: 'geo', // 该系统所使用的坐标系,geo地理坐标系
            zlevel: 2, // zlevel用于 Canvas 分层
            rippleEffect: {
                brushType: 'stroke', // 波纹的绘制方式
                scale: 3 // 动画中波纹的最大缩放比例
            },
            label: {
                normal: {
                    fontSize: 16,
                    offset: [10, 0],
                    show: false,
                    position: 'right',
                    formatter: '{b}'
                },
                emphasis: {
                    show: false
                }
            },
            symbolSize: 10, // 标记的大小
            itemStyle: { // 图形样式
                normal: {
                    color: '#75eef5'
                }
            },
            data: data[0]
        },
        {
            name: '完全中学',
            type: 'effectScatter', // 涟漪
            coordinateSystem: 'geo',
            zlevel: 2,
            rippleEffect: {
                brushType: 'stroke',
                scale: 3
            },
            label: {
                normal: {
                    fontSize: 16,
                    offset: [10, 0],
                    show: false,
                    position: 'right',
                    formatter: '{b}'
                },
                emphasis: {
                    show: false
                }
            },
            symbolSize: 8,
            itemStyle: {
                normal: {
                    color: '#887bff'
                }
            },
            data: data[2]
        }]
    }
}

export default {
    name: 'dingnan-map',
    data() {
        return {
            mapname: config.mapname,
            mapOption: {
                series: [
                ]
            },
            mapChart: null,
            coordData: {
                name: '',
                coord: [],
                id: '',
                children: [],
                url3D: ''
            },
            geoCoordMap: {},
            noticeList: [],
            searchDeviceList: [],
            allSecuityList: [],
            isPlay: false,
            playInt: '',
            playCount: 1,
            schoolName: ''
        }
    },
    mounted() {
        this.mapChart = this.$refs.map.chart
        this.mapChart.showLoading({
            text: '正在加载',
            color: '#25D7FB',
            textColor: '#25D7FB',
            maskColor: 'rgba(19, 25, 83, 0.4)'
        })
        allSchools().then(({ data, headers }) => {
            // 地图中的学校
            if (data.resultCode === 0) {
                let map = new Map()
                for (let item of data.data) {
                    this.coordData.children.push({
                        schoolName: item.schoolName,
                        schoolId: item.schoolCode,
                        url3D: '',
                        coord: [item.longitude, item.latitude],
                        showColor: item.showColor
                    })
                    this.coordData.name = '定南县教体局'
                    this.coordData.coord = [115.049, 24.759872]
                    this.coordData.id = '3607280000'
                    this.coordData.url3D = ''
                    map.set(item.schoolName, [item.longitude, item.latitude])
                }
                this.geoCoordMap = map
                this.mapOption = makeMapOption([this.convertEffectData(1), this.convertEffectData(2), this.convertEffectData(3), this.coordData.coord])
            }
            return { data: null }
        }).catch(err => {
            console.dir(err)
            this.$message.error(err.message || '查询失败,请稍后再试')
        }).finally(() => {
            this.mapChart.hideLoading()
        })
    },
    methods: {
        // 涟漪点
        convertEffectData(colorType) {
            var res = [];
            for (var i = 0; i < this.coordData.children.length; i++) {
                var dataItem = this.coordData.children[i];
                if (dataItem.showColor === colorType) {
                    res.push({
                        name: dataItem.schoolName,
                        value: dataItem.coord,
                        schoolCode: dataItem.schoolId
                    });
                }
            }
            return res;
        }
    }
}
</script>
<style lang="scss" scoped>
.map-wrapper {
    width: 98%;
    height: 100%;
    position: absolute;
    left: 1%;
    right: 1%;
    margin: auto;
    z-index: 1;
}
</style>

最后再啰嗦一下,我的地图小点点是一颗颗闪亮的小星星,这个主要设置好散点的颜色,涟漪点的圈圈数量,闪动的时间,就很好看啦,我的代码里都有注释。
第一次写这么多字,有点小鸡冻,分享就这么多了,如果您有幸看到了我的文章,希望能对您遇到的问题有所帮助,与君共勉,fighting!

03-05 21:26