Cesium--可拖拽气泡弹窗(对话框尾巴,Vue3版)

1.概述

在上一个版本的基础上根据需要把"引导线"改成了对话框形式的尾巴。

2. 使用

2.1 基本使用

vue 复制代码
<template>
  <div class="water-map-content">
    <div id="cesiumContainer"></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'

// 弹窗相关状态
const popups = ref([])
const viewer = ref(null)

// 初始化Cesium地图
onMounted(() => {
  viewer.value = new Cesium.Viewer('cesiumContainer', {
    // 配置选项
  })
  
  // 添加点击事件监听
  viewer.value.screenSpaceEventHandler.setInputAction((click) => {
    const pickedObject = viewer.value.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)
})

// 打开弹窗
const openPopup = (entity, position) => {
  const popupId = `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
  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>

2.2 使用

2.2.1 自定义弹窗内容
vue 复制代码
<template>
  <div class="water-map-content">
    <!-- 其他代码 -->
    <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"
    >
      <template #content>
        <div class="custom-popup-content">
          <h4>{{ popup.pointName }}</h4>
          <p>经度: {{ popup.entity.position.getValue(viewer.clock.currentTime).toString() }}</p>
          <p>纬度: {{ popup.entity.position.getValue(viewer.clock.currentTime).toString() }}</p>
          <button @click="doSomething(popup)">操作</button>
        </div>
      </template>
    </WaterMapPopup>
  </div>
</template>
2.2.2 批量管理弹窗
javascript 复制代码
// 关闭所有弹窗
const closeAllPopups = () => {
  popups.value = []
}

// 根据实体ID关闭弹窗
const closePopupByEntityId = (entityId) => {
  popups.value = popups.value.filter(popup => 
    !popup.entity || !popup.entity.properties || popup.entity.properties.id.getValue() !== entityId
  )
}

3. 组件属性

属性名 类型 必填 默认值 描述
id String - 弹窗唯一标识
visible Boolean false 弹窗是否可见
pointName String "" 弹窗标题
position Object { x: 0, y: 0 } 弹窗初始位置
entity Object null Cesium实体对象
viewer Object - Cesium Viewer实例

4. 组件事件

事件名 参数 描述
close id 弹窗关闭时触发
drag { id, position } 弹窗拖拽时触发

5. 工作原理

5.1 弹窗定位

  • 初始定位:基于传入的position属性
  • 地图移动时:通过监听Cesium的postRender事件,实时更新弹窗位置
  • 拖拽后:保存偏移量,地图移动时保持相对位置

5.2 小尾巴

  • 自动附着:根据控制点位置自动判断最佳附着边
  • 拖拽功能:支持拖拽控制点调整小尾巴方向
  • 响应式:随弹窗和地图位置变化而更新

5.3 多弹窗管理

  • 唯一标识:每个弹窗需要唯一的id
  • 状态管理:通过父组件的popups数组管理多个弹窗
  • 事件通信:通过close和drag事件与父组件通信

6. 常见问题与解决方案

6.1 弹窗位置不更新

问题 :地图移动时,弹窗位置不变
解决方案:确保正确传递viewer和entity属性,组件会自动监听地图渲染事件

6.2 小尾巴显示异常

问题 :小尾巴方向不正确或显示异常
解决方案:检查entity.position是否正确设置,确保能获取到有效的坐标

6.3 拖拽后弹窗位置偏移

问题 :拖拽弹窗后,地图移动时弹窗位置偏移
解决方案:组件内部已处理此问题,会保存拖拽偏移量

6.4 多个弹窗显示问题

问题 :多个弹窗同时打开时显示异常
解决方案:确保每个弹窗都有唯一的id,并正确管理popups数组

7. 性能优化

  1. 合理控制弹窗数量:避免同时打开过多弹窗
  2. 使用虚拟滚动:如果弹窗列表较长,考虑使用虚拟滚动
  3. 优化地图事件监听:避免在弹窗组件中添加过多地图事件监听

8. 示例代码

8.1 完整示例

WaterMapPopup.vue

vue 复制代码
<template>
  <div class="popup-container" v-if="visible">
    <!-- 引导线(对话框小尾巴) -->
    <svg class="guide-line" :style="guideLineStyle" xmlns="http://www.w3.org/2000/svg">
      <polygon :points="tailPoints" fill="rgba(255, 255, 255, 0.95)" stroke="rgba(0, 0, 0, 0.1)" stroke-width="1" />
      <!-- 拖拽控制点 -->
      <circle 
        :cx="dragHandlePoint.x" 
        :cy="dragHandlePoint.y" 
        r="6" 
        fill="white" 
        stroke="#208cd7" 
        stroke-width="2" 
        cursor="move" 
        @mousedown.stop="startTailDrag"
        @mouseover="showDragHandle = true"
        @mouseout="showDragHandle = false"
        :opacity="showDragHandle || isTailDragging ? 1 : 0"
        transition="opacity 0.2s ease"
      />
    </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 isTailDragging = ref(false)
const showDragHandle = ref(false)
const tailDragOffset = ref({ x: 0, y: 0 })
const tailAttachment = ref('bottom') // 小尾巴附着在弹窗的哪个边(top, bottom, left, right)

// 计算弹窗位置样式
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 popupWidth = 200 // 弹窗宽度
  const popupHeight = 80 // 弹窗高度
  
  // 计算弹窗的四个角坐标
  const popupLeft = localPosition.value.x - popupWidth / 2
  const popupRight = localPosition.value.x + popupWidth / 2
  const popupTop = localPosition.value.y
  const popupBottom = localPosition.value.y + popupHeight
  
  // 计算小尾巴的起始位置(弹窗底部中央)
  const tailStartX = localPosition.value.x
  const tailStartY = popupBottom
  
  // 计算引导线容器的位置和大小,确保包含小尾巴的所有部分
  const allX = [pointPosition.x, tailStartX, popupLeft, popupRight]
  const allY = [pointPosition.y, tailStartY, popupTop, popupBottom]
  
  const minX = Math.min(...allX) - 20
  const minY = Math.min(...allY) - 20
  const maxX = Math.max(...allX) + 20
  const maxY = Math.max(...allY) + 20
  
  return {
    position: 'absolute',
    left: `${minX}px`,
    top: `${minY}px`,
    width: `${maxX - minX}px`,
    height: `${maxY - minY}px`,
    zIndex: '1000',
    pointerEvents: 'none'
  }
})

// 计算拖拽控制点的位置
const dragHandlePoint = computed(() => {
  const pointPosition = getPointScreenPosition()
  if (!pointPosition) return { x: 0, y: 0 }
  
  const style = guideLineStyle.value
  const left = parseInt(style.left)
  const top = parseInt(style.top)
  
  // 计算相对于SVG容器的坐标
  const pointX = pointPosition.x - left
  const pointY = pointPosition.y - top
  
  return { x: pointX, y: pointY }
})

// 计算对话框小尾巴的多边形点坐标
const tailPoints = computed(() => {
  const pointPosition = getPointScreenPosition()
  if (!pointPosition) return ''
  
  const style = guideLineStyle.value
  const left = parseInt(style.left)
  const top = parseInt(style.top)
  
  // 计算相对于SVG容器的坐标
  const popupCenterX = localPosition.value.x - left
  const popupTopY = localPosition.value.y - top // 弹窗顶部相对于SVG容器的位置
  
  // 弹窗尺寸
  const popupWidth = 200
  const popupHeight = 80
  
  // 计算弹窗的四个角坐标
  const popupLeft = popupCenterX - popupWidth / 2
  const popupRight = popupCenterX + popupWidth / 2
  const popupBottomY = popupTopY + popupHeight
  
  // 控制点位置(相对于SVG容器)
  const controlPoint = dragHandlePoint.value
  
  // 确定小尾巴附着在弹窗的哪个边
  // 基于控制点的位置自动判断最佳附着边
  const popupCenterY = popupTopY + popupHeight / 2
  
  // 计算控制点相对于弹窗中心的位置
  const dx = controlPoint.x - popupCenterX
  const dy = controlPoint.y - popupCenterY
  
  // 根据距离确定附着边
  const absDx = Math.abs(dx)
  const absDy = Math.abs(dy)
  
  if (absDx > absDy) {
    // 水平方向距离更远,附着在左右边
    tailAttachment.value = dx > 0 ? 'right' : 'left'
  } else {
    // 垂直方向距离更远,附着在上下边
    tailAttachment.value = dy > 0 ? 'bottom' : 'top'
  }
  
  // 计算附着点在弹窗边缘的位置
  let attachX, attachY
  
  switch (tailAttachment.value) {
    case 'top':
      // 附着在顶部
      attachX = popupCenterX + dx * 0.2
      attachY = popupTopY
      break
    case 'bottom':
      // 附着在底部
      attachX = popupCenterX + dx * 0.2
      attachY = popupBottomY
      break
    case 'left':
      // 附着在左侧
      attachX = popupLeft
      attachY = popupCenterY + dy * 0.2
      break
    case 'right':
      // 附着在右侧
      attachX = popupRight
      attachY = popupCenterY + dy * 0.2
      break
  }
  
  // 限制附着点在弹窗边缘范围内
  if (tailAttachment.value === 'top' || tailAttachment.value === 'bottom') {
    attachX = Math.max(popupLeft, Math.min(popupRight, attachX))
  } else {
    attachY = Math.max(popupTopY, Math.min(popupBottomY, attachY))
  }
  
  // 计算小尾巴的两个固定点(贴合弹窗边缘)
  const tailWidth = 16 // 小尾巴宽度,与弹窗边框宽度匹配
  let point1X, point1Y, point2X, point2Y
  
  switch (tailAttachment.value) {
    case 'top':
      // 附着在顶部
      point1X = attachX - tailWidth / 2
      point1Y = attachY
      point2X = attachX + tailWidth / 2
      point2Y = attachY
      break
    case 'bottom':
      // 附着在底部
      point1X = attachX - tailWidth / 2
      point1Y = attachY
      point2X = attachX + tailWidth / 2
      point2Y = attachY
      break
    case 'left':
      // 附着在左侧
      point1X = attachX
      point1Y = attachY - tailWidth / 2
      point2X = attachX
      point2Y = attachY + tailWidth / 2
      break
    case 'right':
      // 附着在右侧
      point1X = attachX
      point1Y = attachY - tailWidth / 2
      point2X = attachX
      point2Y = attachY + tailWidth / 2
      break
  }
  
  // 小尾巴的第三个点(控制点)
  const point3X = controlPoint.x
  const point3Y = controlPoint.y
  
  // 生成多边形点坐标
  return `${point1X},${point1Y} ${point2X},${point2Y} ${point3X},${point3Y}`
})

// 获取点位的屏幕坐标
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 startTailDrag = (event) => {
  isTailDragging.value = true
  
  // 计算拖拽偏移量
  const pointPosition = getPointScreenPosition()
  if (pointPosition) {
    const rect = event.currentTarget.getBoundingClientRect()
    tailDragOffset.value = {
      x: event.clientX - pointPosition.x,
      y: event.clientY - pointPosition.y
    }
  }
  
  // 添加全局事件监听
  document.addEventListener('mousemove', handleTailDrag)
  document.addEventListener('mouseup', stopTailDrag)
}

// 处理小尾巴拖拽
const handleTailDrag = (event) => {
  if (!isTailDragging.value || !props.entity) return
  
  // 更新点位的屏幕坐标(模拟拖拽控制点)
  // 注意:这里我们不能直接修改点位的实际位置,只能修改弹窗相对于点位的偏移量
  // 我们将通过调整offsetFromPoint来实现小尾巴控制点的拖拽效果
  
  // 计算新的点位屏幕坐标
  const newPointX = event.clientX - tailDragOffset.value.x
  const newPointY = event.clientY - tailDragOffset.value.y
  
  // 更新offsetFromPoint,使弹窗保持在原来的位置,而小尾巴指向新的控制点
  const currentPointPosition = getPointScreenPosition()
  if (currentPointPosition) {
    offsetFromPoint.value = {
      x: localPosition.value.x - newPointX,
      y: localPosition.value.y - newPointY
    }
    
    // 标记弹窗已被拖拽,确保地图移动时使用新的偏移量
    hasDragged.value = true
  }
}

// 停止拖拽小尾巴控制点
const stopTailDrag = () => {
  isTailDragging.value = false
  document.removeEventListener('mousemove', handleTailDrag)
  document.removeEventListener('mouseup', stopTailDrag)
}

// 关闭弹窗
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%, 0);
  top: 0;
  left: 50%;
  margin-top: 0;
}

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

.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

vue 复制代码
<template>
  <div class="water-map-content">
    <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";
import * as turf from '@turf/turf'

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

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

// 在经纬度范围内随机生成点数据
const generateRandomPoints = () => {
  const minLng = 82.67
  const maxLng = 96.3
  const minLat = 38.42
  const maxLat = 46.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'
  viewer = new Cesium.Viewer('cesiumContainer', {
    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.imageryLayers.removeAll();
  
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(85.0, 41.0, 3000000), // 中心点经纬度和高度
})

loadPointsToMap(generateRandomPoints());
})

// 组件卸载前清理资源
onBeforeUnmount(() => {
  if (viewer) {
    viewer.destroy()
    viewer = null
  }
})

</script>

<style scoped>
.water-map-content {
  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;
}
/* 液态玻璃风样式 */
.bingtuan-selector {
  background: rgba(255, 255, 255, 0.25);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 16px;
  box-shadow: 
    0 8px 32px rgba(29, 127, 198, 0.1),
    inset 0 1px 0 rgba(255, 255, 255, 0.3);
  padding: 8px 16px;
  font-size: 16px;
  font-weight: 600;
  color: #1e293b;
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  min-width: 200px;
}
</style>

9.2 小尾巴拖拽示例

  1. 将鼠标悬停在小尾巴的控制点上
  2. 点击并拖拽控制点调整小尾巴方向
  3. 释放鼠标完成调整
相关推荐
im_AMBER2 小时前
消失的最后一秒:SSE 流式联调中的“时序竞争”
前端·笔记·学习·http·sse
GDAL2 小时前
Electron IPC 通信深入全面讲解教程
javascript·electron
RFCEO2 小时前
前端编程 课程十、:CSS 系统学习学前知识/准备
前端·css·层叠样式表·动效设计·前端页面布局6大通用法则·黄金分割比例法则·设计美观的前端
白日梦想家6812 小时前
深入浅出 JavaScript 定时器:从基础用法到避坑指南
开发语言·javascript·ecmascript
雄狮少年2 小时前
简单react agent(没有抽象成基类、子类,直接用)--- 非workflow版 ------demo1
前端·react.js·前端框架
一 乐2 小时前
在线考试|基于springboot + vue在线考试系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
plmm烟酒僧2 小时前
《微信小程序demo开发》第一部分-编写页面逻辑
javascript·微信小程序·小程序·html·微信开发者工具·小程序开发
Monly212 小时前
【大前端】前期准备-Trae开发工具安装
前端
2601_949720262 小时前
flutter_for_openharmony手语学习app实战+学习进度实现
javascript·学习·flutter