天地图,cesium,leaflet
按照依赖
pnpm add @types/leaflet leaflet vite-plugin-cesium cesium
vue.config.js配置 cesium
import cesium from 'vite-plugin-cesium';export default defineConfig({plugins: [vue(),cesium(),],
})
CesiumMap组件
<template><div class="content cesiumMap"><div ref="cesiumContainerRef" id="cesiumContainer"></div><!-- 信息弹窗 --><div v-if="showInfoModal" class="info-modal" ><div class="modal-header"><h3>区域信息</h3><button @click="showInfoModal = false" class="close-btn">×</button></div><div class="modal-content"><div class="area-info"><p><strong>区域名称:</strong>{{ selectedAreaInfo.name }}</p><p><strong>行政代码:</strong>{{ selectedAreaInfo.adcode }}</p><p><strong>中心坐标:</strong>{{ selectedAreaInfo.center.join(', ') }}</p><p><strong>级别:</strong>{{ selectedAreaInfo.level }}</p></div><div class="action-buttons"><button class="action-btn" @click="viewDrones">查看无人机</button><button class="action-btn" @click="viewAlarms">查看告警</button></div></div></div></div>
</template>
<script setup>import { getCurrentInstance, onMounted, ref, reactive } from 'vue'import { useRouter } from 'vue-router'const { proxy } = getCurrentInstance()import * as Cesium from 'cesium';import mapJson from '@/assets/map/ganzi.json' let cesiumContainerRef = ref(null)let viewer = '';let dom = reactive({width: 0,height: 0})const showInfoModal = ref(false)const selectedAreaInfo = ref({name: '',adcode: '',center: [],level: ''})const router = useRouter()//初始化cesiumconst initCesium = async () => {viewer = new Cesium.Viewer("cesiumContainer", {infoBox: false, // 关闭cesium右下角的信息框animation: false, // 隐藏动画控件baseLayerPicker: false, // 隐藏图层选择控件fullscreenButton: false, // 隐藏全屏按钮vrButton: false, // 隐藏VR按钮,默认falsegeocoder: false, // 隐藏地名查找控件homeButton: false, // 隐藏Home按钮sceneModePicker: false, // 隐藏场景模式选择控件selectionIndicator: true, // 显示实体对象选择框,默认truetimeline: false, // 隐藏时间线控件navigationHelpButton: false, // 隐藏帮助按钮// scene3DOnly: true, // 每个几何实例将只在3D中呈现,以节省GPU内存shouldAnimate: true, // 开启动画自动播放sceneMode: Cesium.SceneMode.SCENE3D, //SCENE2D, COLUMBUS_VIEW ,SCENE3D,MORPHING requestRenderMode: true, // 减少Cesium渲染新帧总时间并减少Cesium在应用程序中总体CPU使用率// 如场景中的元素没有随仿真时间变化,请考虑将设置maximumRenderTimeChange为较高的值,例如InfinitymaximumRenderTimeChange: Infinity,// 使用默认的影像提供者(Bing Maps)// imageryProvider: new Cesium.BingMapsImageryProvider({// url: 'https://dev.virtualearth.net',// key: 'YOUR_BING_MAPS_KEY',// mapStyle: Cesium.BingMapsStyle.AERIAL_WITH_LABELS_ON_DEMAND// }),//需要纯色背景必须设置contextOptions: {webgl: {alpha: true,}},})//背景设置为透明,让地图显示viewer.scene.backgroundColor = new Cesium.Color(0.0, 0.0, 0.0, 0.0);//开启大气效果viewer.scene.skyAtmosphere.show = true//抗锯齿viewer.scene.fxaa = true;viewer.scene.postProcessStages.fxaa.enabled = true;//开启天空月亮viewer.scene.sun.show = true; viewer.scene.moon.show = true;viewer.scene.skyBox.show = true;//显示地球viewer.scene.undergroundMode = false; viewer.scene.globe.baseColor = new Cesium.Color(0.2, 0.3, 0.6, 1);// 启用地球的真实感效果viewer.scene.globe.enableLighting = true; // 启用光照效果viewer.scene.globe.atmosphereHueShift = 0.0; // 大气色调偏移viewer.scene.globe.atmosphereSaturationShift = 0.0; // 大气饱和度偏移viewer.scene.globe.atmosphereBrightnessShift = 0.0; // 大气亮度偏移viewer._cesiumWidget._creditContainer.style.display = "none"; //去掉logo// 鼠标右键 倾斜操作viewer.scene.screenSpaceCameraController.tiltEventTypes = [Cesium.CameraEventType.RIGHT_DRAG];// 鼠标滑轮 放缩操作viewer.scene.screenSpaceCameraController.zoomEventTypes = [Cesium.CameraEventType.WHEEL,];// 设置相机缩放限制,允许缩放到更远距离以显示完整球体viewer.scene.screenSpaceCameraController.minimumZoomDistance = 1000; // 最小缩放距离viewer.scene.screenSpaceCameraController.maximumZoomDistance = 50000000; // 最大缩放距离,可以看到完整地球// 鼠标左键 viewer.scene.screenSpaceCameraController.rotateEventTypes = [Cesium.CameraEventType.LEFT_DRAG];// 坐标拾取 - 修改为识别区域实体viewer.screenSpaceEventHandler.setInputAction((movement) => {// 首先尝试拾取实体const pickedEntity = viewer.scene.pick(movement.position);if (Cesium.defined(pickedEntity) && Cesium.defined(pickedEntity.id)) {const entity = pickedEntity.id;console.log('点击的实体名称:', entity.name);console.log('实体属性:', entity.properties);// 检查实体是否有区域属性if (entity.properties && entity.properties.areaName) {const areaName = entity.properties.areaName._value || entity.properties.areaName;const adcode = entity.properties.adcode._value || entity.properties.adcode;const center = entity.properties.center._value || entity.properties.center;const level = entity.properties.level._value || entity.properties.level;showInfoModal.value = true;selectedAreaInfo.value = {name: areaName,adcode: adcode || '000000',center: center || [0, 0],level: level || '县级'};console.log('显示区域信息:', selectedAreaInfo.value);return; // 找到区域信息后直接返回}// 兼容旧的命名方式if (entity.name && (entity.name.includes('_polygon_') || entity.name.includes('_line_'))) {// 提取区域名称(去掉后缀)const areaName = entity.name.split('_polygon_')[0] || entity.name.split('_line_')[0];// 从原始数据中查找对应的区域信息const areaFeature = mapJson.features.find(feature => feature.properties?.name === areaName);if (areaFeature) {showInfoModal.value = true;selectedAreaInfo.value = {name: areaFeature.properties.name,adcode: areaFeature.properties.adcode || '000000',center: areaFeature.properties.center || [0, 0],level: areaFeature.properties.level || '县级'};console.log('显示区域信息:', selectedAreaInfo.value);return;}}// 处理标注点击if (entity.name && entity.name.includes('_标注')) {const areaName = entity.name.split('_标注')[0];const areaFeature = mapJson.features.find(feature => feature.properties?.name === areaName);if (areaFeature) {showInfoModal.value = true;selectedAreaInfo.value = {name: areaFeature.properties.name,adcode: areaFeature.properties.adcode || '000000',center: areaFeature.properties.center || [0, 0],level: areaFeature.properties.level || '县级'};console.log('显示区域信息:', selectedAreaInfo.value);return;}}}// 如果没有点击到实体,则进行坐标拾取const pick = viewer.scene.pickPosition(movement.position);if (Cesium.defined(pick)) {const cartesian = viewer.scene.pickPosition(movement.position);if (Cesium.defined(cartesian)) {const cartographic = Cesium.Cartographic.fromCartesian(cartesian);const longitude = Cesium.Math.toDegrees(cartographic.longitude);const latitude = Cesium.Math.toDegrees(cartographic.latitude);const height = cartographic.height;console.log(`经度: ${longitude}, 纬度: ${latitude}, 高度: ${height}`);showInfoModal.value = true;selectedAreaInfo.value = {name: `坐标点 (${longitude.toFixed(6)}, ${latitude.toFixed(6)})`,adcode: '000000',center: [longitude, latitude],level: '坐标点'};}}}, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 解决倾斜飘漂移,但是会损耗性能viewer.scene.globe.depthTestAgainstTerrain = true;}//初始化地图显示const initMap = async () => {// 加载瓦片地图try {// 使用甘孜藏族自治州的地图数据await loadGanziBoundaryData(mapJson);// 首先设置相机到全球视角(显示完整地球球体)viewer.camera.setView({destination: Cesium.Cartesian3.fromDegrees(99.976057, 31.050738, 30000000), // 高度设置得很高以显示整个地球orientation: {heading: Cesium.Math.toRadians(0),pitch: Cesium.Math.toRadians(-30), // 稍微倾斜以显示球体效果roll: 0.0}});// 延迟2秒后开始动画飞行到甘孜州setTimeout(() => {flyToGanzi();}, 2000);console.log('甘孜藏族自治州地图初始化完成');} catch (error) {console.error('地图初始化失败:', error);}}// 飞行到甘孜州的动画函数const flyToGanzi = () => {viewer.camera.flyTo({destination: Cesium.Cartesian3.fromDegrees(99.976057, 31.050738, 1000000),orientation: {heading: Cesium.Math.toRadians(0),pitch: Cesium.Math.toRadians(-90),roll: 0.0},duration: 3.0, // 动画持续时间4秒,更流畅easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT, // 使用缓动函数让动画更自然complete: () => {console.log('飞行到甘孜州完成');// 动画完成后可以添加一些额外的效果viewer.scene.requestRender(); // 请求重新渲染}});}// 加载甘孜藏族自治州边界数据const loadGanziBoundaryData = async (ganziData) => {try {const features = ganziData.features;console.log('加载甘孜藏族自治州数据,共有', features.length, '个区域');features.forEach((feature, index) => {if (feature.geometry && feature.geometry.coordinates) {// 处理 MultiPolygon 类型的几何数据const coordinates = feature.geometry.coordinates;const featureName = feature.properties?.name || `区域${index}`;console.log(`处理区域: ${featureName}, 中心点:`, feature.properties?.center);// 遍历每个多边形coordinates.forEach((polygon, polygonIndex) => {// 创建边界实体const boundaryEntity = viewer.entities.add({name: `${featureName}_polygon_${polygonIndex}`,polygon: {hierarchy: Cesium.Cartesian3.fromDegreesArray(polygon[0].flat()),outline: true,outlineColor: Cesium.Color.fromCssColorString('#FF6B35'), // 橙色边界线outlineWidth: 3, // 加粗边界线height: 0,extrudedHeight: 0, // 不拉伸material: Cesium.Color.TRANSPARENT, // 设置透明填充以便点击},// 添加区域属性信息properties: {areaName: featureName,adcode: feature.properties.adcode,center: feature.properties.center,level: feature.properties.level}});// 添加额外的边界线效果const boundaryLineEntity = viewer.entities.add({name: `${featureName}_line_${polygonIndex}`,polyline: {positions: Cesium.Cartesian3.fromDegreesArray(polygon[0].flat()),width: 2,material: Cesium.Color.fromCssColorString('#FFD700'), // 金色线条clampToGround: true,},// 添加区域属性信息properties: {areaName: featureName,adcode: feature.properties.adcode,center: feature.properties.center,level: feature.properties.level}});});// 添加区域中心点标注if (feature.properties?.center) {const center = feature.properties.center;const labelEntity = viewer.entities.add({name: `${featureName}_标注`,position: Cesium.Cartesian3.fromDegrees(center[0], center[1]),label: {text: featureName,font: '16pt sans-serif',fillColor: Cesium.Color.WHITE,outlineColor: Cesium.Color.BLACK,outlineWidth: 2,style: Cesium.LabelStyle.FILL_AND_OUTLINE,pixelOffset: new Cesium.Cartesian2(0, -10),showBackground: false,scale: 1.0,show: true,disableDepthTestDistance: Number.POSITIVE_INFINITY, // 确保标注始终可见}});console.log(`添加标注: ${featureName} 在位置 [${center[0]}, ${center[1]}]`);}}});console.log('甘孜藏族自治州边界数据加载完成');} catch (error) {console.error('边界数据加载失败:', error);}}const resize = () => {// 获取 DOM 元素的宽度和高度const container = cesiumContainerRef.value;const width = container.offsetWidth;const height = container.offsetHeight;dom.width = width;dom.height = height;}// 无人机查看方法const viewDrones = () => {router.push('/drone-monitoring')}// 告警查看方法const viewAlarms = () => {router.push('/alarm-detail')}onMounted(() => {resize();// 初始化cesiuminitCesium();initMap();})</script><style scoped lang="scss">.cesiumMap{width: 100%;height: 100%;position: relative;#cesiumContainer{width: 100%;height: 100%;}}.info-modal{position: absolute;left: 700px;top: 300px;transform: translate(-50%, -50%);background-color: rgba(0, 0, 0, 0.8);padding: 15px;border-radius: 8px;color: #fff;z-index: 1000;min-width: 250px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);border: 1px solid rgba(255, 255, 255, 0.2);.modal-header{display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;h3 {margin: 0;font-size: 16px;color: #FFD700;}.close-btn {background: none;border: none;color: #fff;font-size: 20px;cursor: pointer;padding: 0;width: 24px;height: 24px;display: flex;align-items: center;justify-content: center;border-radius: 50%;&:hover {background-color: rgba(255, 255, 255, 0.2);}}}.modal-content{display: flex;flex-direction: column;justify-content: space-between;}.area-info{display: flex;flex-direction: column;p {margin: 5px 0;font-size: 14px;strong {color: #FFD700;}}}.action-buttons{display: flex;justify-content: space-between;align-items: center;gap: 10px;margin-top: 15px;cursor: pointer;.action-btn{width: 100px;height: 32px;background-color: #FF6B35;color: #fff;border-radius: 5px;border: none;outline: none;cursor: pointer;font-size: 12px;transition: background-color 0.3s;&:hover{background-color: #FF8C5A;}}}}</style>
LeafletMap组件
<template><div class="map-wrapper"><div id="map-container" class="w-full h-full"></div></div></template><script setup>import { onMounted, onUnmounted, ref } from 'vue'import mapJson from '@/assets/map/ganzi.json'import { useRouter } from 'vue-router'const router = useRouter()const mapInstance = ref(null)const geoJsonLayer = ref(null)const highlightLayer = ref(null)const markers = ref([])const selectedRegion = ref(null)// 新增功能相关的状态const functionMode = ref(null) // 当前功能模式: 'boundary', 'randomPoints', 'areaSelect', 'distance', 'route'const borderHighlightLayer = ref(null) // 边界高亮图层const randomPointsLayer = ref(null) // 随机点图层const areaSelectLayer = ref(null) // 框选区域图层const distanceMeasureLayer = ref(null) // 距离测量图层const routePlanningLayer = ref(null) // 路径规划图层const measurePoints = ref([]) // 测量点数组const routePoints = ref([]) // 路径点数组const isDrawing = ref(false) // 是否正在绘制const drawingLayer = ref(null) // 绘制图层// 无人机查看方法const viewDrones = () => {router.push('/drone-monitoring')}// 告警查看方法const viewAlarms = () => {router.push('/alarm-detail')}// 高德地图样式配置const initMap = () => {// 创建地图实例mapInstance.value = L.map('map-container', {center: [30.05, 101.96], // 甘孜州中心坐标zoom: 8,zoomControl: true})// 添加地图图层 - 可选择不同样式const mapLayers = {// 高德卫星图amap_satellite: L.tileLayer('https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', {attribution: '© 高德地图 - 卫星图',maxZoom: 18}),// 高德街道图(深色)amap_dark: L.tileLayer('https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {attribution: '© 高德地图 - 深色',maxZoom: 18}),// 高德街道图(浅色)amap_light: L.tileLayer('https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}', {attribution: '© 高德地图 - 浅色',maxZoom: 18}),// OpenStreetMaposm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '© OpenStreetMap contributors',maxZoom: 19}),// CartoDB 深色主题cartodb_dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {attribution: '© OpenStreetMap © CartoDB',subdomains: 'abcd',maxZoom: 19}),// CartoDB 浅色主题cartodb_light: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {attribution: '© OpenStreetMap © CartoDB',subdomains: 'abcd',maxZoom: 19})}// 默认使用卫星图const currentLayer = mapLayers.amap_satellite.addTo(mapInstance.value)// 添加图层控制器L.control.layers({'高德卫星图': mapLayers.amap_satellite,'高德深色': mapLayers.amap_dark,'高德浅色': mapLayers.amap_light,'OpenStreetMap': mapLayers.osm,'CartoDB深色': mapLayers.cartodb_dark,'CartoDB浅色': mapLayers.cartodb_light}).addTo(mapInstance.value)// 添加区域边界addRegionBorders()// 添加地图点击事件监听mapInstance.value.on('click', handleMapClick)}// 处理地图点击事件const handleMapClick = (e) => {// 如果处于功能模式,则不触发原有的区域点击事件if (functionMode.value) {e.originalEvent.stopPropagation()switch (functionMode.value) {case 'areaSelect':handleAreaSelectClick(e)breakcase 'distance':handleDistanceMeasureClick(e)breakcase 'route':handleRoutePlanningClick(e)break}return}}// 添加区域描边const addRegionBorders = () => {geoJsonLayer.value = L.geoJSON(mapJson, {style: {fillColor: 'rgba(255, 215, 0, 0.15)', // 金色填充weight: 2,opacity: 0.9,color: '#FFD700', // 金色边框dashArray: '5,5',fillOpacity: 0.15},onEachFeature: (feature, layer) => {// 添加点击事件 - 显示弹框并清除高亮layer.on('click', (e) => {// 如果处于功能模式,阻止默认行为if (functionMode.value) {e.originalEvent.stopPropagation()return}// 清除之前的高亮clearHighlight()// 显示区域信息弹框const popup = L.popup().setLatLng(e.latlng).setContent(`<div class="region-popup"><h3>${feature.properties.name}</h3><p>行政代码: ${feature.properties.adcode || '未知'}</p><p>级别: ${feature.properties.level || '未知'}</p><div class="popup-actions"><button data-action="viewDrones" class="btn-primary">查看无人机</button><button data-action="viewAlarms" class="btn-primary">查看告警</button></div></div>`).openOn(mapInstance.value)// 添加事件委托处理按钮点击setTimeout(() => {const popupButtons = document.querySelectorAll('.region-popup .btn-primary')popupButtons.forEach(button => {button.addEventListener('click', (event) => {const action = event.target.getAttribute('data-action')if (action === 'viewDrones') {viewDrones()} else if (action === 'viewAlarms') {viewAlarms()}})})}, 100)})// 添加hover效果 - 只在非功能模式下生效layer.on('mouseover', (e) => {if (!functionMode.value) {e.target.setStyle({weight: 3,color: '#FF6347', // 番茄红dashArray: '',fillOpacity: 0.3,fillColor: 'rgba(255, 99, 71, 0.2)'})}})layer.on('mouseout', (e) => {if (!functionMode.value) {geoJsonLayer.value.resetStyle(e.target)}})// 添加永久显示的标签if (feature.properties && feature.properties.name && mapInstance.value) {try {layer.bindTooltip(feature.properties.name, {permanent: true,direction: 'center',className: 'region-label'})} catch (error) {console.warn('标签绑定失败:', error)}}}}).addTo(mapInstance.value)// 调整地图视野到数据范围mapInstance.value.fitBounds(geoJsonLayer.value.getBounds())}// ========== 新增功能方法 ==========// 1. 边界线高亮功能const toggleBoundaryHighlight = () => {if (functionMode.value === 'boundary') {// 关闭边界高亮clearBoundaryHighlight()functionMode.value = null} else {// 开启边界高亮clearAllFunctionLayers()functionMode.value = 'boundary'highlightBoundaries()}}const highlightBoundaries = () => {if (borderHighlightLayer.value) {mapInstance.value.removeLayer(borderHighlightLayer.value)}// 创建甘孜州整体的外轮廓边界const outerBoundary = createGanziOuterBoundary(mapJson)borderHighlightLayer.value = L.geoJSON(outerBoundary, {style: {fillColor: 'transparent', // 透明填充,只显示边界线weight: 6,opacity: 1,color: '#00FFFF', // 青色边框dashArray: '10,5', // 虚线效果fillOpacity: 0}}).addTo(mapInstance.value)// 添加闪烁效果let opacity = 0.5let increasing = trueconst flashInterval = setInterval(() => {if (borderHighlightLayer.value && mapInstance.value.hasLayer(borderHighlightLayer.value)) {if (increasing) {opacity += 0.1if (opacity >= 1) increasing = false} else {opacity -= 0.1if (opacity <= 0.3) increasing = true}borderHighlightLayer.value.setStyle({ opacity: opacity })} else {clearInterval(flashInterval)}}, 200)}// 创建甘孜州整体外轮廓的函数const createGanziOuterBoundary = (geoJsonData) => {try {// 提取甘孜州的真实外边界线条const outerBoundarySegments = extractOuterBoundarySegments(geoJsonData)return {type: "FeatureCollection", features: outerBoundarySegments.map((segment, index) => ({type: "Feature",properties: {name: `甘孜州外边界段${index + 1}`},geometry: {type: "LineString",coordinates: segment}}))}} catch (error) {console.error('创建外边界失败:', error)// 如果失败,返回一个简单的边界框return createBoundingBoxBoundary(geoJsonData)}}// 提取外边界线段的函数const extractOuterBoundarySegments = (geoJsonData) => {// 收集所有边界线段const allSegments = []const segmentCounts = new Map() // 统计每个线段出现的次数geoJsonData.features.forEach(feature => {const rings = []if (feature.geometry.type === 'MultiPolygon') {feature.geometry.coordinates.forEach(polygon => {rings.push(...polygon) // 包含外环和内环})} else if (feature.geometry.type === 'Polygon') {rings.push(...feature.geometry.coordinates) // 包含外环和内环}// 处理每个环的边界线段rings.forEach(ring => {for (let i = 0; i < ring.length - 1; i++) {const segment = [ring[i], ring[i + 1]]const segmentKey = createSegmentKey(segment)segmentCounts.set(segmentKey, (segmentCounts.get(segmentKey) || 0) + 1)allSegments.push({ key: segmentKey, coordinates: segment })}})})// 只保留出现次数为1的线段(外边界线段)const outerSegments = allSegments.filter(segment => segmentCounts.get(segment.key) === 1).map(segment => segment.coordinates)// 连接相邻的线段形成连续的边界线return connectSegments(outerSegments)}// 创建线段的唯一标识const createSegmentKey = (segment) => {const [p1, p2] = segment// 标准化线段方向,确保相同线段有相同的keyconst key1 = `${p1[0]},${p1[1]}-${p2[0]},${p2[1]}`const key2 = `${p2[0]},${p2[1]}-${p1[0]},${p1[1]}`return key1 < key2 ? key1 : key2}// 连接线段形成连续的边界线const connectSegments = (segments) => {if (segments.length === 0) return []const connectedPaths = []const usedSegments = new Set()for (let i = 0; i < segments.length; i++) {if (usedSegments.has(i)) continueconst path = [segments[i][0], segments[i][1]]usedSegments.add(i)let extended = truewhile (extended) {extended = false// 尝试在路径末尾添加连接的线段for (let j = 0; j < segments.length; j++) {if (usedSegments.has(j)) continueconst lastPoint = path[path.length - 1]const segment = segments[j]if (pointsEqual(lastPoint, segment[0])) {path.push(segment[1])usedSegments.add(j)extended = truebreak} else if (pointsEqual(lastPoint, segment[1])) {path.push(segment[0])usedSegments.add(j)extended = truebreak}}// 尝试在路径开始添加连接的线段if (!extended) {for (let j = 0; j < segments.length; j++) {if (usedSegments.has(j)) continueconst firstPoint = path[0]const segment = segments[j]if (pointsEqual(firstPoint, segment[0])) {path.unshift(segment[1])usedSegments.add(j)extended = truebreak} else if (pointsEqual(firstPoint, segment[1])) {path.unshift(segment[0])usedSegments.add(j)extended = truebreak}}}}if (path.length > 2) {connectedPaths.push(path)}}return connectedPaths}// 判断两个点是否相等const pointsEqual = (p1, p2) => {const tolerance = 1e-10return Math.abs(p1[0] - p2[0]) < tolerance && Math.abs(p1[1] - p2[1]) < tolerance}// 创建边界框边界的备用方法const createBoundingBoxBoundary = (geoJsonData) => {let minLng = Infinity, maxLng = -Infinitylet minLat = Infinity, maxLat = -InfinitygeoJsonData.features.forEach(feature => {if (feature.geometry.type === 'MultiPolygon') {feature.geometry.coordinates.forEach(polygon => {polygon.forEach(ring => {ring.forEach(point => {minLng = Math.min(minLng, point[0])maxLng = Math.max(maxLng, point[0])minLat = Math.min(minLat, point[1])maxLat = Math.max(maxLat, point[1])})})})} else if (feature.geometry.type === 'Polygon') {feature.geometry.coordinates.forEach(ring => {ring.forEach(point => {minLng = Math.min(minLng, point[0])maxLng = Math.max(maxLng, point[0])minLat = Math.min(minLat, point[1])maxLat = Math.max(maxLat, point[1])})})}})return {type: "Feature",properties: {name: "甘孜州外边界框"},geometry: {type: "Polygon",coordinates: [[[minLng, minLat],[maxLng, minLat],[maxLng, maxLat],[minLng, maxLat],[minLng, minLat]]]}}}const clearBoundaryHighlight = () => {if (borderHighlightLayer.value && mapInstance.value) {mapInstance.value.removeLayer(borderHighlightLayer.value)borderHighlightLayer.value = null}}// 2. 随机点填充功能const addRandomPoints = () => {if (functionMode.value === 'randomPoints') {clearRandomPoints()functionMode.value = null} else {clearAllFunctionLayers()functionMode.value = 'randomPoints'generateRandomPoints()}}const generateRandomPoints = () => {if (randomPointsLayer.value) {mapInstance.value.removeLayer(randomPointsLayer.value)}const bounds = geoJsonLayer.value.getBounds()const pointCount = Math.floor(Math.random() * 15) + 10 // 10-25个随机点const points = []for (let i = 0; i < pointCount; i++) {const lat = bounds.getSouth() + (bounds.getNorth() - bounds.getSouth()) * Math.random()const lng = bounds.getWest() + (bounds.getEast() - bounds.getWest()) * Math.random()const pointType = ['监测站', '巡检点', '风险点', '观测塔', '设备点'][Math.floor(Math.random() * 5)]const riskLevel = ['低', '中', '高'][Math.floor(Math.random() * 3)]points.push({lat,lng,type: pointType,riskLevel,id: `point_${i}`})}randomPointsLayer.value = L.layerGroup()points.forEach(point => {const color = point.riskLevel === '高' ? '#ff4444' : point.riskLevel === '中' ? '#ffaa00' : '#44ff44'const marker = L.circleMarker([point.lat, point.lng], {radius: 8,fillColor: color,color: '#fff',weight: 2,opacity: 1,fillOpacity: 0.8}).bindPopup(`<div class="point-popup"><h4>${point.type}</h4><p>风险级别: <span style="color: ${color}">${point.riskLevel}</span></p><p>坐标: ${point.lat.toFixed(4)}, ${point.lng.toFixed(4)}</p></div>`)randomPointsLayer.value.addLayer(marker)})mapInstance.value.addLayer(randomPointsLayer.value)}const clearRandomPoints = () => {if (randomPointsLayer.value && mapInstance.value) {mapInstance.value.removeLayer(randomPointsLayer.value)randomPointsLayer.value = null}}// 3. 选点框选面积功能const toggleAreaSelect = () => {if (functionMode.value === 'areaSelect') {clearAreaSelect()functionMode.value = nullmapInstance.value.getContainer().style.cursor = ''} else {clearAllFunctionLayers()functionMode.value = 'areaSelect'mapInstance.value.getContainer().style.cursor = 'crosshair'isDrawing.value = falsemeasurePoints.value = []}}const handleAreaSelectClick = (e) => {measurePoints.value.push([e.latlng.lat, e.latlng.lng])if (!drawingLayer.value) {drawingLayer.value = L.layerGroup().addTo(mapInstance.value)}// 添加点标记const pointMarker = L.circleMarker(e.latlng, {radius: 5,fillColor: '#ff0000',color: '#fff',weight: 2,opacity: 1,fillOpacity: 0.8})drawingLayer.value.addLayer(pointMarker)if (measurePoints.value.length >= 3) {// 绘制多边形并计算面积和周长const polygon = L.polygon(measurePoints.value, {color: '#ff0000',fillColor: '#ff0000',fillOpacity: 0.2})if (areaSelectLayer.value) {mapInstance.value.removeLayer(areaSelectLayer.value)}areaSelectLayer.value = L.layerGroup()areaSelectLayer.value.addLayer(polygon)// 计算面积(平方公里)let area = 0if (window.L && window.L.GeometryUtil && window.L.GeometryUtil.geodesicArea) {area = (L.GeometryUtil.geodesicArea(measurePoints.value) / 1000000).toFixed(2)} else {// 备用计算方法:使用简单的多边形面积公式area = (calculatePolygonArea(measurePoints.value) / 1000000).toFixed(2)}// 计算周长(公里)const perimeter = calculatePerimeter(measurePoints.value).toFixed(2)// 计算多边形中心点const center = calculatePolygonCenter(measurePoints.value)// 在中心位置显示面积和周长信息const infoMarker = L.marker([center.lat, center.lng], {icon: L.divIcon({className: 'area-info-marker',html: `<div class="area-info-content"><div class="area-value">面积: ${area} km²</div><div class="perimeter-value">周长: ${perimeter} km</div></div>`,iconSize: [120, 50],iconAnchor: [60, 25]})})areaSelectLayer.value.addLayer(infoMarker)mapInstance.value.addLayer(areaSelectLayer.value)// 添加弹窗到多边形polygon.bindPopup(`<div class="area-popup"><h4>框选区域详情</h4><p>面积: ${area} 平方公里</p><p>周长: ${perimeter} 公里</p><p>顶点数: ${measurePoints.value.length}</p><button onclick="window.clearCurrentArea()">清除</button></div>`)// 暴露清除方法到全局window.clearCurrentArea = () => {clearAreaSelect()functionMode.value = nullmapInstance.value.getContainer().style.cursor = ''}}}// 计算多边形面积的备用方法(使用Shoelace公式)const calculatePolygonArea = (points) => {if (points.length < 3) return 0let area = 0const earthRadius = 6371000 // 地球半径(米)for (let i = 0; i < points.length; i++) {const j = (i + 1) % points.lengthconst lat1 = points[i][0] * Math.PI / 180const lng1 = points[i][1] * Math.PI / 180const lat2 = points[j][0] * Math.PI / 180const lng2 = points[j][1] * Math.PI / 180area += (lng2 - lng1) * (2 + Math.sin(lat1) + Math.sin(lat2))}area = Math.abs(area) * earthRadius * earthRadius / 2return area}// 计算多边形周长const calculatePerimeter = (points) => {if (points.length < 2) return 0let perimeter = 0for (let i = 0; i < points.length; i++) {const j = (i + 1) % points.lengthconst point1 = L.latLng(points[i])const point2 = L.latLng(points[j])perimeter += point1.distanceTo(point2)}return perimeter / 1000 // 转换为公里}// 计算多边形中心点const calculatePolygonCenter = (points) => {if (points.length === 0) return { lat: 0, lng: 0 }let sumLat = 0let sumLng = 0points.forEach(point => {sumLat += point[0]sumLng += point[1]})return {lat: sumLat / points.length,lng: sumLng / points.length}}const clearAreaSelect = () => {if (areaSelectLayer.value && mapInstance.value) {mapInstance.value.removeLayer(areaSelectLayer.value)areaSelectLayer.value = null}if (drawingLayer.value && mapInstance.value) {mapInstance.value.removeLayer(drawingLayer.value)drawingLayer.value = null}measurePoints.value = []}// 4. 距离测量功能const toggleDistanceMeasure = () => {if (functionMode.value === 'distance') {clearDistanceMeasure()functionMode.value = nullmapInstance.value.getContainer().style.cursor = ''} else {clearAllFunctionLayers()functionMode.value = 'distance'mapInstance.value.getContainer().style.cursor = 'crosshair'measurePoints.value = []}}const handleDistanceMeasureClick = (e) => {measurePoints.value.push([e.latlng.lat, e.latlng.lng])if (!distanceMeasureLayer.value) {distanceMeasureLayer.value = L.layerGroup().addTo(mapInstance.value)}// 添加点标记const pointMarker = L.circleMarker(e.latlng, {radius: 5,fillColor: '#0066cc',color: '#fff',weight: 2,opacity: 1,fillOpacity: 0.8}).bindPopup(`点 ${measurePoints.value.length}`)distanceMeasureLayer.value.addLayer(pointMarker)if (measurePoints.value.length >= 2) {// 绘制线段并计算距离const polyline = L.polyline(measurePoints.value, {color: '#0066cc',weight: 3,opacity: 0.7})distanceMeasureLayer.value.addLayer(polyline)// 计算总距离let totalDistance = 0for (let i = 1; i < measurePoints.value.length; i++) {const prev = L.latLng(measurePoints.value[i-1])const curr = L.latLng(measurePoints.value[i])totalDistance += prev.distanceTo(curr)}const distanceKm = (totalDistance / 1000).toFixed(2)// 在最后一点显示总距离const lastPoint = measurePoints.value[measurePoints.value.length - 1]const distanceMarker = L.marker([lastPoint[0], lastPoint[1]], {icon: L.divIcon({className: 'distance-label',html: `<div class="distance-text">${distanceKm} km</div>`,iconSize: [80, 20],iconAnchor: [40, 10]})})distanceMeasureLayer.value.addLayer(distanceMarker)}}const clearDistanceMeasure = () => {if (distanceMeasureLayer.value && mapInstance.value) {mapInstance.value.removeLayer(distanceMeasureLayer.value)distanceMeasureLayer.value = null}measurePoints.value = []}// 5. 随机路径规划功能const addRandomRoute = () => {if (functionMode.value === 'route') {clearRoutePlanning()functionMode.value = null} else {clearAllFunctionLayers()functionMode.value = 'route'generateRandomRoute()}}const generateRandomRoute = () => {if (routePlanningLayer.value) {mapInstance.value.removeLayer(routePlanningLayer.value)}const bounds = geoJsonLayer.value.getBounds()const pointCount = Math.floor(Math.random() * 5) + 3 // 3-8个路径点const routePoints = []for (let i = 0; i < pointCount; i++) {const lat = bounds.getSouth() + (bounds.getNorth() - bounds.getSouth()) * Math.random()const lng = bounds.getWest() + (bounds.getEast() - bounds.getWest()) * Math.random()routePoints.push([lat, lng])}routePlanningLayer.value = L.layerGroup()// 绘制路径线const routeLine = L.polyline(routePoints, {color: '#ff6600',weight: 4,opacity: 0.8,dashArray: '10,5'})routePlanningLayer.value.addLayer(routeLine)// 添加起点和终点标记const startMarker = L.marker(routePoints[0], {icon: L.divIcon({className: 'route-marker start-marker',html: '<div class="marker-content">起点</div>',iconSize: [40, 20],iconAnchor: [20, 10]})})const endMarker = L.marker(routePoints[routePoints.length - 1], {icon: L.divIcon({className: 'route-marker end-marker',html: '<div class="marker-content">终点</div>',iconSize: [40, 20],iconAnchor: [20, 10]})})routePlanningLayer.value.addLayer(startMarker)routePlanningLayer.value.addLayer(endMarker)// 添加中间点标记for (let i = 1; i < routePoints.length - 1; i++) {const waypoint = L.circleMarker(routePoints[i], {radius: 6,fillColor: '#ff6600',color: '#fff',weight: 2,opacity: 1,fillOpacity: 0.8}).bindPopup(`途经点 ${i}`)routePlanningLayer.value.addLayer(waypoint)}// 计算路径总长度let totalDistance = 0for (let i = 1; i < routePoints.length; i++) {const prev = L.latLng(routePoints[i-1])const curr = L.latLng(routePoints[i])totalDistance += prev.distanceTo(curr)}const routeDistance = (totalDistance / 1000).toFixed(2)// 显示路径信息routeLine.bindPopup(`<div class="route-popup"><h4>飞行路径</h4><p>总距离: ${routeDistance} km</p><p>途经点: ${pointCount} 个</p><p>预计飞行时间: ${Math.ceil(totalDistance / 1000 / 50 * 60)} 分钟</p></div>`).openPopup()mapInstance.value.addLayer(routePlanningLayer.value)}const clearRoutePlanning = () => {if (routePlanningLayer.value && mapInstance.value) {mapInstance.value.removeLayer(routePlanningLayer.value)routePlanningLayer.value = null}}// 清除所有功能图层const clearAllFunctionLayers = () => {clearBoundaryHighlight()clearRandomPoints()clearAreaSelect()clearDistanceMeasure()clearRoutePlanning()functionMode.value = nullmapInstance.value.getContainer().style.cursor = ''}// 高亮选中区域const highlightRegion = (layer, feature) => {// 清除之前的高亮if (highlightLayer.value && mapInstance.value) {if (mapInstance.value.hasLayer(highlightLayer.value)) {mapInstance.value.removeLayer(highlightLayer.value)}highlightLayer.value = null}// 创建高亮图层highlightLayer.value = L.geoJSON(feature, {style: {fillColor: '#32CD32', // 酸橙绿weight: 4,opacity: 1,color: '#228B22', // 森林绿dashArray: '',fillOpacity: 0.4}}).addTo(mapInstance.value)// 更新选中区域信息selectedRegion.value = feature.properties// 添加标记点到区域中心addMarkerToRegion(feature)console.log('选中区域:', feature.properties.name)}// 在区域中心添加标记点const addMarkerToRegion = (feature) => {const center = feature.properties.center || feature.properties.centroidif (center && center.length === 2) {const marker = L.marker([center[1], center[0]], {icon: L.divIcon({className: 'custom-marker',html: `<div class="marker-pin"><div class="marker-icon">🌲</div></div>`,iconSize: [30, 30],iconAnchor: [15, 30]})}).addTo(mapInstance.value)try {marker.bindPopup(`<div class="marker-popup"><h3>${feature.properties.name}</h3><p>行政代码: ${feature.properties.adcode}</p><p>级别: ${feature.properties.level}</p><div class="popup-actions"><button data-action="viewDrones" class="btn-primary">查看无人机</button><button data-action="viewAlarms" class="btn-primary">查看告警</button></div></div>`)// 为标记点弹窗添加事件监听marker.on('popupopen', () => {setTimeout(() => {const popupButtons = document.querySelectorAll('.marker-popup .btn-primary')popupButtons.forEach(button => {button.addEventListener('click', (event) => {const action = event.target.getAttribute('data-action')if (action === 'viewDrones') {viewDrones()} else if (action === 'viewAlarms') {viewAlarms()}})})}, 100)})} catch (error) {console.warn('Popup绑定失败:', error)}markers.value.push(marker)}}// 清除所有标记点const clearMarkers = () => {if (mapInstance.value) {markers.value.forEach(marker => {if (marker && mapInstance.value.hasLayer(marker)) {mapInstance.value.removeLayer(marker)}})}markers.value = []}// 清除高亮const clearHighlight = () => {if (highlightLayer.value && mapInstance.value) {if (mapInstance.value.hasLayer(highlightLayer.value)) {mapInstance.value.removeLayer(highlightLayer.value)}highlightLayer.value = null}selectedRegion.value = null}// 重置视图const resetView = () => {if (geoJsonLayer.value && mapInstance.value) {mapInstance.value.fitBounds(geoJsonLayer.value.getBounds())}}onMounted(() => {// 动态加载Leaflet CSS和JSconst loadLeaflet = () => {return new Promise((resolve, reject) => {if (window.L) {resolve()return}// 加载CSSconst link = document.createElement('link')link.rel = 'stylesheet'link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'document.head.appendChild(link)// 加载JSconst script = document.createElement('script')script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'script.onload = () => {// 加载Leaflet GeometryUtil插件用于面积计算const geometryScript = document.createElement('script')geometryScript.src = 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js'geometryScript.onload = resolvegeometryScript.onerror = resolve // 即使插件加载失败也继续document.head.appendChild(geometryScript)}script.onerror = rejectdocument.head.appendChild(script)})}loadLeaflet().then(() => {try {initMap()} catch (error) {console.error('地图初始化失败:', error)}}).catch((error) => {console.error('Leaflet库加载失败:', error)})})onUnmounted(() => {// 清理所有图层和标记clearMarkers()clearHighlight()clearAllFunctionLayers()// 清理geoJson图层if (geoJsonLayer.value && mapInstance.value) {if (mapInstance.value.hasLayer(geoJsonLayer.value)) {mapInstance.value.removeLayer(geoJsonLayer.value)}geoJsonLayer.value = null}// 销毁地图实例if (mapInstance.value) {mapInstance.value.off() // 移除所有事件监听器mapInstance.value.remove()mapInstance.value = null}// 清理全局方法if (window.clearCurrentArea) {delete window.clearCurrentArea}})// 暴露方法给父组件defineExpose({clearMarkers,clearHighlight,addMarkerToRegion,resetView,// 新增功能方法toggleBoundaryHighlight,addRandomPoints,toggleAreaSelect,toggleDistanceMeasure,addRandomRoute,clearAllFunctionLayers})</script><style scoped>:deep(.leaflet-control-attribution){display: none;}.map-wrapper {position: relative;height: 100%;width: 100%;background: #000;}#map-container {height: 100%;width: 100%;background: #000;}.map-controls {position: absolute;top: 10px;right: 10px;z-index: 1000;display: flex;flex-direction: column;gap: 10px;}.control-panel {background: rgba(0, 0, 0, 0.95);backdrop-filter: blur(10px);border-radius: 8px;padding: 10px;box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);display: flex;flex-direction: column;gap: 8px;min-width: 120px;border: 1px solid #444;}.control-btn {display: flex;align-items: center;gap: 8px;padding: 8px 12px;background: #333;border: 1px solid #555;border-radius: 6px;cursor: pointer;transition: all 0.2s ease;font-size: 14px;color: #fff;}.control-btn:hover {background: #444;border-color: #777;transform: translateY(-1px);box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2);}.btn-icon {font-size: 16px;}.btn-text {font-weight: 500;}.info-panel {background: rgba(0, 0, 0, 0.8);color: white;border-radius: 8px;padding: 12px;backdrop-filter: blur(10px);min-width: 200px;}.info-panel h4 {margin: 0 0 8px 0;font-size: 16px;color: #fff;}.info-panel p {margin: 4px 0;font-size: 14px;color: #ccc;}:deep(.custom-marker) {background: transparent;border: none;}:deep(.marker-pin) {width: 30px;height: 30px;display: flex;align-items: center;justify-content: center;background: linear-gradient(135deg, #32CD32, #228B22);border: 2px solid #fff;border-radius: 50% 50% 50% 0;transform: rotate(-45deg);box-shadow: 0 4px 12px rgba(34, 139, 34, 0.4);}:deep(.marker-icon) {transform: rotate(45deg);font-size: 16px;}:deep(.marker-popup),:deep(.region-popup),:deep(.point-popup),:deep(.area-popup),:deep(.route-popup) {font-family: Arial, sans-serif;}:deep(.marker-popup h3),:deep(.region-popup h3),:deep(.point-popup h4),:deep(.area-popup h4),:deep(.route-popup h4) {margin: 0 0 8px 0;color: white;font-size: 16px;}:deep(.marker-popup p),:deep(.region-popup p),:deep(.point-popup p),:deep(.area-popup p),:deep(.route-popup p) {margin: 4px 0;color: #ccc;font-size: 14px;}:deep(.popup-actions) {margin-top: 10px;text-align: center;}:deep(.popup-actions button) {background: #444;color: white;border: 1px solid #666;padding: 6px 12px;border-radius: 4px;cursor: pointer;font-size: 12px;transition: all 0.2s ease;margin: 2px;}:deep(.popup-actions button:hover) {background: #555;border-color: #888;}:deep(.popup-actions .btn-primary) {background: #007bff;border-color: #007bff;color: white;}:deep(.popup-actions .btn-primary:hover) {background: #0056b3;border-color: #0056b3;}:deep(.region-label) {background: transparent;color: white;border: none;padding: 2px 4px;font-size: 14px;font-weight: 600;pointer-events: none;white-space: nowrap;text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);}/* 新增功能样式 */:deep(.distance-label) {background: transparent;border: none;}:deep(.distance-text) {background: rgba(0, 102, 204, 0.9);color: white;padding: 4px 8px;border-radius: 4px;font-size: 12px;font-weight: bold;text-align: center;border: 1px solid #fff;}:deep(.route-marker) {background: transparent;border: none;}:deep(.route-marker .marker-content) {background: #ff6600;color: white;padding: 4px 8px;border-radius: 4px;font-size: 12px;font-weight: bold;text-align: center;border: 1px solid #fff;}:deep(.start-marker .marker-content) {background: #00aa00;}:deep(.end-marker .marker-content) {background: #aa0000;}/* 框选面积信息标记样式 */:deep(.area-info-marker) {background: transparent;border: none;}:deep(.area-info-content) {background: rgba(0, 0, 0, 0.9);color: white;padding: 8px 12px;border-radius: 8px;font-size: 12px;font-weight: bold;text-align: center;border: 2px solid #ff0000;box-shadow: 0 4px 12px rgba(255, 0, 0, 0.4);backdrop-filter: blur(5px);min-width: 100px;}:deep(.area-info-content .area-value) {color: #ffff00;font-size: 13px;margin-bottom: 2px;}:deep(.area-info-content .perimeter-value) {color: #00ff00;font-size: 12px;}:deep(.leaflet-popup-content-wrapper) {background: rgba(0, 0, 0, 0.9);border-radius: 8px;box-shadow: 0 4px 12px rgba(0,0,0,0.5);border: 1px solid #444;}:deep(.leaflet-popup-tip) {background: rgba(0, 0, 0, 0.9);border: 1px solid #444;}:deep(.leaflet-control-container){position: absolute;right: 500px;bottom: 300px;}/* Leaflet 控件样式 - 黑色主题 */:deep(.leaflet-control-layers) {background: rgba(0, 0, 0, 0.95);border: 1px solid #444;border-radius: 8px;box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);}:deep(.leaflet-control-layers-expanded) {background: rgba(0, 0, 0, 0.95);color: #fff;}:deep(.leaflet-control-layers label) {color: #fff;}:deep(.leaflet-control-zoom) {background: rgba(0, 0, 0, 0.95);border: 1px solid #444;border-radius: 8px;box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);}:deep(.leaflet-control-zoom a) {background: #333;color: #fff;border: 1px solid #555;}:deep(.leaflet-control-zoom a:hover) {background: #444;color: #fff;}</style>
TdMap组件
<template><div class="td-map"><div id="td-map-container" class="td-map-container"></div><!-- 弹框组件 --><div v-if="popup.visible" class="popup-overlay"><div class="popup-content"@click.stop><!-- 区域弹框 --><div v-if="popup.data.type !== 'risk-marker' && popup.data.type !== 'grid-resource' && popup.data.type !== 'risk-area'" class="region-popup"><h3>{{ popup.data.name }}</h3><p>行政代码: {{ popup.data.adcode || '未知' }}</p><p>级别: {{ popup.data.level || '未知' }}</p><div class="popup-actions"><button @click="viewDrones" class="btn-primary">查看无人机</button><button @click="viewAlarms" class="btn-primary">查看告警</button></div></div><!-- 风险标记点弹框 --><div v-else-if="popup.data.type === 'risk-marker'" class="risk-marker-popup"><h3>{{ popup.data.name }}</h3><p class="risk-level" :class="`${popup.data.level}-risk-text`">风险等级: {{ popup.data.level === 'high' ? '高风险' : popup.data.level === 'medium' ? '中风险' : '低风险' }}</p><p class="risk-type">风险类型: {{ popup.data.riskType }}</p><p class="risk-description">{{ popup.data.description }}</p><p class="last-incident">最近事件: {{ popup.data.lastIncident }}</p><div class="popup-actions"><button @click="viewRiskDetail" :class="popup.data.level === 'high' ? 'btn-danger' : popup.data.level === 'medium' ? 'btn-warning' : 'btn-success'">查看详情</button><button @click="reportRisk" class="btn-primary">上报处理</button></div></div><!-- 网格资源弹框 --><div v-else-if="popup.data.type === 'grid-resource'" class="grid-resource-popup"><h3>{{ popup.data.name }}</h3><div class="resource-list"><h4>资源配置:</h4><ul><li v-for="resource in popup.data.resources" :key="resource">{{ resource }}</li></ul></div><div class="popup-actions"><button @click="viewGridDetail" class="btn-primary">查看详情</button><button @click="manageGrid" class="btn-success">资源管理</button></div></div><!-- 风险区域弹框 --><div v-else-if="popup.data.type === 'risk-area'" class="risk-area-popup"><h3>{{ popup.data.name }}</h3><p class="risk-level" :class="`${popup.data.level}-risk-text`">风险等级: {{ popup.data.level === 'high' ? '高风险' : popup.data.level === 'medium' ? '中风险' : '低风险' }}</p><div class="risk-factors"><h4>风险因素:</h4><ul><li v-for="factor in popup.data.riskFactors" :key="factor">{{ factor }}</li></ul></div><div class="popup-actions"><button @click="viewAreaDetail" class="btn-primary">查看详情</button><button @click="manageRisk" :class="popup.data.level === 'high' ? 'btn-danger' : popup.data.level === 'medium' ? 'btn-warning' : 'btn-success'">风险管理</button></div></div><button @click="closePopup" class="close-btn">×</button></div></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import mapJson from '@/assets/map/ganzi.json'
import { useRouter } from 'vue-router'const router = useRouter()// 定义组件属性
const props = defineProps({riskMarkers: {type: Array,default: () => []}
})// 响应式数据
const map = ref(null)
const currentHighlightLayer = ref(null)
const riskMarkersOverlays = ref([])
const gridResourcesOverlays = ref([])
const riskAreaOverlays = ref([])
const popup = ref({visible: false,x: 0,y: 0,data: {}
})// 功能开关状态
const featureStates = ref({gridResources: false,riskMarkers: false,riskArea: false
})// 标记是否点击了区域内部
const isClickingArea = ref(false)// 天地图密钥 - 已更新修复坐标转换问题
const TIANDITU_KEY = '59c74f20e85621a8694b9a3737773a70'// 初始化地图
const initMap = () => {// 创建地图实例map.value = new T.Map('td-map-container', {projection: 'EPSG:3857' // Web墨卡托投影})// 设置地图中心点和缩放级别(以甘孜州为中心)const center = new T.LngLat(99.964057, 32.050738)map.value.centerAndZoom(center, 7)// 方案1:尝试使用天地图JavaScript API的内置图层try {// 使用天地图API内置的卫星图层const satelliteLayer = new T.ImageLayer()map.value.addLayer(satelliteLayer)const satelliteLabelLayer = new T.LabelLayer()map.value.addLayer(satelliteLabelLayer)} catch (error) {console.log('使用内置图层失败,尝试备用方案')// 方案2:使用单一域名的WMTS服务const satelliteLayer = new T.TileLayer(`https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`, {attribution: '© 天地图 卫星影像'})map.value.addLayer(satelliteLayer)const satelliteLabelLayer = new T.TileLayer(`https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`, {attribution: '© 天地图 标注'})map.value.addLayer(satelliteLabelLayer)}// 设置地图控件map.value.addControl(new T.Control.Zoom())map.value.addControl(new T.Control.Scale())// 添加地图点击事件(点击空白区域关闭弹框)map.value.addEventListener('click', (e) => {console.log('地图点击事件触发,isClickingArea:', isClickingArea.value)// 延迟检查,确保区域点击事件先执行setTimeout(() => {// 只有在不是点击区域内部且弹框打开时才关闭弹框if (!isClickingArea.value && popup.value.visible) {console.log('点击空白区域关闭弹框');closePopup()}}, 150)})// 添加边界线addBoundaries()// 如果有初始标记点,添加它们if (props.riskMarkers.length > 0) {addRiskMarkers(props.riskMarkers)}
}// 添加边界线
const addBoundaries = () => {if (!mapJson.features) returnmapJson.features.forEach(feature => {if (feature.geometry && feature.geometry.coordinates) {const polygons = convertGeoJSONToTianditu(feature.geometry.coordinates, feature.geometry.type)polygons.forEach(polygon => {// 创建多边形const tPolygon = new T.Polygon(polygon, {color: '#00FF00', // 绿色边界线,在卫星地图上更明显weight: 3,opacity: 1,fillColor: 'transparent',fillOpacity: 0,lineStyle: 'solid'})// 添加到地图map.value.addOverLay(tPolygon)// 计算区域中心点并添加文字标签const center = calculatePolygonCenter(polygon)if (center && feature.properties && feature.properties.name) {// 使用天地图的文字标注 - 简化版本const textLabel = new T.Label({text: feature.properties.name,position: center,offset: new T.Point(0, 0)})// 添加文字标签到地图map.value.addOverLay(textLabel)}// 绑定点击事件tPolygon.addEventListener('click', (e) => {console.log('点击区域内部')// 阻止事件冒泡到地图if (e.originalEvent) {e.originalEvent.stopPropagation()}// 标记正在点击区域内部isClickingArea.value = truehandleAreaClick(e, feature.properties, tPolygon)// 延长重置时间,确保地图点击事件处理完成setTimeout(() => {isClickingArea.value = false}, 200)})// 绑定鼠标悬停事件tPolygon.addEventListener('mouseover', () => {tPolygon.setFillColor('#00BFFF') // 天蓝色悬停效果tPolygon.setFillOpacity(0.4)tPolygon.setColor('#FFFF00') // 黄色边框tPolygon.setWeight(4)})tPolygon.addEventListener('mouseout', () => {if (currentHighlightLayer.value !== tPolygon) {tPolygon.setFillColor('transparent')tPolygon.setFillOpacity(0)tPolygon.setColor('#00FF00') // 恢复绿色边框tPolygon.setWeight(3)}})})}})
}// 转换GeoJSON坐标为天地图格式
const convertGeoJSONToTianditu = (coordinates, type) => {const polygons = []if (type === 'Polygon') {const points = coordinates[0].map(coord => new T.LngLat(coord[0], coord[1]))polygons.push(points)} else if (type === 'MultiPolygon') {coordinates.forEach(polygon => {const points = polygon[0].map(coord => new T.LngLat(coord[0], coord[1]))polygons.push(points)})}return polygons
}// 计算多边形的中心点
const calculatePolygonCenter = (polygon) => {if (!polygon || polygon.length === 0) {return null}let totalLng = 0let totalLat = 0let count = 0polygon.forEach(point => {if (point && point.lng !== undefined && point.lat !== undefined) {totalLng += point.lngtotalLat += point.latcount++}})if (count === 0) return nullreturn new T.LngLat(totalLng / count, totalLat / count)
}// 处理区域点击
const handleAreaClick = (event, properties, polygon) => {// 清除之前的高亮if (currentHighlightLayer.value && currentHighlightLayer.value !== polygon) {currentHighlightLayer.value.setFillColor('transparent')currentHighlightLayer.value.setFillOpacity(0)}// 高亮当前区域polygon.setFillColor('#FF6B6B')polygon.setFillOpacity(0.5)currentHighlightLayer.value = polygon// 显示弹框showPopup(event, properties)
}// 显示弹框 - 居中显示版本
const showPopup = (event, data) => {console.log('showPopup called with:', event, data) // 调试信息// 弹框居中显示,不需要复杂的位置计算popup.value = {visible: true,data: data}console.log(popup.value);}// 关闭弹框
const closePopup = () => {popup.value.visible = false
}// 键盘事件处理
const handleKeydown = (event) => {if (event.key === 'Escape' && popup.value.visible) {closePopup()}
}// 无人机查看方法
const viewDrones = () => {closePopup()router.push('/drone-monitoring')
}// 告警查看方法
const viewAlarms = () => {closePopup()router.push('/alarm-detail')
}// 查看风险详情
const viewRiskDetail = () => {closePopup()// 可以跳转到风险详情页面或显示更多信息console.log('查看风险详情')
}// 上报风险处理
const reportRisk = () => {closePopup()// 可以跳转到上报处理页面console.log('上报风险处理')
}// ========== 新增弹框处理方法 ==========// 网格资源相关方法
const viewGridDetail = () => {closePopup()console.log('查看网格详情')// 可以跳转到网格详情页面
}const manageGrid = () => {closePopup()console.log('网格资源管理')// 可以跳转到资源管理页面
}// 风险区域相关方法
const viewAreaDetail = () => {closePopup()console.log('查看区域详情')// 可以跳转到区域详情页面
}const manageRisk = () => {closePopup()console.log('风险管理')// 可以跳转到风险管理页面
}// 添加风险标记点
const addRiskMarkers = (markers) => {// 清除之前的标记点clearRiskMarkers()markers.forEach(marker => {// 创建标记点const markerPoint = new T.LngLat(marker.lng, marker.lat)// 根据风险等级创建不同的图标let iconSvg = ''if (marker.level === 'high') {iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36"><circle cx="18" cy="18" r="14" fill="#ff4d4d" stroke="#fff" stroke-width="3"/><path d="M18 8L19.8 14.2L26 14.2L21.1 18L22.9 24.2L18 20.4L13.1 24.2L14.9 18L10 14.2L16.2 14.2Z" fill="#fff"/><circle cx="18" cy="18" r="16" fill="none" stroke="#ff4d4d" stroke-width="1" opacity="0.3"/></svg>`} else if (marker.level === 'medium') {iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="12" fill="#ffc107" stroke="#fff" stroke-width="2"/><path d="M16 6L17.2 11.8L22 11.8L18.4 15L19.6 20.8L16 17.6L12.4 20.8L13.6 15L10 11.8L14.8 11.8Z" fill="#fff"/><circle cx="16" cy="16" r="14" fill="none" stroke="#ffc107" stroke-width="1" opacity="0.2"/></svg>`} else if (marker.level === 'low') {iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><circle cx="14" cy="14" r="10" fill="#28a745" stroke="#fff" stroke-width="2"/><circle cx="14" cy="14" r="3" fill="#fff"/><path d="M14 8v4M14 16v4M8 14h4M16 14h4" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/><circle cx="14" cy="14" r="12" fill="none" stroke="#28a745" stroke-width="1" opacity="0.15"/></svg>`}// 创建自定义图标const iconSize = marker.level === 'high' ? 36 : marker.level === 'medium' ? 32 : 28const icon = new T.Icon({iconUrl: 'data:image/svg+xml;base64,' + btoa(iconSvg),iconSize: new T.Point(iconSize, iconSize),iconAnchor: new T.Point(iconSize/2, iconSize/2)})// 创建标记const tMarker = new T.Marker(markerPoint, { icon: icon })// 添加点击事件tMarker.addEventListener('click', (e) => {// 阻止事件冒泡if (e.originalEvent) {e.originalEvent.stopPropagation()}isClickingArea.value = true// 显示风险点信息弹框showRiskMarkerPopup(e, marker)setTimeout(() => {isClickingArea.value = false}, 200)})// 添加到地图map.value.addOverLay(tMarker)riskMarkersOverlays.value.push(tMarker)})
}// 清除风险标记点
const clearRiskMarkers = () => {riskMarkersOverlays.value.forEach(marker => {map.value.removeOverLay(marker)})riskMarkersOverlays.value = []
}// 显示风险标记点弹框
const showRiskMarkerPopup = (event, markerData) => {popup.value = {visible: true,data: {name: markerData.name,level: markerData.level,description: markerData.description,riskType: markerData.riskType,lastIncident: markerData.lastIncident,type: 'risk-marker'}}
}// 加载天地图API
const loadTiandituAPI = () => {return new Promise((resolve, reject) => {if (window.T) {resolve()return}const script = document.createElement('script')script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${TIANDITU_KEY}`script.onload = resolvescript.onerror = (error) => {console.error('天地图API加载失败:', error)reject(error)}document.head.appendChild(script)})
}// 监听风险标记点变化
watch(() => props.riskMarkers, (newMarkers) => {if (map.value) {addRiskMarkers(newMarkers)}
}, { deep: true })// 组件挂载
onMounted(async () => {try {await loadTiandituAPI()initMap()// 添加键盘事件监听document.addEventListener('keydown', handleKeydown)} catch (error) {console.error('加载天地图API失败:', error)}
})// 组件卸载
onUnmounted(() => {// 移除键盘事件监听document.removeEventListener('keydown', handleKeydown)if (map.value) {clearRiskMarkers()clearAllOverlays()map.value.clearOverLays()map.value = null}
})// ========== 新增功能方法 ==========// 1. 网格资源功能
const toggleGridResources = () => {featureStates.value.gridResources = !featureStates.value.gridResourcesif (featureStates.value.gridResources) {showGridResources()} else {clearGridResources()}
}const showGridResources = () => {// 模拟网格区域数据const gridData = [{ id: 1, name: '网格A1', points: [[100.1, 31.7], [100.3, 31.7], [100.3, 31.9], [100.1, 31.9], [100.1, 31.7]], resources: ['无人机2架', '护林员3人', '瞭望塔1座'] },{ id: 2, name: '网格B2', points: [[99.7, 32.1], [99.9, 32.1], [99.9, 32.3], [99.7, 32.3], [99.7, 32.1]], resources: ['无人机1架', '护林员2人', '水弹50发'] },{ id: 3, name: '网格C3', points: [[100.4, 32.4], [100.6, 32.4], [100.6, 32.6], [100.4, 32.6], [100.4, 32.4]], resources: ['护林员4人', '瞭望塔2座', '卫星监控'] },{ id: 4, name: '网格D4', points: [[99.4, 31.4], [99.6, 31.4], [99.6, 31.6], [99.4, 31.6], [99.4, 31.4]], resources: ['无人机3架', '护林员1人', '水弹30发'] }]gridData.forEach(grid => {// 创建网格区域多边形const points = grid.points.map(point => new T.LngLat(point[0], point[1]))const gridPolygon = new T.Polygon(points, {color: '#4CAF50',weight: 3,opacity: 0.9,fillColor: '#4CAF50',fillOpacity: 0.3,dashArray: [8, 4] // 虚线边框表示网格})// 添加点击事件gridPolygon.addEventListener('click', (e) => {if (e.originalEvent) {e.originalEvent.stopPropagation()}isClickingArea.value = trueshowGridResourcePopup(e, grid)setTimeout(() => {isClickingArea.value = false}, 200)})// 添加悬停效果gridPolygon.addEventListener('mouseover', () => {gridPolygon.setFillOpacity(0.5)gridPolygon.setWeight(4)})gridPolygon.addEventListener('mouseout', () => {gridPolygon.setFillOpacity(0.3)gridPolygon.setWeight(3)})map.value.addOverLay(gridPolygon)gridResourcesOverlays.value.push(gridPolygon)})
}const clearGridResources = () => {gridResourcesOverlays.value.forEach(overlay => {map.value.removeOverLay(overlay)})gridResourcesOverlays.value = []
}const showGridResourcePopup = (event, gridData) => {popup.value = {visible: true,data: {name: gridData.name,resources: gridData.resources,type: 'grid-resource'}}
}// 3. 风险区域功能
const toggleRiskArea = () => {featureStates.value.riskArea = !featureStates.value.riskAreaif (featureStates.value.riskArea) {showRiskAreas()} else {clearRiskAreas()}
}const showRiskAreas = () => {const riskAreas = [{name: '高风险区域A',level: 'high',points: [[99.4, 31.4], [100.0, 31.4], [100.0, 32.0], [99.4, 32.0], [99.4, 31.4]],riskFactors: ['干燥气候', '植被密集', '历史火灾点']},{name: '中风险区域B',level: 'medium', points: [[100.2, 32.0], [100.9, 32.0], [100.9, 32.7], [100.2, 32.7], [100.2, 32.0]],riskFactors: ['坡度较陡', '人员活动频繁']},{name: '低风险区域C',level: 'low',points: [[99.6, 32.4], [100.4, 32.4], [100.4, 33.2], [99.6, 33.2], [99.6, 32.4]],riskFactors: ['湿度适宜', '植被稀疏']}]riskAreas.forEach(area => {const points = area.points.map(point => new T.LngLat(point[0], point[1]))let color, fillColorswitch (area.level) {case 'high':color = '#ff4d4d'fillColor = '#ff4d4d'breakcase 'medium':color = '#ffc107'fillColor = '#ffc107'breakcase 'low':color = '#28a745'fillColor = '#28a745'break}const riskPolygon = new T.Polygon(points, {color: color,weight: 3,opacity: 0.8,fillColor: fillColor,fillOpacity: 0.3,dashArray: [5, 5] // 虚线边框})// 添加点击事件riskPolygon.addEventListener('click', (e) => {if (e.originalEvent) {e.originalEvent.stopPropagation()}isClickingArea.value = trueshowRiskAreaPopup(e, area)setTimeout(() => {isClickingArea.value = false}, 200)})map.value.addOverLay(riskPolygon)riskAreaOverlays.value.push(riskPolygon)})
}const clearRiskAreas = () => {riskAreaOverlays.value.forEach(overlay => {map.value.removeOverLay(overlay)})riskAreaOverlays.value = []
}const showRiskAreaPopup = (event, areaData) => {popup.value = {visible: true,data: {name: areaData.name,level: areaData.level,riskFactors: areaData.riskFactors,type: 'risk-area'}}
}// 清除所有图层
const clearAllOverlays = () => {clearGridResources()clearRiskAreas()clearRiskMarkers()
}// 暴露方法给父组件
defineExpose({toggleGridResources,toggleRiskArea,addRiskMarkers,clearRiskMarkers,featureStates
})
</script><style scoped>
.td-map {position: relative;width: 100%;height: 100%;
}.td-map-container {width: 100%;height: 100%;border-radius: 8px;overflow: hidden;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}/* 弹框样式 - 与LeafletMap保持一致 */
.popup-overlay {z-index: 1000;display: flex;align-items: center;justify-content: center;pointer-events: none; /* 不阻挡地图点击事件 */
}.popup-content {position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);background: rgba(0, 0, 0, 0.9);border-radius: 8px;box-shadow: 0 4px 12px rgba(0,0,0,0.5);border: 1px solid #444;min-width: 250px;max-width: 350px;z-index: 1001;font-family: Arial, sans-serif;pointer-events: auto; /* 弹框内容可以交互 */
}.region-popup {padding: 16px;
}.region-popup h3 {margin: 0 0 8px 0;color: white;font-size: 16px;
}.region-popup p {margin: 4px 0;color: #ccc;font-size: 14px;
}.popup-actions {margin-top: 10px;text-align: center;
}.popup-actions button {background: #444;color: white;border: 1px solid #666;padding: 6px 12px;border-radius: 4px;cursor: pointer;font-size: 12px;transition: all 0.2s ease;margin: 2px;
}.popup-actions button:hover {background: #555;border-color: #888;
}.popup-actions .btn-primary {background: #007bff;border-color: #007bff;color: white;
}.popup-actions .btn-primary:hover {background: #0056b3;border-color: #0056b3;
}.popup-actions .btn-danger {background: #dc3545;border-color: #dc3545;color: white;
}.popup-actions .btn-danger:hover {background: #c82333;border-color: #bd2130;
}.popup-actions .btn-warning {background: #ffc107;border-color: #ffc107;color: #212529;
}.popup-actions .btn-warning:hover {background: #e0a800;border-color: #d39e00;
}.popup-actions .btn-success {background: #28a745;border-color: #28a745;color: white;
}.popup-actions .btn-success:hover {background: #218838;border-color: #1e7e34;
}.risk-marker-popup {padding: 16px;
}.risk-marker-popup h3 {margin: 0 0 8px 0;color: white;font-size: 16px;
}.risk-marker-popup .risk-level {margin: 4px 0;font-size: 14px;font-weight: bold;
}.risk-marker-popup .high-risk-text {color: #ff6b6b;
}.risk-marker-popup .medium-risk-text {color: #ffc107;
}.risk-marker-popup .low-risk-text {color: #28a745;
}.risk-marker-popup .risk-type {margin: 4px 0;color: #87ceeb;font-size: 13px;font-weight: 500;
}.risk-marker-popup .last-incident {margin: 6px 0;color: #aaa;font-size: 12px;font-style: italic;
}.risk-marker-popup .risk-description {margin: 8px 0;color: #ccc;font-size: 14px;line-height: 1.4;
}.close-btn {position: absolute;top: 8px;right: 8px;background: none;border: none;color: #ccc;font-size: 18px;cursor: pointer;padding: 0;width: 24px;height: 24px;display: flex;align-items: center;justify-content: center;border-radius: 50%;transition: all 0.2s;
}.close-btn:hover {background-color: rgba(255, 255, 255, 0.1);color: white;
}/* 天地图文字标签样式 */
:deep(.tdt-label) {color: white !important;font-size: 14px !important;font-weight: bold !important;text-shadow: 2px 2px 4px rgba(0,0,0,0.8) !important;background: transparent !important;border: none !important;padding: 0 !important;
}/* 响应式设计 */
@media (max-width: 768px) {.td-map-container {height: 400px;}.popup-content {min-width: 280px;margin: 0 20px;}
}/* ========== 新增弹框样式 ========== *//* 网格资源弹框样式 */
.grid-resource-popup {padding: 16px;
}.grid-resource-popup h3 {margin: 0 0 12px 0;color: white;font-size: 16px;
}.grid-resource-popup .resource-list h4 {margin: 8px 0 6px 0;color: #87ceeb;font-size: 14px;
}.grid-resource-popup .resource-list ul {margin: 0;padding-left: 16px;list-style-type: disc;
}.grid-resource-popup .resource-list li {margin: 4px 0;color: #ccc;font-size: 13px;
}/* 风险区域弹框样式 */
.risk-area-popup {padding: 16px;
}.risk-area-popup h3 {margin: 0 0 8px 0;color: white;font-size: 16px;
}.risk-area-popup .risk-factors h4 {margin: 8px 0 6px 0;color: #87ceeb;font-size: 14px;
}.risk-area-popup .risk-factors ul {margin: 0;padding-left: 16px;list-style-type: disc;
}.risk-area-popup .risk-factors li {margin: 4px 0;color: #ccc;font-size: 13px;
}</style>
home页面
<template><div class="dashboard-container"><!-- 地图区域 --><div class="map-container"><component :is="components[currentMap]" :ref="currentMap === 'TdMap' ? 'tdMapRef' : currentMap === 'LeafletMap' ? 'leafletMapRef' : null" /></div><!-- 主要内容区域 --><!-- <div class="dashboard-main"> --><!-- 左侧边栏 --><aside class="dashboard-sidebar left-sidebar"><!-- 天气动态 --><div class="sidebar-card"><h3 class="card-title">天气动态<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg></h3><div class="data-list" style="grid-template-columns: 1fr 1fr;align-items: center;"><div class="data-item"><span class="label">天气变化趋势</span><span class="value">晴朗转多云</span></div><!-- 天气图标 --><div class="weather-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2"/><path d="M12 21v2"/><path d="M4.22 4.22l1.42 1.42"/><path d="M18.36 18.36l1.42 1.42"/><path d="M1 12h2"/><path d="M21 12h2"/><path d="M4.22 19.78l1.42-1.42"/><path d="M18.36 5.64l1.42-1.42"/></svg></div></div><div class="data-list"><div class="data-item"><span class="label">天气预警</span><span class="value warning">大风预警</span></div><div class="data-item"><span class="label">预警风险区</span><span class="value">12</span></div><div class="data-item"><span class="label">预警网格</span><span class="value">45</span></div></div></div><!-- 林草概况静态数据 --><div class="sidebar-card"><h3 class="card-title">林草概况静态数据<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 7l10 5 10-5"/></svg></h3><div class="data-list" style="grid-template-columns: 1fr 1fr;"><div class="data-item"><span class="label">林草总面积</span><span class="value">2,456,789 公顷</span></div><div class="data-item"><span class="label">低空巡检面积</span><span class="value">1,234,567 公顷</span></div><div class="data-item"><span class="label">网格数</span><span class="value">1,234</span></div><div class="data-item"><span class="label">不同级别风险区数量</span><span class="value">89</span></div><div class="data-item"><span class="label">隐患点数量</span><span class="value">156</span></div></div></div><!-- 资源视窗 --><div class="sidebar-card"><h3 class="card-title">资源视窗<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/></svg></h3><div class="data-list"><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/></svg></div><span class="label">卫星数</span><span class="value">8</span></div><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M12 2v20"/><path d="M2 7h20"/><path d="M2 17h20"/><path d="M6 2v4"/><path d="M18 2v4"/></svg></div><span class="label">无人机数</span><span class="value">24</span></div><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2v20"/><path d="M2 12h20"/></svg></div><span class="label">水弹数</span><span class="value">156</span></div><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><span class="label">当值网格员数</span><span class="value">89</span></div><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/><path d="M12 2v5"/><path d="M8 7h8"/></svg></div><span class="label">护林员数</span><span class="value">234</span></div><div class="data-item"><div class="item-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M12 2v20"/><path d="M2 7h20"/><path d="M2 17h20"/><path d="M6 2v4"/><path d="M18 2v4"/></svg></div><span class="label">瞭望塔数</span><span class="value">67</span></div></div></div></aside><!-- 中央地图区域 --><main class="dashboard-center"><!-- 飞行统计摘要 --><div class="flight-summary"><div class="summary-item"><span class="label">累计已飞行架次</span><span class="value">1,234</span></div><div class="summary-item"><span class="label">累计飞行里程</span><span class="value">45,678 km</span></div><div class="summary-item"><span class="label">累计覆盖面积</span><span class="value">2,345,678 公顷</span></div></div><!-- 切换地图 --><div class="map-switch"><!-- 单选框 --><el-radio-group v-model="currentMap"><el-radio-button value="CesiumMap">Cesium地图</el-radio-button><el-radio-button value="LeafletMap">Leaflet地图</el-radio-button><el-radio-button value="TdMap">天地图</el-radio-button></el-radio-group><!-- 功能按钮 --><div class="function-buttons" v-if="currentMap === 'TdMap'"><el-button @click="openGridResources"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>网格资源</el-button><el-button @click="openRiskMarkers"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><circle cx="12" cy="12" r="10"/><path d="M12 6v6"/><path d="M12 18h.01"/></svg>风险标记</el-button><el-button @click="openRiskArea"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><polygon points="13,2 3,14 12,14 11,22 21,10 12,10"/></svg>风险区域</el-button><!-- 区域画面 圆形画面 多边形画面 手动绘制 --></div><div class="function-buttons" v-if="currentMap === 'LeafletMap'"><el-button @click="toggleBoundaryHighlight"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27,6.96 12,12.01 20.73,6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>边界高亮</el-button><el-button @click="addRandomPoints"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="1" r="1"/><circle cx="12" cy="23" r="1"/><circle cx="1" cy="12" r="1"/><circle cx="23" cy="12" r="1"/><circle cx="5.64" cy="5.64" r="1"/><circle cx="18.36" cy="18.36" r="1"/><circle cx="5.64" cy="18.36" r="1"/><circle cx="18.36" cy="5.64" r="1"/></svg>随机点位</el-button><el-button @click="toggleAreaSelect"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z"/><path d="M21 11h-4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z"/><path d="M7 2H5a2 2 0 0 0-2 2v2h4V4a2 2 0 0 0-2-2z"/><path d="M19 2h-2a2 2 0 0 0-2 2v2h4V4a2 2 0 0 0-2-2z"/></svg>框选面积</el-button><el-button @click="toggleDistanceMeasure"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>距离测量</el-button><el-button @click="addRandomRoute"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px; margin-right: 6px;"><path d="M3 20h18L12 4z"/><path d="M12 4v16"/><path d="M8 12h8"/></svg>路径规划</el-button></div></div></main><!-- 右侧边栏 --><aside class="dashboard-sidebar right-sidebar"><!-- 当日飞行任务统计 --><div class="sidebar-card"><h3 class="card-title">当日飞行任务统计<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M12 2v20"/><path d="M2 7h20"/><path d="M2 17h20"/><path d="M6 2v4"/><path d="M18 2v4"/></svg></h3><div class="data-list" ><div class="data-item"><span class="label">执飞飞机数量</span><span class="value">12</span></div><div class="data-item"><span class="label">飞行时长</span><span class="value">8.5 小时</span></div><div class="data-item"><span class="label">飞行里程</span><span class="value">456 km</span></div><div class="data-item"><span class="label">巡检覆盖面积</span><span class="value">123,456 公顷</span></div></div></div><!-- 今日告警统计 --><div class="sidebar-card"><h3 class="card-title">今日告警统计<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6"/><path d="M12 18h.01"/></svg></h3><div class="alarm-stats"><div class="alarm-item"><span class="alarm-type">火险告警</span><span class="alarm-count">3</span></div><div class="alarm-item"><span class="alarm-type">非法采伐</span><span class="alarm-count">1</span></div><div class="alarm-item"><span class="alarm-type">病虫害</span><span class="alarm-count">2</span></div><div class="alarm-item"><span class="alarm-type">其他</span><span class="alarm-count">1</span></div></div></div><!-- 不同类型告警趋势统计 --><div class="sidebar-card"><h3 class="card-title">不同类型告警趋势统计<svg class="title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></h3><div class="trend-chart"></div></div></aside><!-- </div> --></div>
</template><script setup>
import { ref } from 'vue'
import CesiumMap from '@/components/CesiumMap.vue'
import LeafletMap from '@/components/LeafletMap.vue'
import TdMap from '@/components/TdMap.vue'// 动态组件映射
const components = {CesiumMap,LeafletMap,TdMap
}// 组件逻辑可以在这里添加
const currentMap = ref('LeafletMap')// 天地图组件引用
const tdMapRef = ref(null)
// LeafletMap组件引用
const leafletMapRef = ref(null)// 按钮点击处理函数
const openGridResources = () => {if (currentMap.value === 'TdMap') {// 通过组件引用调用天地图的网格资源功能if (tdMapRef.value && tdMapRef.value.toggleGridResources) {tdMapRef.value.toggleGridResources()} else {console.log('天地图组件未加载完成,请稍后再试')}} else {console.log('请先切换到天地图查看网格资源')}
}const openRiskMarkers = () => {if (currentMap.value === 'TdMap') {// 可以传递一些示例风险标记数据const sampleRiskMarkers = [{name: '高风险点A',lng: 100.1,lat: 31.9,level: 'high',riskType: '火灾风险',description: '植被干燥,易燃物较多',lastIncident: '2024年3月15日'},{name: '中风险点B',lng: 99.9,lat: 32.1,level: 'medium',riskType: '病虫害',description: '发现松毛虫迹象',lastIncident: '2024年2月28日'},{name: '低风险点C',lng: 100.3,lat: 32.3,level: 'low',riskType: '监测点',description: '定期巡检点',lastIncident: '无'}]if (tdMapRef.value && tdMapRef.value.addRiskMarkers) {tdMapRef.value.addRiskMarkers(sampleRiskMarkers)} else {console.log('天地图组件未加载完成,请稍后再试')}} else {console.log('请先切换到天地图查看风险标记')}
}const openRiskArea = () => {if (currentMap.value === 'TdMap') {if (tdMapRef.value && tdMapRef.value.toggleRiskArea) {tdMapRef.value.toggleRiskArea()} else {console.log('天地图组件未加载完成,请稍后再试')}} else {console.log('请先切换到天地图查看风险区域')}
}// LeafletMap功能按钮处理函数
const toggleBoundaryHighlight = () => {if (currentMap.value === 'LeafletMap') {if (leafletMapRef.value && leafletMapRef.value.toggleBoundaryHighlight) {leafletMapRef.value.toggleBoundaryHighlight()} else {console.log('LeafletMap组件未加载完成,请稍后再试')}} else {console.log('请先切换到LeafletMap查看边界高亮')}
}const addRandomPoints = () => {if (currentMap.value === 'LeafletMap') {if (leafletMapRef.value && leafletMapRef.value.addRandomPoints) {leafletMapRef.value.addRandomPoints()} else {console.log('LeafletMap组件未加载完成,请稍后再试')}} else {console.log('请先切换到LeafletMap添加随机点')}
}const toggleAreaSelect = () => {if (currentMap.value === 'LeafletMap') {if (leafletMapRef.value && leafletMapRef.value.toggleAreaSelect) {leafletMapRef.value.toggleAreaSelect()} else {console.log('LeafletMap组件未加载完成,请稍后再试')}} else {console.log('请先切换到LeafletMap使用框选面积功能')}
}const toggleDistanceMeasure = () => {if (currentMap.value === 'LeafletMap') {if (leafletMapRef.value && leafletMapRef.value.toggleDistanceMeasure) {leafletMapRef.value.toggleDistanceMeasure()} else {console.log('LeafletMap组件未加载完成,请稍后再试')}} else {console.log('请先切换到LeafletMap使用距离测量功能')}
}const addRandomRoute = () => {if (currentMap.value === 'LeafletMap') {if (leafletMapRef.value && leafletMapRef.value.addRandomRoute) {leafletMapRef.value.addRandomRoute()} else {console.log('LeafletMap组件未加载完成,请稍后再试')}} else {console.log('请先切换到LeafletMap查看随机路径规划')}
}</script><style scoped lang="scss">
.dashboard-container {width: 100%;height: 100%;color: #fff;display: flex;flex-direction: column;overflow: hidden;position: relative;
}// 主要内容区域
.dashboard-main {flex: 1;display: flex;gap: 20px;padding: 20px;overflow: hidden;
}
.left-sidebar{position: absolute;top: 20px;left: 20px;width: 400px;z-index: 10;
}
// 侧边栏样式
.dashboard-sidebar {display: flex;flex-direction: column;gap: 20px;.sidebar-card {background: rgba(0, 0, 0, 0.4);backdrop-filter: blur(10px);border-radius: 12px;padding: 20px;border: 1px solid rgba(255, 255, 255, 0.1);box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);.card-title {font-size: 18px;font-weight: bold;margin-bottom: 20px;color: #87ceeb;border-bottom: 2px solid rgba(135, 206, 235, 0.3);padding-bottom: 10px;display: flex;align-items: center;gap: 8px;.title-icon {width: 20px;height: 20px;color: #87ceeb;opacity: 0.8;transition: all 0.3s ease;}&:hover .title-icon {opacity: 1;transform: scale(1.1);color: #4facfe;}}.data-list {display: grid;grid-template-columns: 1fr 1fr 1fr;gap: 12px;.data-item {display: flex;flex-direction: column;justify-content: space-between;align-items: center;padding: 8px 0;position: relative;.item-icon {width: 24px;height: 24px;margin-bottom: 8px;color: #87ceeb;opacity: 0.8;transition: all 0.3s ease;svg {width: 100%;height: 100%;}}&:hover .item-icon {opacity: 1;transform: scale(1.1);color: #4facfe;}.label {font-size: 14px;color: #b0c4de;}.value {font-size: 16px;font-weight: bold;color: #fff;&.warning {color: #ff6b6b;}}}.weather-icon {width: 40px;height: 40px;margin: 0 auto;color: #87ceeb;opacity: 0.9;transition: all 0.3s ease;animation: rotate 20s linear infinite;svg {width: 100%;height: 100%;}&:hover {opacity: 1;transform: scale(1.1);color: #4facfe;}}}}.weather-link {.weather-btn {width: 100%;padding: 12px;background: linear-gradient(45deg, #667eea, #764ba2);border: none;border-radius: 8px;color: #fff;font-size: 16px;cursor: pointer;transition: all 0.3s ease;&:hover {transform: translateY(-2px);box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);}}}
}// 中央区域样式
.dashboard-center {position: absolute;top: 20px;left: 50%;z-index: 10;transform: translateX(-50%);width: 1000px;display: flex;flex-direction: column;gap: 20px;.flight-summary {display: flex;gap: 20px;background: rgba(0, 0, 0, 0.4);backdrop-filter: blur(10px);border-radius: 12px;padding: 20px;border: 1px solid rgba(255, 255, 255, 0.1);.summary-item {flex: 1;text-align: center;.label {display: block;font-size: 14px;color: #b0c4de;margin-bottom: 8px;}.value {display: block;font-size: 24px;font-weight: bold;color: #87ceeb;}}}.alarm-styles {display: flex;gap: 20px;.alarm-banner, .alarm-popup {flex: 1;padding: 15px;background: rgba(255, 59, 48, 0.2);border: 1px solid rgba(255, 59, 48, 0.3);border-radius: 8px;text-align: center;font-size: 14px;color: #ff6b6b;}}.map-switch {backdrop-filter: blur(10px);width: fit-content;border-radius: 12px;padding: 20px;border: 1px solid rgba(255, 255, 255, 0.1);box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);display: flex;flex-direction: column;gap: 15px;:deep(.el-radio-group) {display: flex;gap: 10px;margin-bottom: 5px;}:deep(.el-radio-button) {.el-radio-button__inner {background: rgba(135, 206, 235, 0.2);border: 1px solid rgba(135, 206, 235, 0.3);color: #87ceeb;padding: 8px 16px;border-radius: 8px;transition: all 0.3s ease;&:hover {background: rgba(135, 206, 235, 0.3);border-color: rgba(135, 206, 235, 0.5);}}&.is-active .el-radio-button__inner {background: linear-gradient(45deg, #667eea, #764ba2);border-color: #667eea;color: #fff;box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);}}.function-buttons {display: flex;gap: 12px;flex-wrap: wrap;:deep(.el-button) {background: rgba(0, 0, 0, 0.6);border: 1px solid rgba(135, 206, 235, 0.4);color: #87ceeb;padding: 10px 18px;border-radius: 8px;font-size: 14px;font-weight: 500;transition: all 0.3s ease;backdrop-filter: blur(5px);box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);display: flex;align-items: center;white-space: nowrap;&:hover {background: linear-gradient(45deg, rgba(135, 206, 235, 0.3), rgba(79, 172, 254, 0.3));border-color: #4facfe;color: #fff;transform: translateY(-2px);box-shadow: 0 6px 16px rgba(79, 172, 254, 0.4);}&:active {transform: translateY(0);box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);}// 不同功能按钮的特殊样式&:nth-child(1) {border-color: rgba(76, 175, 80, 0.4);color: #4CAF50;&:hover {background: linear-gradient(45deg, rgba(76, 175, 80, 0.3), rgba(76, 175, 80, 0.4));border-color: #4CAF50;box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);}}&:nth-child(2) {border-color: rgba(255, 107, 107, 0.4);color: #ff6b6b;&:hover {background: linear-gradient(45deg, rgba(255, 107, 107, 0.3), rgba(255, 107, 107, 0.4));border-color: #ff6b6b;box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4);}}&:nth-child(3) {border-color: rgba(255, 193, 7, 0.4);color: #ffc107;&:hover {background: linear-gradient(45deg, rgba(255, 193, 7, 0.3), rgba(255, 193, 7, 0.4));border-color: #ffc107;box-shadow: 0 6px 16px rgba(255, 193, 7, 0.4);}}svg {opacity: 0.8;transition: all 0.3s ease;}&:hover svg {opacity: 1;transform: scale(1.1);}}}}}
.map-container {width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: 1;}
// 右侧边栏特殊样式
.right-sidebar {position: absolute;top: 20px;right: 20px;width: 400px;display: flex;flex-direction: column;gap: 20px;z-index: 10; .alarm-stats {.alarm-item {display: flex;justify-content: space-between;align-items: center;padding: 8px 0;border-bottom: 1px solid rgba(255, 255, 255, 0.1);.alarm-type {font-size: 14px;color: #b0c4de;}.alarm-count {font-size: 16px;font-weight: bold;color: #ff6b6b;}}}.trend-chart {// ECharts 图表容器样式由组件内部处理}
}// 动画效果
@keyframes pulse {0% {transform: scale(1);opacity: 1;}50% {transform: scale(1.2);opacity: 0.7;}100% {transform: scale(1);opacity: 1;}
}@keyframes rotate {from {transform: rotate(0deg);}to {transform: rotate(360deg);}
}
</style>
json数据源
DataV.GeoAtlas地理小工具系列