|
@@ -1,140 +1,342 @@
|
|
|
|
+<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 v-if="selectedDevice" class="device-info-popup">
|
|
|
|
+ <div class="popup-header">
|
|
|
|
+ <h3>设备详情</h3>
|
|
|
|
+ <button @click="closeDeviceInfo">×</button>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="popup-content">
|
|
|
|
+ <p><strong>设备ID:</strong> {{ selectedDevice.id }}</p>
|
|
|
|
+ <p><strong>设备名称:</strong> {{ selectedDevice.name }}</p>
|
|
|
|
+ <p><strong>位置:</strong> {{ selectedDevice.location }}</p>
|
|
|
|
+ <p><strong>状态:</strong> {{ deviceStatusMap[selectedDevice.status] }}</p>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
|
|
-<script setup lang="ts">
|
|
|
|
-import { onMounted } from 'vue'
|
|
|
|
|
|
+<script lang="ts">
|
|
|
|
+import { defineComponent, onMounted, onBeforeUnmount, ref } from 'vue';
|
|
|
|
|
|
-interface DeviceData {
|
|
|
|
- count: number
|
|
|
|
- children?: Record<string, number>
|
|
|
|
|
|
+interface Device {
|
|
|
|
+ id: string;
|
|
|
|
+ name: string;
|
|
|
|
+ location: string;
|
|
|
|
+ status: number;
|
|
|
|
+ lng: number;
|
|
|
|
+ lat: number;
|
|
}
|
|
}
|
|
|
|
|
|
-interface Window {
|
|
|
|
- BMapGL: any
|
|
|
|
- onBMapCallback: () => void
|
|
|
|
|
|
+interface Cluster {
|
|
|
|
+ lng: number;
|
|
|
|
+ lat: number;
|
|
|
|
+ count: number;
|
|
|
|
+ devices?: Device[];
|
|
}
|
|
}
|
|
|
|
|
|
-const deviceData: Record<string, DeviceData> = {
|
|
|
|
- '新疆': {
|
|
|
|
- count: 30,
|
|
|
|
- children: {
|
|
|
|
- '克拉玛依': 10,
|
|
|
|
- '乌鲁木齐': 10,
|
|
|
|
- '库尔勒': 10
|
|
|
|
- }
|
|
|
|
- },
|
|
|
|
- '北京': { count: 50 },
|
|
|
|
- '上海': { count: 45 },
|
|
|
|
- '广东': { count: 60 }
|
|
|
|
-}
|
|
|
|
|
|
+export default defineComponent({
|
|
|
|
+ name: 'ChinaDeviceMap',
|
|
|
|
+ setup() {
|
|
|
|
+ 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<Device | null>(null);
|
|
|
|
+ const hoverDevice = ref<Device | null>(null);
|
|
|
|
+ const clusters = ref<Cluster[]>([]);
|
|
|
|
|
|
-const loadBMap = (ak: string): Promise<any> => {
|
|
|
|
- return new Promise((resolve) => {
|
|
|
|
- if (window.BMapGL) return resolve(window.BMapGL)
|
|
|
|
|
|
+ // 设备数据示例
|
|
|
|
+ const devices = ref<Device[]>([
|
|
|
|
+ { id: 'D001', name: '设备1', location: '北京', status: 1, lng: 116.404, lat: 39.915 },
|
|
|
|
+ { id: 'D002', name: '设备2', location: '上海', status: 2, lng: 121.474, lat: 31.230 },
|
|
|
|
+ { id: 'D003', name: '设备3', location: '广州', status: 1, lng: 113.264, lat: 23.129 },
|
|
|
|
+ { id: 'D004', name: '设备4', location: '深圳', status: 3, lng: 114.058, lat: 22.543 },
|
|
|
|
+ { id: 'D005', name: '设备5', location: '成都', status: 1, lng: 104.065, lat: 30.659 },
|
|
|
|
+ { id: 'D006', name: '设备6', location: '武汉', status: 2, lng: 114.305, lat: 30.593 },
|
|
|
|
+ { id: 'D007', name: '设备7', location: '西安', status: 1, lng: 108.948, lat: 34.263 },
|
|
|
|
+ { id: 'D008', name: '设备8', location: '杭州', status: 3, lng: 120.155, lat: 30.274 },
|
|
|
|
+ { id: 'D009', name: '设备9', location: '南京', status: 1, lng: 118.797, lat: 32.060 },
|
|
|
|
+ { id: 'D010', name: '设备10', location: '重庆', status: 2, lng: 106.505, lat: 29.533 },
|
|
|
|
+ ]);
|
|
|
|
|
|
- const script = document.createElement('script')
|
|
|
|
- script.src = `https://api.map.baidu.com/api?type=webgl&v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0&callback=onBMapCallback`
|
|
|
|
- document.head.appendChild(script)
|
|
|
|
|
|
+ const deviceStatusMap = {
|
|
|
|
+ 1: '在线',
|
|
|
|
+ 2: '离线',
|
|
|
|
+ 3: '异常'
|
|
|
|
+ };
|
|
|
|
|
|
- window.onBMapCallback = () => resolve(window.BMapGL)
|
|
|
|
- })
|
|
|
|
-}
|
|
|
|
|
|
+ // 初始化地图
|
|
|
|
+ const initMap = () => {
|
|
|
|
+ if (!mapContainer.value) return;
|
|
|
|
|
|
-const setupMap = async () => {
|
|
|
|
- try {
|
|
|
|
- const BMapGL = await loadBMap('您的AK密钥')
|
|
|
|
- const map = new BMapGL.Map('map-container')
|
|
|
|
-
|
|
|
|
- // 地球模式配置
|
|
|
|
- map.centerAndZoom(new BMapGL.Point(105, 35), 5)
|
|
|
|
- map.enableScrollWheelZoom()
|
|
|
|
- // map.setMapType(BMapGL.constants.MapType.EARTH)
|
|
|
|
-
|
|
|
|
- // 中国边界绘制
|
|
|
|
- new BMapGL.Boundary().get('中国', (rs: any) => {
|
|
|
|
- rs.boundaries.forEach((boundary: string) => {
|
|
|
|
- map.addOverlay(new BMapGL.Polygon(boundary, {
|
|
|
|
- strokeWeight: 2,
|
|
|
|
- strokeColor: "#ff0000"
|
|
|
|
- }))
|
|
|
|
- })
|
|
|
|
- })
|
|
|
|
-
|
|
|
|
- // 初始省级标注
|
|
|
|
- addProvinceLabels(map, BMapGL)
|
|
|
|
-
|
|
|
|
- // 缩放事件监听
|
|
|
|
- map.addEventListener('zoomend', () => {
|
|
|
|
- map.getZoom() >= 7
|
|
|
|
- ? showCityLabels(map, BMapGL)
|
|
|
|
- : addProvinceLabels(map, BMapGL)
|
|
|
|
- })
|
|
|
|
- } catch (error) {
|
|
|
|
- console.error('地图加载失败:', error)
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
|
|
+ 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, 5);
|
|
|
|
+
|
|
|
|
+ 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();
|
|
|
|
+
|
|
|
|
+ if (zoomLevel > 10) {
|
|
|
|
+ // 高缩放级别下显示单个设备标记
|
|
|
|
+ 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: Device, point: any) => {
|
|
|
|
+ debugger
|
|
|
|
+ const marker = new (window as any).BMap.Marker(point, {
|
|
|
|
+ icon: new (window as any).BMap.Icon('/images/dingweida.png', new (window as any).BMap.Size(103, 105),
|
|
|
|
+ {
|
|
|
|
+ anchor: new (window as any).BMap.Size(10, 25),
|
|
|
|
+ imageOffset: new (window as any).BMap.Size(0, 0 - device.status * 25)
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ });
|
|
|
|
|
|
-const addProvinceLabels = (map: any, BMapGL: any) => {
|
|
|
|
- map.clearOverlays()
|
|
|
|
- Object.entries(deviceData).forEach(([province, data]) => {
|
|
|
|
- const point = getCenterPoint(province)
|
|
|
|
- if (point) {
|
|
|
|
- const label = new BMapGL.Label(`${province}: ${data.count}台`, {
|
|
|
|
|
|
+ // 添加点击事件
|
|
|
|
+ marker.addEventListener('click', () => {
|
|
|
|
+ showDeviceInfo(device);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 添加鼠标悬停事件
|
|
|
|
+ marker.addEventListener('mouseover', () => {
|
|
|
|
+ hoverDevice.value = device;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ marker.addEventListener('mouseout', () => {
|
|
|
|
+ hoverDevice.value = null;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return marker;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const createClusterLabel = (cluster: Cluster, point: any) => {
|
|
|
|
+ const label = new (window as any).BMap.Label(cluster.count.toString(), {
|
|
position: point,
|
|
position: point,
|
|
- offset: new BMapGL.Size(20, -10)
|
|
|
|
- })
|
|
|
|
- label.setStyle(labelStyle)
|
|
|
|
- map.addOverlay(label)
|
|
|
|
- }
|
|
|
|
- })
|
|
|
|
-}
|
|
|
|
|
|
+ offset: new (window as any).BMap.Size(-10, -10)
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ label.setStyle({
|
|
|
|
+ color: '#fff',
|
|
|
|
+ backgroundColor: '#1890ff',
|
|
|
|
+ borderRadius: '50%',
|
|
|
|
+ width: '20px',
|
|
|
|
+ height: '20px',
|
|
|
|
+ textAlign: 'center',
|
|
|
|
+ lineHeight: '20px',
|
|
|
|
+ cursor: 'pointer',
|
|
|
|
+ fontSize: '12px',
|
|
|
|
+ fontWeight: 'bold'
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 添加点击事件
|
|
|
|
+ label.addEventListener('click', () => {
|
|
|
|
+ map.value?.setZoom(11);
|
|
|
|
+ map.value?.panTo(point);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return label;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const clusterDevices = (devices: Device[], map: any): Cluster[] => {
|
|
|
|
+ const clusters: Cluster[] = [];
|
|
|
|
+ const gridSize = getGridSize(map.getZoom());
|
|
|
|
+
|
|
|
|
+ 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.5;
|
|
|
|
+ return 0.1;
|
|
|
|
+ };
|
|
|
|
|
|
-const showCityLabels = (map: any, BMapGL: any) => {
|
|
|
|
- const xinjiang = deviceData['新疆']
|
|
|
|
- if (xinjiang?.children) {
|
|
|
|
- Object.entries(xinjiang.children).forEach(([city, count]) => {
|
|
|
|
- const point = getCenterPoint(city)
|
|
|
|
- if (point) {
|
|
|
|
- const label = new BMapGL.Label(`${city}: ${count}台`, {
|
|
|
|
- position: point,
|
|
|
|
- offset: new BMapGL.Size(20, -10)
|
|
|
|
- })
|
|
|
|
- label.setStyle({...labelStyle, fontSize: '12px'})
|
|
|
|
- map.addOverlay(label)
|
|
|
|
|
|
+ const showDeviceInfo = (device: Device) => {
|
|
|
|
+ selectedDevice.value = device;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const closeDeviceInfo = () => {
|
|
|
|
+ selectedDevice.value = null;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ 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]);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ onMounted(() => {
|
|
|
|
+ initMap();
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ onBeforeUnmount(() => {
|
|
|
|
+ if (map.value) {
|
|
|
|
+ map.value.destroy();
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ mapContainer,
|
|
|
|
+ selectedDevice,
|
|
|
|
+ hoverDevice,
|
|
|
|
+ deviceStatusMap,
|
|
|
|
+ zoomIn,
|
|
|
|
+ zoomOut,
|
|
|
|
+ toggleMapType,
|
|
|
|
+ showDeviceInfo,
|
|
|
|
+ closeDeviceInfo
|
|
|
|
+ };
|
|
}
|
|
}
|
|
|
|
+});
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style scoped>
|
|
|
|
+.map-container {
|
|
|
|
+ position: relative;
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100vh;
|
|
}
|
|
}
|
|
|
|
|
|
-const labelStyle = {
|
|
|
|
- color: '#333',
|
|
|
|
- fontSize: '14px',
|
|
|
|
- border: '1px solid #ccc',
|
|
|
|
- backgroundColor: 'white',
|
|
|
|
- padding: '5px'
|
|
|
|
|
|
+#baidu-map {
|
|
|
|
+ width: 100%;
|
|
|
|
+ height: 100%;
|
|
}
|
|
}
|
|
|
|
|
|
-const getCenterPoint = (name: string): any => {
|
|
|
|
- const points: Record<string, [number, number]> = {
|
|
|
|
- '新疆': [87.6168, 43.8256],
|
|
|
|
- '北京': [116.4074, 39.9042],
|
|
|
|
- '上海': [121.4737, 31.2304],
|
|
|
|
- '广东': [113.2644, 23.1291],
|
|
|
|
- '克拉玛依': [84.8739, 45.5886],
|
|
|
|
- '乌鲁木齐': [87.6168, 43.8256],
|
|
|
|
- '库尔勒': [86.1467, 41.7686]
|
|
|
|
- }
|
|
|
|
- return points[name] && new BMapGL.Point(...points[name])
|
|
|
|
|
|
+.map-controls {
|
|
|
|
+ position: absolute;
|
|
|
|
+ top: 20px;
|
|
|
|
+ left: 20px;
|
|
|
|
+ z-index: 1000;
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 10px;
|
|
}
|
|
}
|
|
|
|
|
|
-onMounted(setupMap)
|
|
|
|
-</script>
|
|
|
|
|
|
+.map-controls button {
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
+ background: #fff;
|
|
|
|
+ border: 1px solid #ccc;
|
|
|
|
+ border-radius: 3px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+}
|
|
|
|
|
|
-<template>
|
|
|
|
- <div id="map-container"></div>
|
|
|
|
-</template>
|
|
|
|
|
|
+.device-info-popup {
|
|
|
|
+ position: absolute;
|
|
|
|
+ top: 20px;
|
|
|
|
+ right: 20px;
|
|
|
|
+ z-index: 1000;
|
|
|
|
+ width: 300px;
|
|
|
|
+ background: #fff;
|
|
|
|
+ border-radius: 5px;
|
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
|
|
+ padding: 15px;
|
|
|
|
+}
|
|
|
|
|
|
-<style scoped>
|
|
|
|
-#map-container {
|
|
|
|
- width: 100%;
|
|
|
|
- height: 100vh;
|
|
|
|
|
|
+.popup-header {
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ align-items: center;
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.popup-header button {
|
|
|
|
+ background: none;
|
|
|
|
+ border: none;
|
|
|
|
+ font-size: 18px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.popup-content p {
|
|
|
|
+ margin: 5px 0;
|
|
}
|
|
}
|
|
</style>
|
|
</style>
|