소스 검색

功能模块分开

WQQ 1 개월 전
부모
커밋
8a3050a01e

+ 5 - 3
WebVue/TaiHufenglang/src/App.vue

@@ -1,13 +1,14 @@
 <template>
   <MapContainer />
-  <Cesium />  
+  <Cesium />
 </template>
 
 <script setup>
 import { onMounted, onBeforeUnmount } from 'vue';
-import MapContainer from './components/MapContainer.vue';
-import Cesium from './components/TyphoonViewer.vue'
+import MapContainer from './components/MapContainer.vue'; 
+import Cesium from './components/Cesium/CesiumViewer.vue'
 import autofit from 'autofit.js';
+import { elRectification } from 'autofit.js'
 
 // 初始化自动适配(Vue3组合式API写法)
 const initAutoFit = () => {
@@ -29,6 +30,7 @@ const initAutoFit = () => {
 // 生命周期钩子
 onMounted(() => {
   initAutoFit();
+  // elRectification("#cesiumContainer");
   window.addEventListener('resize', handleResize);
 });
 

+ 39 - 38
WebVue/TaiHufenglang/src/components/Cesium.vue

@@ -48,64 +48,65 @@ onMounted(async () => {
     infoBox: false,
     //加载地形效果
     terrainProvider: await Cesium.createWorldTerrainAsync({
-      url: 'http://10.8.11.98:9003/terrain/GlobeDEM/layer.json', // 注意修正URL中的locallhost为localhost
+      // url: 'http://10.8.11.98:9003/terrain/GlobeDEM/layer.json', // 地形
       requestVertexNormals: true,
       // requestWaterMask: true,     
     })
   });
 
- try {
-    // 1. 请求并解析TMS元数据XML
-    const xmlUrl = 'http://10.8.11.98:9003/image/tms/HTHDOM/tilemapresource.xml';
-    const response = await fetch(xmlUrl);
-    const xmlText = await response.text();
-    const parser = new DOMParser();
-    const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
+//加载DOM
+//  try {
+//     // 1. 请求并解析TMS元数据XML
+//     const xmlUrl = 'http://10.8.11.98:9003/image/tms/HTHDOM/tilemapresource.xml';
+//     const response = await fetch(xmlUrl);
+//     const xmlText = await response.text();
+//     const parser = new DOMParser();
+//     const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
 
-    // 2. 从XML中提取关键参数
-    const tileFormat = xmlDoc.querySelector('TileFormat').getAttribute('extension'); // 如"png"
-    const maxLevel = xmlDoc.querySelectorAll('TileSet').length - 1; // 最大层级(假设层级从0开始)
-    const srs = xmlDoc.querySelector('SRS').textContent; // 如"EPSG:3857"
+//     // 2. 从XML中提取关键参数
+//     const tileFormat = xmlDoc.querySelector('TileFormat').getAttribute('extension'); // 如"png"
+//     const maxLevel = xmlDoc.querySelectorAll('TileSet').length - 1; // 最大层级(假设层级从0开始)
+//     const srs = xmlDoc.querySelector('SRS').textContent; // 如"EPSG:3857"
 
-    // 3. 加载TMS影像
-    const tmsImagery = new Cesium.UrlTemplateImageryProvider({
-      url: `http://10.8.11.98:9003/image/tms/HTHDOM/{z}/{x}/{y}.${tileFormat}`,
-      tileWidth: 256, // 从XML的TileFormat中获取width
-      tileHeight: 256, // 从XML的TileFormat中获取height
-      maximumLevel: maxLevel,
-      projection: srs === 'EPSG:3857' ? Cesium.WebMercatorProjection : Cesium.GeographicProjection,
-      // 核心:TMS行号反转(解决上下颠倒)
-      urlTemplateFunction: (x, y, level) => {
-        const tmsY = Math.pow(2, level) - 1 - y; // 反转行号
-        return `http://10.8.11.98:9003/image/tms/HTHDOM//${level}/${x}/${tmsY}.${tileFormat}`;
-      }
-    });
+//     // 3. 加载TMS影像
+//     const tmsImagery = new Cesium.UrlTemplateImageryProvider({
+//       // url: `http://10.8.11.98:9003/image/tms/HTHDOM/{z}/{x}/{y}.${tileFormat}`,
+//       tileWidth: 256, // 从XML的TileFormat中获取width
+//       tileHeight: 256, // 从XML的TileFormat中获取height
+//       maximumLevel: maxLevel,
+//       projection: srs === 'EPSG:3857' ? Cesium.WebMercatorProjection : Cesium.GeographicProjection,
+//       // 核心:TMS行号反转(解决上下颠倒)
+//       urlTemplateFunction: (x, y, level) => {
+//         const tmsY = Math.pow(2, level) - 1 - y; // 反转行号
+//         // return `http://10.8.11.98:9003/image/tms/HTHDOM//${level}/${x}/${tmsY}.${tileFormat}`;
+//       }
+//     });
 
-    viewer.imageryLayers.addImageryProvider(tmsImagery);
-    console.log('TMS影像加载成功');
+//     viewer.imageryLayers.addImageryProvider(tmsImagery);
+//     console.log('TMS影像加载成功');
+
+//   } 
+//   catch (error) {
+//     console.error('加载TMS影像失败:', error);
+//     // 检查XML URL是否正确(可能拼写错误,如localhost是否少写字母)
+//     if (error.message.includes('404')) {
+//       console.warn('请确认XML路径正确:', xmlUrl);
+//     }
+//   }
 
-  } 
-  catch (error) {
-    console.error('加载TMS影像失败:', error);
-    // 检查XML URL是否正确(可能拼写错误,如localhost是否少写字母)
-    if (error.message.includes('404')) {
-      console.warn('请确认XML路径正确:', xmlUrl);
-    }
-  }
 
 // 多块倾斜摄影瓦片集的URL列表(根据实际路径修改)
 const tilesetUrls = [
   "http://localhost:9003/model/TSQ1234/tileset.json",
   "http://localhost:9003/model/SY123/tileset.json",
-  "http://10.8.11.98:9003/model/tvhy8PnM8/tileset.json",
-  "http://10.8.11.98:9003/model/t9or0ZtaT/tileset.json"
+  // "http://10.8.11.98:9003/model/tvhy8PnM8/tileset.json",
+  // "http://10.8.11.98:9003/model/t9or0ZtaT/tileset.json"    //夹浦倾斜
 
 ];
 
 // 存储加载成功的瓦片集(可选,用于后续管理)
 const loadedTilesets = [];
 
-
 // 批量加载瓦片集(不调整视角)
 async function loadMultipleTilesets() {
   try {

+ 243 - 0
WebVue/TaiHufenglang/src/components/Cesium/CesiumViewer.vue

@@ -0,0 +1,243 @@
+<template>
+  <div id="cesiumContainer" style="height: 100%;width: 100%;"></div>
+
+  <!-- 控制按钮 -->
+  <div class="control-buttons">
+    <button class="control-btn" @click="goToHomeView" title="返回首页视角">
+      <i class="fas fa-home"></i>
+    </button>
+    <button class="control-btn" @click="togglePOIDisplay" title="显示/隐藏POI点">
+      <i class="fas fa-map-marker-alt" :class="{ 'active': poiVisible }"></i>
+    </button>
+    <button class="control-btn" @click="handleToggleTyphoon" title="显示/隐藏台风并切换视角">
+      <i class="fas fa-wind" :class="{ 'active': typhoonVisible }"></i>
+    </button>
+  </div>
+
+  <!-- 只有当viewer初始化完成后才渲染子组件 -->
+  <template v-if="viewer">
+    <!-- 引入POI可视化组件 -->
+    <POIVisualization 
+      :viewer="viewer" 
+      :visible="poiVisible"
+      :data="poiData"
+      @onPointSelected="handlePointSelected"
+    />
+
+    <!-- 引入台风可视化组件 -->
+    <TyphoonVisualization 
+      :viewer="viewer" 
+      :visible="typhoonVisible"
+      @onToggle="handleTyphoonToggle"
+    />
+  </template>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import * as Cesium from 'cesium';
+import "cesium/Build/CesiumUnminified/Widgets/widgets.css";
+import JYLData from '@/assets/Data/THJYL.json'
+import POIVisualization from './POIVisualization.vue';
+import TyphoonVisualization from './TyphoonVisualization.vue';
+
+// 变量定义
+const TDTTK = "d9e7aa2ad204aba6aeedea6f5ab48ed9";
+let viewer = ref(null);
+
+// 状态管理
+const poiVisible = ref(true);
+const typhoonVisible = ref(true);
+const poiData = ref(JYLData); // POI点数据
+
+// 处理POI点选中事件
+const handlePointSelected = (pointData) => {
+  // 可以在这里处理POI点选中后的逻辑
+  console.log('选中的POI点:', pointData);
+};
+
+// 返回首页视角
+const goToHomeView = () => {
+  if (viewer.value) {
+    viewer.value.camera.flyTo({
+      destination: Cesium.Cartesian3.fromDegrees(120.169103, 31.226174, 500000),
+      orientation: {
+        heading: Cesium.Math.toRadians(0),
+        pitch: Cesium.Math.toRadians(-90),
+      },
+      duration: 1
+    });
+  }
+};
+
+// 切换POI点显示状态
+const togglePOIDisplay = () => {
+  poiVisible.value = !poiVisible.value;
+};
+
+// 处理台风切换事件
+const handleToggleTyphoon = () => {
+  const newState = !typhoonVisible.value;
+  typhoonVisible.value = newState;
+  
+  if (newState && viewer.value) {
+    viewer.value.camera.flyTo({
+      destination: Cesium.Cartesian3.fromDegrees(120, 20, 4025692.0),
+    });
+  }
+};
+
+// 处理台风组件状态变化
+const handleTyphoonToggle = (newState) => {
+  typhoonVisible.value = newState;
+};
+
+onMounted(async () => {
+  // 初始化Cesium viewer
+  viewer.value = new Cesium.Viewer('cesiumContainer', {
+    timeline: false,
+    baseLayer: false,
+    geocoder: false,
+    homeButton: false,
+    sceneModePicker: false,
+    navigationHelpButton: false,
+    animation: false,
+    fullscreenButton: false,
+    vrButton: false,
+    selectionIndicator: false,
+    infoBox: false,
+    // 加载地形效果
+    terrainProvider: await Cesium.createWorldTerrainAsync({
+      requestVertexNormals: true,
+    })
+  });
+
+  // 多块倾斜摄影瓦片集的URL列表
+  const tilesetUrls = [
+    "http://localhost:9003/model/TSQ1234/tileset.json",
+    "http://localhost:9003/model/SY123/tileset.json",
+  ];
+
+  // 存储加载成功的瓦片集
+  const loadedTilesets = [];
+
+  // 批量加载瓦片集
+  async function loadMultipleTilesets() {
+    try {
+      const promises = tilesetUrls.map(async (url, index) => {
+        try {
+          const tileset = await Cesium.Cesium3DTileset.fromUrl(url, {
+            maximumScreenSpaceError: 32,
+            dynamicScreenSpaceError: true,
+            skipLevelOfDetail: true,
+            maximumConcurrentRequests: 5,
+            tileCacheSize: 100
+          });
+          viewer.value.scene.primitives.add(tileset);
+          loadedTilesets.push(tileset);
+          console.log(`第${index + 1}块瓦片集加载完成`);
+          return tileset;
+        } catch (error) {
+          console.error(`第${index + 1}块瓦片集加载失败:`, error);
+          return null;
+        }
+      });
+
+      await Promise.all(promises);
+      console.log("所有瓦片集加载操作已完成");
+    } catch (error) {
+      console.error("批量加载逻辑出错:", error);
+    }
+  }
+
+  // 执行加载
+  loadMultipleTilesets();
+
+  // 添加天地图图层
+//   const tdtLayer = new Cesium.WebMapTileServiceImageryProvider({
+//     url: `https://t0.tianditu.com/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&tk=${TDTTK}`,
+//     layer: "tdt",
+//     style: "default",
+//     format: "image/jpeg",
+//     tileMatrixSetID: "w",
+//     maximumLevel: 16,
+//     show: true,
+//   });
+//   viewer.value.imageryLayers.addImageryProvider(tdtLayer);
+
+//   const tdtAnnotionLayer = new Cesium.WebMapTileServiceImageryProvider({
+//     url: `http://t0.tianditu.com/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&tk=${TDTTK}`,
+//     layer: "tdtAnno",
+//     style: "default",
+//     format: "image/jpeg",
+//     tileMatrixSetID: "w",
+//     maximumLevel: 18,
+//     show: false,
+//   });
+//   viewer.value.imageryLayers.addImageryProvider(tdtAnnotionLayer);
+
+  // 初始化视图
+  viewer.value.cesiumWidget.creditContainer.style.display = "none";
+  viewer.value.camera.setView({
+    destination: Cesium.Cartesian3.fromDegrees(120.169103, 31.226174, 500000),
+    orientation: {
+      heading: Cesium.Math.toRadians(0),
+      pitch: Cesium.Math.toRadians(-90),
+    },
+  });
+
+  // 初始触发一次 resize 确保地图正确显示
+  viewer.value.resize();
+});
+
+onUnmounted(() => {
+  if (viewer.value && !viewer.value.isDestroyed()) {
+    viewer.value.destroy();
+  }
+});
+</script>
+
+<style scoped>
+/* 控制按钮样式 */
+.control-buttons {
+  position: absolute;
+  top: 200px;
+  left: 150px;
+  z-index: 100;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.control-btn {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background-color: rgba(255, 255, 255, 0.9);
+  border: none;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  color: #333;
+}
+
+.control-btn:hover {
+  background-color: white;
+  transform: scale(1.1);
+}
+
+.control-btn .active {
+  color: #1E88E5;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+  .control-buttons {
+    top: 150px;
+    left: 20px;
+  }
+}
+</style>

+ 500 - 0
WebVue/TaiHufenglang/src/components/Cesium/POIVisualization.vue

@@ -0,0 +1,500 @@
+<template>
+    <!-- POI点信息弹窗 -->
+    <div v-if="selectedPoint" class="custom-popup" :style="{
+        left: `${popupPosition.x}px`,
+        top: `${popupPosition.y}px`
+    }">
+        <div class="popup-content" :class="{ 'bottom': popupPosition.bottom }">
+            <h3>{{ selectedPoint.STNM || '未知点' }}</h3>
+            <p><strong>经度:</strong> {{ selectedPoint.LGTD }}</p>
+            <p><strong>纬度:</strong> {{ selectedPoint.LTTD }}</p>
+            <div class="popup-arrow"></div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
+import * as Cesium from 'cesium';
+
+// 接收父组件传递的参数
+const props = defineProps({
+    viewer: {
+        type: Object,
+        required: true
+    },
+    visible: {
+        type: Boolean,
+        default: true
+    },
+    data: {
+        type: Array,
+        default: () => []
+    },
+    pointImage: {
+        type: String,
+        default: '/src/assets/icon/blue.png'
+    },
+    flyToOptions: {
+        type: Object,
+        default: () => ({
+            duration: 1.5,
+            offset: new Cesium.HeadingPitchRange(0, -0.5, 1500),
+            maximumHeight: 5000000
+        })
+    }
+});
+
+// 暴露事件
+const emits = defineEmits(['onPointSelected', 'onPointClicked', 'onFlyFailed']);
+
+// POI相关状态
+const selectedPoint = ref(null);
+const popupPosition = ref({ x: 0, y: 0, bottom: false });
+const poiEntities = ref([]);
+const entityDataMap = ref(new Map());
+const popupUpdateCallback = ref(null);
+let handler = null;
+const flying = ref(false); // 飞行状态标记
+
+// 获取Cesium容器元素
+const getCesiumContainer = () => {
+    return document.getElementById('cesiumContainer');
+};
+
+// 计算页面缩放比例(与autofit配置一致)
+const getScaleRatio = () => {
+    const designWidth = 1920;
+    const designHeight = 1080;
+    return Math.min(window.innerWidth / designWidth, window.innerHeight / designHeight);
+};
+
+// 更新POI弹窗位置
+const updatePOIPopupPosition = (entity) => {
+    if (popupUpdateCallback.value) {
+        props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
+        popupUpdateCallback.value = null;
+    }
+
+    if (!entity || !entity.position || !selectedPoint.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('.custom-popup .popup-content');
+                if (!popupEl) return;
+
+                const popupRect = popupEl.getBoundingClientRect();
+                const popupWidth = popupRect.width;
+                const popupHeight = popupRect.height;
+                const arrowHeight = 10;
+
+                // 计算POI点大小
+                const billboardScale = entity.billboard?.scale || 0.4;
+                const pointRadius = (20 * billboardScale) / 2;
+
+                // 计算弹窗位置
+                let x = (entityPosition.x - containerRect.left) - (popupWidth / 2);
+                let y, bottom = false;
+
+                // 默认显示在点的上方
+                y = (entityPosition.y - containerRect.top) - popupHeight - arrowHeight - pointRadius;
+
+                // 如果上方空间不足,显示在点的下方
+                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();
+};
+
+// 隐藏POI弹窗
+const hidePOIPopup = () => {
+    selectedPoint.value = null;
+    if (popupUpdateCallback.value) {
+        props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
+        popupUpdateCallback.value = null;
+    }
+};
+
+// 创建POI点实体
+const createPOIEntities = () => {
+    // 清除现有实体
+    clearPOIEntities();
+
+    // 定义距离显示条件和缩放属性
+    const distanceDisplayCondition = new Cesium.DistanceDisplayCondition(0, 10000000);
+    const pointNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 1000000, 0.3);
+    const labelNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 400000, 0);
+
+    // 创建新实体
+    props.data.forEach((item) => {
+        // 验证经纬度数据是否有效
+        if (!item.LGTD || !item.LTTD) {
+            console.warn('POI点数据缺少经纬度信息:', item);
+            return;
+        }
+
+        try {
+            const longitude = parseFloat(item.LGTD);
+            const latitude = parseFloat(item.LTTD);
+
+            // 验证经纬度是否在有效范围内
+            if (isNaN(longitude) || isNaN(latitude) || longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90) {
+                console.warn('无效的经纬度数据:', item);
+                return;
+            }
+
+            const position = Cesium.Cartesian3.fromDegrees(longitude, latitude);
+            const entity = props.viewer.entities.add({
+                position: position,
+                billboard: {
+                    image: props.pointImage,
+                    scale: 0.4,
+                    color: Cesium.Color.YELLOW,
+                    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+                    distanceDisplayCondition: distanceDisplayCondition,
+                    scaleByDistance: pointNearFarScalar,
+                    show: props.visible
+                },
+                label: {
+                    text: item.STNM || '未知点',
+                    font: '25px 微软雅黑',
+                    fillColor: Cesium.Color.WHITE,
+                    backgroundColor: new Cesium.Color(0.1, 0.1, 0.1, 0.7),
+                    backgroundPadding: new Cesium.Cartesian2(8, 4),
+                    showBackground: true,
+                    cornerRadius: 4,
+                    outlineColor: Cesium.Color.BLACK,
+                    outlineWidth: 1,
+                    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+                    pixelOffset: new Cesium.Cartesian2(0, -32),
+                    scale: 1.0,
+                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
+                    distanceDisplayCondition: distanceDisplayCondition,
+                    scaleByDistance: labelNearFarScalar,
+                    show: props.visible
+                },
+                id: `point-${item.STCD || item.LGTD + '-' + item.LTTD}`,
+                properties: {
+                    data: item
+                }
+            });
+            entityDataMap.value.set(entity.id, item);
+            poiEntities.value.push(entity);
+        } catch (error) {
+            console.error('创建POI实体失败:', error, '数据:', item);
+        }
+    });
+};
+
+// 清除POI实体
+const clearPOIEntities = () => {
+    poiEntities.value.forEach(entity => {
+        props.viewer.entities.remove(entity);
+    });
+    poiEntities.value = [];
+    entityDataMap.value.clear();
+};
+
+// 优化:直接使用坐标飞行到POI点(移除实体飞行方式)
+const flyToPOIPoint = async (entity, data) => {
+    // 防止重复触发飞行
+    if (flying.value) return false;
+
+    if (!props.viewer || !data || !data.LGTD || !data.LTTD) {
+        console.error('飞行参数不完整');
+        emits('onFlyFailed', data, '参数不完整');
+        return false;
+    }
+
+    flying.value = true;
+    let success = false;
+
+    try {
+        // 直接使用经纬度坐标飞行(移除实体飞行方式)
+        const longitude = parseFloat(data.LGTD);
+        const latitude = parseFloat(data.LTTD);
+
+        if (isNaN(longitude) || isNaN(latitude)) {
+            throw new Error('经纬度解析失败');
+        }
+
+        // 计算目标高度 - 确保合理
+        const height = Math.min(
+            props.flyToOptions.offset.range,
+            props.flyToOptions.maximumHeight
+        );
+
+        // 坐标微调参数(根据实际偏差调整)
+        const lonOffset = -0.001;  // 经度偏移量
+        const latOffset = -0.025;  // 纬度偏移量
+        
+        // 计算最终目标位置(应用微调)
+        const destination = Cesium.Cartesian3.fromDegrees(
+            longitude + lonOffset,
+            latitude + latOffset,
+            height
+        );
+
+        // 验证目标位置是否有效
+        if (Cesium.Cartesian3.equals(destination, Cesium.Cartesian3.ZERO)) {
+            throw new Error('计算的目标位置无效');
+        }
+
+        // 使用相机直接飞行
+        await new Promise((resolve, reject) => {
+            props.viewer.camera.flyTo({
+                destination: destination,
+                orientation: {
+                    heading: props.flyToOptions.offset.heading || Cesium.Math.toRadians(0),
+                    pitch: props.flyToOptions.offset.pitch || Cesium.Math.toRadians(-60),  // 更平缓的角度
+                    roll: 0
+                },
+                duration: props.flyToOptions.duration || 1.5,
+                complete: resolve,
+                cancel: () => reject(new Error('用户取消飞行'))
+            });
+        });
+
+        success = true;
+        console.log('使用坐标飞行成功:', data.STNM);
+    } catch (coordError) {
+        console.error('坐标飞行失败:', coordError.message);
+        emits('onFlyFailed', data, coordError.message);
+
+        // 最终备选方案:直接设置相机位置
+        try {
+            const longitude = parseFloat(data.LGTD);
+            const latitude = parseFloat(data.LTTD);
+
+            if (!isNaN(longitude) && !isNaN(latitude)) {
+                props.viewer.camera.setView({
+                    destination: Cesium.Cartesian3.fromDegrees(
+                        longitude,
+                        latitude,
+                        props.flyToOptions.offset.range
+                    ),
+                    orientation: {
+                        heading: props.flyToOptions.offset.heading || 0,
+                        pitch: props.flyToOptions.offset.pitch || -Cesium.Math.PI_OVER_TWO,
+                        roll: 0
+                    }
+                });
+                success = true;
+                console.log('使用setView直接定位成功');
+            }
+        } catch (finalError) {
+            console.error('所有飞行方法均失败:', finalError.message);
+        }
+    } finally {
+        flying.value = false;
+    }
+
+    return success;
+};
+
+// 初始化事件监听
+const initEventListeners = () => {
+    // 清除现有处理器
+    if (handler) {
+        handler.destroy();
+    }
+
+    handler = new Cesium.ScreenSpaceEventHandler(props.viewer.scene.canvas);
+
+    // 点击事件处理
+    handler.setInputAction(async (click) => {
+        const scaleRatio = getScaleRatio();
+        const correctedX = click.position.x / scaleRatio;
+        const correctedY = click.position.y / scaleRatio;
+
+        if (!props.visible || flying.value) return; // 飞行中不响应新点击
+
+        // 标准拾取
+        const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
+
+        // 隐藏弹窗
+        hidePOIPopup();
+
+        if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
+            const entityId = pickedObject.id.id;
+            const data = entityDataMap.value.get(entityId) ||
+                pickedObject.id.properties?.data?.getValue();
+
+            if (data && !entityId.startsWith('typhoon-point-')) {
+                selectedPoint.value = data;
+
+                // 计算图标屏幕坐标,固定显示在图标上方
+                const entityPosition = props.viewer.scene.cartesianToCanvasCoordinates(pickedObject.id.position._value);
+                if (entityPosition) {
+                    popupPosition.value = {
+                        x: (entityPosition.x / scaleRatio) - 100,
+                        y: (entityPosition.y / scaleRatio) - 130,  // 130 = 弹框高度 + 间距
+                        bottom: false
+                    };
+                }
+
+                // 通知父组件
+                emits('onPointSelected', data);
+                emits('onPointClicked', data);
+
+                // 跳转到POI点
+                const flySuccess = await flyToPOIPoint(pickedObject.id, data);
+                if (!flySuccess) {
+                    console.warn('飞行到POI点最终失败');
+                }
+            }
+        } else {
+            selectedPoint.value = null;
+            emits('onPointSelected', null);
+        }
+    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
+};
+
+// 监听visible属性变化
+watch(() => props.visible, (newVal) => {
+    poiEntities.value.forEach(entity => {
+        if (entity && entity.billboard) {
+            entity.billboard.show = newVal;
+            entity.label.show = newVal;
+        }
+    });
+});
+
+// 监听数据变化
+watch(() => props.data, (newVal) => {
+    if (newVal && newVal.length > 0) {
+        createPOIEntities();
+    }
+}, { deep: true });
+
+// 监听viewer变化
+watch(() => props.viewer, (newVal) => {
+    if (newVal) {
+        initEventListeners();
+        if (props.data && props.data.length > 0) {
+            createPOIEntities();
+        }
+    }
+});
+
+// 组件挂载时初始化
+onMounted(() => {
+    if (props.viewer) {
+        initEventListeners();
+
+        // 初始创建POI点
+        if (props.data && props.data.length > 0) {
+            createPOIEntities();
+        }
+    } else {
+        console.warn('POI组件挂载时viewer尚未初始化');
+    }
+});
+
+// 组件卸载时清理
+onUnmounted(() => {
+    // 移除弹窗更新回调
+    if (popupUpdateCallback.value) {
+        props.viewer?.scene.postRender.removeEventListener(popupUpdateCallback.value);
+    }
+
+    // 清除事件处理器
+    if (handler) {
+        handler.destroy();
+    }
+
+    // 清除所有POI实体
+    clearPOIEntities();
+});
+
+// 暴露方法
+defineExpose({
+    hidePOIPopup,
+    flyToPOIPoint
+});
+</script>
+
+<style scoped>
+.custom-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: 768px) {
+    .popup-content {
+        width: 200px;
+        padding: 8px 12px;
+    }
+}
+</style>

+ 1102 - 0
WebVue/TaiHufenglang/src/components/Cesium/TyphoonVisualization.vue

@@ -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>

+ 9 - 7
WebVue/TaiHufenglang/src/components/TyphoonViewer.vue

@@ -247,7 +247,6 @@ const updatePOIPopupPosition = (entity) => {
   popupUpdateCallbacks.value.poi();
 };
 
-// 其他函数保持不变
 const getScaleRatio = () => {
   const designWidth = 1920;
   const designHeight = 1080;
@@ -408,11 +407,7 @@ const hidePOIPopup = () => {
 };
 
 onMounted(async () => {
-  // 设置HTML和body样式确保没有边距
-  document.documentElement.style.height = '100%';
-  document.body.style.height = '100%';
-  document.body.style.margin = '0';
-  document.body.style.padding = '0';
+
 
   // 初始化Cesium viewer
   viewer = new Cesium.Viewer('cesiumContainer', {
@@ -853,12 +848,19 @@ const processPoints = (points, typhoonData) => {
   }
 };
 
-// 添加一个专门清除风圈的函数
+// 修改removes函数,在移除风圈时同时移除风眼图标
 const removes = () => {
+  // 移除风圈
   fengquanLayers.value.forEach(entity => {
     viewer.entities.remove(entity);
   });
   fengquanLayers.value = [];
+
+  // 移除风眼图标
+  if (tbentity.value) {
+    viewer.entities.remove(tbentity.value);
+    tbentity.value = null;
+  }
 };