吉安感知网项目-前端
罗广辉
4 days ago 1d552a3bad95caae4af15322df28e6240b797693
applications/mobile-web-view/src/appComponents/workMap/index.vue
@@ -1,77 +1,34 @@
<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'
@@ -79,24 +36,12 @@
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,
@@ -119,6 +64,21 @@
   })
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])
@@ -140,9 +100,7 @@
   },
]
const layerChangePopupShow = ref(false)
const setSelectMapLayerKey = ref(1) // 选择的地图图层
let boundaryInstance = useAreaBoundary({ initFly: true })
let markersLayer = null
const initMap = () => {
   if (map) return
@@ -155,7 +113,6 @@
      doubleClickZoom: false,
      editable: true, //绘制控件
   }).setView([25.992338, 114.823254], 13)
   boundaryInstance.initBoundary(map)
   markersLayer = L.layerGroup().addTo(map) // 创建一个标注层,便于管理和移除
   if (createWorkShow) {
@@ -174,9 +131,9 @@
let locationFlag = false
let watchId = null
let userLocationMarker = null
const setMapLocation = () => {
   locationFlag = true
// 初始化实时位置监听
const initLocationWatch = () => {
   // 在WebView加载的网页中
   // 检查浏览器是否支持 geolocation
   if (navigator.geolocation) {
@@ -184,7 +141,6 @@
         // 停止监听位置
         navigator.geolocation.clearWatch(watchId)
      }
      // 开始持续获取用户的位置
      watchId = navigator.geolocation.watchPosition(
         function (position) {
@@ -192,27 +148,27 @@
            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) {
            // 如果获取位置失败,执行回调
@@ -225,20 +181,62 @@
         }
      )
   } 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
@@ -311,31 +309,13 @@
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) {
@@ -373,6 +353,19 @@
   // 存储关联数据
   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({
@@ -385,10 +378,11 @@
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)
@@ -398,37 +392,31 @@
   { 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()
@@ -453,83 +441,8 @@
      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;
@@ -547,99 +460,54 @@
         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>