Cesium--可拖拽气泡弹窗(Vue3版)

1. 概述

支持拖拽功能、引导线连接和动态位置跟随。

1.1 特点

  • 动态位置跟随:弹窗位置跟随地图点位实时更新
  • 拖拽功能:支持用户拖拽弹窗到任意位置
  • 引导线连接:使用虚线引导线连接弹窗和点位,清晰展示关联关系
  • 相对位置保持:拖拽后保持弹窗与点位的相对位置关系
  • 多弹窗支持:支持同时显示多个弹窗实例

2. 主要功能与用途

2.1 核心功能

2.1.1 弹窗显示与隐藏
  • 通过 visible 属性控制弹窗的显示状态
  • 支持动态切换显示/隐藏
  • 平滑的过渡动画效果
2.1.2 拖拽功能
  • 通过拖拽手柄实现弹窗拖拽
  • 拖拽过程中实时更新引导线
  • 拖拽结束后保持相对位置关系
  • 支持全局鼠标事件监听
2.1.3 引导线连接
  • 使用 SVG 绘制虚线引导线,可通过svg形式绘制其他图形,如气泡三角的符号等
  • 动态计算引导线起点和终点
  • 引导线始终连接点位和弹窗
2.1.4 位置跟随
  • 监听地图渲染事件
  • 实时更新弹窗位置
  • 区分拖拽状态和自动跟随状态
  • 保持拖拽后的偏移量

3. Props 属性说明

3.1 属性列表

属性名 类型 必填 默认值 描述
visible Boolean false 控制弹窗的显示状态,true 为显示,false 为隐藏
pointName String '' 弹窗中显示的点位名称或标题
position Object { x: 0, y: 0 } 弹窗的初始屏幕坐标位置,包含 xy 属性
entity Object null Cesium 实体对象,用于获取点位的实时屏幕坐标
viewer Object - Cesium Viewer 实例,用于地图渲染事件监听和坐标转换
id String - 弹窗的唯一标识符,用于区分不同的弹窗实例

3.2 属性详细说明

3.2.1 visible
  • 类型Boolean
  • 默认值false
  • 描述 :控制弹窗的显示状态。设置为 true 时显示弹窗,设置为 false 时隐藏弹窗。
  • 使用场景:动态控制弹窗的显示和隐藏,响应用户交互或业务逻辑。
3.2.2 pointName
  • 类型String
  • 默认值''
  • 描述:弹窗中显示的点位名称或标题。该文本会显示在弹窗的标题栏和内容区域。
  • 使用场景:显示点位名称、标注信息或其他相关文本。
3.2.3 position
  • 类型Object
  • 默认值{ x: 0, y: 0 }
  • 描述 :弹窗的初始屏幕坐标位置,包含 xy 两个属性,分别表示屏幕坐标的横纵坐标。
  • 使用场景:设置弹窗的初始位置,通常设置为点位的屏幕坐标。
3.2.4 entity
  • 类型Object
  • 默认值null
  • 描述 :Cesium 实体对象,用于获取点位的实时屏幕坐标。该对象应包含 position 属性。
  • 使用场景:关联弹窗与地图点位,实现位置跟随功能。因为使用entity所以不建议数据量大的时候使用
3.2.5 viewer
  • 类型Object
  • 必填:是
  • 描述:Cesium Viewer 实例,用于地图渲染事件监听和坐标转换。
  • 使用场景:提供地图实例,实现弹窗与地图的交互。
3.2.6 id
  • 类型String
  • 必填:是
  • 描述:弹窗的唯一标识符,用于区分不同的弹窗实例。
  • 使用场景:在多弹窗场景中标识不同的弹窗,便于管理和操作。

4. 事件触发机制

4.1 事件列表

事件名 参数 描述
close popupId: String 关闭弹窗时触发,传递弹窗的唯一标识符
drag dragInfo: Object 拖拽弹窗时触发,传递拖拽信息对象

4.2 事件详细说明

4.2.1 close 事件
  • 触发时机:用户点击关闭按钮时触发
  • 参数
    • popupId:弹窗的唯一标识符(String 类型)
  • 使用示例
