| | |
| | | <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> |
| | |
| | | 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 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]) |
| | |
| | | let eventMarkersLayer = null |
| | | let addressMarkersLayer = null |
| | | |
| | | let boundaryInstance = useAreaBoundary({ initFly: true }) |
| | | |
| | | const initMap = () => { |
| | | if (map) return |
| | | map = L.map('map', { |
| | |
| | | doubleClickZoom: false, |
| | | editable: true, //绘制控件 |
| | | }).setView([25.992338, 114.823254], 3) |
| | | boundaryInstance.initBoundary(map) |
| | | |
| | | map.whenReady(() => { |
| | | isMapReady.value = true |
| | |
| | | } |
| | | ) |
| | | } else { |
| | | console.log('Geolocation is not supported by this browser.') |
| | | console.log('该浏览器不支持地理定位功能。') |
| | | } |
| | | } |
| | | 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) |
| | | } |
| | | |
| | | if (device_sn !== '') { |
| | | getSignDroneEvents(device_sn) |
| | | } else { |
| | | initEvents() |
| | | } |
| | | } |
| | | |
| | | 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() |
| | |
| | | EventBus.on('mapSetView', mapSetView) |
| | | EventBus.on('mapAddMarker', mapAddMarker) |
| | | EventBus.on('mapClearMarker', mapClearMarker) |
| | | EventBus.on('mapOnlyShowDrone', mapOnlyShowDrone) |
| | | |
| | | initDrone() |
| | | initEvents() |
| | | // initEvents() |
| | | // L.circle([24, 112], { |
| | | // color: 'red', // 边框颜色 |
| | | // fillColor: '#f03', // 填充颜色 |
| | |
| | | EventBus.off('mapSetView', mapSetView) |
| | | EventBus.off('mapAddMarker', mapAddMarker) |
| | | 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 { |