| | |
| | | <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="back-btn" @click="goBackWork" v-show="workNavigationShow"> |
| | | <van-image width="20" height="20" :src="backIcon" /> |
| | | <div class="label">返回</div> |
| | | </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> |
| | | <!-- 事件导航按钮 --> |
| | | <div class="work-btn" v-show="workNavigationShow"> |
| | | <van-button round block type="primary" color="#1D6FE9" @click="openAmap">事件导航</van-button> |
| | | </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 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 class="location-btn" @click="setMapLocation" v-show="locationShow"> |
| | | <van-image width="20" height="20" :src="locationIcon" /> |
| | | <div class="label">定位</div> |
| | | </div> |
| | | </div> |
| | | </van-popup> |
| | | <div class="nav-btn" @click="openNavigation" v-show="navigationTarget"> |
| | | <van-image width="20" height="20" :src="mapnavIcon" /> |
| | | <div class="label">导航</div> |
| | | </div> |
| | | <van-action-sheet |
| | | v-model:show="navigationSheetShow" |
| | | :actions="navigationActions" |
| | | cancel-text="取消" |
| | | @select="handleNavigationSelect" |
| | | /> |
| | | |
| | | |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { searchGeocoder } from '@/utils/util' |
| | | import { showToast } from 'vant' |
| | | import mapnavIcon from '@/appDataSource/appwork/mapnav.svg' |
| | | import backIcon from '@/appDataSource/leafletMapIcon/back-icon.svg' |
| | | import incidentPoint from '@/appDataSource/appwork/positioning.svg' |
| | | import droneIcon from '@/appDataSource/leafletMapIcon/drone-icon.svg' |
| | | import incidentPoint from '@/appDataSource/appwork/positioning1.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 { getDroneSuggest } from '@/api/home/common' |
| | | import DroneMarkerPopup from '@/appComponents/DroneMarkerPopup/index.vue' |
| | | import EventMarkerPopup from '@/appComponents/EventMarkerPopup/index.vue' |
| | | import { basemapLayer0, basemapLayer1, basemapLayer2, basemapLayer3 } from '@/const/leafletConst' |
| | | |
| | | import L from 'leaflet' |
| | |
| | | import sl from '@/appDataSource/leafletMapIcon/sl.svg' |
| | | import yx from '@/appDataSource/leafletMapIcon/yx.svg' |
| | | import EventBus from '@/utils/eventBus' |
| | | import { polygonTransformScale } from '@/utils/turfFunc' |
| | | import { getEventImage } from '@/utils/stateToImageMap/event' |
| | | import { getDroneStatusImage } from '@/utils/stateToImageMap/drone' |
| | | import { WEATHER } from '@/const/weather' |
| | | |
| | | import { wgs84ToGcj02 } from '@/utils/coordinateTransformation' |
| | | const emit = defineEmits(['location-selected']) |
| | | import _ from 'lodash' |
| | | import { useAreaBoundary } from '@/hooks/useAreaBoundary' |
| | | const { weatherShow, layerShow, locationShow, createWorkShow, workNavigationShow, mapCurrentDetail, clearMarker } = |
| | | const { locationShow, createWorkShow, workNavigationShow, mapCurrentDetail, clearMarker } = |
| | | defineProps({ |
| | | weatherShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | layerShow: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | locationShow: { |
| | | type: Boolean, |
| | | default: false, |
| | |
| | | }) |
| | | const currentAddress = ref('') |
| | | const AMAP_KEY = '5b8ba312d053e4bdb44911733ece7d63' |
| | | const navigationSheetShow = ref(false) |
| | | const navigationActions = [{ name: '高德地图' }, { name: '百度地图' }, { name: '腾讯地图' }] |
| | | const navigationTarget = computed(() => { |
| | | if (!workNavigationShow || !mapCurrentDetail?.longitude || !mapCurrentDetail?.latitude) return null |
| | | |
| | | const lat = parseFloat(mapCurrentDetail.latitude) |
| | | const lng = parseFloat(mapCurrentDetail.longitude) |
| | | if (Number.isNaN(lat) || Number.isNaN(lng)) return null |
| | | |
| | | return { |
| | | lat, |
| | | lng, |
| | | name: mapCurrentDetail.eventLocation || mapCurrentDetail.address || '工单位置', |
| | | } |
| | | }) |
| | | |
| | | const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1]) |
| | | const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3]) |
| | |
| | | }, |
| | | ] |
| | | |
| | | const layerChangePopupShow = ref(false) |
| | | const setSelectMapLayerKey = ref(1) // 选择的地图图层 |
| | | let boundaryInstance = useAreaBoundary({ initFly: true }) |
| | | let markersLayer = null |
| | | const initMap = () => { |
| | | if (map) return |
| | |
| | | doubleClickZoom: false, |
| | | editable: true, //绘制控件 |
| | | }).setView([25.992338, 114.823254], 13) |
| | | boundaryInstance.initBoundary(map) |
| | | |
| | | markersLayer = L.layerGroup().addTo(map) // 创建一个标注层,便于管理和移除 |
| | | if (createWorkShow) { |
| | |
| | | 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 (userLocationMarker) { |
| | | userLocationMarker.setLatLng([lat, lng]) |
| | | } |
| | | |
| | | if (locationFlag) { |
| | | } else { |
| | | userLocationMarker = L.marker([lat, lng], { |
| | | icon: L.icon({ |
| | | iconUrl: userLocationIcon, // 图片路径 |
| | | iconSize: [24, 24], // 图标尺寸 |
| | | iconAnchor: [12, 12], // 锚点位置 |
| | | }), |
| | | iconSize: [24, 24], // 图标尺寸 |
| | | iconAnchor: [12, 12], // 锚点位置 |
| | | }), |
| | | }).addTo(map) |
| | | } |
| | | |
| | | // 如果是点击定位按钮触发的,将地图视图定位到当前位置 |
| | | if (locationFlag) { |
| | | mapSetView({ |
| | | lat, |
| | | lng, |
| | | }) |
| | | |
| | | locationFlag = false |
| | | } |
| | | // 可以根据位置更新地图或界面 |
| | | }, |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | |
| | | function openNavigation() { |
| | | if (!navigationTarget.value) { |
| | | showToast('暂无可导航的位置') |
| | | return |
| | | } |
| | | |
| | | // 获取定位信息 |
| | | // uni.getLocation({ |
| | | // type: 'wgs84', // 返回的经纬度是 WGS84 坐标系(适合地图定位) |
| | | // isHighAccuracy: true, |
| | | // success: function (res) { |
| | | // location.value = res |
| | | // }, |
| | | // fail: function (err) { |
| | | // console.log('定位失败:', err) |
| | | // }, |
| | | // }) |
| | | navigationSheetShow.value = true |
| | | } |
| | | |
| | | function handleNavigationSelect(action) { |
| | | navigationSheetShow.value = false |
| | | const url = getNavigationUrl(action.name, navigationTarget.value) |
| | | if (url) { |
| | | window.location.href = url |
| | | } |
| | | } |
| | | |
| | | function getNavigationUrl(type, target) { |
| | | const [gcjLng, gcjLat] = wgs84ToGcj02(target.lng, target.lat) |
| | | const name = encodeURIComponent(target.name) |
| | | |
| | | if (type === '高德地图') { |
| | | return `https://uri.amap.com/navigation?to=${gcjLng},${gcjLat},${name}&mode=car&policy=1&coordinate=gaode&callnative=1` |
| | | } |
| | | |
| | | if (type === '百度地图') { |
| | | return `https://api.map.baidu.com/direction?destination=latlng:${gcjLat},${gcjLng}|name:${name}&mode=driving&coord_type=gcj02&output=html&src=ja-web` |
| | | } |
| | | |
| | | if (type === '腾讯地图') { |
| | | return `https://apis.map.qq.com/uri/v1/routeplan?type=drive&tocoord=${gcjLat},${gcjLng}&to=${name}&referer=ja-web` |
| | | } |
| | | |
| | | return '' |
| | | } |
| | | |
| | | let lastLocationMarker = null |
| | |
| | | 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 goBackWork = () => { |
| | | const transmitData = { data: { type: 'workDetailback' } } |
| | | wx.miniProgram.navigateBack({ |
| | | delta: 1, |
| | | }) |
| | | wx.miniProgram.postMessage(transmitData) |
| | | uni.postMessage(transmitData) |
| | | |
| | | markersLayer.removeLayer(incidentMarker) |
| | | } |
| | | // 存储事件点标记实例,用于后续更新或移除 |
| | | let incidentMarker = null |
| | | |
| | | function formatCoordinate(value) { |
| | | const numberValue = Number(value) |
| | | return Number.isNaN(numberValue) ? value : numberValue.toFixed(6) |
| | | } |
| | | |
| | | // 添加事件点标记 |
| | | function addIncidentMarker(data) { |
| | |
| | | |
| | | // 存储关联数据 |
| | | incidentMarker.options.customData = data |
| | | incidentMarker.bindTooltip( |
| | | `<div class="coordinate-label"> |
| | | <div>经度:${formatCoordinate(lng)}</div> |
| | | <div>纬度:${formatCoordinate(lat)}</div> |
| | | </div>`, |
| | | { |
| | | permanent: true, |
| | | direction: 'top', |
| | | offset: L.point(0, -16), |
| | | opacity: 1, |
| | | className: 'incident-coordinate-tooltip', |
| | | } |
| | | ) |
| | | |
| | | // 定位到该标记点 |
| | | mapSetView({ |
| | |
| | | watch( |
| | | () => mapCurrentDetail, |
| | | newVal => { |
| | | const newDataMap=newVal |
| | | // 只有 workNavigationShow 为 true 且有经纬度时才添加标记 |
| | | if (workNavigationShow && newVal?.longitude && newVal?.latitude) { |
| | | addIncidentMarker(newVal) |
| | | mapCurrentDetailData.value = newVal |
| | | if (workNavigationShow && newDataMap?.longitude && newDataMap?.latitude) { |
| | | addIncidentMarker(newDataMap) |
| | | mapCurrentDetailData.value = newDataMap |
| | | } else if (!workNavigationShow && incidentMarker) { |
| | | // 当 workNavigationShow 变为 false 时,移除已添加的标记 |
| | | markersLayer.removeLayer(incidentMarker) |
| | |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | const openAmap = () => { |
| | | if (!mapCurrentDetailData.value.longitude || !mapCurrentDetailData.value.latitude) { |
| | | showToast('未获取到事件坐标') |
| | | return |
| | | } |
| | | // wgs84ToGcj02(Number(item.longitude), Number(item.latitude)) |
| | | // wgs84ToGcj02(mapCurrentDetailData.value.longitude, mapCurrentDetailData.value.latitude) |
| | | const [gcjLng, gcjLat] = wgs84ToGcj02( |
| | | Number(mapCurrentDetailData.value.longitude), |
| | | Number(mapCurrentDetailData.value.latitude) |
| | | ) |
| | | |
| | | // 高德地图URL Scheme |
| | | const url = `https://uri.amap.com/marker?position=${gcjLng},${gcjLat}&name=%E4%BA%8B%E4%BB%B6%E7%82%B9&from='ztzf'` |
| | | |
| | | const transmitData = { data: { type: 'browser', data: { url: url } } } |
| | | wx.miniProgram.navigateTo({ url: `/subPackages/browser/index?url=${encodeURIComponent(url)}` }) |
| | | wx.miniProgram.postMessage(transmitData) |
| | | uni.postMessage(transmitData) |
| | | } |
| | | onMounted(async () => { |
| | | getWeatherSuggest() |
| | | |
| | | await nextTick() |
| | | initMap() |
| | | map.addLayer(layers[0].map) |
| | | |
| | | // 初始化实时位置监听 |
| | | initLocationWatch() |
| | | |
| | | EventBus.on('mapSetView', mapSetView) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | // 清理位置监听 |
| | | if (watchId) { |
| | | navigator.geolocation.clearWatch(watchId) |
| | | watchId = null |
| | | } |
| | | // 移除位置标记 |
| | | if (userLocationMarker) { |
| | | map.removeLayer(userLocationMarker) |
| | | userLocationMarker = null |
| | | } |
| | | |
| | | EventBus.off('mapSetView', mapSetView) |
| | | if (map) { |
| | | map.remove() |
| | |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | // 事件导航按钮 |
| | | .work-btn { |
| | | position: absolute; |
| | | bottom: 80px; |
| | | left: 0; |
| | | right: 0; |
| | | z-index: 996; |
| | | padding: 0 16px; |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .work-btn :deep(.van-button) { |
| | | width: 100%; |
| | | max-width: 194px; |
| | | height: 38px; |
| | | } |
| | | |
| | | .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, |
| | | .back-btn { |
| | | .nav-btn{ |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | |
| | | font-size: 10px; |
| | | } |
| | | } |
| | | .back-btn { |
| | | bottom: 185px; |
| | | } |
| | | .layer-btn { |
| | | bottom: 130px; |
| | | } |
| | | |
| | | .location-btn { |
| | | bottom: 76px; |
| | | } |
| | | .nav-btn { |
| | | bottom: 28px; |
| | | } |
| | | } |
| | | |
| | | .popup-container { |
| | | padding: 10px 22px; |
| | | background: #fff; |
| | | border-radius: 12px 12px 0 0; |
| | | // 位置标记闪烁效果 |
| | | .location-blink { |
| | | animation: blink 1s ease-in-out; |
| | | } |
| | | |
| | | .category-content { |
| | | .header { |
| | | 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; |
| | | } |
| | | :deep(.incident-coordinate-tooltip) { |
| | | padding: 0; |
| | | border: 0; |
| | | background: transparent; |
| | | box-shadow: none; |
| | | |
| | | .content { |
| | | padding: 12px; |
| | | display: flex; |
| | | background: #fff; |
| | | border-radius: 16px; |
| | | &::before { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .layer-box { |
| | | margin-left: calc((100% / 3 - 84px) * 3 / 2); |
| | | :deep(.coordinate-label) { |
| | | padding: 4px 7px; |
| | | background: rgba(255, 255, 255, 0.95); |
| | | border: 1px solid rgba(76, 133, 255, 0.35); |
| | | border-radius: 4px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); |
| | | font-size: 11px; |
| | | line-height: 16px; |
| | | color: #222324; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | &: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; |
| | | } |
| | | } |
| | | @keyframes blink { |
| | | 0% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | 50% { |
| | | transform: scale(1.2); |
| | | opacity: 0.8; |
| | | } |
| | | 100% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | } |
| | | </style> |