javascript 复制代码
const closePopup = (popupId) => {
  // 根据 popupId 关闭对应的弹窗
  const index = popups.value.findIndex(popup => popup.id === popupId)
  if (index > -1) {
    popups.value.splice(index, 1)
  }
}
4.2.2 drag 事件
  • 触发时机:用户拖拽弹窗时触发
  • 参数
    • dragInfo:拖拽信息对象,包含以下属性:
      • id:弹窗的唯一标识符(String 类型)
      • position:拖拽后的屏幕坐标位置(Object 类型,包含 xy 属性)
  • 使用示例
javascript 复制代码
const handlePopupDrag = (dragInfo) => {
  // 根据 dragInfo.id 更新对应弹窗的位置
  const popup = popups.value.find(p => p.id === dragInfo.id)
  if (popup) {
    popup.position = dragInfo.position
  }
}

5. 插槽使用方法

5.1 插槽说明

当前版本的 WaterMapPopup 组件不提供自定义插槽。组件的内部结构和样式已经固定,包括标题栏、内容区域和关闭按钮。

5.2 自定义内容

如果需要自定义弹窗内容,可以通过修改组件模板或创建新的弹窗组件来实现。当前组件的内容区域默认显示 pointName 属性的值。

6. 样式定制指南

6.1 样式结构

组件使用 scoped 样式,样式类名如下:

  • .popup-container:弹窗容器,包含引导线和弹窗主体
  • .water-map-popup:弹窗主体,包含标题栏和内容区域
  • .popup-header:弹窗标题栏,包含拖拽手柄、标题和关闭按钮
  • .drag-handle:拖拽手柄,用于拖拽弹窗
  • .drag-icon:拖拽图标
  • .popup-header h3:标题文本
  • .close-btn:关闭按钮
  • .popup-content:弹窗内容区域
  • .guide-line:引导线 SVG 容器

6.2 样式定制

由于组件使用 scoped 样式,无法直接从外部修改组件内部样式。如果需要定制样式,建议:

  1. 修改组件源码 :直接修改组件的 <style> 部分
  2. 使用深度选择器 :在父组件中使用 :deep() 选择器覆盖样式
  3. 创建新组件:基于当前组件创建新的定制化组件

6.3 样式示例

css 复制代码
/* 在父组件中使用深度选择器覆盖样式 */
:deep(.water-map-popup) {
  background: rgba(255, 255, 255, 0.98);
  border-radius: 12px;
}

:deep(.popup-header) {
  background: linear-gradient(135deg, #ff6b6b 0%, #c92a2a 100%);
}

:deep(.guide-line line) {
  stroke: #ff6b6b;
  stroke-width: 3;
}

7. 完整的使用示例代码

7.1 基础使用示例

vue 复制代码
<template>
  <div class="map-container">
    <div id="cesiumContainer" class="cesium-container"></div>
    
    <!-- 单个弹窗示例 -->
    <WaterMapPopup 
      :visible="popupVisible" 
      :point-name="selectedPointName" 
      :position="popupPosition" 
      :entity="selectedPointEntity"
      :viewer="viewer"
      :id="popupId"
      @close="closePopup"
      @drag="handlePopupDrag"
    />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from '@/components/WaterMapPopup.vue'

// 地图实例
let viewer = null

// 弹窗状态
const popupVisible = ref(false)
const selectedPointName = ref('')
const popupPosition = ref({ x: 0, y: 0 })
const selectedPointEntity = ref(null)
const popupId = ref('popup_001')

onMounted(() => {
  // 初始化 Cesium Viewer
  viewer = new Cesium.Viewer('cesiumContainer', {
    // 配置选项
  })
  
  // 添加点位实体
  const entity = viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(85.0, 41.0),
    point: {
      pixelSize: 10,
      color: Cesium.Color.RED
    },
    name: '测试点位'
  })
  
  selectedPointEntity.value = entity
})

// 关闭弹窗
const closePopup = (popupId) => {
  popupVisible.value = false
}

// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
  popupPosition.value = dragInfo.position
}
</script>

7.2 多弹窗使用示例

