|
@@ -0,0 +1,376 @@
|
|
|
+<template>
|
|
|
+ <div class="map-container">
|
|
|
+ <div id="baidu-map" ref="mapContainer"></div>
|
|
|
+ <div class="map-controls">
|
|
|
+ <button @click="zoomIn">放大</button>
|
|
|
+ <button @click="zoomOut">缩小</button>
|
|
|
+ <button @click="toggleMapType"
|
|
|
+ >切换地图类型({{ mapType === 'BMAP_NORMAL_MAP' ? '地图' : '卫星' }})</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <DeviceMonitorDrawer :model-value="drawerVisible" @update:model-value="(val) => (drawerVisible = val)" :id="deviceId" :deviceName="deviceName" :lastLineTime="lastLineTime"
|
|
|
+ :ifLine="ifLine" :dept="dept" :deviceCode="deviceCode"
|
|
|
+ ref="showDrawer" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
|
|
+import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
|
|
|
+import DeviceMonitorDrawer from '@/views/pms/map/DeviceMonitorDrawer.vue'
|
|
|
+import * as DeptApi from '@/api/system/dept'
|
|
|
+
|
|
|
+interface Cluster {
|
|
|
+ lng: number
|
|
|
+ lat: number
|
|
|
+ count: number
|
|
|
+ devices?: IotDeviceVO[]
|
|
|
+}
|
|
|
+defineOptions({ name: 'DeviceMap' })
|
|
|
+const showDrawer = ref()
|
|
|
+const drawerVisible = ref<boolean>(false)
|
|
|
+const mapContainer = ref<HTMLElement | null>(null)
|
|
|
+const map = ref<any>(null)
|
|
|
+const mapType = ref<'BMAP_NORMAL_MAP' | 'BMAP_SATELLITE_MAP'>('BMAP_NORMAL_MAP')
|
|
|
+const selectedDevice = ref<IotDeviceVO | null>(null)
|
|
|
+const hoverDevice = ref<IotDeviceVO | null>(null)
|
|
|
+const clusters = ref<Cluster[]>([])
|
|
|
+const deviceId = ref()
|
|
|
+const deviceName = ref('')
|
|
|
+const lastLineTime = ref('')
|
|
|
+const ifLine = ref()
|
|
|
+const dept = ref('')
|
|
|
+const deviceCode = ref('')
|
|
|
+
|
|
|
+// 设备数据示例
|
|
|
+const devices = ref<IotDeviceVO[]>()
|
|
|
+
|
|
|
+// 初始化地图
|
|
|
+const initMap = () => {
|
|
|
+ if (!mapContainer.value) return
|
|
|
+
|
|
|
+ const script = document.createElement('script')
|
|
|
+ script.src = `https://api.map.baidu.com/api?v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0&type=webgl`
|
|
|
+ script.async = true
|
|
|
+ script.onload = () => {
|
|
|
+ if ((window as any).BMap) {
|
|
|
+ map.value = new (window as any).BMap.Map(mapContainer.value)
|
|
|
+ const point = new (window as any).BMap.Point(104.114129, 37.550339)
|
|
|
+ map.value.centerAndZoom(point, 6)
|
|
|
+
|
|
|
+ map.value.enableScrollWheelZoom(true)
|
|
|
+ map.value.setMapType((window as any)[mapType.value])
|
|
|
+
|
|
|
+ map.value.addControl(new (window as any).BMap.NavigationControl())
|
|
|
+ map.value.addControl(new (window as any).BMap.ScaleControl())
|
|
|
+
|
|
|
+ initDeviceMarkers()
|
|
|
+ map.value.addEventListener('zoomend', () => {
|
|
|
+ initDeviceMarkers()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.error('百度地图API加载失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ script.onerror = () => {
|
|
|
+ console.error('百度地图API加载失败')
|
|
|
+ }
|
|
|
+ document.head.appendChild(script)
|
|
|
+}
|
|
|
+
|
|
|
+const initDeviceMarkers = () => {
|
|
|
+ if (!map.value) return
|
|
|
+
|
|
|
+ map.value.clearOverlays()
|
|
|
+
|
|
|
+ const zoomLevel = map.value.getZoom()
|
|
|
+ debugger
|
|
|
+ if (zoomLevel > 9) {
|
|
|
+ // 高缩放级别下显示单个设备标记
|
|
|
+ devices.value.forEach((device) => {
|
|
|
+ const point = new (window as any).BMap.Point(device.lng, device.lat)
|
|
|
+ const marker = createDeviceMarker(device, point)
|
|
|
+ map.value.addOverlay(marker)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 低缩放级别下进行聚合
|
|
|
+ clusters.value = clusterDevices(devices.value, map.value)
|
|
|
+ clusters.value.forEach((cluster) => {
|
|
|
+ if (cluster.count === 1) {
|
|
|
+ // 只有一个设备时显示设备图标
|
|
|
+ const device = cluster.devices?.[0]
|
|
|
+ if (device) {
|
|
|
+ const point = new (window as any).BMap.Point(device.lng, device.lat)
|
|
|
+ const marker = createDeviceMarker(device, point)
|
|
|
+ map.value.addOverlay(marker)
|
|
|
+ }
|
|
|
+ } else if (cluster.count > 1) {
|
|
|
+ // 多个设备时显示聚合标签
|
|
|
+ const point = new (window as any).BMap.Point(cluster.lng, cluster.lat)
|
|
|
+ const label = createClusterLabel(cluster, point)
|
|
|
+ map.value.addOverlay(label)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const createDeviceMarker = (device: IotDeviceVO, point: any) => {
|
|
|
+ // 根据设备是否在线选择不同的图标
|
|
|
+ const iconUrl = device.ifInline === 3
|
|
|
+ ? 'https://iot.deepoil.cc/images/dinggreen.svg'
|
|
|
+ : 'https://iot.deepoil.cc/images/dingout.svg';
|
|
|
+
|
|
|
+ const marker = new (window as any).BMap.Marker(point, {
|
|
|
+ icon: new (window as any).BMap.Icon(
|
|
|
+ iconUrl,
|
|
|
+ new (window as any).BMap.Size(40, 40),
|
|
|
+ {
|
|
|
+ anchor: new (window as any).BMap.Size(25, 40)
|
|
|
+ // imageOffset: new (window as any).BMap.Size(0, -5)
|
|
|
+ }
|
|
|
+ )
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加点击事件
|
|
|
+ marker.addEventListener('click', () => {
|
|
|
+ showDeviceInfoWindow(device, point);
|
|
|
+ });
|
|
|
+
|
|
|
+ return marker;
|
|
|
+};
|
|
|
+
|
|
|
+const createClusterLabel = (cluster: Cluster, point: any) => {
|
|
|
+ const label = new (window as any).BMap.Label(cluster.count.toString(), {
|
|
|
+ position: point,
|
|
|
+ offset: new (window as any).BMap.Size(-10, -10)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 创建一个 style 标签并添加到 head 中,定义呼吸动画
|
|
|
+ const style = document.createElement('style')
|
|
|
+ style.textContent = `
|
|
|
+ @keyframes breathing {
|
|
|
+ 0% {
|
|
|
+ border-color: rgba(255, 255, 255, 0.3);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ border-color: rgba(255, 255, 255, 0.8);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ border-color: rgba(255, 255, 255, 0.3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `
|
|
|
+ document.head.appendChild(style)
|
|
|
+
|
|
|
+ // 初始样式
|
|
|
+ label.setStyle({
|
|
|
+ color: '#fff',
|
|
|
+ backgroundColor: 'blue',
|
|
|
+ borderRadius: '50%',
|
|
|
+ width: '50px',
|
|
|
+ height: '50px',
|
|
|
+ textAlign: 'center',
|
|
|
+ lineHeight: '40px',
|
|
|
+ cursor: 'pointer',
|
|
|
+ fontSize: '18px',
|
|
|
+ fontWeight: 'bold',
|
|
|
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
|
|
|
+ transition: 'all 0.3s ease',
|
|
|
+ border: '6px solid rgba(255, 255, 255, 0.5)', // 添加带有不透明度的边框
|
|
|
+ animation: 'breathing 1.5s infinite' // 添加呼吸动画
|
|
|
+ })
|
|
|
+
|
|
|
+ // 鼠标悬停样式
|
|
|
+ const hoverStyle = {
|
|
|
+ backgroundColor: '#2196df',
|
|
|
+ transform: 'scale(1.1)'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 鼠标移出样式
|
|
|
+ const normalStyle = {
|
|
|
+ backgroundColor: '#c38f65',
|
|
|
+ transform: 'scale(1)'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加点击事件
|
|
|
+ label.addEventListener('click', () => {
|
|
|
+ if (map.value) {
|
|
|
+ const currentZoom = map.value.getZoom();
|
|
|
+ const MAX_ZOOM = 19; // 手动设定最大缩放级别
|
|
|
+ const newZoom = Math.min(currentZoom + 3, MAX_ZOOM);
|
|
|
+ map.value.setZoom(newZoom);
|
|
|
+ map.value.panTo(point);
|
|
|
+ initDeviceMarkers(); // 重新计算并显示标记和聚合标签
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return label
|
|
|
+}
|
|
|
+
|
|
|
+const clusterDevices = (devices: IotDeviceVO[], map: any): Cluster[] => {
|
|
|
+ const clusters: Cluster[] = []
|
|
|
+ const gridSize = getGridSize(map.getZoom())
|
|
|
+ debugger
|
|
|
+
|
|
|
+ const gridMap = new Map<string, Cluster>()
|
|
|
+
|
|
|
+ devices.forEach((device) => {
|
|
|
+ const gridKey = `${Math.floor(device.lng / gridSize)}_${Math.floor(device.lat / gridSize)}`
|
|
|
+
|
|
|
+ if (!gridMap.has(gridKey)) {
|
|
|
+ gridMap.set(gridKey, {
|
|
|
+ lng: Math.floor(device.lng / gridSize) * gridSize + gridSize / 2,
|
|
|
+ lat: Math.floor(device.lat / gridSize) * gridSize + gridSize / 2,
|
|
|
+ count: 0,
|
|
|
+ devices: []
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const cluster = gridMap.get(gridKey)!
|
|
|
+ cluster.count++
|
|
|
+ if (!cluster.devices) cluster.devices = []
|
|
|
+ cluster.devices.push(device)
|
|
|
+ })
|
|
|
+
|
|
|
+ return Array.from(gridMap.values())
|
|
|
+}
|
|
|
+
|
|
|
+const getGridSize = (zoom: number): number => {
|
|
|
+ if (zoom <= 5) return 2
|
|
|
+ if (zoom <= 8) return 1
|
|
|
+ if (zoom < 10) return 0.03
|
|
|
+ return 0.1
|
|
|
+}
|
|
|
+
|
|
|
+const showDeviceInfoWindow = (device: IotDeviceVO, point: any) => {
|
|
|
+ DeptApi.getDept(device.deptId).then(res=>{
|
|
|
+ dept.value = res.name
|
|
|
+ const content = `
|
|
|
+<div style="display: flex;flex-direction: column;justify-content: center;border: 1px solid #ccc;">
|
|
|
+ <div style="margin-top: 1px;padding: 8px">
|
|
|
+ <p><strong>设备编码:</strong> ${device.deviceCode}</p>
|
|
|
+ <p><strong>设备名称:</strong> ${device.deviceName}</p>
|
|
|
+ <p><strong>所在部门:</strong> ${res.name}</p>
|
|
|
+ <p><strong>位置:</strong> ${device.location}</p>
|
|
|
+ <p><strong>状态:</strong> ${getDictLabel(DICT_TYPE.PMS_DEVICE_STATUS, device.deviceStatus)}</p>
|
|
|
+ <p><strong>是否在线:</strong> ${getDictLabel(DICT_TYPE.IOT_DEVICE_STATUS, device.ifInline)}</p>
|
|
|
+ <p><strong>最后在线时间:</strong> ${device.lastInlineTime}</p>
|
|
|
+ </div>
|
|
|
+ <div style="margin-bottom: 5px;padding: 8px">
|
|
|
+ <button id="device-detail-btn" style=" background-color: #2196f3;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ cursor: pointer;">查看</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `
|
|
|
+ const infoWindow = new (window as any).BMap.InfoWindow(content, {
|
|
|
+ width: 350,
|
|
|
+ height: 270
|
|
|
+ })
|
|
|
+ map.value.openInfoWindow(infoWindow, point)
|
|
|
+
|
|
|
+ // 事件绑定(需延迟确保DOM加载)
|
|
|
+ setTimeout(function () {
|
|
|
+ document.getElementById('device-detail-btn').addEventListener('click', function () {
|
|
|
+ drawerVisible.value = true
|
|
|
+ deviceId.value = device.id;
|
|
|
+ deviceCode.value = device.deviceCode
|
|
|
+ deviceName.value = device.deviceName
|
|
|
+ ifLine.value = device.ifInline
|
|
|
+ lastLineTime.value = device.lastInlineTime
|
|
|
+ showDrawer.value.openDrawer()
|
|
|
+
|
|
|
+ })
|
|
|
+ }, 200)
|
|
|
+ })
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+const zoomIn = () => {
|
|
|
+ if (map.value) {
|
|
|
+ map.value.zoomIn()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const zoomOut = () => {
|
|
|
+ if (map.value) {
|
|
|
+ map.value.zoomOut()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleMapType = () => {
|
|
|
+ if (!map.value) return
|
|
|
+
|
|
|
+ mapType.value = mapType.value === 'BMAP_NORMAL_MAP' ? 'BMAP_SATELLITE_MAP' : 'BMAP_NORMAL_MAP'
|
|
|
+ map.value.setMapType((window as any)[mapType.value])
|
|
|
+}
|
|
|
+
|
|
|
+const getData = async () => {
|
|
|
+ await IotDeviceApi.getMapDevice().then((res) => {
|
|
|
+ devices.value = res
|
|
|
+ initMap()
|
|
|
+ })
|
|
|
+}
|
|
|
+onMounted(async () => {
|
|
|
+ await getData()
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ if (map.value) {
|
|
|
+ // 清除地图上的覆盖物
|
|
|
+ map.value.clearOverlays();
|
|
|
+ // 移除地图容器中的内容
|
|
|
+ if (mapContainer.value) {
|
|
|
+ mapContainer.value.innerHTML = '';
|
|
|
+ }
|
|
|
+ // 解除地图的事件绑定
|
|
|
+ map.value.removeEventListener('zoomend', initDeviceMarkers);
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// return {
|
|
|
+// mapContainer,
|
|
|
+// selectedDevice,
|
|
|
+// hoverDevice,
|
|
|
+// // deviceStatusMap,
|
|
|
+// zoomIn,
|
|
|
+// zoomOut,
|
|
|
+// toggleMapType
|
|
|
+// }
|
|
|
+// })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.map-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+#baidu-map {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.map-controls {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px; /* 修改为right定位 */
|
|
|
+ z-index: 1000;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column; /* 垂直排列按钮 */
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-controls button {
|
|
|
+ padding: 5px 10px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 3px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+</style>
|