vue3 开发电子地图功能

在这里插入图片描述

文章目录

  • 一、项目背景
  • 二、页面效果
  • 三、代码
    • 1.ElectronicMap.vue
    • 2.TransferDeskRSSIMap.vue
    • 3.Map.js
    • 4.src/stores/index.js Vuex存储属性
  • 四、注意点
  • 本人其他相关文章链接

一、项目背景

项目采用:vue3+java+Arco Design+SpringBoot+OpenStreetMap 数据的地图切片服务。我们的项目会上报或者手动添加多台中转台,中转台有属性:经度、纬度、海拔。我们想在在线/离线地图上展示设备信息。

二、页面效果

电子地图

在这里插入图片描述

中转台RSSI地图

在这里插入图片描述

点击工具,测距

在这里插入图片描述

点击工具,开启中转台覆盖范围

在这里插入图片描述

三、代码

1.ElectronicMap.vue

<template><layout_2 style="position: relative"><div id="electronic_map"></div><div class="transparent-box"></div><div class="transparent-box-bottom"></div><div class="--search-line in-map-tl"><div><div class="key">{{$t('TreeViewRepeater')}}&nbsp;:</div><div class="val"><a-tree-select class="arco-tree-select --arco-select" style="width: 230px":field-names="{ key: 'serialNo', title: 'name', children: 'children' }" :data="treeSelectNodeData":multiple="true" :tree-checkable="true" tree-checked-strategy="child" :max-tag-count="1"v-model:model-value="param.repeaterSNs"></a-tree-select></div></div><a-button class="huge" @click="queryTopoView" type="primary"><template #icon><icon-search size="18" /></template>{{$t('Query')}}</a-button><a-checkbox :default-checked="isChecked" @change="changeOnlineMap">{{$t('OnLineMap')}}</a-checkbox></div><statistic-repeater ref="statisticRepeaterRef" @click-transfer-desk="queryAllDeviceListByTypeFunction"></statistic-repeater></layout_2><base-info v-model:visible="baseInfoShow" v-if="baseInfoShow" ref="baseInfoRef" @refresh-flag="successFresh"></base-info><site-alias v-model:visible="siteAliasShow" v-if="siteAliasShow"></site-alias><monitor-alarm v-model:visible="monitorAlarmShow" v-if="monitorAlarmShow" ref="monitorAlarmRef"></monitor-alarm><device-param v-model:visible="deviceParamShow" v-if="deviceParamShow" ref="deviceParamRef"></device-param><a-popover arrow-class="--arrow-none" v-model:popup-visible="popoverVisible" :style="overlayStyle" content-class="--arco-popover-popup-content"@mouseenter="handlePopoverMouseEnter" @mouseleave="handlePopoverMouseLeave"><template #content><div class="dropdownBasic"><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="base-info"></svg-loader></div><div class="text" @click="openBaseInfo">{{$t('BaseInfo')}}</div></div><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="alarm-monitor"></svg-loader></div><div class="text" @click="openMonitorAlarm">{{$t('MonitoringAlarm')}}</div></div><div class="dropdownItemitem"><div class="baselayersFWrapper"><svg-loader class="baselayersFIcon" name="device-param"></svg-loader></div><div class="text" @click="openDeviceParam">{{$t('ParameterSetting')}}</div></div></div></template></a-popover><a-modal v-model:visible="showVisible" @ok="handleOk" :hide-cancel="true"><template #title>{{$t('Prompt')}}</template><div>{{$t('RepeaterOffline')}}</div></a-modal>
</template><script setup>
import Layout_2 from "@/views/pages/_common/layout_2.vue";
import {computed, inject, nextTick, onMounted, onUnmounted, provide, reactive, ref} from "vue";
import {LeafletMap} from "@/views/pages/_class/Map";
import {qryTransferNodeList} from "@/views/pages/topology/_request";
import {queryButtonValue} from "@/views/pages/_common/enum";
import StatisticRepeater from "@/views/pages/topology/StatisticRepeater.vue";
import SiteAlias from "@/views/pages/topology/topologyView/SiteAlias.vue";
import BaseInfo from "@/views/pages/topology/topologyView/BaseInfo.vue";
import MonitorAlarm from "@/views/pages/topology/topologyView/MonitorAlarm.vue";
import DeviceParam from "./topologyView/DeviceParam.vue";
import {useStore} from "@/stores";
import {queryAllDeviceList, queryAllDeviceListByType} from "@/views/pages/system/system.js";const initTreeLayout = ref(1)
provide('initTreeLayout', initTreeLayout.value)
const isChecked = ref(true);
const mapClass = ref(new LeafletMap())
const treeSelectNodeData = ref([])
const t = inject('t')
const deviceManageList = ref([])
const statisticRepeaterRef = ref(null)
const param = reactive({repeaterSNs: [],
})const computedPopoverVisible = computed(() => {return useStore().popoverVisible;
})
const popoverVisible = computedPopoverVisible
const computedPopoverPosition = computed(() => {return useStore().popoverPosition;
})
const popoverPosition = computedPopoverPosition
const computedSelectTopoNode = computed(() => {return useStore().selectTopoNode;
})
const selectTopoNode = computedSelectTopoNode
const baseInfoShow = ref(false)
const siteAliasShow = ref(false)
const monitorAlarmShow = ref(false)
const deviceParamShow = ref(false)
const showVisible = ref(false)
const sipLoading = ref(true)const baseInfoRef = ref(null)
const deviceParamRef = ref(null)
const monitorAlarmRef = ref(null)const queryAllDeviceListByTypeFunction = (index) => {queryAllDeviceListByType({"rptState": index}).then(response => {if (response.data) {deviceManageList.value = response.data;mapClass.value.handleAllMarkerInMap(deviceManageList.value)}})
}const queryTopoView = async () => {await getAllDeviceManageListFunction();mapClass.value.handleAllMarkerInMap(deviceManageList.value)
}const openBaseInfo = () => {baseInfoShow.value = truesipLoading.value = truenextTick(() => {const repeater = useStore().websocketRepeaterList.find(repeater => repeater.serialNo === selectTopoNode.value.serialNo);const rptState = repeater ? repeater.rptState : null;baseInfoRef.value.setData(useStore().selectTopoNode.serialNo, rptState)})
}const openMonitorAlarm = () => {if (useStore().selectTopoNode.rptState != 1 && useStore().selectTopoNode.rptState != 2) {showVisible.value = truereturn}monitorAlarmShow.value = truenextTick(() => {monitorAlarmRef.value.setRssiId(useStore().selectTopoNode.serialNo)})
}const openDeviceParam = () => {if (useStore().selectTopoNode.rptState != 1 && useStore().selectTopoNode.rptState != 2) {showVisible.value = truereturn}deviceParamShow.value = truenextTick(() => {deviceParamRef.value.baseSettingXptFunction(useStore().selectTopoNode.serialNo)})}const handlePopoverMouseEnter = () => {useStore().popoverVisible = true;
};const handlePopoverMouseLeave = () => {useStore().popoverVisible = false;
};const overlayStyle = computed(() => ({position: 'absolute',top: `${popoverPosition.value.top}px`,left: `${popoverPosition.value.left}px`,zIndex: 1000,
}));const successFresh = () => {baseInfoShow.value = false
}const handleOk = () => {showVisible.value = false
}const getTransferNodeList = () => {const principal = sessionStorage.getItem('principal');if (principal) {const principalObject = JSON.parse(principal);qryTransferNodeList({"userName": principalObject.userName}).then(response => {if (response.data) {treeSelectNodeData.value = [{serialNo: '-1',name: t(queryButtonValue[22]),children: response.data}]}})}
}
const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val)
}const getAllDeviceManageListFunction = async () => {await queryAllDeviceList({"serialNoArr": param.repeaterSNs}).then(response => {if (response.data) {deviceManageList.value = response.data;}})
}let webSocket = nullconst connectWebSocket = (url) => {if (webSocket) webSocket.close()webSocket = new WebSocket(url)webSocket.onopen = () => console.log('ElectronicMap.vue WebSocket已连接')webSocket.onmessage = handleWebSocketMessagewebSocket.onclose = () => console.log('ElectronicMap.vue WebSocket已关闭')webSocket.onerror = (error) => console.error('ElectronicMap.vue WebSocket错误:', error)
}const handleWebSocketMessage = (event) => {try {const message = JSON.parse(event.data)const index = useStore().websocketRepeaterList.findIndex(item => item.serialNo === message.serialNo);if (index !== -1) {useStore().websocketRepeaterList[index] = message;} else {useStore().websocketRepeaterList.push(message);}const receiveRepeaterId = message.repeaterIdconst receiveRptState = message.rptStatedeviceManageList.value.forEach(repeater => {if (repeater.repeaterId == receiveRepeaterId) repeater.rptState = receiveRptState;})mapClass.value.handleAllMarkerInMap(deviceManageList.value)statisticRepeaterRef.value.queryTypeCountFunction();} catch (error) {console.error('ElectronicMap.vue WebSocket消息处理错误:', error)}
}onMounted(async () => {mapClass.value.initMap('electronic_map', deviceManageList.value)await getAllDeviceManageListFunction()mapClass.value.handleAllMarkerInMap(deviceManageList.value)changeOnlineMap(true);connectWebSocket('/ws/topoView');
})onUnmounted(() => {if (webSocket) webSocket.close()
})const init = () => {getTransferNodeList();
}
init()
</script><style scoped lang="less">
#electronic_map {height: 100%;
}
.transparent-box {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to bottom, #FFFFFF, transparent);opacity: 0.8;top: 0;height: 80px;width: 100%;
}
.transparent-box-bottom {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to top, #FFFFFF, transparent);opacity: 0.8;bottom: 0;height: 80px;width: 100%;
}
.in-map-tl {position: absolute;z-index: 401;left: 20px;top: 17px;
}
.in-map-rt {position: absolute;display: flex;flex-direction: column;gap: 20px;z-index: 401;top: 16px;right: 20px;.card-item {box-sizing: border-box;padding: 16px;border-radius: 12px;background: #F7F9FC;box-shadow: 0px 3px 6px 0px rgba(193, 203, 214, 0.70);width: 172px;cursor: pointer;&.active {border-radius: 12px;border: 2px solid #7FAFFF;padding: 14px;background: #FAFCFF;box-shadow: 0px 3px 6px 0px rgba(193, 203, 214, 0.70);}&-title {color: #202B40;font-family: "PingFang SC";font-size: 14px;font-style: normal;font-weight: 400;line-height: 22px;}&-count {position: relative;margin-top: 4px;height: 48px;color: #202B40;font-family: Roboto;font-size: 32px;font-style: normal;font-weight: 600;line-height: 48px;&-percentage {position: absolute;top: 19px;right: 0;color: #A7B1C6;text-align: right;font-family: "PingFang SC";font-size: 14px;font-style: normal;font-weight: 400;line-height: 22px;}}.percentage-chart {}}
}
</style>
<style>
.custom-popupp .leaflet-popup-content-wrapper {background: #EFF8FF;width: 330px
}.custom-popupp .leaflet-popup-tip-container {display: none;
}.--arrow-none {display: none
}.--arco-popover-popup-content {box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1), 0px 8px 24px rgba(0, 0, 0, 0.1);border-radius: 12px;background: linear-gradient(180deg, #eff8ff, #fff);border: 1.5px solid #fff;box-sizing: border-box;width: 170px;
}.baselayersFIcon {width: 16px;position: relative;height: 16px;overflow: hidden;flex-shrink: 0;
}.baselayersFWrapper {width: 24px;border-radius: 80px;background-color: #fff;border: 1px solid #dfdfdf;box-sizing: border-box;height: 24px;display: flex;flex-direction: row;align-items: center;justify-content: center;padding: 8px;
}.text {flex: 1;position: relative;line-height: 22px;display: inline-block;height: 22px;cursor: pointer;
}.dropdownItemitem {align-self: stretch;height: 36px;display: flex;flex-direction: row;align-items: center;justify-content: flex-start;padding: 5px 12px 5px 16px;box-sizing: border-box;gap: 8px;
}.dropdownItemitem:hover {background: #E8F7FF;font-weight: bold;color: #3348FF;
}.dropdownBasic {width: 100%;position: relative;display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 8px 0px;text-align: left;font-size: 14px;color: #202b40;font-family: 'PingFang SC';
}
</style>

2.TransferDeskRSSIMap.vue

<template><layout_2 style="position: relative"><div id="transfer_rssi_map"></div><div class="legend"><div class="legend-item"><div class="legend-item-color" style="background-color: #00AB07"></div><div class="legend-item-text">Good</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #3374FF"></div><div class="legend-item-text">Normal</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #DC6300"></div><div class="legend-item-text">Average</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #913DFF"></div><div class="legend-item-text">Bad</div></div><div class="legend-item"><div class="legend-item-color" style="background-color: #F53F3F"></div><div class="legend-item-text">Very Bad</div></div></div><div class="transparent-box"></div><div class="--search-line search-in-map-tl"><div><div class="key">发送方ID&nbsp;</div><div class="val"><a-range-pickerstyle="width: 280px":allow-clear="false"v-model="param.timeRange":disabled-date="disabledDate"><template #suffix-icon><svg-loader :width="20" :height="20" name="clock"></svg-loader></template><template #separator><svg-loader:width="16":height="16"name="arrow-right"></svg-loader></template></a-range-picker></div></div><div><div class="key">目的ID&nbsp;</div><div class="val"><a-tree-selectclass="arco-tree-select --arco-select"style="width: 230px":field-names="{key: 'serialNo',title: 'name',children: 'children',}":data="treeSelectNodeData":multiple="true":tree-checkable="true"tree-checked-strategy="child":max-tag-count="1"v-model:model-value="param.repeaterSNs"></a-tree-select></div></div><a-button class="huge" @click="search" type="primary"><template #icon><icon-search size="18" /> </template>{{ $t(queryButtonValue[2]) }}</a-button><a-checkbox :default-checked="isChecked" @change="changeOnlineMap">{{$t(queryButtonValue[5])}}</a-checkbox></div><div class="search-in-map-br"><a-tooltip :content="居中显示" position="left"><div class="btn-item"><img :src="centerImg" /></div></a-tooltip><a-tooltip :content="测距" position="left"><divclass="btn-item":class="{ active: drawDistanceSwitch }"@click="drawDistanceSwitchChange"><img :src="distanceImg" /></div></a-tooltip><a-tooltip :content="开启中转台覆盖范围" position="left"><div class="btn-item":class="{ active: rangeFlag }"@click="rangeClick"><img :src="coverageImg" /></div></a-tooltip></div></layout_2>
</template><script setup>
import Layout_2 from "@/views/pages/_common/layout_2.vue";
import { LeafletMap } from "@/views/pages/_class/Map";
import { onMounted, reactive, ref, inject } from "vue";
import * as moment from "moment/moment";
import centerImg from "@/assets/img/center.png";
import distanceImg from "@/assets/img/distance.png";
import coverageImg from "@/assets/img/coverage.png";
import {qryTransferNodeList,qryTransferRSSIList,
} from "@/views/pages/topology/_request";
import { commonResponse } from "@/views/pages/_common";
import { TransferDesk } from "@/views/pages/_class/TransferDesk";
import { queryButtonValue, queryColumnValue } from "@/views/pages/_common/enum";
import {queryAllDeviceList} from "@/views/pages/system/system.js";
const t = inject("t");
const isChecked = ref(true);
const param = reactive({timeRange: [null, null],repeaterSNs: [],
});
let reqParam = {startTime: null,endTime: null,repeaterSNs: [],
};
const mapClass = ref(new LeafletMap());
const transferDeskClass = ref(new TransferDesk({ mapClass }));const disabledDate = (date) => {return date.getTime() > moment().format("x");
};const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val);
};const drawDistanceSwitch = ref(false);
const drawDistanceSwitchChange = () => {drawDistanceSwitch.value = !drawDistanceSwitch.value;if (drawDistanceSwitch.value) {mapClass.value.openDrawDistance();} else {mapClass.value.closeDrawDistance();}
};const treeSelectNodeData = ref([]);
const getTransferNodeList = () => {const principal = sessionStorage.getItem('principal');if (principal) {const principalObject = JSON.parse(principal);qryTransferNodeList({"userName": principalObject.userName}).then(response => {    commonResponse({response,onSuccess: () => {treeSelectNodeData.value = [{serialNo: "-1",name: t(queryButtonValue[22]),children: response.data,},];},});});}
};const transferRssiMap = ref(new Map());
const getRSSIList = () => {qryTransferRSSIList({...reqParam,}).then((response) => {commonResponse({response,onSuccess: () => {handleTransferDesk(response.data);},});});
};const handleTransferDesk = (data) => {transferRssiMap.value.clear();data.repeaterList.forEach((item) => {item.longitude = item.lng;item.latitude = item.lat;transferRssiMap.value.set(item.serialNo, item);});data.queryRssiResults.forEach((item) => {let obj = transferRssiMap.value.get(item.repeaterSN);obj = {...obj,...item,};if (item.pos?.longitude && item.pos?.latitude) {obj.longitude = item.pos?.longitude;obj.latitude = item.pos?.latitude;}transferRssiMap.value.set(item.repeaterSN, obj);});for (let [key, val] of transferRssiMap.value) {handleTransferDeskInMap(val);}
};const handleTransferDeskInMap = (item) => {transferDeskClass.value.handleTransferDeskInMap(item);// if (item.)
};const search = () => {reqParam = {...reqParam,...param,};reqParam.startTime = moment(reqParam.timeRange[0]).format("YYYY-MM-DD");reqParam.endTime = moment(reqParam.timeRange[1]).format("YYYY-MM-DD");delete reqParam.timeRange;getRSSIList();
};const getAllTransferInMap = () => {queryAllDeviceList({"serialNoArr": param.repeaterSNs}).then((response) => {commonResponse({response,onSuccess: () => {mapClass.value.handleAllMarkerInMap(response.data)},});});
}const rangeFlag = ref(false)
const rangeClick = () => {if (rangeFlag.value) {mapClass.value.closeCoverageRange()} else {mapClass.value.openCoverageRange()}rangeFlag.value = !rangeFlag.value
}const init = () => {param.timeRange = [moment().add(-9, "days"), moment()];param.repeaterSNs = [];getTransferNodeList();getAllTransferInMap()
};init();onMounted(() => {mapClass.value.initMap("transfer_rssi_map");changeOnlineMap(true);
});
</script><style scoped lang="less">
#transfer_rssi_map {height: 100%;
}
.transparent-box {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to bottom, #ffffff, transparent);opacity: 0.8;top: 0;height: 80px;width: 100%;
}
.legend {position: absolute;box-sizing: border-box;display: flex;flex-direction: column;gap: 8px;border-radius: 6px;padding: 12px 14px;z-index: 401;left: 20px;bottom: 14px;width: 134px;height: 166px;background-color: rgba(255, 255, 255, 0.70);stroke-width: 1.5px;stroke: #FFF;filter: drop-shadow(0px 8px 24px rgba(0, 0, 0, 0.10));backdrop-filter: blur(4px);&-item {position: relative;height: 22px;&-color {position: absolute;top: 6px;border-radius: 5px;height: 10px;width: 10px;}&-text {margin-left: 18px;line-height: 22px;color: var(--80, #202B40);font-family: "PingFang SC";font-size: 13px;font-style: normal;font-weight: 400;}}
}
.transparent-box-bottom {position: absolute;z-index: 400;pointer-events: none;background-image: linear-gradient(to top, #ffffff, transparent);opacity: 0.8;bottom: 0;height: 80px;width: 100%;
}
.search-in-map-tl {position: absolute;z-index: 401;left: 20px;top: 17px;
}
.search-in-map-br {position: absolute;display: flex;flex-direction: column;gap: 20px;z-index: 401;right: 32px;bottom: 36px;width: 42px;.btn-item {box-sizing: border-box;padding: 9px;border-radius: 21px;height: 42px;width: 42px;background-color: #404750;cursor: pointer;&:hover {background-color: #3348ff;}&.active {background-color: #3348ff !important;}}
}
</style>

3.Map.js

import * as L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import './map.less'
import deviceImg0 from '@/assets/img/device-0.png'
import {ref} from "vue";
import deviceImg1 from "@/assets/img/device-1.png";
import deviceImg2 from "@/assets/img/device-2.png";
import {useStore} from "@/stores";const baseUrl = ref("")export class LeafletMap {constructor() {baseUrl.value = window.location.originthis.markers = []; // 用于存储地图上的标记}mapUrl = {online: 'https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png',offline: '/mapShow/{z}/{x}/{y}.png'}getMap = () => {return this.map}rangeLayerGroup = L.layerGroup()initMap (containerId, list) {document.querySelector(`#${containerId}`).innerHTML += `<div class="latlng-box"><div class="inline">纬度:</div><div class="inline" id="${containerId}_lat"></div>,&nbsp;&nbsp;<div class="inline">经度:</div><div class="inline" id="${containerId}_lng"></div></div>`const latBox = document.querySelector(`#${containerId}_lat`)const lngBox = document.querySelector(`#${containerId}_lng`)this.map = L.map(containerId, {center: [45.7531, 126.6343],zoom: 5,minZoom: 1,maxZoom: 16,contextmenu: true,contextmenuWidth: 160,contextmenuHeight: 640,});this.tileLayer = L.tileLayer(this.mapUrl.offline)this.tileLayer.addTo(this.map)this.map.addEventListener('mousemove', (e) => {latBox.innerHTML = e.latlng.latlngBox.innerHTML = e.latlng.lng})this.rangeLayerGroup.addTo(this.map)list?.forEach(item => {this.addMarkerWithPopup(item);})}deviceMap = new Map()handleAllMarkerInMap = (list) => {// 清除之前的设备标记this.markers.forEach(marker => marker.remove());this.markers = [];this.deviceMap.clear();list?.forEach(item => {this.deviceMap.set(item.serialNo, item);this.addMarkerWithPopup(item);});this.map.invalidateSize(); // 更新地图视图}addMarkerWithPopup (info) {const {lat, lng, rptState, alias} = infoif (!lat || !lng) returnconst marker = L.marker([lat, lng], {icon: L.icon({iconUrl: rptState == 1 ? deviceImg1 : rptState == 2 ? deviceImg2 : deviceImg0, // 使用 deviceImg0 作为图标iconSize: [32, 32],  // 设置图标的大小iconAnchor: [16, 16] // 设置图标的锚点})}).addTo(this.map);this.markers.push(marker); // 将标记添加到数组中// 添加悬停事件处理逻辑marker.on('mouseover', () => {marker.bindTooltip(alias, ).openTooltip();});const handleNodeClick = (params) => {event.preventDefault();useStore().selectTopoNode = info;// 计算弹出框的位置const chartDom = document.querySelector('#electronic_map');const chartRect = chartDom.getBoundingClientRect();const offsetX = params.containerPoint.x;const offsetY = params.containerPoint.y;useStore().popoverPosition = {top: chartRect.top + offsetY + window.scrollY,left: chartRect.left + offsetX + window.scrollX,};useStore().popoverVisible = true;}marker.on('contextmenu', handleNodeClick);}changeOnlineMap = (onlineStatus) => {if (onlineStatus) {this.tileLayer.setUrl(this.mapUrl.online)} else {this.tileLayer.setUrl(this.mapUrl.offline)}}/** 测距功能 start */lastClickPoint = nulltotalDistance = 0pointMarkerArr = []lastMovePointObj = nulllastMoveLineObj = nulldrawPermission = falseopenDrawDistance = () => {this.map.addEventListener('click', (e) => {this.drawPermission = trueconst { lat, lng } = e.latlngif (!this.lastClickPoint) {const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-start' }) })marker.bindTooltip('0km', {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()marker.addTo(this.map)this.pointMarkerArr.push(marker)} else {const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-process' }) })const distance = this.calculateDistance(this.lastClickPoint[0], this.lastClickPoint[1], lat, lng) / 1000this.totalDistance += distancemarker.bindTooltip(`${this.totalDistance.toFixed(3)}km`, {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()marker.addTo(this.map)const polyline = L.polyline([this.lastClickPoint, [lat, lng]], { color: '#FFA100' })polyline.addTo(this.map)this.pointMarkerArr.push(marker)this.pointMarkerArr.push(polyline)}this.lastClickPoint = [lat, lng]})this.map.addEventListener('mousemove', (e) => {if (!this.drawPermission) returnconst { lat, lng } = e.latlngif (this.lastClickPoint) {const polyline = L.polyline([this.lastClickPoint, [lat, lng]], {color: '#FFA100',dashArray: '8'})const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'point-start' }) })marker.bindTooltip(`右键取消`, {offset: [0, -14],permanent: true,direction: 'top'}).openTooltip()this.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()polyline.addTo(this.map)marker.addTo(this.map)this.lastMoveLineObj = polylinethis.lastMovePointObj = marker}})this.map.addEventListener('contextmenu', () => {this.drawPermission = falsethis.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()this.lastMoveLineObj = nullthis.lastMovePointObj = null})}closeDrawDistance = () => {this.map.removeEventListener('click')this.map.removeEventListener('mousemove')this.lastClickPoint = nullthis.pointMarkerArr.forEach(item => item.remove())this.pointMarkerArr = []this.lastMoveLineObj && this.lastMoveLineObj.remove()this.lastMovePointObj && this.lastMovePointObj.remove()this.lastMoveLineObj = nullthis.lastMovePointObj = nullthis.totalDistance = 0}// Vincenty公式进行计算(更准确但计算复杂度较高)calculateDistance (lat1, lon1, lat2, lon2) {const a = 6378137; // 长轴半径,单位为米const b = 6356752.314245; // 短轴半径,单位为米const f = 1 / 298.257223563; // 扁率const L = (lon2 - lon1) * Math.PI / 180;const U1 = Math.atan((1 - f) * Math.tan(lat1 * Math.PI / 180));const U2 = Math.atan((1 - f) * Math.tan(lat2 * Math.PI / 180));const sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);const sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);let iterLimit = 100;let lambda = L, lambdaP, sinSigma, cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM;do {const sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda);sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +(cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));if (sinSigma === 0) {return 0; // 两点重合}cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;sigma = Math.atan2(sinSigma, cosSigma);sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;cosSqAlpha = 1 - sinAlpha * sinAlpha;cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;const C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));lambdaP = lambda;lambda = L + (1 - C) * f * sinAlpha *(sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));} while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);if (iterLimit === 0) {return NaN; // 迭代次数过多}const uSq = cosSqAlpha * (a * a - b * b) / (b * b);const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 *(cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) *(-3 + 4 * cos2SigmaM * cos2SigmaM)));const distance = b * A * (sigma - deltaSigma);return distance;}/** 测距功能 end */openCoverageRange = () => {for (const [key, value] of this.deviceMap) {const {lat, lng} = valueL.circle([lat, lng], {radius: 100000,color: 'rgba(51, 72, 255, 0.50)',fillColor: 'rgba(51, 116, 255, 0.30)',}).addTo(this.rangeLayerGroup) // 磊哥说写个假的}}closeCoverageRange = () => {this.rangeLayerGroup.clearLayers()}
}

4.src/stores/index.js Vuex存储属性

/*** @Name:* @Author:贾志博* @description:*/
import {defineStore} from "pinia";
import {ref} from 'vue'export const useStore = defineStore('main', () => {const mode = ref(0)const setMode = (modeVal) => {mode.value = modeVal}const getMode = () => {return mode.value}const openMenuItem = ref([]);const setOpenMenuItemFunction = (modeVal) => {openMenuItem.value = modeVal}const getOpenMenuItemFunction = () => {return openMenuItem}const selectedMenuItemKey = ref(null);const setSelectedMenuItemKeyFunction = (modeVal) => {selectedMenuItemKey.value = modeVal}const getSelectedMenuItemKeyFunction = () => {return selectedMenuItemKey}const selectedMenuKey = ref("");const setSelectedMenuKeyFunction = (modeVal) => {selectedMenuKey.value = modeVal}const getSelectedMenuKeyFunction = () => {return selectedMenuKey}const hasAuth = ref(false)const routes = ref([])const popoverVisible = ref(false);const popoverPosition = ref({ top: 0, left: 0 });const selectTopoNode = ref(null)const websocketRepeaterList = ref([])return {getMode,setMode,setSelectedMenuKeyFunction,getSelectedMenuKeyFunction,setSelectedMenuItemKeyFunction,getSelectedMenuItemKeyFunction,setOpenMenuItemFunction,getOpenMenuItemFunction,hasAuth,routes,popoverVisible,popoverPosition,selectTopoNode,websocketRepeaterList,}
})

四、注意点

注意点1:地图分在线地图/离线地图,离线地图需要上传瓦片地图。

注意点2:在线地图采用,在线地图服务:https://a.tile.geofabrik.de/

注意点3:使用OpenStreetMap地图步骤
要获取类似于 https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png 的地图切片 URL,您可以按照以下步骤进行:

  1. 选择地图服务提供商
    您可以选择不同的地图服务提供商,Geofabrik 是一个提供基于 OpenStreetMap 数据的切片服务的选项。其他常见的提供商包括:
  • OpenStreetMap: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
  • Mapbox: 需要注册并获取 API 密钥。
  • Carto: 需要注册并获取 API 密钥。
  1. 确定区域和数据集
    如果您希望使用 Geofabrik 的服务,您需要确定您想要的地图区域。Geofabrik 提供了不同区域的切片,您可以在其网站上找到这些区域。通常,URL 中的标识符(例如 15173cf79060ee4a66573954f6017ab0)对应于特定的区域。

  2. 获取区域的切片 URL,访问 Geofabrik 网站

  • 访问 Geofabrik 的切片服务页面:
    • 前往 Geofabrik 网站。
  • 选择区域:
    • 在网站上,您可以选择特定的区域(如国家或城市)以获取相应的切片服务。
  • 查找切片服务的 URL:
    • 在选择的区域页面上,您通常可以找到用于该区域的切片服务 URL,类似于 https://a.tile.geofabrik.de/{区域标识}/{z}/{x}/{y}.png。
  1. 其他注意事项
  • 使用条款: 在使用任何地图切片服务之前,请确保遵循其使用条款和条件,尤其是在商业应用中。
  • API 密钥: 某些地图服务(如 Mapbox 和 Carto)需要您注册并获取 API 密钥才能使用其服务。

通过上述步骤,您可以获取并使用类似于 https://a.tile.geofabrik.de/15173cf79060ee4a66573954f6017ab0/{z}/{x}/{y}.png 的地图切片 URL。

注意点4:地图下方会实时显示鼠标的经纬度信息。

注意点5:地图右下角还有3个小工具:居中显示、测距、开启中转台覆盖范围

  • 居中显示:未开发
  • 测距:已开发,
  • 开启中转台覆盖范围:这个是用户会配置颜色范围
    在这里插入图片描述

操作手册中有详细介绍

在这里插入图片描述

注意点6:由于代码比较乱,这里详细介绍下具体使用。

电子地图页面显示

<div id="electronic_map"></div>import {onMounted, ref} from "vue";
import {LeafletMap} from "@/views/pages/_class/Map";const mapClass = ref(new LeafletMap())
const deviceManageList = ref([])
const isChecked = ref(true);const changeOnlineMap = (val) => {mapClass.value.changeOnlineMap(val)
}onMounted(async () => {mapClass.value.initMap('electronic_map', deviceManageList.value)mapClass.value.handleAllMarkerInMap(deviceManageList.value)changeOnlineMap(true);
})

右下角工具

<div class="search-in-map-br"><a-tooltip :content="$t(queryColumnValue[25])" position="left"><div class="btn-item"><img :src="centerImg" /></div></a-tooltip><a-tooltip :content="$t(queryColumnValue[26])" position="left"><divclass="btn-item":class="{ active: drawDistanceSwitch }"@click="drawDistanceSwitchChange"><img :src="distanceImg" /></div></a-tooltip><a-tooltip :content="$t(queryColumnValue[27])" position="left"><div class="btn-item":class="{ active: rangeFlag }"@click="rangeClick"><img :src="coverageImg" /></div></a-tooltip>
</div>import centerImg from "@/assets/img/center.png";
import distanceImg from "@/assets/img/distance.png";
import coverageImg from "@/assets/img/coverage.png";const drawDistanceSwitch = ref(false);
const rangeFlag = ref(false)const drawDistanceSwitchChange = () => {drawDistanceSwitch.value = !drawDistanceSwitch.value;if (drawDistanceSwitch.value) {mapClass.value.openDrawDistance();} else {mapClass.value.closeDrawDistance();}
};const rangeClick = () => {if (rangeFlag.value) {mapClass.value.closeCoverageRange()} else {mapClass.value.openCoverageRange()}rangeFlag.value = !rangeFlag.value
}

本人其他相关文章链接

1.vue3 开发电子地图功能
2.vue java 实现大地图切片上传
3.java导入excel更新设备经纬度度数或者度分秒
4.快速上手Vue3国际化 (i18n)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/900647.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle 存储体系结构

oracle 存储体系结构 参考&#xff1a; Logical Storage Structures (oracle.com)

python-leetcode 66.寻找旋转排序数组中的最小值

题目&#xff1a; 已知一个长度为n的数组&#xff0c;预先按照升序排列&#xff0c;经由1到n次旋转后&#xff0c;得到输入数组&#xff0c;例如&#xff0c;原数组 nums [0,1,2,4,5,6,7] 在变化后可能得到&#xff1a; 若旋转 4 次&#xff0c;则可以得到 [4,5,6,7,0,1,2]若…

【MATLAB第113期】基于MATLAB的EFAST扩展傅里叶幅度敏感性分析方法(有目标函数)

【MATLAB第113期】基于MATLAB的EFAST扩展傅里叶幅度敏感性分析方法&#xff08;有目标函数&#xff09; 一、方法概述 扩展傅里叶幅度敏感性检验&#xff08;EFAST&#xff09;是一种基于频域分析的全局敏感性分析方法&#xff0c;能够同时评估模型参数的一阶敏感性&#xff…

Tiktok 关键字 视频及评论信息爬虫(1) [2025.04.07]

&#x1f64b;‍♀️Tiktok APP的基于关键字检索的视频及评论信息爬虫共分为两期&#xff0c;希望对大家有所帮助。 第一期见下文。 第二期&#xff1a;基于视频URL的评论信息爬取 1. Node.js环境配置 首先配置 JavaScript 运行环境&#xff08;如 Node.js&#xff09;&#x…

【愚公系列】《高效使用DeepSeek》058-选题策划

🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…

零基础教程:Windows电脑安装Linux系统(双系统/虚拟机)全攻略

一、安装方式选择 方案对比表 特性双系统安装虚拟机安装性能原生硬件性能依赖宿主机资源分配磁盘空间需要独立分区&#xff08;建议50GB&#xff09;动态分配&#xff08;默认20GB起&#xff09;内存占用独占全部内存需手动分配&#xff08;建议4GB&#xff09;启动方式开机选…

LeetCode 2968.执行操作使频率分数最大

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。 你可以对数组执行 至多 k 次操作&#xff1a; 从数组中选择一个下标 i &#xff0c;将 nums[i] 增加 或者 减少 1 。 最终数组的频率分数定义为数组中众数的 频率 。 请你返回你可以得到的 最大 频率分数。 众数指的…

excel经验

Q:我现在有一个excel&#xff0c;有一列数据&#xff0c;大概两千多行。如何在这一列中 筛选出具有关键字的内容&#xff0c;并输出到另外一列中。 A: 假设数据在A列&#xff08;A1开始&#xff09;&#xff0c;关键字为“ABC”在相邻空白列&#xff08;如B1&#xff09;输入公…

HTTP查询参数示例(XMLHttpRequest查询参数)(带查询参数的HTTP接口示例——以python flask接口为例)flask查询接口

文章目录 HTTP查询参数请求示例接口文档——获取城市列表代码示例效果 带查询参数的HTTP接口示例——以python flask接口为例app.pyREADME.md运行应用API示例客户端示例关键实现说明&#xff1a;运行方法&#xff1a; HTTP查询参数请求示例 接口文档——获取城市列表 代码示例…

将飞帆制作的网页作为 div 集成到自己的网页中

并且自己的网页可以和飞帆中的控件相互调用函数。效果&#xff1a; 上链接 将飞帆制作的网页作为 div 集成到自己的网页中 - 文贝 进入可以复制、运行代码

Redis主从复制:告别单身Redis!

目录 一、 为什么需要主从复制&#xff1f;&#x1f914;二、 如何搭建主从架构&#xff1f;前提条件✅步骤&#x1f4c1; 创建工作目录&#x1f4dc; 创建 Docker Compose 配置文件&#x1f680; 启动所有 Redis&#x1f50d; 验证主从状态 &#x1f4a1; 重要提示和后续改进 …

k8s 1.30.6版本部署(使用canal插件)

#系统环境准备 参考 https://blog.csdn.net/dingzy1/article/details/147062698?spm1001.2014.3001.5501 #配置下载源 curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/deb/Release.key |gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyri…

机器学习的一百个概念(7)独热编码

前言 本文隶属于专栏《机器学习的一百个概念》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见[《机器学习的一百个概念》 ima 知识库 知识库广场搜索&…

RHCSA复习

在Linux中&#xff0c; wrx 分别代表写&#xff08;write&#xff09;、读&#xff08;read&#xff09;和执行&#xff08;execute&#xff09;权限&#xff0c;它们对应的权限值分别是&#xff1a; - r &#xff08;读权限&#xff09;&#xff1a;权限值为4。 - w &am…

“乐企“平台如何重构业财税票全流程生态?

2025年&#xff0c;国家税务总局持续推进的"便民办税春风行动"再次推进数字化服务升级&#xff0c;其中"乐企"平台作为税务信息化的重要载体&#xff0c;持续优化数电票服务能力&#xff0c;为企业提供更高效、更规范的税务管理支持。在这一背景下&#xf…

Android audio(6)-audiopolicyservice介绍

AudioPolicyService 是策略的制定者&#xff0c;比如某种 Stream 类型不同设备的音量&#xff08;index/DB&#xff09;是多少、某种 Stream 类型的音频数据流对应什么设备等等。而 AudioFlinger 则是策略的执行者&#xff0c;例如具体如何与音频设备通信&#xff0c;维护现有系…

Boost库搜索引擎项目(版本1)

Boost库搜索引擎 项目开源地址 Github&#xff1a;https://github.com/H0308/BoostSearchingEngine Gitee&#xff1a;https://gitee.com/EPSDA/BoostSearchingEngine 版本声明 当前为最初版本&#xff0c;后续会根据其他内容对当前项目进行修改&#xff0c;具体见后续版本…

git分支合并信息查看

TortoiseGit工具 1、选择"Revision graph" 2、勾选view中的 Show branchings and merges Arrows point towards merges 3、图案说明 红色部分‌&#xff1a;代表当前分支 橙色部分‌&#xff1a;代表远程分支 黄色部分‌&#xff1a;代表一个tag 绿色部分‌&#xf…

Java学习笔记(多线程):ReentrantLock 源码分析

本文是自己的学习笔记&#xff0c;主要参考资料如下 JavaSE文档 1、AQS 概述1.1、锁的原理1.2、任务队列1.2.1、结点的状态变化 1.3、加锁和解锁的简单流程 2、ReentrantLock2.1、加锁源码分析2.1.1、tryAcquire()的具体实现2.1.2、acquirQueued()的具体实现2.1.3、tryLock的具…

在C++11及后续标准中,auto和decltype是用于类型推导的关键特性,它们的作用和用法。

在C11及后续标准中&#xff0c;auto和decltype是用于类型推导的关键特性&#xff0c;它们的作用和用法有所不同。以下是详细说明&#xff1a; 1. auto 关键字 基本作用 自动推导变量的类型&#xff08;根据初始化表达式&#xff09;主要用于简化代码&#xff0c;避免显式书写…