vue 复制代码
<template>
  <div class="map-container">
    <div id="cesiumContainer" class="cesium-container"></div>
    
    <!-- 多个弹窗示例 -->
    <WaterMapPopup 
      v-for="popup in popups" 
      :key="popup.id"
      :id="popup.id"
      :visible="popup.visible" 
      :point-name="popup.pointName" 
      :position="popup.position" 
      :entity="popup.entity"
      :viewer="viewer"
      @close="closePopup"
      @drag="handlePopupDrag"
    />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from '@/components/WaterMapPopup.vue'

// 地图实例
let viewer = null

// 弹窗状态
const popups = ref([])

onMounted(() => {
  // 初始化 Cesium Viewer
  viewer = new Cesium.Viewer('cesiumContainer', {
    // 配置选项
  })
  
  // 添加多个点位
  const points = [
    { name: '点位1', lng: 85.0, lat: 41.0 },
    { name: '点位2', lng: 86.0, lat: 42.0 },
    { name: '点位3', lng: 87.0, lat: 43.0 }
  ]
  
  points.forEach((point, index) => {
    const entity = viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(point.lng, point.lat),
      point: {
        pixelSize: 10,
        color: Cesium.Color.RED
      },
      name: point.name
    })
    
    // 添加点击事件
    viewer.screenSpaceEventHandler.setInputAction((click) => {
      const pickedObject = viewer.scene.pick(click.position)
      if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
        const pickedEntity = pickedObject.id
        if (pickedEntity.point) {
          openPopup(pickedEntity, click.position)
        }
      }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
  })
})

// 生成唯一弹窗ID
const generatePopupId = () => {
  return `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
}

// 打开弹窗
const openPopup = (entity, position) => {
  // 检查是否已经打开该实体的弹窗
  const existingPopup = popups.value.find(popup => popup.entity === entity)
  if (existingPopup) {
    return
  }
  
  // 创建新弹窗实例
  const popupId = generatePopupId()
  popups.value.push({
    id: popupId,
    visible: true,
    pointName: entity.name,
    position: { x: position.x, y: position.y },
    entity: entity
  })
}

// 关闭弹窗
const closePopup = (popupId) => {
  const index = popups.value.findIndex(popup => popup.id === popupId)
  if (index > -1) {
    popups.value.splice(index, 1)
  }
}

// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
  const popup = popups.value.find(p => p.id === dragInfo.id)
  if (popup) {
    popup.position = dragInfo.position
  }
}
</script>

7.3 完整集成示例

vue 复制代码
<template>
  <div class="map-container">
    <div id="cesiumContainer" class="cesium-container"></div>
    <!-- 多个弹窗 -->
    <WaterMapPopup 
      v-for="popup in popups" 
      :key="popup.id"
      :id="popup.id"
      :visible="popup.visible" 
      :point-name="popup.pointName" 
      :position="popup.position" 
      :entity="popup.entity"
      :viewer="viewer"
      @close="closePopup"
      @drag="handlePopupDrag"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from '@/components/WaterMapPopup.vue'

// 地图实例
let viewer = null

// 弹窗状态
const popups = ref([])

// 在新疆经纬度范围内随机生成点数据
const generateRandomPoints = () => {
  const minLng = 73.67
  const maxLng = 96.3
  const minLat = 34.42
  const maxLat = 48.17
  
  const pointCount = Math.floor(Math.random() * 11) + 10
  const points = []
  
  for (let i = 0; i < pointCount; i++) {
    const lng = minLng + Math.random() * (maxLng - minLng)
    const lat = minLat + Math.random() * (maxLat - minLat)
    
    points.push({
      id: i + 1,
      name: `新疆点位${i + 1}`,
      lng: lng,
      lat: lat
    })
  }
  
  return points
}

// 将点数据加载到地图上
const loadPointsToMap = (points) => {
  points.forEach(point => {
    const entity = viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(point.lng, point.lat),
      point: {
        pixelSize: 10,
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2
      },
      name: point.name,
      properties: {
        id: point.id
      }
    })
  })
  
  // 添加点击事件监听
  viewer.screenSpaceEventHandler.setInputAction((click) => {
    const pickedObject = viewer.scene.pick(click.position)
    
    if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
      const entity = pickedObject.id
      if (entity.point) {
        openPopup(entity, click.position)
      }
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

// 生成唯一弹窗ID
const generatePopupId = () => {
  return `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
}

