| New file |
| | |
| | | <template> |
| | | <div class="page-container"> |
| | | <div class="map" id="map"></div> |
| | | <!-- <div class="layer-btn" @click="layerChangePopupShow = true" v-show="layerShow"> |
| | | <van-image width="20" height="20" :src="layerIcon" /> |
| | | <div class="label">图层</div> |
| | | </div> |
| | | |
| | | <div class="location-btn" @click="setMapLocation" v-show="locationShow"> |
| | | <van-image width="20" height="20" :src="locationIcon" /> |
| | | <div class="label">定位</div> |
| | | </div> --> |
| | | <slot name="searchBar"></slot> |
| | | <van-popup v-model:show="layerChangePopupShow" position="bottom" round> |
| | | <div class="popup-container"> |
| | | <div class="category-content"> |
| | | <div class="header"> |
| | | <div class="title">底图切换</div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div |
| | | class="layer-box" |
| | | :class="{ on: setSelectMapLayerKey === item.key }" |
| | | v-for="(item, ind) in layers" |
| | | :key="ind" |
| | | @click="setMapLayer(item)" |
| | | > |
| | | <div class="image"> |
| | | <van-image :src="item.src" width="100%" height="100%"></van-image> |
| | | </div> |
| | | <div class="label">{{ item.name }}</div> |
| | | </div> |
| | | </div> |
| | | </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 { 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' |
| | | const emit = defineEmits(['location-selected']) |
| | | const { weatherShow, layerShow, locationShow, createWorkShow } = defineProps({ |
| | | weatherShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | layerShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | locationShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | createWorkShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | }) |
| | | |
| | | const isProd = import.meta.env.VITE_APP_ENV === 'production' |
| | | |
| | | import { basemapLayer0, basemapLayer1, basemapLayer2, basemapLayer3 } from '@/const/leafletConst' |
| | | |
| | | const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1]) |
| | | const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3]) |
| | | |
| | | let map = null |
| | | const isMapReady = ref(false) |
| | | |
| | | const layers = [ |
| | | { |
| | | src: sl, |
| | | name: '矢量', |
| | | key: 1, |
| | | map: basemap0, |
| | | }, |
| | | { |
| | | src: yx, |
| | | name: '影像', |
| | | key: 2, |
| | | map: basemap1, |
| | | }, |
| | | ] |
| | | |
| | | const layerChangePopupShow = ref(false) |
| | | const setSelectMapLayerKey = ref(1) // 选择的地图图层 |
| | | |
| | | let droneMarkersLayer = null |
| | | let eventMarkersLayer = null |
| | | let addressMarkersLayer = null |
| | | let mapLayerSourceLayer = null // 空域信息 |
| | | |
| | | const initMap = () => { |
| | | if (map) return |
| | | map = L.map('map', { |
| | | preferCanvas: true, |
| | | crs: L.CRS.EPSG4326, |
| | | zoomControl: false, |
| | | attributionControl: false, |
| | | doubleClickZoom: false, |
| | | editable: true, //绘制控件 |
| | | }).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() |
| | | if (currentZoom >= 11) { |
| | | eventMarkersLayer.addTo(map) |
| | | } else { |
| | | eventMarkersLayer.remove() |
| | | } |
| | | }) |
| | | |
| | | if (createWorkShow) { |
| | | getMapLocation() |
| | | } |
| | | } |
| | | |
| | | const getCurMap = () => { |
| | | return map |
| | | } |
| | | |
| | | const setMapLayer = item => { |
| | | map.removeLayer(layers.find(i => i.key === setSelectMapLayerKey.value).map) |
| | | |
| | | map.addLayer(item.map) |
| | | |
| | | setSelectMapLayerKey.value = item.key |
| | | } |
| | | |
| | | let locationFlag = false |
| | | let watchId = null |
| | | let userLocationMarker = null |
| | | const setMapLocation = () => { |
| | | locationFlag = true |
| | | |
| | | // 在WebView加载的网页中 |
| | | // 检查浏览器是否支持 geolocation |
| | | if (navigator.geolocation) { |
| | | if (watchId) { |
| | | // 停止监听位置 |
| | | 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({ |
| | | iconUrl: userLocationIcon, // 图片路径 |
| | | iconSize: [24, 24], // 图标尺寸 |
| | | iconAnchor: [12, 12], // 锚点位置 |
| | | }), |
| | | }).addTo(map) |
| | | } |
| | | |
| | | mapSetView({ |
| | | lat, |
| | | lng, |
| | | }) |
| | | |
| | | locationFlag = false |
| | | |
| | | return |
| | | } |
| | | |
| | | if (userLocationMarker) { |
| | | userLocationMarker.setLatLng([lat, lng]) |
| | | } |
| | | }, |
| | | function (error) { |
| | | // 如果获取位置失败,执行回调 |
| | | console.error('Error occurred: ' + error.message) |
| | | }, |
| | | { |
| | | enableHighAccuracy: true, // 尝试获取更高精度的定位 |
| | | timeout: 10000, // 如果10秒内没有获取到位置,就中止 |
| | | maximumAge: 0, // 始终请求新的定位,不使用缓存 |
| | | } |
| | | ) |
| | | } else { |
| | | console.log('该浏览器不支持地理定位功能。') |
| | | } |
| | | } |
| | | let lastLocationMarker = null |
| | | const getMapLocation = () => { |
| | | map.off('click') |
| | | map.on('click', function (e) { |
| | | const { lat, lng } = e.latlng |
| | | |
| | | if (lastLocationMarker) { |
| | | map.removeLayer(lastLocationMarker) |
| | | } |
| | | lastLocationMarker = L.marker([lat, lng]).addTo(map) |
| | | emit('location-selected', { |
| | | latitude: lat, |
| | | longitude: lng, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const mapSetView = data => { |
| | | const { lat, lng, zoom = 16 } = data |
| | | |
| | | if (!map) return |
| | | |
| | | map.setView([lat, lng], zoom, { |
| | | animate: false, // 使用动画过渡 |
| | | }) |
| | | } |
| | | |
| | | const mapAddMarker = data => { |
| | | if (!map) return |
| | | const { lat, lng, zoom = 16, name = '', customClassName = '' } = data |
| | | mapSetView(data) |
| | | |
| | | // 创建带有图标和文字的 divIcon |
| | | let droneHtmlIcon = L.divIcon({ |
| | | html: ` |
| | | <div class="content ${customClassName}"> |
| | | <img src="${addressLocationIcon}"> |
| | | <span> |
| | | ${name} |
| | | </span> |
| | | </div> |
| | | `, |
| | | className: 'ztzf-address-custom-icon', // 可选,用于添加CSS类 |
| | | iconSize: [24, 38], // 设置图标大小 |
| | | iconAnchor: [12, 19], |
| | | }) |
| | | |
| | | L.marker([lat, lng], { icon: droneHtmlIcon }).addTo(addressMarkersLayer) |
| | | } |
| | | |
| | | // 吉安项目-空域信息 |
| | | // 统一的飞行函数 |
| | | function flyVisual(coordinates) { |
| | | // 确保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); |
| | | } |
| | | } |
| | | // 点击单个飞行 |
| | | function signFlyToMarker(item) { |
| | | const positionsData = JSON.parse(item.geoJson); |
| | | const coordinates = positionsData.coordinates; |
| | | |
| | | // 更全面的空值检查 |
| | | 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.geoJson) |
| | | .forEach(item => { |
| | | const parseParsed = JSON.parse(item.geoJson); // 转为数组 |
| | | 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() |
| | | } |
| | | |
| | | // 天气 |
| | | const store = useStore() |
| | | const selectedAreaCode = computed(() => store.state.user.selectedAreaCode) |
| | | |
| | | onMounted(async () => { |
| | | |
| | | await nextTick() |
| | | initMap() |
| | | map.addLayer(layers[0].map) |
| | | |
| | | EventBus.on('mapSetView', mapSetView) |
| | | EventBus.on('mapAddMarker', mapAddMarker) |
| | | EventBus.on('mapAddAirMarker', mapAddAirMarker) |
| | | EventBus.on('signFlyToMarker', signFlyToMarker) |
| | | EventBus.on('mapClearAirMarker', mapClearAirMarker) |
| | | EventBus.on('mapClearMarker', mapClearMarker) |
| | | |
| | | // L.circle([24, 112], { |
| | | // color: 'red', // 边框颜色 |
| | | // fillColor: '#f03', // 填充颜色 |
| | | // fillOpacity: 0.5, // 填充透明度 |
| | | // radius: 5000, // 半径,单位是米 |
| | | // }).addTo(map) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | EventBus.off('mapSetView', mapSetView) |
| | | EventBus.off('mapAddMarker', mapAddMarker) |
| | | EventBus.off('mapAddAirMarker', mapAddAirMarker) |
| | | EventBus.off('signFlyToMarker', signFlyToMarker) |
| | | EventBus.off('mapClearAirMarker', mapClearAirMarker) |
| | | EventBus.off('mapClearMarker', mapClearMarker) |
| | | }) |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .page-container { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | |
| | | .map { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | |
| | | .location-btn, |
| | | .layer-btn { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | position: absolute; |
| | | right: 8px; |
| | | width: 40px; |
| | | height: 40px; |
| | | background: #ffffff; |
| | | box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.2); |
| | | border-radius: 6px 6px 6px 6px; |
| | | z-index: 996; |
| | | |
| | | .label { |
| | | font-size: 10px; |
| | | } |
| | | } |
| | | |
| | | .layer-btn { |
| | | bottom: 130px; |
| | | } |
| | | |
| | | .location-btn { |
| | | bottom: 80px; |
| | | } |
| | | } |
| | | |
| | | .popup-container { |
| | | height: 300px; |
| | | overflow-y: auto; |
| | | padding: 10px 22px; |
| | | background: #fff; |
| | | border-radius: 12px 12px 0 0; |
| | | |
| | | .category-content { |
| | | margin-bottom: 10px; |
| | | .header { |
| | | margin-bottom: 10px; |
| | | height: 20px; |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | color: #222324; |
| | | line-height: 20px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | .base-header { |
| | | margin-top: 10px; |
| | | } |
| | | .content { |
| | | //padding: 12px; |
| | | display: flex; |
| | | background: #fff; |
| | | border-radius: 16px; |
| | | |
| | | .layer-box { |
| | | margin-left: calc((100% / 3 - 84px) * 3 / 2); |
| | | |
| | | &:first-child { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | &.on { |
| | | .image { |
| | | position: relative; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | border-radius: 4px 4px 4px 4px; |
| | | border: 4px solid #1d6fe9; |
| | | z-index: 1; |
| | | box-sizing: border-box; |
| | | } |
| | | } |
| | | |
| | | .label { |
| | | color: #1d6fe9; |
| | | } |
| | | } |
| | | |
| | | .image { |
| | | width: 84px; |
| | | height: 82px; |
| | | border-radius: 4px 4px 4px 4px; |
| | | overflow: hidden; |
| | | box-sizing: border-box; |
| | | border: none; |
| | | } |
| | | |
| | | .label { |
| | | margin-top: 6px; |
| | | height: 20px; |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #222324; |
| | | line-height: 20px; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | |
| | | .layer-box:nth-child(3n + 1) { |
| | | margin-left: 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |