| | |
| | | <template> |
| | | <div class="page-container"> |
| | | <div class="map" id="map"></div> |
| | | |
| | | <div class="weather-box" v-show="weatherShow"> |
| | | <div class="icon"> |
| | | <van-image :src="WEATHER?.[weather] || ''" width="100%" height="100%"></van-image> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <span class="tq">{{ weather }} {{ temperature }}</span> |
| | | <span class="tq">风速:{{ windVelocity }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="layer-btn" @click="layerChangePopupShow = true" v-show="layerShow"> |
| | | <van-image width="20" height="20" :src="layerIcon" /> |
| | | <div class="label">图层</div> |
| | |
| | | <van-image width="20" height="20" :src="locationIcon" /> |
| | | <div class="label">定位</div> |
| | | </div> |
| | | |
| | | <slot name="searchBar"></slot> |
| | | <slot name="eventNav"></slot> |
| | | |
| | | <van-popup v-model:show="layerChangePopupShow" position="bottom" round> |
| | | <div class="popup-container"> |
| | | <div class="category-content"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="category-content"> |
| | | <div class="header base-header"> |
| | | <div class="title">基础图层</div> |
| | | </div> |
| | | <div class="content"> |
| | | <BaseLayerManage v-if="isMapReady" :getCurMap="getCurMap" /> |
| | | <!-- <el-tree--> |
| | | <!-- :props="defaultProps"--> |
| | | <!-- :data="treeData"--> |
| | | <!-- show-checkbox--> |
| | | <!-- node-key="id"--> |
| | | <!-- :expand-on-click-node="false"--> |
| | | <!-- :check-on-click-node="false"--> |
| | | <!-- :check-on-click-leaf="false"--> |
| | | <!-- @check="handleCheckChange"--> |
| | | <!-- />--> |
| | | </div> |
| | | </div> |
| | | <div class="category-content"> |
| | | <MapLayerManage |
| | | v-for="item in customizeLayer" |
| | | :options="item" |
| | | :key="item.id" |
| | | v-if="isMapReady" |
| | | :getCurMap="getCurMap" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </van-popup> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as turf from '@turf/turf' |
| | | import addressLocationIcon from '@/appDataSource/leafletMapIcon/address-location.svg' |
| | | import droneIcon from '@/appDataSource/leafletMapIcon/drone-icon.svg' |
| | | import userLocationIcon from '@/appDataSource/leafletMapIcon/user-location.svg' |
| | | import layerIcon from '@/appDataSource/leafletMapIcon/layer-icon.svg' |
| | | import locationIcon from '@/appDataSource/leafletMapIcon/location-icon.svg' |
| | | import { useStore } from 'vuex' |
| | | import { getEventPageSimpleApi } from '@/api/home/event' |
| | | import { getDroneSuggest, getLayerTreeApi } from '@/api/home/common' |
| | | import DroneMarkerPopup from '@/appComponents/DroneMarkerPopup/index.vue' |
| | | import EventMarkerPopup from '@/appComponents/EventMarkerPopup/index.vue' |
| | | import MapLayerManage from './MapLayerManage.vue' |
| | | import BaseLayerManage from '@/appComponents/LeafletMap/BaseLayerManage.vue' |
| | | import { getDeviceRegion, getMapEvents } from '@/api/home/aggregation' |
| | | import { getIimitationArea } from '@/api/map/dji' |
| | | import { showToast } from 'vant' |
| | | import L from 'leaflet' |
| | | import 'leaflet/dist/leaflet.css' |
| | | import sl from '@/appDataSource/leafletMapIcon/sl.svg' |
| | | import yx from '@/appDataSource/leafletMapIcon/yx.svg' |
| | | import EventBus from '@/utils/eventBus' |
| | | import { getEventImage } from '@/utils/stateToImageMap/event' |
| | | import { WEATHER } from '@/const/weather' |
| | | const emit = defineEmits(['location-selected']) |
| | | const { weatherShow, layerShow, locationShow, createWorkShow } = defineProps({ |
| | | weatherShow: { |
| | |
| | | const isProd = import.meta.env.VITE_APP_ENV === 'production' |
| | | |
| | | import { basemapLayer0, basemapLayer1, basemapLayer2, basemapLayer3 } from '@/const/leafletConst' |
| | | import { useAreaBoundary } from '@/hooks/useAreaBoundary' |
| | | |
| | | const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1]) |
| | | const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3]) |
| | |
| | | const layers = [ |
| | | { |
| | | src: sl, |
| | | name: '天地图电子', |
| | | name: '矢量', |
| | | key: 1, |
| | | map: basemap0, |
| | | }, |
| | | { |
| | | src: yx, |
| | | name: '天地图影像', |
| | | name: '影像', |
| | | key: 2, |
| | | map: basemap1, |
| | | }, |
| | |
| | | let droneMarkersLayer = null |
| | | let eventMarkersLayer = null |
| | | let addressMarkersLayer = null |
| | | |
| | | let boundaryInstance = useAreaBoundary({ initFly: true }) |
| | | let mapLayerSourceLayer = null // 空域信息 |
| | | |
| | | const initMap = () => { |
| | | if (map) return |
| | |
| | | attributionControl: false, |
| | | doubleClickZoom: false, |
| | | editable: true, //绘制控件 |
| | | }).setView([25.992338, 114.823254], 3) |
| | | boundaryInstance.initBoundary(map) |
| | | }).setView([27.117445, 114.984917], 10) |
| | | |
| | | map.whenReady(() => { |
| | | isMapReady.value = true |
| | |
| | | droneMarkersLayer = L.layerGroup([], { pane: 'drones' }).addTo(map) // 创建一个标注层,便于管理和移除 |
| | | eventMarkersLayer = L.layerGroup([], { pane: 'events' }).addTo(map) // 创建一个标注层,便于管理和移除 |
| | | addressMarkersLayer = L.layerGroup([], { pane: 'addresses' }).addTo(map) // 创建一个标注层,便于管理和移除 |
| | | // 空域信息 |
| | | mapLayerSourceLayer = L.layerGroup([], { pane: 'mapLayerSource' }).addTo(map) |
| | | |
| | | map.on('zoomend', function () { |
| | | var currentZoom = map.getZoom() |
| | |
| | | let locationFlag = false |
| | | let watchId = null |
| | | let userLocationMarker = null |
| | | const setMapLocation = () => { |
| | | locationFlag = true |
| | | |
| | | // 初始化实时位置监听 |
| | | const initLocationWatch = () => { |
| | | // 在WebView加载的网页中 |
| | | // 检查浏览器是否支持 geolocation |
| | | if (navigator.geolocation) { |
| | |
| | | // 停止监听位置 |
| | | navigator.geolocation.clearWatch(watchId) |
| | | } |
| | | |
| | | // 开始持续获取用户的位置 |
| | | watchId = navigator.geolocation.watchPosition( |
| | | function (position) { |
| | |
| | | const lat = position.coords.latitude // 纬度 |
| | | const lng = position.coords.longitude // 经度 |
| | | |
| | | if (locationFlag) { |
| | | if (userLocationMarker) { |
| | | userLocationMarker.setLatLng([lat, lng]) |
| | | } else { |
| | | userLocationMarker = L.marker([lat, lng], { |
| | | icon: L.icon({ |
| | | // 确保位置标记存在并更新位置 |
| | | if (userLocationMarker) { |
| | | userLocationMarker.setLatLng([lat, lng]) |
| | | } else { |
| | | userLocationMarker = L.marker([lat, lng], { |
| | | icon: L.icon({ |
| | | iconUrl: userLocationIcon, // 图片路径 |
| | | iconSize: [24, 24], // 图标尺寸 |
| | | iconAnchor: [12, 12], // 锚点位置 |
| | | }), |
| | | }).addTo(map) |
| | | } |
| | | iconSize: [24, 24], // 图标尺寸 |
| | | iconAnchor: [12, 12], // 锚点位置 |
| | | }), |
| | | }).addTo(map) |
| | | } |
| | | |
| | | // 如果是点击定位按钮触发的,将地图视图定位到当前位置 |
| | | if (locationFlag) { |
| | | mapSetView({ |
| | | lat, |
| | | lng, |
| | | }) |
| | | |
| | | locationFlag = false |
| | | |
| | | return |
| | | } |
| | | |
| | | if (userLocationMarker) { |
| | | userLocationMarker.setLatLng([lat, lng]) |
| | | } |
| | | }, |
| | | function (error) { |
| | |
| | | } |
| | | ) |
| | | } else { |
| | | console.log('Geolocation is not supported by this browser.') |
| | | console.log('该浏览器不支持地理定位功能。') |
| | | } |
| | | } |
| | | |
| | | const setMapLocation = () => { |
| | | locationFlag = true |
| | | // 如果还没有开始监听位置,先初始化监听 |
| | | if (!watchId) { |
| | | initLocationWatch() |
| | | } else { |
| | | // 已经在监听位置,会自动定位到当前位置 |
| | | // 可以添加一个视觉反馈,比如位置标记闪烁效果 |
| | | if (userLocationMarker) { |
| | | // 添加闪烁效果 |
| | | userLocationMarker.getElement().classList.add('location-blink') |
| | | setTimeout(() => { |
| | | userLocationMarker.getElement().classList.remove('location-blink') |
| | | }, 1000) |
| | | } |
| | | } |
| | | } |
| | | let lastLocationMarker = null |
| | |
| | | |
| | | const mapAddMarker = data => { |
| | | if (!map) return |
| | | |
| | | const { lat, lng, zoom = 16, name = '', customClassName = '' } = data |
| | | |
| | | mapSetView(data) |
| | | |
| | | // 创建带有图标和文字的 divIcon |
| | |
| | | L.marker([lat, lng], { icon: droneHtmlIcon }).addTo(addressMarkersLayer) |
| | | } |
| | | |
| | | const mapOnlyShowDrone = (data = {}) => { |
| | | const { device_sn = '' } = data |
| | | eventMarkersLayer.clearLayers() |
| | | |
| | | if (droneAllData.length > 0 && device_sn !== '') { |
| | | mapInitDroneMarkers(device_sn) |
| | | } else { |
| | | initDrone(device_sn) |
| | | // 吉安项目-空域信息 |
| | | // 统一的飞行函数 |
| | | function flyVisual(coordinates) { |
| | | console.log(coordinates, '0000') |
| | | // 确保map对象存在且坐标有效 |
| | | if (!map || !coordinates || !coordinates.length) return; |
| | | |
| | | try { |
| | | // 改进坐标格式判断 |
| | | let isSinglePoint = false; |
| | | let latLng; |
| | | let bounds; |
| | | |
| | | // 处理不同格式的坐标输入 |
| | | if (Array.isArray(coordinates[0])) { |
| | | if (coordinates[0].length === 2) { |
| | | // 格式: [[lng, lat]] - 单个点 |
| | | isSinglePoint = true; |
| | | latLng = [coordinates[0][1], coordinates[0][0]]; // 转换为 [lat, lng] |
| | | } else if (coordinates[0].length > 2 && Array.isArray(coordinates[0][0])) { |
| | | // 格式: [[[lng, lat], [lng, lat], ...]] - 多边形 |
| | | const polygonCoords = coordinates[0].map(coord => [coord[1], coord[0]]); |
| | | bounds = L.latLngBounds(polygonCoords); |
| | | } else { |
| | | // 格式: [[lng, lat], [lng, lat], ...] - 多个点或线 |
| | | if (coordinates.length === 1) { |
| | | // 单个点 |
| | | isSinglePoint = true; |
| | | latLng = [coordinates[0][1], coordinates[0][0]]; |
| | | } else { |
| | | // 多个点 |
| | | const leafletCoords = coordinates.map(coord => [coord[1], coord[0]]); |
| | | bounds = L.latLngBounds(leafletCoords); |
| | | } |
| | | } |
| | | } else if (coordinates.length === 2) { |
| | | // 格式: [lng, lat] - 单个点 |
| | | isSinglePoint = true; |
| | | latLng = [coordinates[1], coordinates[0]]; |
| | | } |
| | | |
| | | if (isSinglePoint && latLng) { |
| | | // 单个坐标点 |
| | | map.flyTo(latLng, 10, { |
| | | duration: 1, |
| | | easeLinearity: 0.2 |
| | | }); |
| | | } else if (bounds) { |
| | | // 多边形或多个点 |
| | | map.flyToBounds(bounds, { |
| | | padding: [30, 30], |
| | | duration: 2, |
| | | maxZoom: 13 |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error('flyVisual 函数执行错误:', error); |
| | | } |
| | | |
| | | if (device_sn !== '') { |
| | | getSignDroneEvents(device_sn) |
| | | } else { |
| | | initEvents() |
| | | } |
| | | // 点击单个飞行 |
| | | function signFlyToMarker(item) { |
| | | const positionsData = JSON.parse(item.content); |
| | | const coordinates = positionsData.coordinates; |
| | | console.log('coordinates数据:', coordinates, '类型:', typeof coordinates, '长度:', coordinates?.length); |
| | | |
| | | // 更全面的空值检查 |
| | | let hasValidCoordinates = false; |
| | | |
| | | // 检查不同格式的坐标数据 |
| | | if (Array.isArray(coordinates)) { |
| | | if (coordinates.length > 0) { |
| | | // 检查是否有有效的坐标点 |
| | | if (Array.isArray(coordinates[0])) { |
| | | if (coordinates[0].length === 2 || coordinates[0].length === 3) { |
| | | // 格式: [lng, lat] 或 [lng, lat, alt] |
| | | hasValidCoordinates = true; |
| | | } else if (Array.isArray(coordinates[0][0])) { |
| | | // 格式: [[lng, lat], [lng, lat], ...] |
| | | hasValidCoordinates = coordinates[0].length > 0; |
| | | } else if (Array.isArray(coordinates[0][0][0])) { |
| | | // 格式: [[[lng, lat], [lng, lat], ...]] |
| | | hasValidCoordinates = coordinates[0][0].length > 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!hasValidCoordinates) { |
| | | console.log('没有有效的坐标数据,显示提示'); |
| | | return showToast('暂无区域数据'); |
| | | } |
| | | |
| | | // 单个位置飞行 |
| | | flyVisual(coordinates); |
| | | } |
| | | // 清空所有空域信息 |
| | | function mapClearAirMarker() { |
| | | if (!map) return |
| | | // 清空所有图层 |
| | | if (mapLayerSourceLayer) { |
| | | mapLayerSourceLayer.clearLayers(); |
| | | } |
| | | } |
| | | function mapAddAirMarker(data) { |
| | | // 清空所有图层 |
| | | mapClearAirMarker() |
| | | |
| | | data |
| | | .filter(item => item.content) |
| | | .forEach(item => { |
| | | const parseParsed = JSON.parse(item.content); // 转为数组 |
| | | if (parseParsed.type !== 'Polygon') return |
| | | const parsed = parseParsed.coordinates[0] |
| | | |
| | | const grouped = parsed.flat(); // 再展开 |
| | | |
| | | // 3. 计算多边形质心 |
| | | const turfPolygon = turf.polygon([parsed]); |
| | | const center = turf.centerOfMass(turfPolygon); |
| | | const centerLatLng = [center.geometry.coordinates[1], center.geometry.coordinates[0]]; |
| | | |
| | | const labelMarker = L.marker(centerLatLng, { |
| | | icon: L.divIcon({ |
| | | className: 'polygon-label', |
| | | html: ` |
| | | <div style=" |
| | | color: ${item.airspaceType === '0' ? '#008013' : '#ff0000'}; |
| | | font-weight: bold; |
| | | font-size: 12px; |
| | | font-family: 'Source Han Sans CN', 'Source Han Sans CN'; |
| | | white-space: nowrap; |
| | | "> |
| | | ${item.airspaceType === '1' ? '禁飞区' : item.airspaceType === '2' ? '危险区' : item.airspaceType === '3' ? '限制区' : item.airspaceType === '0' ? '适飞区' : '未知'} |
| | | </div> |
| | | `, |
| | | iconSize: null, // 让div决定大小 |
| | | iconAnchor: null // 不需要锚点,用CSS transform居中 |
| | | }), |
| | | interactive: false, // 标签不参与交互 |
| | | // zIndexOffset: 1000 // 确保标签在最上层 |
| | | }); |
| | | |
| | | // 将坐标数组转换为 Leaflet 格式 [lat, lng, lat, lng...] -> [[lat, lng], [lat, lng]...] |
| | | const latLngs = []; |
| | | for (let i = 0; i < grouped.length; i += 2) { |
| | | latLngs.push([grouped[i + 1], grouped[i]]); // 注意:Leaflet 是 [lat, lng] 顺序 |
| | | } |
| | | |
| | | // 创建多边形(填充区域) |
| | | const polygon = L.polygon(latLngs, { |
| | | color: item.airspaceType === '0' ? '#00ff06' : '#ff0000', |
| | | fillColor: item.airspaceType === '0' ? '#00ff06' : '#ff0000', |
| | | fillOpacity: 0.5, |
| | | weight: 0, // 隐藏多边形边框,使用折线作为边框 |
| | | }); |
| | | |
| | | // 创建折线(边框) |
| | | const polyline = L.polyline([...latLngs, latLngs[0]], { // 闭合多边形 |
| | | color: item.airspaceType === '0' ? '#00ff06' : '#ff0000', |
| | | weight: 1.5, |
| | | opacity: 1, |
| | | fill: false |
| | | }); |
| | | |
| | | // 将两个图层组合在一起 |
| | | const group = L.featureGroup([polygon, polyline, labelMarker]); |
| | | // 添加到图层组 |
| | | mapLayerSourceLayer.addLayer(group); |
| | | }); |
| | | } |
| | | |
| | | const mapClearMarker = () => { |
| | | addressMarkersLayer.clearLayers() |
| | | } |
| | | |
| | | let droneAllData = [] |
| | | |
| | | const initDrone = (device_sn = '') => { |
| | | getDeviceRegion({ |
| | | hidden_flag: 0, |
| | | }).then(res => { |
| | | const data = res?.data?.data || [] |
| | | |
| | | droneAllData = data.filter(item => item.status !== 'OFFLINE') |
| | | |
| | | mapInitDroneMarkers(device_sn) |
| | | }) |
| | | } |
| | | |
| | | const mapInitDroneMarkers = (device_sn = '') => { |
| | | droneMarkersLayer.clearLayers() |
| | | |
| | | let materialColor = 'rgba(27, 215, 89, 40)' |
| | | let frameColor = 'rgba(27, 215, 89, 255)' |
| | | |
| | | droneAllData |
| | | .filter(item => item.device_sn === device_sn || device_sn === '') |
| | | .forEach(item => { |
| | | let event_num = item.event_num || 0 |
| | | |
| | | if (event_num > 100) { |
| | | materialColor = 'rgba(255, 37, 9, 40)' |
| | | frameColor = 'rgba(255, 37, 9, 255)' |
| | | } else if (event_num > 50) { |
| | | materialColor = 'rgba(255, 171, 54, 40)' |
| | | frameColor = 'rgba(255, 171, 54, 255)' |
| | | } else { |
| | | materialColor = 'rgba(27, 215, 89, 10)' |
| | | frameColor = 'rgba(27, 215, 89, 255)' |
| | | } |
| | | |
| | | buildCirclePolygon(item.latitude, item.longitude, materialColor, frameColor).addTo(droneMarkersLayer) |
| | | |
| | | publicAddMakers({ |
| | | ...item, |
| | | makerType: 1, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const initEvents = () => { |
| | | getMapEvents({}).then(res => { |
| | | const resData = res?.data?.data?.data || [] |
| | | |
| | | resData.forEach(item => { |
| | | publicAddMakers({ |
| | | ...item, |
| | | makerType: 2, |
| | | }) |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const getSignDroneEvents = (device_sn = '') => { |
| | | const params = { |
| | | 'device_sn': device_sn, |
| | | } |
| | | |
| | | getEventPageSimpleApi(params, { current: 1, size: 999999 }).then(res => { |
| | | const resData = res?.data?.data || [] |
| | | |
| | | resData.forEach(item => { |
| | | publicAddMakers({ |
| | | ...item, |
| | | makerType: 2, |
| | | }) |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 天气 |
| | | const store = useStore() |
| | | const selectedAreaCode = computed(() => store.state.user.selectedAreaCode) |
| | | |
| | | const weather = ref('') |
| | | // 风速 |
| | | const windVelocity = ref('') |
| | | |
| | | const temperature = ref('') |
| | | |
| | | // 获取天气建议 |
| | | const getWeatherSuggest = () => { |
| | | getDroneSuggest({ areaCode: selectedAreaCode.value }).then(res => { |
| | | if (res.data.code !== 0) return |
| | | weather.value = res.data.data.weather |
| | | windVelocity.value = res.data.data.windPower |
| | | temperature.value = res.data.data.temperature |
| | | }) |
| | | } |
| | | |
| | | // 获取后台自定义图层 |
| | | const customizeLayer = ref(null) |
| | | function getLayerTree() { |
| | | getLayerTreeApi().then(res => { |
| | | const colorList = ['#00ff06', '#ff0000', '#00ffea'] |
| | | // const imgList = [dzwlSvg,zdyjfqSvg,gtkjghSvg] |
| | | customizeLayer.value = res.data.data.map((item, index) => ({ |
| | | ...item, |
| | | color: colorList[index], |
| | | imgIcon: [], // imgList[index] |
| | | })) |
| | | if (isProd) { |
| | | customizeLayer.value = customizeLayer.value.filter(i => i.id === 1) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | getLayerTree() |
| | | getWeatherSuggest() |
| | | |
| | | await nextTick() |
| | | initMap() |
| | | map.addLayer(layers[0].map) |
| | | |
| | | // 初始化实时位置监听 |
| | | initLocationWatch() |
| | | |
| | | EventBus.on('mapSetView', mapSetView) |
| | | EventBus.on('mapAddMarker', mapAddMarker) |
| | | EventBus.on('mapAddAirMarker', mapAddAirMarker) |
| | | EventBus.on('signFlyToMarker', signFlyToMarker) |
| | | EventBus.on('mapClearAirMarker', mapClearAirMarker) |
| | | EventBus.on('mapClearMarker', mapClearMarker) |
| | | EventBus.on('mapOnlyShowDrone', mapOnlyShowDrone) |
| | | |
| | | initDrone() |
| | | initEvents() |
| | | // initEvents() |
| | | // L.circle([24, 112], { |
| | | // color: 'red', // 边框颜色 |
| | | // fillColor: '#f03', // 填充颜色 |
| | |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | // 清理位置监听 |
| | | if (watchId) { |
| | | navigator.geolocation.clearWatch(watchId) |
| | | watchId = null |
| | | } |
| | | // 移除位置标记 |
| | | if (userLocationMarker) { |
| | | map.removeLayer(userLocationMarker) |
| | | userLocationMarker = null |
| | | } |
| | | |
| | | EventBus.off('mapSetView', mapSetView) |
| | | EventBus.off('mapAddMarker', mapAddMarker) |
| | | EventBus.off('mapAddAirMarker', mapAddAirMarker) |
| | | EventBus.off('signFlyToMarker', signFlyToMarker) |
| | | EventBus.off('mapClearAirMarker', mapClearAirMarker) |
| | | EventBus.off('mapClearMarker', mapClearMarker) |
| | | EventBus.off('mapOnlyShowDrone', mapOnlyShowDrone) |
| | | }) |
| | | |
| | | function publicAddMakers(data) { |
| | | // makerType 1 = 机巢, 2 = 事件点 |
| | | const { makerType, latitude, longitude } = data |
| | | |
| | | const customIcon = L.icon({ |
| | | iconUrl: makerType === 1 ? droneIcon : getEventImage(data.status), // 替换为你的图片路径 |
| | | iconSize: makerType === 1 ? [24, 24] : [20, 20], // 图标大小 |
| | | iconAnchor: makerType === 1 ? [12, 12] : [10, 10], // 图标的锚点(设置图片的底部中心) |
| | | }) |
| | | |
| | | let marker = null |
| | | |
| | | if (makerType === 1) { |
| | | // 创建带有图标和文字的 divIcon |
| | | let droneHtmlIcon = L.divIcon({ |
| | | html: ` |
| | | <div class="content"> |
| | | <img src="${droneIcon}"> |
| | | <span> |
| | | ${data.nickname} |
| | | </span> |
| | | </div> |
| | | `, |
| | | className: 'ztzf-drone-custom-icon', // 可选,用于添加CSS类 |
| | | iconSize: [24, 24], // 设置图标大小 |
| | | iconAnchor: [12, 12], |
| | | }) |
| | | |
| | | marker = L.marker([latitude, longitude], { icon: droneHtmlIcon, zIndexOffset: 9 }).addTo(droneMarkersLayer) |
| | | } else { |
| | | marker = L.marker([latitude, longitude], { |
| | | icon: customIcon, |
| | | zIndexOffset: 1, |
| | | }).addTo(eventMarkersLayer) |
| | | } |
| | | |
| | | marker.on('click', e => { |
| | | // 当前点击要素---关联数据 |
| | | marker.unbindPopup() |
| | | |
| | | const zoomLevel = map.getZoom() |
| | | |
| | | if (makerType === 1) { |
| | | if (zoomLevel < 11) { |
| | | mapSetView({ |
| | | lat: e.latlng.lat, |
| | | lng: e.latlng.lng, |
| | | zoom: 11, |
| | | }) |
| | | |
| | | return |
| | | } |
| | | } |
| | | |
| | | if (makerType !== 1) { |
| | | if (zoomLevel < 13) { |
| | | mapSetView({ |
| | | lat: e.latlng.lat, |
| | | lng: e.latlng.lng, |
| | | zoom: 13, |
| | | }) |
| | | |
| | | return |
| | | } |
| | | } |
| | | |
| | | const container = document.createElement('div') |
| | | |
| | | const popupApp = createApp(makerType === 1 ? DroneMarkerPopup : EventMarkerPopup, { |
| | | data: e.target.options.customData, |
| | | onClose: () => { |
| | | marker.closePopup() |
| | | popupApp.unmount() // 卸载 Vue 应用 |
| | | }, |
| | | }) |
| | | |
| | | popupApp.mount(container) |
| | | |
| | | marker |
| | | .bindPopup(container, { |
| | | closeButton: false, |
| | | className: 'custom-leaflet-popup', |
| | | offset: L.point(80, 0), |
| | | }) |
| | | .openPopup() |
| | | }) |
| | | |
| | | // 使用 marker 的 id 作为标识符 |
| | | marker.options.customData = data |
| | | } |
| | | |
| | | // 构建圆形多边形 |
| | | function buildCirclePolygon(lat, lng, materialColor, frameColor) { |
| | | var radius = 5000 / 100460 // 5000米转为大约的纬度差(纬度上的1度大约是111km) |
| | | var parts = [] |
| | | |
| | | for (var i = 0; i < 360; i++) { |
| | | var radians = ((i + 1) * Math.PI) / 180 |
| | | |
| | | // 计算新的点 |
| | | var circlePoint = [ |
| | | Math.cos(radians) * radius + lat, // 纬度偏移 |
| | | Math.sin(radians) * radius + lng, // 经度偏移,需考虑纬度的影响 |
| | | ] |
| | | |
| | | parts[i] = circlePoint |
| | | } |
| | | |
| | | // 生成多边形,近似圆形 |
| | | var polygon = L.polygon(parts, { |
| | | color: frameColor, |
| | | fillColor: materialColor, |
| | | weight: 1, |
| | | }) |
| | | |
| | | return polygon |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | |
| | | height: 100%; |
| | | } |
| | | |
| | | .weather-box { |
| | | padding: 4px 0; |
| | | padding-right: 6px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | position: absolute; |
| | | top: 12px; |
| | | left: 12px; |
| | | |
| | | height: 48px; |
| | | background: #fff; |
| | | z-index: 996; |
| | | |
| | | box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.18); |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | .icon { |
| | | margin-left: 10px; |
| | | width: 24px; |
| | | height: 24px; |
| | | } |
| | | |
| | | .content { |
| | | height: 100%; |
| | | margin-left: 4px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-around; |
| | | |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #757575; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .tq { |
| | | margin: 0 5px; |
| | | } |
| | | .qk { |
| | | margin-left: 5px; |
| | | color: #04f043; |
| | | } |
| | | .yellowqk { |
| | | margin-left: 5px; |
| | | color: yellow; |
| | | } |
| | | .redqk { |
| | | margin-left: 5px; |
| | | color: red; |
| | | } |
| | | } |
| | | |
| | | .location-btn, |
| | | .layer-btn { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 位置标记闪烁效果 |
| | | .location-blink { |
| | | animation: blink 1s ease-in-out; |
| | | } |
| | | |
| | | @keyframes blink { |
| | | 0% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | 50% { |
| | | transform: scale(1.2); |
| | | opacity: 0.8; |
| | | } |
| | | 100% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | } |
| | | </style> |