// 打开弹窗
const openPopup = (entity, position) => {
  const existingPopup = popups.value.find(popup => popup.entity === entity)
  if (existingPopup) {
    return
  }
  
  const popupId = generatePopupId()
  popups.value.push({
    id: popupId,
    visible: true,
    pointName: entity.name,
    position: { x: position.x, y: position.y },
    entity: entity
  })
}

// 关闭弹窗
const closePopup = (popupId) => {
  const index = popups.value.findIndex(popup => popup.id === popupId)
  if (index > -1) {
    popups.value.splice(index, 1)
  }
}

// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
  const popup = popups.value.find(p => p.id === dragInfo.id)
  if (popup) {
    popup.position = dragInfo.position
  }
}

onMounted(() => {
  viewer = new Cesium.Viewer('cesiumContainer', {
    imageryProvider: false,
    animation: false,
    timeline: false,
    homeButton: false,
    sceneModePicker: false,
    baseLayerPicker: false,
    geocoder: false,
    navigationHelpButton: false,
    fullscreenButton: false,
    infoBox: false,
    selectionIndicator: false
  })
  
  viewer.cesiumWidget.creditContainer.style.display = "none"
  
  // 生成随机点数据并加载到地图上
  const randomPoints = generateRandomPoints()
  loadPointsToMap(randomPoints)
})

onBeforeUnmount(() => {
  if (viewer) {
    viewer.destroy()
    viewer = null
  }
})
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
  position: relative;
}

.cesium-container {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
</style>

8. 常见问题解答及注意事项

8.1 注意事项

8.1.1 性能优化
  1. 避免频繁更新:弹窗位置更新会在每次地图渲染时触发,确保更新逻辑高效
  2. 合理使用多弹窗:过多的弹窗会影响性能,建议控制弹窗数量
  3. 事件监听清理:组件卸载时会自动清理事件监听,无需手动处理
8.1.2 事件处理
  1. 拖拽事件:拖拽事件会频繁触发,确保处理逻辑高效
  2. 关闭事件:关闭事件会传递弹窗 ID,确保正确处理
  3. 事件监听:组件会自动添加和清理事件监听,无需手动处理
8.1.3 实践
  1. 唯一标识 :确保每个弹窗有唯一的 id,便于管理和操作
  2. 实体关联 :正确传递 entity 属性,确保位置跟随功能正常
  3. Viewer 实例 :确保 viewer 属性正确传递,避免空引用错误
  4. 状态管理:使用响应式数据管理弹窗状态,确保界面更新及时

使用中的代码如下:

WaterMapPopup.vue

复制代码
<template>
  <div class="popup-container" v-if="visible">
    <!-- 引导线 -->
    <svg class="guide-line" :style="guideLineStyle" xmlns="http://www.w3.org/2000/svg">
      <line :x1="lineStart.x" :y1="lineStart.y" :x2="lineEnd.x" :y2="lineEnd.y" stroke="#208cd7" stroke-width="2" stroke-dasharray="5,5" />
    </svg>
    
    <!-- 弹窗主体 -->
    <div 
      class="water-map-popup" 
      :style="popupStyle"
      @mousedown="startDrag"
    >
      <div class="popup-header">
        <div class="drag-handle" @mousedown.stop="startDrag">
          <span class="drag-icon">⋮⋮</span>
        </div>
        <h3>{{ pointName }}</h3>
        <button class="close-btn" @click="closePopup">
          <span>×</span>
        </button>
      </div>
      <div class="popup-content">
        <p>{{ pointName }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import * as Cesium from 'cesium'

// 定义组件属性
const props = defineProps({
  visible: {
    type: Boolean,
    default: false
  },
  pointName: {
    type: String,
    default: ''
  },
  position: {
    type: Object,
    default: () => ({ x: 0, y: 0 })
  },
  entity: {
    type: Object,
    default: null
  },
  viewer: {
    type: Object,
    required: true
  },
  id: {
    type: String,
    required: true
  }
})

// 定义事件
const emit = defineEmits(['close', 'drag'])

// 拖拽状态
const isDragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
const localPosition = ref({ ...props.position })
const offsetFromPoint = ref({ x: 0, y: 0 }) // 保存弹窗相对于点位的偏移量
const hasDragged = ref(false) // 标记弹窗是否被拖拽过

// 计算弹窗位置样式
const popupStyle = computed(() => {
  return {
    left: `${localPosition.value.x}px`,
    top: `${localPosition.value.y}px`
  }
})

// 计算引导线容器样式
const guideLineStyle = computed(() => {
  // 获取点位屏幕坐标
  const pointPosition = getPointScreenPosition()
  if (!pointPosition) return { display: 'none' }
  
  // 计算引导线容器的位置和大小
  const minX = Math.min(pointPosition.x, localPosition.value.x)
  const minY = Math.min(pointPosition.y, localPosition.value.y)
  const maxX = Math.max(pointPosition.x, localPosition.value.x)
  const maxY = Math.max(pointPosition.y, localPosition.value.y)
  
  return {
    position: 'absolute',
    left: `${minX}px`,
    top: `${minY}px`,
    width: `${maxX - minX + 20}px`,
    height: `${maxY - minY + 20}px`,
    zIndex: '1000',
    pointerEvents: 'none'
  }
})

// 计算引导线起点和终点
const lineStart = computed(() => {
  const pointPosition = getPointScreenPosition()
  if (!pointPosition) return { x: 0, y: 0 }
  
  const style = guideLineStyle.value
  return {
    x: pointPosition.x - parseInt(style.left),
    y: pointPosition.y - parseInt(style.top)
  }
})

const lineEnd = computed(() => {
  const style = guideLineStyle.value
  return {
    x: localPosition.value.x - parseInt(style.left),
    y: localPosition.value.y - parseInt(style.top)
  }
})

// 获取点位的屏幕坐标
const getPointScreenPosition = () => {
  if (!props.entity || !props.viewer) return null
  
  const cartesianPosition = props.entity.position.getValue(props.viewer.clock.currentTime)
  if (!cartesianPosition) return null
  
  const canvasPosition = props.viewer.scene.cartesianToCanvasCoordinates(cartesianPosition)
  if (!canvasPosition) return null
  
  return { x: canvasPosition.x, y: canvasPosition.y }
}

// 开始拖拽
const startDrag = (event) => {
  isDragging.value = true
  dragOffset.value = {
    x: event.clientX - localPosition.value.x,
    y: event.clientY - localPosition.value.y
  }
  
  // 添加全局事件监听
  document.addEventListener('mousemove', handleDrag)
  document.addEventListener('mouseup', stopDrag)
}

// 处理拖拽
const handleDrag = (event) => {
  if (!isDragging.value) return
  
  localPosition.value = {
    x: event.clientX - dragOffset.value.x,
    y: event.clientY - dragOffset.value.y
  }
  
  // 发送拖拽事件
  emit('drag', {
    id: props.id,
    position: localPosition.value
  })
}

// 停止拖拽
const stopDrag = () => {
  isDragging.value = false
  hasDragged.value = true
  
  // 计算并保存拖拽后的偏移量
  if (props.entity) {
    const pointPosition = getPointScreenPosition()
    if (pointPosition) {
      offsetFromPoint.value = {
        x: localPosition.value.x - pointPosition.x,
        y: localPosition.value.y - pointPosition.y
      }
    }
  }
  
  document.removeEventListener('mousemove', handleDrag)
  document.removeEventListener('mouseup', stopDrag)
}

// 关闭弹窗
const closePopup = () => {
  emit('close', props.id)
}

// 监听props.position变化,更新localPosition
watch(
  () => props.position,
  (newPosition) => {
    if (!isDragging.value) {
      localPosition.value = { ...newPosition }
    }
  },
  { deep: true }
)

// 组件卸载时清理事件监听
onUnmounted(() => {
  document.removeEventListener('mousemove', handleDrag)
  document.removeEventListener('mouseup', stopDrag)
})

// 监听地图渲染事件,更新引导线和弹窗位置
onMounted(() => {
  if (props.viewer) {
    const updateGuideLineAndPosition = () => {
      // 如果没有在拖拽状态,更新弹窗主体位置
      if (!isDragging.value && props.entity) {
        const pointPosition = getPointScreenPosition()
        if (pointPosition) {
          if (hasDragged.value) {
            // 如果弹窗被拖拽过,使用偏移量计算新位置
            localPosition.value = {
              x: pointPosition.x + offsetFromPoint.value.x,
              y: pointPosition.y + offsetFromPoint.value.y
            }
          } else {
            // 否则使用点位的实时屏幕坐标
            localPosition.value = { ...pointPosition }
          }
        }
      }
      // 触发引导线重新计算
      localPosition.value = { ...localPosition.value }
    }
    
    props.viewer.scene.postRender.addEventListener(updateGuideLineAndPosition)
    
    // 组件卸载时移除事件监听
    onUnmounted(() => {
      props.viewer.scene.postRender.removeEventListener(updateGuideLineAndPosition)
    })
  }
})
</script>

<style scoped>
.popup-container {
  position: absolute;
  pointer-events: none;
  z-index: 1000;
}

.water-map-popup {
  position: absolute;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(10px);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  min-width: 200px;
  z-index: 1001;
  border: 1px solid rgba(0, 0, 0, 0.1);
  pointer-events: auto;
  transform: translate(-50%, -100%);
}

.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 15px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  background: linear-gradient(135deg, #208cd7 0%, #1557b0 100%);
  color: white;
  border-radius: 8px 8px 0 0;
}

.drag-handle {
  cursor: move;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 4px;
  transition: all 0.2s ease;
}

.drag-handle:hover {
  background: rgba(255, 255, 255, 0.2);
}

.drag-icon {
  font-size: 12px;
  line-height: 1;
}

.popup-header h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  flex: 1;
  text-align: center;
}

.close-btn {
  background: transparent;
  border: none;
  color: white;
  font-size: 18px;
  cursor: pointer;
  padding: 0;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition: all 0.2s ease;
}

.close-btn:hover {
  background: rgba(255, 255, 255, 0.2);
}

.popup-content {
  padding: 15px;
  font-size: 14px;
  color: #333;
}

.popup-content p {
  margin: 0;
}

.guide-line {
  position: absolute;
  pointer-events: none;
  z-index: 1000;
}
</style>

MapViewer.vue (父组件)

复制代码
<template>
  <div class="map-container">
    <div id="cesiumContainer" class="cesium-container"></div>
    <WaterMapSidebar 
      @toggle-layer="handleToggleLayer" 
      @collapse-change="handleCollapseChange" 
    />
    <!-- 渲染多个弹窗实例 -->
    <WaterMapPopup 
      v-for="popup in popups" 
      :key="popup.id"
      :id="popup.id"
      :visible="popup.visible" 
      :point-name="popup.pointName" 
      :position="popup.position" 
      :entity="popup.entity"
      :viewer="viewer"
      @close="closePopup"
      @drag="handlePopupDrag"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from "@/components/WaterMapPopup.vue";

// 初始化Cesium Viewer实例
let viewer = null

// 弹窗相关状态
const popups = ref([]) // 保存多个弹窗实例
const generatedPoints = ref([])

// 范围内随机生成点数据
const generateRandomPoints = () => {
  const minLng = 73.67
  const maxLng = 96.3
  const minLat = 34.42
  const maxLat = 48.17
  
  // 生成10~20个随机点
  const pointCount = Math.floor(Math.random() * 11) + 10
  const points = []
  
  for (let i = 0; i < pointCount; i++) {
    const lng = minLng + Math.random() * (maxLng - minLng)
    const lat = minLat + Math.random() * (maxLat - minLat)
    
    points.push({
      id: i + 1,
      name: `点位${i + 1}`,
      lng: lng,
      lat: lat
    })
  }
  
  return points
}

// 将点数据加载到地图上
const loadPointsToMap = (points) => {
  points.forEach(point => {
    // 创建点实体
    const entity = viewer.entities.add({
      position: Cesium.Cartesian3.fromDegrees(point.lng, point.lat),
      point: {
        pixelSize: 10,
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2
      },
      name: point.name,
      properties: {
        id: point.id
      }
    })
    
    generatedPoints.value.push(entity)
  })
  
  // 添加点击事件监听
  viewer.screenSpaceEventHandler.setInputAction((click) => {
    const pickedObject = viewer.scene.pick(click.position)
    
    if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
      const entity = pickedObject.id
      if (entity.point) {
        // 打开弹窗,支持同时打开多个弹窗
        openPopup(entity, click.position)
      }
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

// 生成唯一弹窗ID
const generatePopupId = () => {
  return `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
}

// 打开弹窗
const openPopup = (entity, position) => {
  // 检查是否已经打开该实体的弹窗
  const existingPopup = popups.value.find(popup => popup.entity === entity)
  if (existingPopup) {
    return // 已存在,不再重复打开
  }
  
  // 创建新弹窗实例
  const popupId = generatePopupId()
  popups.value.push({
    id: popupId,
    visible: true,
    pointName: entity.name,
    position: { x: position.x, y: position.y },
    entity: entity
  })
}

// 关闭弹窗
const closePopup = (popupId) => {
  const index = popups.value.findIndex(popup => popup.id === popupId)
  if (index > -1) {
    popups.value.splice(index, 1)
  }
}

// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
  const popup = popups.value.find(p => p.id === dragInfo.id)
  if (popup) {
    popup.position = dragInfo.position
  }
}

onMounted(() => {
  // 配置Cesium Ion访问令牌(如果需要)
  // Cesium.Ion.defaultAccessToken = 'your-access-token'
   // 禁用Ion服务
  Cesium.Ion.defaultAccessToken = undefined
  viewer = new Cesium.Viewer('cesiumContainer', {
    imageryProvider: false,
    animation: false,
    timeline: false,
    homeButton: false,
    sceneModePicker: false,
    baseLayerPicker: false,
    geocoder: false,
    navigationHelpButton: false,
    fullscreenButton: false,
    infoBox: false,
    selectionIndicator: false
  })
  //隐藏logo
  viewer.cesiumWidget.creditContainer.style.display = "none";

  viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(85.0, 41.0, 3000000),
  })
  
  // 生成随机点数据并加载到地图上
  const randomPoints = generateRandomPoints()
  loadPointsToMap(randomPoints)
  
  // 添加窗口大小变化事件监听,确保地图适配不同屏幕尺寸
  window.addEventListener('resize', handleResize)
})



// 处理窗口大小变化,调整地图尺寸
const handleResize = () => {
  if (viewer) {
    viewer.resize()
  }
}

// 组件卸载前清理资源
onBeforeUnmount(() => {
  if (viewer) {
    viewer.destroy()
    viewer = null
  }
  window.removeEventListener('resize', handleResize)
})
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
  position: relative;
}

.cesium-container {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

/* 确保地图容器在不同屏幕尺寸下都能正确显示 */
:deep(.cesium-viewer) {
  width: 100%;
  height: 100%;
}

:deep(.cesium-viewer-cesiumWidgetContainer) {
  width: 100%;
  height: 100%;
}

:deep(.cesium-widget) {
  width: 100%;
  height: 100%;
}

:deep(.cesium-widget-credits) {
  display: none;
}
</style>
相关推荐
跟着珅聪学java2 小时前
JavaScript 中定义全局变量的教程
javascript
午安~婉2 小时前
整理知识点
前端·javascript·vue
向前V2 小时前
Flutter for OpenHarmony数独游戏App实战:底部导航栏
javascript·flutter·游戏
人道领域2 小时前
JavaWeb从入门到进阶(javaScript)
开发语言·javascript·ecmascript
军军君012 小时前
Three.js基础功能学习十二:常量与核心
前端·javascript·学习·3d·threejs·three·三维
不绝1913 小时前
C#核心——面向对象:封装
开发语言·javascript·c#
27669582923 小时前
dy bd-ticket-guard-client-data bd-ticket-guard-ree-public-key 逆向
前端·javascript·python·abogus·bd-ticket·mstoken·ticket-guard
WX-bisheyuange3 小时前
基于SpringBoot的交通管理在线服务系统
前端·javascript·vue.js·毕业设计
WHOVENLY4 小时前
揭秘正则表达式的基础语法与应用
开发语言·javascript·正则表达式