|  | @@ -0,0 +1,1102 @@
 | 
	
		
			
				|  |  | +<template>
 | 
	
		
			
				|  |  | +  <!-- 台风图例面板 -->
 | 
	
		
			
				|  |  | +  <ul class="legend" v-if="visible">
 | 
	
		
			
				|  |  | +    <li>
 | 
	
		
			
				|  |  | +      <span class="dot green"></span>
 | 
	
		
			
				|  |  | +      <span>热带低压</span>
 | 
	
		
			
				|  |  | +    </li>
 | 
	
		
			
				|  |  | +    <li>
 | 
	
		
			
				|  |  | +      <span class="dot blue"></span>
 | 
	
		
			
				|  |  | +      <span>热带风暴</span>
 | 
	
		
			
				|  |  | +    </li>
 | 
	
		
			
				|  |  | +    <li>
 | 
	
		
			
				|  |  | +      <span class="dot yellow"></span>
 | 
	
		
			
				|  |  | +      <span>强热带风暴</span>
 | 
	
		
			
				|  |  | +    </li>
 | 
	
		
			
				|  |  | +    <li>
 | 
	
		
			
				|  |  | +      <span class="dot orange"></span>
 | 
	
		
			
				|  |  | +      <span>台风</span>
 | 
	
		
			
				|  |  | +    </li>
 | 
	
		
			
				|  |  | +    <li>
 | 
	
		
			
				|  |  | +      <span class="dot red"></span>
 | 
	
		
			
				|  |  | +      <span>超强台风</span>
 | 
	
		
			
				|  |  | +    </li>
 | 
	
		
			
				|  |  | +  </ul>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  <!-- 台风路径点信息弹窗 -->
 | 
	
		
			
				|  |  | +  <div v-if="infoVisible && visible" class="typhoon-popup" :style="{
 | 
	
		
			
				|  |  | +    left: `${popupPosition.x}px`,
 | 
	
		
			
				|  |  | +    top: `${popupPosition.y}px`
 | 
	
		
			
				|  |  | +  }">
 | 
	
		
			
				|  |  | +    <div class="popup-content" :class="{ 'bottom': popupPosition.bottom }">
 | 
	
		
			
				|  |  | +      <h3>{{ info.name }}({{ info.enname }})</h3>
 | 
	
		
			
				|  |  | +      <p>{{ info.time }}</p>
 | 
	
		
			
				|  |  | +      <p><strong>中心位置:</strong> {{ info.lng }}° / {{ info.lat }}°</p>
 | 
	
		
			
				|  |  | +      <p><strong>风速风力:</strong> {{ info.speed }} m/s ({{ info.power }}级)</p>
 | 
	
		
			
				|  |  | +      <p><strong>中心气压:</strong> {{ info.pressure }} hPa</p>
 | 
	
		
			
				|  |  | +      <p><strong>移速移向:</strong> {{ info.movedirection }} {{ info.movespeed }} km/h</p>
 | 
	
		
			
				|  |  | +      <p><strong>七级半径:</strong> {{ info.radius7 || '--' }}</p>
 | 
	
		
			
				|  |  | +      <p><strong>十级半径:</strong> {{ info.radius10 || '--' }}</p>
 | 
	
		
			
				|  |  | +      <p><strong>十二级半径:</strong> {{ info.radius12 || '--' }}</p>
 | 
	
		
			
				|  |  | +      <div class="popup-arrow"></div>
 | 
	
		
			
				|  |  | +    </div>
 | 
	
		
			
				|  |  | +  </div>
 | 
	
		
			
				|  |  | +</template>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<script setup>
 | 
	
		
			
				|  |  | +import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
 | 
	
		
			
				|  |  | +import * as Cesium from 'cesium';
 | 
	
		
			
				|  |  | +import axios from 'axios'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 接收父组件传递的Cesium viewer实例和显示状态
 | 
	
		
			
				|  |  | +const props = defineProps({
 | 
	
		
			
				|  |  | +  viewer: {
 | 
	
		
			
				|  |  | +    type: Object,
 | 
	
		
			
				|  |  | +    required: false, // 不再强制要求
 | 
	
		
			
				|  |  | +    default: null    // 默认值为null
 | 
	
		
			
				|  |  | +  },
 | 
	
		
			
				|  |  | +  visible: {
 | 
	
		
			
				|  |  | +    type: Boolean,
 | 
	
		
			
				|  |  | +    default: true
 | 
	
		
			
				|  |  | +  },
 | 
	
		
			
				|  |  | +  dataUrl: {
 | 
	
		
			
				|  |  | +    type: String,
 | 
	
		
			
				|  |  | +    default: '../assets/Data/202508.json'
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 暴露事件
 | 
	
		
			
				|  |  | +const emits = defineEmits(['onToggle']);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 台风相关状态
 | 
	
		
			
				|  |  | +const infoVisible = ref(false);
 | 
	
		
			
				|  |  | +const info = ref({});
 | 
	
		
			
				|  |  | +const popupPosition = ref({ x: 0, y: 0, bottom: false });
 | 
	
		
			
				|  |  | +const pointDataMap = ref(new Map());
 | 
	
		
			
				|  |  | +const currentEntity = ref(null);
 | 
	
		
			
				|  |  | +const entityCollection = ref(null);
 | 
	
		
			
				|  |  | +const tbentity = ref(null);
 | 
	
		
			
				|  |  | +const iii = ref(0);
 | 
	
		
			
				|  |  | +const currentPointObj = ref(null);
 | 
	
		
			
				|  |  | +let typhoonInterval = null;
 | 
	
		
			
				|  |  | +const fengquanLayers = ref([]);
 | 
	
		
			
				|  |  | +const relatedEntities = ref({
 | 
	
		
			
				|  |  | +  paths: [],
 | 
	
		
			
				|  |  | +  forecasts: [],
 | 
	
		
			
				|  |  | +  warnings: []
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +const popupUpdateCallback = ref(null);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取Cesium容器元素
 | 
	
		
			
				|  |  | +const getCesiumContainer = () => {
 | 
	
		
			
				|  |  | +  return document.getElementById('cesiumContainer');
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取Cesium容器的位置信息
 | 
	
		
			
				|  |  | +const getCesiumContainerRect = () => {
 | 
	
		
			
				|  |  | +  const container = getCesiumContainer();
 | 
	
		
			
				|  |  | +  return container ? container.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 更新台风弹窗位置
 | 
	
		
			
				|  |  | +const updatePopupPosition = (entity) => {
 | 
	
		
			
				|  |  | +  if (popupUpdateCallback.value) {
 | 
	
		
			
				|  |  | +    props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
 | 
	
		
			
				|  |  | +    popupUpdateCallback.value = null;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  if (!entity || !entity.position || !infoVisible.value) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  popupUpdateCallback.value = () => {
 | 
	
		
			
				|  |  | +    const container = getCesiumContainer();
 | 
	
		
			
				|  |  | +    const containerRect = container.getBoundingClientRect();
 | 
	
		
			
				|  |  | +    const entityPosition = props.viewer.scene.cartesianToCanvasCoordinates(
 | 
	
		
			
				|  |  | +      entity.position.getValue(props.viewer.clock.currentTime)
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (entityPosition) {
 | 
	
		
			
				|  |  | +      nextTick(() => {
 | 
	
		
			
				|  |  | +        const popupEl = document.querySelector('.typhoon-popup .popup-content');
 | 
	
		
			
				|  |  | +        if (!popupEl) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 获取弹窗尺寸和样式信息
 | 
	
		
			
				|  |  | +        const popupRect = popupEl.getBoundingClientRect();
 | 
	
		
			
				|  |  | +        const popupWidth = popupRect.width;
 | 
	
		
			
				|  |  | +        const popupHeight = popupRect.height;
 | 
	
		
			
				|  |  | +        const arrowHeight = 10; // 箭头高度
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 获取台风点大小
 | 
	
		
			
				|  |  | +        const pointSize = entity.point?.pixelSize || getDynamicPointSize();
 | 
	
		
			
				|  |  | +        const pointRadius = pointSize / 2;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 计算弹窗水平位置
 | 
	
		
			
				|  |  | +        const offsetX = -15; // 向左微调
 | 
	
		
			
				|  |  | +        let x = (entityPosition.x - containerRect.left) - (popupWidth / 2) + offsetX;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 计算弹窗垂直位置
 | 
	
		
			
				|  |  | +        let y, bottom = false;
 | 
	
		
			
				|  |  | +        const topSpacing = 35;
 | 
	
		
			
				|  |  | +        y = (entityPosition.y - containerRect.top) - popupHeight - arrowHeight - pointRadius - topSpacing;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 如果上方空间不足,显示在点的下方
 | 
	
		
			
				|  |  | +        if (y < 0) {
 | 
	
		
			
				|  |  | +          y = (entityPosition.y - containerRect.top) + pointRadius + arrowHeight;
 | 
	
		
			
				|  |  | +          bottom = true;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // 边界检查
 | 
	
		
			
				|  |  | +        if (x < 0) x = 0;
 | 
	
		
			
				|  |  | +        if (x + popupWidth > containerRect.width) {
 | 
	
		
			
				|  |  | +          x = containerRect.width - popupWidth;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        popupPosition.value = { x, y, bottom };
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  props.viewer.scene.postRender.addEventListener(popupUpdateCallback.value);
 | 
	
		
			
				|  |  | +  popupUpdateCallback.value();
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 隐藏台风弹窗
 | 
	
		
			
				|  |  | +const hidePopup = () => {
 | 
	
		
			
				|  |  | +  infoVisible.value = false;
 | 
	
		
			
				|  |  | +  if (popupUpdateCallback.value) {
 | 
	
		
			
				|  |  | +    props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
 | 
	
		
			
				|  |  | +    popupUpdateCallback.value = null;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取缩放比例
 | 
	
		
			
				|  |  | +const getScaleRatio = () => {
 | 
	
		
			
				|  |  | +  const designWidth = 1920;
 | 
	
		
			
				|  |  | +  const designHeight = 1080;
 | 
	
		
			
				|  |  | +  return Math.min(window.innerWidth / designWidth, window.innerHeight / designHeight);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取像素比例
 | 
	
		
			
				|  |  | +const getPixelRatio = () => {
 | 
	
		
			
				|  |  | +  return window.devicePixelRatio || 1;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取动态点大小
 | 
	
		
			
				|  |  | +const getDynamicPointSize = () => {
 | 
	
		
			
				|  |  | +  const baseSize = 8;
 | 
	
		
			
				|  |  | +  const scaleRatio = getScaleRatio();
 | 
	
		
			
				|  |  | +  const pixelRatio = getPixelRatio();
 | 
	
		
			
				|  |  | +  return Math.max(baseSize * scaleRatio * pixelRatio, 6);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取动态拾取半径
 | 
	
		
			
				|  |  | +const getDynamicPickRadius = () => {
 | 
	
		
			
				|  |  | +  const baseRadius = 10;
 | 
	
		
			
				|  |  | +  const scaleRatio = getScaleRatio();
 | 
	
		
			
				|  |  | +  return baseRadius * (1 / scaleRatio);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 检查点是否在点击范围内
 | 
	
		
			
				|  |  | +const isPointInClickRange = (clickPosition, pointPosition, radius) => {
 | 
	
		
			
				|  |  | +  const dx = clickPosition.x - pointPosition.x;
 | 
	
		
			
				|  |  | +  const dy = clickPosition.y - pointPosition.y;
 | 
	
		
			
				|  |  | +  return Math.sqrt(dx * dx + dy * dy) <= radius;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 检查台风点点击
 | 
	
		
			
				|  |  | +const checkPointClick = (clickPosition) => {
 | 
	
		
			
				|  |  | +  if (!entityCollection.value || !entityCollection.value.entities) return null;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const radius = getDynamicPickRadius();
 | 
	
		
			
				|  |  | +  const pixelRatio = getPixelRatio();
 | 
	
		
			
				|  |  | +  const scaledClickPos = {
 | 
	
		
			
				|  |  | +    x: clickPosition.x * pixelRatio,
 | 
	
		
			
				|  |  | +    y: clickPosition.y * pixelRatio
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const entities = entityCollection.value.entities.values;
 | 
	
		
			
				|  |  | +  for (let i = 0; i < entities.length; i++) {
 | 
	
		
			
				|  |  | +    const entity = entities[i];
 | 
	
		
			
				|  |  | +    if (entity.id && entity.id.startsWith('typhoon-point-') && entity.position) {
 | 
	
		
			
				|  |  | +      const pointPosition = props.viewer.scene.cartesianToCanvasCoordinates(entity.position._value);
 | 
	
		
			
				|  |  | +      if (pointPosition && isPointInClickRange(scaledClickPos, pointPosition, radius)) {
 | 
	
		
			
				|  |  | +        return entity;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return null;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 更新台风点大小
 | 
	
		
			
				|  |  | +const updatePointSizes = () => {
 | 
	
		
			
				|  |  | +  if (!entityCollection.value || !entityCollection.value.entities) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const newSize = getDynamicPointSize();
 | 
	
		
			
				|  |  | +  const entities = entityCollection.value.entities.values;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  for (let i = 0; i < entities.length; i++) {
 | 
	
		
			
				|  |  | +    const entity = entities[i];
 | 
	
		
			
				|  |  | +    if (entity.id && entity.id.startsWith('typhoon-point-') && entity.point) {
 | 
	
		
			
				|  |  | +      entity.point.pixelSize = newSize;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 处理台风数据
 | 
	
		
			
				|  |  | +const processPoints = (points, typhoonData) => {
 | 
	
		
			
				|  |  | +  const lineArr = [];
 | 
	
		
			
				|  |  | +  const pointSize = getDynamicPointSize();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 清除现有台风点
 | 
	
		
			
				|  |  | +  if (entityCollection.value && entityCollection.value.entities) {
 | 
	
		
			
				|  |  | +    entityCollection.value.entities.removeAll();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 重新添加所有台风点
 | 
	
		
			
				|  |  | +  points.forEach((element, index) => {
 | 
	
		
			
				|  |  | +    const lng = Number(element.lng);
 | 
	
		
			
				|  |  | +    const lat = Number(element.lat);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    let color = Cesium.Color.RED;
 | 
	
		
			
				|  |  | +    switch (element.strong) {
 | 
	
		
			
				|  |  | +      case "热带低压":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.GREEN;
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      case "热带风暴":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.BLUE;
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      case "强热带风暴":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.YELLOW;
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      case "台风":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.fromCssColorString("#FBC712");
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      case "强台风":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.PLUM;
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +      case "超强台风":
 | 
	
		
			
				|  |  | +        color = Cesium.Color.RED;
 | 
	
		
			
				|  |  | +        break;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    lineArr.push(lng, lat);
 | 
	
		
			
				|  |  | +    const pointId = `typhoon-point-${index}`;
 | 
	
		
			
				|  |  | +    const entity = new Cesium.Entity({
 | 
	
		
			
				|  |  | +      id: pointId,
 | 
	
		
			
				|  |  | +      position: Cesium.Cartesian3.fromDegrees(lng, lat),
 | 
	
		
			
				|  |  | +      point: {
 | 
	
		
			
				|  |  | +        pixelSize: pointSize,
 | 
	
		
			
				|  |  | +        color: color,
 | 
	
		
			
				|  |  | +        outlineColor: Cesium.Color.WHITE,
 | 
	
		
			
				|  |  | +        outlineWidth: 2,
 | 
	
		
			
				|  |  | +        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
 | 
	
		
			
				|  |  | +        show: props.visible
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    entityCollection.value.entities.add(entity);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    pointDataMap.value.set(pointId, {
 | 
	
		
			
				|  |  | +      name: typhoonData.name || '',
 | 
	
		
			
				|  |  | +      enname: typhoonData.enname || '',
 | 
	
		
			
				|  |  | +      time: element.time || '',
 | 
	
		
			
				|  |  | +      lng: element.lng,
 | 
	
		
			
				|  |  | +      lat: element.lat,
 | 
	
		
			
				|  |  | +      speed: element.speed,
 | 
	
		
			
				|  |  | +      power: element.power,
 | 
	
		
			
				|  |  | +      pressure: element.pressure,
 | 
	
		
			
				|  |  | +      movedirection: element.movedirection,
 | 
	
		
			
				|  |  | +      movespeed: element.movespeed,
 | 
	
		
			
				|  |  | +      radius7: element.radius7 || '--',
 | 
	
		
			
				|  |  | +      radius10: element.radius10 || '--',
 | 
	
		
			
				|  |  | +      radius12: element.radius12 || '--'
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 清除现有路径
 | 
	
		
			
				|  |  | +  relatedEntities.value.paths.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.paths = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 创建新路径
 | 
	
		
			
				|  |  | +  if (lineArr.length >= 4) {
 | 
	
		
			
				|  |  | +    const pathEntity = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +      polyline: {
 | 
	
		
			
				|  |  | +        positions: Cesium.Cartesian3.fromDegreesArray(lineArr),
 | 
	
		
			
				|  |  | +        width: 3,
 | 
	
		
			
				|  |  | +        clampToGround: true,
 | 
	
		
			
				|  |  | +        material: Cesium.Color.RED,
 | 
	
		
			
				|  |  | +        show: props.visible
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    relatedEntities.value.paths.push(pathEntity);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  if (points.length > 0) {
 | 
	
		
			
				|  |  | +    // 清除现有预报
 | 
	
		
			
				|  |  | +    relatedEntities.value.forecasts.forEach(entity => {
 | 
	
		
			
				|  |  | +      props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    relatedEntities.value.forecasts = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    initForeast(points[points.length - 1]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 初始化动画
 | 
	
		
			
				|  |  | +    iii.value = 0;
 | 
	
		
			
				|  |  | +    removes();
 | 
	
		
			
				|  |  | +    adds(points, typhoonData);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 初始化预报
 | 
	
		
			
				|  |  | +const initForeast = (data) => {
 | 
	
		
			
				|  |  | +  const forecast = data.forecast || [];
 | 
	
		
			
				|  |  | +  const colorArr = [
 | 
	
		
			
				|  |  | +    Cesium.Color.fromCssColorString("#2D12FB"),
 | 
	
		
			
				|  |  | +    Cesium.Color.fromCssColorString("#15E5E7"),
 | 
	
		
			
				|  |  | +    Cesium.Color.fromCssColorString("#15E74A"),
 | 
	
		
			
				|  |  | +    Cesium.Color.fromCssColorString("#E76F15"),
 | 
	
		
			
				|  |  | +    Cesium.Color.fromCssColorString("#15D9E7")
 | 
	
		
			
				|  |  | +  ];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  forecast.forEach((ele, ii) => {
 | 
	
		
			
				|  |  | +    if (ele.forecastpoints && ele.forecastpoints.length > 0) {
 | 
	
		
			
				|  |  | +      const lineArr = [];
 | 
	
		
			
				|  |  | +      ele.forecastpoints.forEach(e => {
 | 
	
		
			
				|  |  | +        const lng = Number(e.lng);
 | 
	
		
			
				|  |  | +        const lat = Number(e.lat);
 | 
	
		
			
				|  |  | +        lineArr.push(lng, lat);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const entity = new Cesium.Entity({
 | 
	
		
			
				|  |  | +          position: Cesium.Cartesian3.fromDegrees(lng, lat),
 | 
	
		
			
				|  |  | +          point: {
 | 
	
		
			
				|  |  | +            pixelSize: getDynamicPointSize() * 0.8,
 | 
	
		
			
				|  |  | +            color: colorArr[ii % colorArr.length]
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +        entityCollection.value.entities.add(entity);
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const forecastEntity = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +        polyline: {
 | 
	
		
			
				|  |  | +          positions: Cesium.Cartesian3.fromDegreesArray(lineArr),
 | 
	
		
			
				|  |  | +          width: 2,
 | 
	
		
			
				|  |  | +          clampToGround: true,
 | 
	
		
			
				|  |  | +          material: new Cesium.PolylineDashMaterialProperty({
 | 
	
		
			
				|  |  | +            color: colorArr[ii % colorArr.length]
 | 
	
		
			
				|  |  | +          })
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      relatedEntities.value.forecasts.push(forecastEntity);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 初始化警戒线
 | 
	
		
			
				|  |  | +const initWarningLines = () => {
 | 
	
		
			
				|  |  | +  // 清除现有警戒线
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 24小时警戒线
 | 
	
		
			
				|  |  | +  const line24h = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +    name: '24小时警戒线',
 | 
	
		
			
				|  |  | +    polyline: {
 | 
	
		
			
				|  |  | +      positions: Cesium.Cartesian3.fromDegreesArray([
 | 
	
		
			
				|  |  | +        127, 34, 127, 22, 119, 18, 119, 11, 113, 4.5, 105, 0
 | 
	
		
			
				|  |  | +      ]),
 | 
	
		
			
				|  |  | +      width: 2,
 | 
	
		
			
				|  |  | +      material: Cesium.Color.RED,
 | 
	
		
			
				|  |  | +      clampToGround: true,
 | 
	
		
			
				|  |  | +      show: props.visible
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.push(line24h);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 48小时警戒线
 | 
	
		
			
				|  |  | +  const line48h = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +    name: '48小时警戒线',
 | 
	
		
			
				|  |  | +    polyline: {
 | 
	
		
			
				|  |  | +      positions: Cesium.Cartesian3.fromDegreesArray([
 | 
	
		
			
				|  |  | +        132, 34, 132, 22, 119, 0, 105, 0
 | 
	
		
			
				|  |  | +      ]),
 | 
	
		
			
				|  |  | +      width: 2,
 | 
	
		
			
				|  |  | +      material: Cesium.Color.YELLOW,
 | 
	
		
			
				|  |  | +      clampToGround: true,
 | 
	
		
			
				|  |  | +      show: props.visible
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.push(line48h);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 警戒线标签
 | 
	
		
			
				|  |  | +  const label24h = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +    position: Cesium.Cartesian3.fromDegrees(126.129019, 29.104287),
 | 
	
		
			
				|  |  | +    label: {
 | 
	
		
			
				|  |  | +      fillColor: Cesium.Color.RED,
 | 
	
		
			
				|  |  | +      text: '24小时警戒线',
 | 
	
		
			
				|  |  | +      font: '14pt monospace',
 | 
	
		
			
				|  |  | +      show: props.visible
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.push(label24h);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const label48h = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +    position: Cesium.Cartesian3.fromDegrees(132, 20),
 | 
	
		
			
				|  |  | +    label: {
 | 
	
		
			
				|  |  | +      fillColor: Cesium.Color.YELLOW,
 | 
	
		
			
				|  |  | +      text: '48小时警戒线',
 | 
	
		
			
				|  |  | +      font: '14pt monospace',
 | 
	
		
			
				|  |  | +      show: props.visible
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.push(label48h);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 移除台风相关元素
 | 
	
		
			
				|  |  | +const removes = () => {
 | 
	
		
			
				|  |  | +  // 移除风圈
 | 
	
		
			
				|  |  | +  fengquanLayers.value.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  fengquanLayers.value = [];
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 移除风眼图标
 | 
	
		
			
				|  |  | +  if (tbentity.value) {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(tbentity.value);
 | 
	
		
			
				|  |  | +    tbentity.value = null;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 添加台风动画
 | 
	
		
			
				|  |  | +const adds = (data, typhoonData) => {
 | 
	
		
			
				|  |  | +  addEye(typhoonData);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 清除可能存在的旧定时器
 | 
	
		
			
				|  |  | +  if (typhoonInterval) {
 | 
	
		
			
				|  |  | +    clearInterval(typhoonInterval);
 | 
	
		
			
				|  |  | +    typhoonInterval = null;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 重置计数器
 | 
	
		
			
				|  |  | +  iii.value = 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 创建新的定时器,只播放一次
 | 
	
		
			
				|  |  | +  typhoonInterval = setInterval(() => {
 | 
	
		
			
				|  |  | +    if (!props.visible) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 当计数器达到数据长度时,清除定时器
 | 
	
		
			
				|  |  | +    if (iii.value >= data.length) {
 | 
	
		
			
				|  |  | +      clearInterval(typhoonInterval);
 | 
	
		
			
				|  |  | +      typhoonInterval = null;
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const kkk = iii.value * 2;
 | 
	
		
			
				|  |  | +    const currentData = data[iii.value];
 | 
	
		
			
				|  |  | +    currentPointObj.value = {
 | 
	
		
			
				|  |  | +      lon: Number(currentData.lng),
 | 
	
		
			
				|  |  | +      lat: Number(currentData.lat),
 | 
	
		
			
				|  |  | +      circle7: {
 | 
	
		
			
				|  |  | +        radius1: 350 - kkk,
 | 
	
		
			
				|  |  | +        radius2: 450 - kkk,
 | 
	
		
			
				|  |  | +        radius3: 400 - kkk,
 | 
	
		
			
				|  |  | +        radius4: 350 - kkk
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      circle10: {
 | 
	
		
			
				|  |  | +        radius1: 250 - kkk,
 | 
	
		
			
				|  |  | +        radius2: 270 - kkk,
 | 
	
		
			
				|  |  | +        radius3: 250 - kkk,
 | 
	
		
			
				|  |  | +        radius4: 220 - kkk
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      circle12: {
 | 
	
		
			
				|  |  | +        radius1: 170 - kkk,
 | 
	
		
			
				|  |  | +        radius2: 150 - kkk,
 | 
	
		
			
				|  |  | +        radius3: 150 - kkk,
 | 
	
		
			
				|  |  | +        radius4: 170 - kkk
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (tbentity.value) {
 | 
	
		
			
				|  |  | +      tbentity.value.position = Cesium.Cartesian3.fromDegrees(
 | 
	
		
			
				|  |  | +        Number(currentData.lng),
 | 
	
		
			
				|  |  | +        Number(currentData.lat)
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 继续下一个帧
 | 
	
		
			
				|  |  | +    iii.value++;
 | 
	
		
			
				|  |  | +    removeTFLayer();
 | 
	
		
			
				|  |  | +    if (props.visible) {
 | 
	
		
			
				|  |  | +      addTyphoonCircle();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }, 200);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 添加台风眼
 | 
	
		
			
				|  |  | +const addEye = (typhoonData) => {
 | 
	
		
			
				|  |  | +  const SuperGif = window.SuperGif;
 | 
	
		
			
				|  |  | +  if (typeof SuperGif !== 'function') {
 | 
	
		
			
				|  |  | +    console.warn('SuperGif未加载成功,使用默认标记');
 | 
	
		
			
				|  |  | +    useDefaultMarker(typhoonData);
 | 
	
		
			
				|  |  | +    return;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const img = document.createElement('img');
 | 
	
		
			
				|  |  | +  img.src = '/tf.gif';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  img.onload = () => {
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const rub = new SuperGif({ gif: img });
 | 
	
		
			
				|  |  | +      rub.load(() => {
 | 
	
		
			
				|  |  | +        tbentity.value = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +          position: Cesium.Cartesian3.fromDegrees(
 | 
	
		
			
				|  |  | +            Number(typhoonData.centerlng || 123.75),
 | 
	
		
			
				|  |  | +            Number(typhoonData.centerlat || 28.95)
 | 
	
		
			
				|  |  | +          ),
 | 
	
		
			
				|  |  | +          billboard: {
 | 
	
		
			
				|  |  | +            image: new Cesium.CallbackProperty(() => {
 | 
	
		
			
				|  |  | +              return rub.get_canvas().toDataURL('image/png');
 | 
	
		
			
				|  |  | +            }, false),
 | 
	
		
			
				|  |  | +            scale: 0.1 * getScaleRatio(),
 | 
	
		
			
				|  |  | +            show: props.visible
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('GIF处理失败,使用默认标记', error);
 | 
	
		
			
				|  |  | +      useDefaultMarker(typhoonData);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  img.onerror = () => {
 | 
	
		
			
				|  |  | +    console.error('GIF加载失败,使用默认标记');
 | 
	
		
			
				|  |  | +    useDefaultMarker(typhoonData);
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 使用默认标记
 | 
	
		
			
				|  |  | +const useDefaultMarker = (typhoonData) => {
 | 
	
		
			
				|  |  | +  tbentity.value = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +    position: Cesium.Cartesian3.fromDegrees(
 | 
	
		
			
				|  |  | +      Number(typhoonData.centerlng || 123.75),
 | 
	
		
			
				|  |  | +      Number(typhoonData.centerlat || 28.95)
 | 
	
		
			
				|  |  | +    ),
 | 
	
		
			
				|  |  | +    point: {
 | 
	
		
			
				|  |  | +      pixelSize: getDynamicPointSize() * 1.5,
 | 
	
		
			
				|  |  | +      color: Cesium.Color.RED,
 | 
	
		
			
				|  |  | +      outlineColor: Cesium.Color.WHITE,
 | 
	
		
			
				|  |  | +      outlineWidth: 2,
 | 
	
		
			
				|  |  | +      show: props.visible
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 移除台风风圈图层
 | 
	
		
			
				|  |  | +const removeTFLayer = () => {
 | 
	
		
			
				|  |  | +  fengquanLayers.value.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  fengquanLayers.value = [];
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 添加台风风圈
 | 
	
		
			
				|  |  | +const addTyphoonCircle = () => {
 | 
	
		
			
				|  |  | +  if (!currentPointObj.value || !props.visible) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const circles = ['circle7', 'circle10', 'circle12'];
 | 
	
		
			
				|  |  | +  circles.forEach(item => {
 | 
	
		
			
				|  |  | +    const entity = props.viewer.entities.add({
 | 
	
		
			
				|  |  | +      id: `tf_polygon_${item}`,
 | 
	
		
			
				|  |  | +      polygon: {
 | 
	
		
			
				|  |  | +        hierarchy: new Cesium.CallbackProperty(() => {
 | 
	
		
			
				|  |  | +          const points = currentPointObj.value[item]
 | 
	
		
			
				|  |  | +            ? getTyphoonPolygonPoints(currentPointObj.value, item)
 | 
	
		
			
				|  |  | +            : [];
 | 
	
		
			
				|  |  | +          return new Cesium.PolygonHierarchy(
 | 
	
		
			
				|  |  | +            Cesium.Cartesian3.fromDegreesArray(points)
 | 
	
		
			
				|  |  | +          );
 | 
	
		
			
				|  |  | +        }, false),
 | 
	
		
			
				|  |  | +        material: Cesium.Color.ORANGE.withAlpha(0.05),
 | 
	
		
			
				|  |  | +        extrudedHeight: 1000,
 | 
	
		
			
				|  |  | +        outline: true,
 | 
	
		
			
				|  |  | +        outlineColor: Cesium.Color.ORANGE,
 | 
	
		
			
				|  |  | +        outlineWidth: 2,
 | 
	
		
			
				|  |  | +        show: props.visible
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      polyline: {
 | 
	
		
			
				|  |  | +        positions: new Cesium.CallbackProperty(() => {
 | 
	
		
			
				|  |  | +          const points = currentPointObj.value[item]
 | 
	
		
			
				|  |  | +            ? getTyphoonPolygonPoints(currentPointObj.value, item)
 | 
	
		
			
				|  |  | +            : [];
 | 
	
		
			
				|  |  | +          return Cesium.Cartesian3.fromDegreesArray(points);
 | 
	
		
			
				|  |  | +        }, false),
 | 
	
		
			
				|  |  | +        material: Cesium.Color.ORANGE,
 | 
	
		
			
				|  |  | +        width: 2,
 | 
	
		
			
				|  |  | +        height: 1000,
 | 
	
		
			
				|  |  | +        show: props.visible
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    fengquanLayers.value.push(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 获取台风风圈多边形点
 | 
	
		
			
				|  |  | +const getTyphoonPolygonPoints = (pointObj, cNum) => {
 | 
	
		
			
				|  |  | +  const points = [];
 | 
	
		
			
				|  |  | +  const center = [Number(pointObj.lon), Number(pointObj.lat)];
 | 
	
		
			
				|  |  | +  const radiusList = [
 | 
	
		
			
				|  |  | +    pointObj[cNum].radius1,
 | 
	
		
			
				|  |  | +    pointObj[cNum].radius2,
 | 
	
		
			
				|  |  | +    pointObj[cNum].radius3,
 | 
	
		
			
				|  |  | +    pointObj[cNum].radius4
 | 
	
		
			
				|  |  | +  ];
 | 
	
		
			
				|  |  | +  const startAngleList = [0, 90, 180, 270];
 | 
	
		
			
				|  |  | +  let fx, fy;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  startAngleList.forEach((startAngle, index) => {
 | 
	
		
			
				|  |  | +    const radius = radiusList[index] / 100;
 | 
	
		
			
				|  |  | +    const pointNum = 90;
 | 
	
		
			
				|  |  | +    const endAngle = startAngle + 90;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    for (let i = 0; i <= pointNum; i++) {
 | 
	
		
			
				|  |  | +      const angle = startAngle + ((endAngle - startAngle) * i) / pointNum;
 | 
	
		
			
				|  |  | +      const sin = Math.sin((angle * Math.PI) / 180);
 | 
	
		
			
				|  |  | +      const cos = Math.cos((angle * Math.PI) / 180);
 | 
	
		
			
				|  |  | +      const x = center[0] + radius * sin;
 | 
	
		
			
				|  |  | +      const y = center[1] + radius * cos;
 | 
	
		
			
				|  |  | +      points.push(x, y);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (startAngle === 0 && i === 0) {
 | 
	
		
			
				|  |  | +        fx = x;
 | 
	
		
			
				|  |  | +        fy = y;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  points.push(fx, fy);
 | 
	
		
			
				|  |  | +  return points;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 加载台风数据
 | 
	
		
			
				|  |  | +const loadTyphoonData = () => {
 | 
	
		
			
				|  |  | +  const jsonUrl = new URL(props.dataUrl, import.meta.url).href;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  axios.get(jsonUrl)
 | 
	
		
			
				|  |  | +    .then(response => {
 | 
	
		
			
				|  |  | +      const typhoonData = response.data;
 | 
	
		
			
				|  |  | +      processPoints(typhoonData.points, typhoonData);
 | 
	
		
			
				|  |  | +      updatePointSizes();
 | 
	
		
			
				|  |  | +    })
 | 
	
		
			
				|  |  | +    .catch(error => {
 | 
	
		
			
				|  |  | +      console.error('加载台风JSON数据失败,使用测试数据', error);
 | 
	
		
			
				|  |  | +      const testTyphoonData = {
 | 
	
		
			
				|  |  | +        "name": "竹节草",
 | 
	
		
			
				|  |  | +        "enname": "CO-MAY",
 | 
	
		
			
				|  |  | +        "points": [
 | 
	
		
			
				|  |  | +          {
 | 
	
		
			
				|  |  | +            "time": "2025-07-23 14:00:00",
 | 
	
		
			
				|  |  | +            "lng": "119.20",
 | 
	
		
			
				|  |  | +            "lat": "18.40",
 | 
	
		
			
				|  |  | +            "strong": "热带低压",
 | 
	
		
			
				|  |  | +            "power": "7",
 | 
	
		
			
				|  |  | +            "speed": "15",
 | 
	
		
			
				|  |  | +            "pressure": "998",
 | 
	
		
			
				|  |  | +            "movespeed": "14",
 | 
	
		
			
				|  |  | +            "movedirection": "南西",
 | 
	
		
			
				|  |  | +            "radius7": "180|120|200|150",
 | 
	
		
			
				|  |  | +            "radius10": "",
 | 
	
		
			
				|  |  | +            "radius12": "",
 | 
	
		
			
				|  |  | +            "forecast": [
 | 
	
		
			
				|  |  | +              {
 | 
	
		
			
				|  |  | +                "tm": "中国",
 | 
	
		
			
				|  |  | +                "forecastpoints": [
 | 
	
		
			
				|  |  | +                  { "lng": 121, "lat": 19 },
 | 
	
		
			
				|  |  | +                  { "lng": 122, "lat": 18 }
 | 
	
		
			
				|  |  | +                ]
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +            ]
 | 
	
		
			
				|  |  | +          },
 | 
	
		
			
				|  |  | +          {
 | 
	
		
			
				|  |  | +            "time": "2025-07-23 17:00:00",
 | 
	
		
			
				|  |  | +            "lng": "119.10",
 | 
	
		
			
				|  |  | +            "lat": "18.00",
 | 
	
		
			
				|  |  | +            "strong": "热带低压",
 | 
	
		
			
				|  |  | +            "power": "7",
 | 
	
		
			
				|  |  | +            "speed": "15",
 | 
	
		
			
				|  |  | +            "pressure": "996",
 | 
	
		
			
				|  |  | +            "movespeed": "13",
 | 
	
		
			
				|  |  | +            "movedirection": "南南西",
 | 
	
		
			
				|  |  | +            "radius7": "180|120|200|150",
 | 
	
		
			
				|  |  | +            "radius10": "",
 | 
	
		
			
				|  |  | +            "radius12": "",
 | 
	
		
			
				|  |  | +            "forecast": []
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +      processPoints(testTyphoonData.points, testTyphoonData);
 | 
	
		
			
				|  |  | +      updatePointSizes();
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 初始化台风可视化
 | 
	
		
			
				|  |  | +const initVisualization = () => {
 | 
	
		
			
				|  |  | +  // 初始化实体集合
 | 
	
		
			
				|  |  | +  if (entityCollection.value) {
 | 
	
		
			
				|  |  | +    props.viewer.dataSources.remove(entityCollection.value);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  entityCollection.value = new Cesium.CustomDataSource("typhoonPoints");
 | 
	
		
			
				|  |  | +  props.viewer.dataSources.add(entityCollection.value);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 初始化警戒线
 | 
	
		
			
				|  |  | +  initWarningLines();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 加载台风数据
 | 
	
		
			
				|  |  | +  loadTyphoonData();
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 监听visible属性变化
 | 
	
		
			
				|  |  | +watch(() => props.visible, (newVal, oldVal) => {
 | 
	
		
			
				|  |  | +  if (newVal !== oldVal) {
 | 
	
		
			
				|  |  | +    // 更新所有实体的可见性
 | 
	
		
			
				|  |  | +    if (entityCollection.value) {
 | 
	
		
			
				|  |  | +      entityCollection.value.show = newVal;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    fengquanLayers.value.forEach(entity => {
 | 
	
		
			
				|  |  | +      if (entity) {
 | 
	
		
			
				|  |  | +        entity.show = newVal;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    relatedEntities.value.paths.forEach(entity => {
 | 
	
		
			
				|  |  | +      if (entity) {
 | 
	
		
			
				|  |  | +        entity.show = newVal;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    relatedEntities.value.forecasts.forEach(entity => {
 | 
	
		
			
				|  |  | +      if (entity) {
 | 
	
		
			
				|  |  | +        entity.show = newVal;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    relatedEntities.value.warnings.forEach(entity => {
 | 
	
		
			
				|  |  | +      if (entity) {
 | 
	
		
			
				|  |  | +        entity.show = newVal;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (tbentity.value) {
 | 
	
		
			
				|  |  | +      tbentity.value.show = newVal;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 如果显示,则重新初始化
 | 
	
		
			
				|  |  | +    if (newVal) {
 | 
	
		
			
				|  |  | +      initVisualization();
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      // 如果隐藏,清除定时器和弹窗
 | 
	
		
			
				|  |  | +      hidePopup();
 | 
	
		
			
				|  |  | +      if (typhoonInterval) {
 | 
	
		
			
				|  |  | +        clearInterval(typhoonInterval);
 | 
	
		
			
				|  |  | +        typhoonInterval = null;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      removeTFLayer();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 初始化事件监听
 | 
	
		
			
				|  |  | +const initEventListeners = () => {
 | 
	
		
			
				|  |  | +  // 窗口大小变化事件
 | 
	
		
			
				|  |  | +  window.addEventListener('resize', updatePointSizes);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 点击事件处理
 | 
	
		
			
				|  |  | +  const handler = new Cesium.ScreenSpaceEventHandler(props.viewer.scene.canvas);
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  // 左键点击事件
 | 
	
		
			
				|  |  | +  handler.setInputAction((click) => {
 | 
	
		
			
				|  |  | +    if (!props.visible) return;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    const scaleRatio = getScaleRatio();
 | 
	
		
			
				|  |  | +    const correctedX = click.position.x / scaleRatio;
 | 
	
		
			
				|  |  | +    const correctedY = click.position.y / scaleRatio;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 尝试标准拾取
 | 
	
		
			
				|  |  | +    const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
 | 
	
		
			
				|  |  | +    // 尝试自定义范围拾取台风点
 | 
	
		
			
				|  |  | +    const typhoonPoint = pickedObject ? null : checkPointClick(click.position);
 | 
	
		
			
				|  |  | +    const finalPickedObject = pickedObject || typhoonPoint;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 隐藏弹窗
 | 
	
		
			
				|  |  | +    hidePopup();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (Cesium.defined(finalPickedObject) && Cesium.defined(finalPickedObject.id)) {
 | 
	
		
			
				|  |  | +      const entityId = finalPickedObject.id.id;
 | 
	
		
			
				|  |  | +      const data = pointDataMap.value.get(entityId);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      if (data && entityId.startsWith('typhoon-point-')) {
 | 
	
		
			
				|  |  | +        // 处理台风点点击
 | 
	
		
			
				|  |  | +        currentEntity.value = finalPickedObject.id;
 | 
	
		
			
				|  |  | +        info.value = data;
 | 
	
		
			
				|  |  | +        infoVisible.value = true;
 | 
	
		
			
				|  |  | +        updatePopupPosition(finalPickedObject.id);
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        props.viewer.flyTo(finalPickedObject.id, {
 | 
	
		
			
				|  |  | +          offset: new Cesium.HeadingPitchRange(0, -0.5, 1500)
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +      currentEntity.value = null;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 鼠标移动事件
 | 
	
		
			
				|  |  | +  handler.setInputAction((movement) => {
 | 
	
		
			
				|  |  | +    if (!props.visible) {
 | 
	
		
			
				|  |  | +      hidePopup();
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const scaleRatio = getScaleRatio();
 | 
	
		
			
				|  |  | +    const correctedX = movement.endPosition.x / scaleRatio;
 | 
	
		
			
				|  |  | +    const correctedY = movement.endPosition.y / scaleRatio;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 尝试标准拾取
 | 
	
		
			
				|  |  | +    const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
 | 
	
		
			
				|  |  | +    // 尝试自定义范围拾取台风点
 | 
	
		
			
				|  |  | +    const typhoonPoint = pickedObject ? null : checkPointClick(movement.endPosition);
 | 
	
		
			
				|  |  | +    const finalPickedObject = pickedObject || typhoonPoint;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 检查是否悬停在台风路径点上
 | 
	
		
			
				|  |  | +    if (Cesium.defined(finalPickedObject) && Cesium.defined(finalPickedObject.id)) {
 | 
	
		
			
				|  |  | +      const entityId = finalPickedObject.id.id;
 | 
	
		
			
				|  |  | +      if (entityId && entityId.startsWith('typhoon-point-')) {
 | 
	
		
			
				|  |  | +        const typhoonData = pointDataMap.value.get(entityId);
 | 
	
		
			
				|  |  | +        if (typhoonData) {
 | 
	
		
			
				|  |  | +          currentEntity.value = finalPickedObject.id;
 | 
	
		
			
				|  |  | +          info.value = typhoonData;
 | 
	
		
			
				|  |  | +          infoVisible.value = true;
 | 
	
		
			
				|  |  | +          updatePopupPosition(finalPickedObject.id);
 | 
	
		
			
				|  |  | +          return;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 不是台风路径点,隐藏弹窗
 | 
	
		
			
				|  |  | +    hidePopup();
 | 
	
		
			
				|  |  | +  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return handler;
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 组件挂载时初始化
 | 
	
		
			
				|  |  | +onMounted(() => {
 | 
	
		
			
				|  |  | +  if (props.viewer) {
 | 
	
		
			
				|  |  | +    const handler = initEventListeners();
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // 初始加载台风数据
 | 
	
		
			
				|  |  | +    if (props.visible) {
 | 
	
		
			
				|  |  | +      initVisualization();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 在组件卸载时清理事件处理器
 | 
	
		
			
				|  |  | +    onUnmounted(() => {
 | 
	
		
			
				|  |  | +      if (handler) {
 | 
	
		
			
				|  |  | +        handler.destroy();
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 组件卸载时清理
 | 
	
		
			
				|  |  | +onUnmounted(() => {
 | 
	
		
			
				|  |  | +  // 清除定时器
 | 
	
		
			
				|  |  | +  if (typhoonInterval) {
 | 
	
		
			
				|  |  | +    clearInterval(typhoonInterval);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 移除弹窗更新回调
 | 
	
		
			
				|  |  | +  if (popupUpdateCallback.value) {
 | 
	
		
			
				|  |  | +    props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 移除事件监听
 | 
	
		
			
				|  |  | +  window.removeEventListener('resize', updatePointSizes);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  // 清理所有实体
 | 
	
		
			
				|  |  | +  if (entityCollection.value) {
 | 
	
		
			
				|  |  | +    props.viewer.dataSources.remove(entityCollection.value);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  relatedEntities.value.paths.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  relatedEntities.value.forecasts.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  relatedEntities.value.warnings.forEach(entity => {
 | 
	
		
			
				|  |  | +    props.viewer.entities.remove(entity);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  removes();
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// 暴露切换台风显示状态的方法
 | 
	
		
			
				|  |  | +const toggleTyphoon = () => {
 | 
	
		
			
				|  |  | +  emits('onToggle', !props.visible);
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +defineExpose({
 | 
	
		
			
				|  |  | +  toggleTyphoon
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +</script>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<style scoped>
 | 
	
		
			
				|  |  | +/* 台风图例样式 */
 | 
	
		
			
				|  |  | +.legend {
 | 
	
		
			
				|  |  | +  position: absolute;
 | 
	
		
			
				|  |  | +  z-index: 100;
 | 
	
		
			
				|  |  | +  bottom: 30px;
 | 
	
		
			
				|  |  | +  right: 10px;
 | 
	
		
			
				|  |  | +  color: #fff;
 | 
	
		
			
				|  |  | +  background: rgba(0, 0, 0, 0.8);
 | 
	
		
			
				|  |  | +  list-style: none;
 | 
	
		
			
				|  |  | +  padding: 10px 15px;
 | 
	
		
			
				|  |  | +  border-radius: 4px;
 | 
	
		
			
				|  |  | +  margin: 0;
 | 
	
		
			
				|  |  | +  transition: opacity 0.3s ease, transform 0.3s ease;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.legend li {
 | 
	
		
			
				|  |  | +  display: flex;
 | 
	
		
			
				|  |  | +  align-items: center;
 | 
	
		
			
				|  |  | +  margin: 5px 0;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.dot {
 | 
	
		
			
				|  |  | +  border-radius: 50%;
 | 
	
		
			
				|  |  | +  width: 10px;
 | 
	
		
			
				|  |  | +  height: 10px;
 | 
	
		
			
				|  |  | +  display: inline-block;
 | 
	
		
			
				|  |  | +  margin-right: 8px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.green {
 | 
	
		
			
				|  |  | +  background: green;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.blue {
 | 
	
		
			
				|  |  | +  background: blue;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.yellow {
 | 
	
		
			
				|  |  | +  background: yellow;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.orange {
 | 
	
		
			
				|  |  | +  background: #FBC712;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.red {
 | 
	
		
			
				|  |  | +  background: red;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 台风弹窗样式 */
 | 
	
		
			
				|  |  | +.typhoon-popup {
 | 
	
		
			
				|  |  | +  position: absolute;
 | 
	
		
			
				|  |  | +  z-index: 1000;
 | 
	
		
			
				|  |  | +  pointer-events: none;
 | 
	
		
			
				|  |  | +  top: 0;
 | 
	
		
			
				|  |  | +  left: 0;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.popup-content {
 | 
	
		
			
				|  |  | +  background-color: white;
 | 
	
		
			
				|  |  | +  border-radius: 5px;
 | 
	
		
			
				|  |  | +  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
 | 
	
		
			
				|  |  | +  padding: 10px 15px;
 | 
	
		
			
				|  |  | +  width: 240px;
 | 
	
		
			
				|  |  | +  pointer-events: all;
 | 
	
		
			
				|  |  | +  min-width: 200px;
 | 
	
		
			
				|  |  | +  box-sizing: border-box;
 | 
	
		
			
				|  |  | +  position: relative;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.popup-arrow {
 | 
	
		
			
				|  |  | +  position: absolute;
 | 
	
		
			
				|  |  | +  width: 0;
 | 
	
		
			
				|  |  | +  height: 0;
 | 
	
		
			
				|  |  | +  border-left: 10px solid transparent;
 | 
	
		
			
				|  |  | +  border-right: 10px solid transparent;
 | 
	
		
			
				|  |  | +  left: 50%;
 | 
	
		
			
				|  |  | +  transform: translateX(-50%);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 上方弹窗的箭头(指向下方节点) */
 | 
	
		
			
				|  |  | +.popup-content:not(.bottom) .popup-arrow {
 | 
	
		
			
				|  |  | +  bottom: -10px;
 | 
	
		
			
				|  |  | +  border-top: 10px solid white;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 下方弹窗的箭头(指向上方节点) */
 | 
	
		
			
				|  |  | +.popup-content.bottom .popup-arrow {
 | 
	
		
			
				|  |  | +  top: -10px;
 | 
	
		
			
				|  |  | +  border-bottom: 10px solid white;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.popup-content h3 {
 | 
	
		
			
				|  |  | +  margin-top: 0;
 | 
	
		
			
				|  |  | +  margin-bottom: 8px;
 | 
	
		
			
				|  |  | +  color: #333;
 | 
	
		
			
				|  |  | +  font-size: 16px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.popup-content p {
 | 
	
		
			
				|  |  | +  margin: 5px 0;
 | 
	
		
			
				|  |  | +  color: #666;
 | 
	
		
			
				|  |  | +  font-size: 14px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 响应式调整 */
 | 
	
		
			
				|  |  | +@media (max-width: 1200px) {
 | 
	
		
			
				|  |  | +  .legend {
 | 
	
		
			
				|  |  | +    padding: 5px 10px;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  .legend li {
 | 
	
		
			
				|  |  | +    font-size: 12px;
 | 
	
		
			
				|  |  | +    margin: 3px 0;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +@media (max-width: 768px) {
 | 
	
		
			
				|  |  | +  .popup-content {
 | 
	
		
			
				|  |  | +    width: 200px;
 | 
	
		
			
				|  |  | +    padding: 8px 12px;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  .popup-content h3 {
 | 
	
		
			
				|  |  | +    font-size: 14px;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  .popup-content p {
 | 
	
		
			
				|  |  | +    font-size: 12px;
 | 
	
		
			
				|  |  | +    margin: 3px 0;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* 高DPI屏幕适配 */
 | 
	
		
			
				|  |  | +@media (min-resolution: 192dpi) {
 | 
	
		
			
				|  |  | +  .popup-content {
 | 
	
		
			
				|  |  | +    transform: scale(0.95);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  .popup-content.bottom {
 | 
	
		
			
				|  |  | +    transform-origin: center bottom;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +</style>
 |