BAI преди 2 седмици
родител
ревизия
3654fcb2e1

+ 43 - 86
WebVue/TaiHufenglang/src/components/Cesium.vue

@@ -18,7 +18,7 @@
 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'
+
 
 const TDTTK = "d9e7aa2ad204aba6aeedea6f5ab48ed9";
 const selectedPoint = ref(null);
@@ -48,65 +48,64 @@ onMounted(async () => {
     infoBox: false,
     //加载地形效果
     terrainProvider: await Cesium.createWorldTerrainAsync({
-      // url: 'http://10.8.11.98:9003/terrain/GlobeDEM/layer.json', // 地形
+      url: 'http://10.8.11.98:9003/terrain/GlobeDEM/layer.json', // 注意修正URL中的locallhost为localhost
       requestVertexNormals: true,
       // requestWaterMask: true,     
     })
   });
 
-//加载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"
-
-//     // 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影像加载成功');
-
-//   } 
-//   catch (error) {
-//     console.error('加载TMS影像失败:', error);
-//     // 检查XML URL是否正确(可能拼写错误,如localhost是否少写字母)
-//     if (error.message.includes('404')) {
-//       console.warn('请确认XML路径正确:', xmlUrl);
-//     }
-//   }
+ 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"
 
+    // 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影像加载成功');
+
+  } 
+  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 {
@@ -147,49 +146,7 @@ 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);
 
-// 存储实体与数据的映射(保持不变)
-const entityDataMap = new Map();
-JYLData.forEach((item) => {
-  const position = Cesium.Cartesian3.fromDegrees(
-    parseFloat(item.LGTD),
-    parseFloat(item.LTTD)
-  );
-  const entity = viewer.entities.add({
-    position: position,
-    billboard: {
-      image: '/src/assets/icon/blue.png',
-      scale: 0.4,
-      color: Cesium.Color.YELLOW,
-      horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
-      verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
-      distanceDisplayCondition: distanceDisplayCondition,
-      scaleByDistance: pointNearFarScalar
-    },
-    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
-    },
-    id: `point-${item.STCD || item.LGTD + '-' + item.LTTD}`,
-    properties: {
-      data: item
-    }
-  });
-  entityDataMap.set(entity.id, item);
-});
+
 
 // 天地图图层(保持不变)
 const tdtLayer = new Cesium.WebMapTileServiceImageryProvider({

+ 249 - 114
WebVue/TaiHufenglang/src/components/Cesium/CesiumViewer.vue

@@ -12,19 +12,32 @@
     <button class="control-btn" @click="handleToggleTyphoon" title="显示/隐藏台风并切换视角">
       <i class="fas fa-wind" :class="{ 'active': typhoonVisible }"></i>
     </button>
+    <button class="control-btn" @click="toggleWmsLayer" title="显示/隐藏WMS风暴潮图层">
+      <i class="fas fa-layer-group" :class="{ 'active': wmsVisible }"></i>
+    </button>
+  </div>
+
+  <!-- 时间轴组件:强制渲染 + 最高层级 -->
+  <div class="timeline-wrapper">
+    <TimeSlider
+      title="风暴潮WMS时间轴"
+      :start-time="startTime.getTime()"
+      :end-time="endTime.getTime()"
+      :current-time="currentTime.getTime()"
+      :step="timeStep"
+      :play-speed="2000"
+      @timeChange="handleTimeChange"
+    />
   </div>
 
-  <!-- 只有当viewer初始化完成后才渲染子组件 -->
+  <!-- POI/台风组件:保留viewer判断 -->
   <template v-if="viewer">
-    <!-- 引入POI可视化组件 -->
     <POIVisualization 
       :viewer="viewer" 
       :visible="poiVisible"
       :data="poiData"
       @onPointSelected="handlePointSelected"
     />
-
-    <!-- 动态控制台风组件的创建和销毁,而不仅仅是visible属性 -->
     <TyphoonVisualization 
       v-if="typhoonVisible"  
       :viewer="viewer" 
@@ -35,27 +48,34 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted, nextTick} from 'vue'
+import { ref, onMounted, onUnmounted, nextTick } 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';
+import TimeSlider from './TimeSlider.vue';
 
-
-
-// 变量定义
+// 天地图密钥
 const TDTTK = "d9e7aa2ad204aba6aeedea6f5ab48ed9";
 let viewer = ref(null);
 
-// 状态管理
+// WMS核心配置
+let wmsLayer = null;
+const wmsVisible = ref(true);
+// 时间范围:2025102700 ~ 2025110822
+const startTime = ref(new Date('2025-10-27T00:00:00'));
+const endTime = ref(new Date('2025-11-08T22:00:00'));
+const currentTime = ref(new Date('2025-10-27T00:00:00'));
+const timeStep = ref(10 * 3600 * 1000); // 10小时步长
+
+// POI/台风状态
 const poiVisible = ref(true);
 const typhoonVisible = ref(true);
-const poiData = ref(JYLData); // POI点数据
+const poiData = ref(JYLData);
 
-// 处理POI点选中事件
+// 处理POI选中
 const handlePointSelected = (pointData) => {
-  // 可以在这里处理POI点选中后的逻辑
   console.log('选中的POI点:', pointData);
 };
 
@@ -63,32 +83,28 @@ const handlePointSelected = (pointData) => {
 const goToHomeView = () => {
   if (viewer.value) {
     viewer.value.camera.flyTo({
-      destination: Cesium.Cartesian3.fromDegrees(120.169103, 31.226174, 500000),
+      destination: Cesium.Cartesian3.fromDegrees(121.75, 31.15, 30000),
       orientation: {
         heading: Cesium.Math.toRadians(0),
-        pitch: Cesium.Math.toRadians(-90),
+        pitch: Cesium.Math.toRadians(-60),
       },
       duration: 1
     });
   }
 };
 
-// 切换POI显示状态
+// 切换POI显示
 const togglePOIDisplay = () => {
   poiVisible.value = !poiVisible.value;
 };
 
-// 处理台风切换事件 - 增加延迟确保清理完成
+// 切换台风显示
 const handleToggleTyphoon = async () => {
   const newState = !typhoonVisible.value;
-  
-  // 如果是从显示切换到隐藏,先等待一段时间再更新状态
   if (!newState) {
     typhoonVisible.value = newState;
   } else {
-    // 如果是从隐藏切换到显示,先确保状态更新后再飞行到视角
     typhoonVisible.value = newState;
-    // 等待组件重新创建
     await nextTick();
     if (viewer.value) {
       viewer.value.camera.flyTo({
@@ -97,110 +113,188 @@ const handleToggleTyphoon = async () => {
     }
   }
 };
-// 处理台风组件状态变化
+
+// 处理台风组件状态
 const handleTyphoonToggle = (newState) => {
   typhoonVisible.value = newState;
 };
 
+// 加载/更新WMS图层(带严格类型校验)
+const loadWmsLayer = (time) => {
+  try {
+    if (!viewer.value || !viewer.value.imageryLayers) return;
+
+    // 移除旧图层
+    if (wmsLayer && wmsLayer instanceof Cesium.ImageryLayer) {
+      viewer.value.imageryLayers.remove(wmsLayer);
+      wmsLayer = null;
+    }
+
+    // 格式化时间
+    const timeStr = time.getFullYear() + 
+                    String(time.getMonth() + 1).padStart(2, '0') + 
+                    String(time.getDate()).padStart(2, '0') + 
+                    String(time.getHours()).padStart(2, '0');
+
+    // 创建WMS提供者(严格校验)
+    const wmsProvider = new Cesium.WebMapServiceImageryProvider({
+      url: 'http://localhost:8080/geoserver/surge_ws/wms',
+      layers: `surge_ws:ZS${timeStr}`,
+      parameters: {
+        service: 'WMS',
+        version: '1.1.0',
+        request: 'GetMap',
+        format: 'image/png',
+        transparent: true,
+        srs: 'EPSG:4326'
+      },
+      // 核心:强制限定图层的地理范围(必须和你WMS图层的实际范围一致)
+      rectangle: Cesium.Rectangle.fromDegrees(121.0, 30.5, 122.5, 31.8),
+      // 禁用Cesium的自动瓦片拆分,按整个范围出单张图
+      tilingScheme: new Cesium.GeographicTilingScheme({
+        rectangle: Cesium.Rectangle.fromDegrees(121.0, 30.5, 122.5, 31.8)
+      }),
+      // 固定瓦片尺寸为图层范围(避免拆分)
+      tileWidth: 2048,
+      tileHeight: 2048,
+      // 限制缩放级别(避免放大后重复请求)
+      maximumLevel: 10,
+      minimumLevel: 0,
+      enablePickFeatures: false
+
+    });
+
+    // 仅当提供者有效时创建图层
+    if (wmsProvider) {
+      wmsLayer = new Cesium.ImageryLayer(wmsProvider); // 显式创建ImageryLayer
+      wmsLayer.alpha = 0.7;
+      wmsLayer.show = wmsVisible.value;
+      viewer.value.imageryLayers.add(wmsLayer); // 加入图层集合
+      console.log(`成功加载WMS图层: surge_ws:ZS${timeStr}`, wmsLayer);
+    }
+  } catch (error) {
+    console.error('加载WMS图层失败:', error);
+    wmsLayer = null;
+  }
+};
+
+// 切换WMS显示/隐藏
+const toggleWmsLayer = () => {
+  console.log('点击WMS按钮,当前图层状态:', wmsLayer);
+  
+  if (!wmsLayer) {
+    console.log('WMS图层未初始化,加载默认时间图层');
+    loadWmsLayer(currentTime.value);
+    return;
+  }
+
+  wmsVisible.value = !wmsVisible.value;
+  wmsLayer.show = wmsVisible.value;
+  console.log('WMS图层显示状态:', wmsVisible.value);
+};
+
+// 时间轴时间变化响应
+const handleTimeChange = (timeStamp) => {
+  const newTime = new Date(timeStamp);
+  currentTime.value = newTime;
+  loadWmsLayer(newTime);
+};
+
 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;
-        }
+  try {
+    // 👉 核心修复1:禁用baseLayer,手动创建底图(避免布尔值冲突)
+    viewer.value = new Cesium.Viewer('cesiumContainer', {
+      timeline: false,
+      baseLayer: false, // 关键:设为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,
+      })
+    });
+
+    // 👉 核心修复2:手动创建天地图底图(确保是ImageryLayer实例)
+    if (viewer.value && viewer.value.imageryLayers) {
+      const tdtProvider = 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: "img",
+        style: "default",
+        format: "image/jpeg",
+        tileMatrixSetID: "w",
+        maximumLevel: 16,
+        show: true
       });
+      // 显式创建ImageryLayer并添加
+      const tdtLayer = new Cesium.ImageryLayer(tdtProvider);
+      viewer.value.imageryLayers.add(tdtLayer);
+    }
 
-      await Promise.all(promises);
-      console.log("所有瓦片集加载操作已完成");
-    } catch (error) {
-      console.error("批量加载逻辑出错:", error);
+    // 加载倾斜摄影瓦片集
+    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();
 
-  // 执行加载
-  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();
+    // 初始化视图
+    viewer.value.cesiumWidget.creditContainer.style.display = "none";
+    viewer.value.camera.setView({
+      destination: Cesium.Cartesian3.fromDegrees(121.75, 31.15, 30000),
+      orientation: {
+        heading: Cesium.Math.toRadians(0),
+        pitch: Cesium.Math.toRadians(-60),
+      },
+    });
+    viewer.value.resize();
+
+    // 初始化加载WMS图层
+    await nextTick();
+    loadWmsLayer(currentTime.value);
+  } catch (error) {
+    // 捕获Viewer初始化错误,避免页面崩溃
+    console.error('Cesium Viewer初始化失败:', error);
+    viewer.value = null;
+  }
 });
 
 onUnmounted(() => {
+  // 销毁Viewer实例,释放资源
   if (viewer.value && !viewer.value.isDestroyed()) {
     viewer.value.destroy();
   }
@@ -213,7 +307,7 @@ onUnmounted(() => {
   position: absolute;
   top: 200px;
   left: 150px;
-  z-index: 100;
+  z-index: 1000;
   display: flex;
   flex-direction: column;
   gap: 10px;
@@ -243,11 +337,52 @@ onUnmounted(() => {
   color: #1E88E5;
 }
 
+/* 时间轴样式:强制显示 + 最高层级 */
+.timeline-wrapper {
+  position: absolute !important;
+  bottom: 50px !important;
+  left: 0 !important;
+  right: 0 !important;
+  z-index: 9999 !important;
+  width: 80% !important;
+  margin: 0 auto !important;
+}
+
+/* 穿透TimeSlider组件样式 */
+:deep(.time-slider-container) {
+  width: 100% !important;
+  min-width: 300px !important;
+  background: rgba(0, 0, 0, 0.85) !important;
+  color: #fff !important;
+  padding: 15px !important;
+  border-radius: 8px !important;
+  box-sizing: border-box !important;
+}
+
+:deep(.time-slider-container .slider) {
+  width: 100% !important;
+  cursor: pointer !important;
+  accent-color: #409eff;
+}
+
+:deep(.time-slider-container .play-btn) {
+  background: #409eff !important;
+  color: #fff !important;
+  border: none !important;
+  padding: 4px 12px !important;
+  border-radius: 4px !important;
+  cursor: pointer !important;
+}
+
 /* 响应式调整 */
 @media (max-width: 1200px) {
   .control-buttons {
     top: 150px;
     left: 20px;
   }
+  .timeline-wrapper {
+    width: 90% !important;
+    bottom: 30px !important;
+  }
 }
-</style>
+</style>

+ 171 - 0
WebVue/TaiHufenglang/src/components/Cesium/TimeSlider.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="time-slider-container">
+    <h4 class="time-title">{{ title }}</h4>
+    <div class="time-info">当前时间:{{ formatTime(currentTimeStamp) }}</div>
+    <input
+      type="range"
+      :min="startTimeStamp"
+      :max="endTimeStamp"
+      :value="currentTimeStamp"
+      :step="step"
+      @input="handleSliderChange"
+      class="slider"
+    />
+    <div class="time-range">
+      <span>{{ formatTime(startTimeStamp) }}</span>
+      <span>{{ formatTime(endTimeStamp) }}</span>
+    </div>
+    <div class="time-controls" v-if="showPlayBtn">
+      <button @click="togglePlay" class="play-btn">
+        {{ isPlaying ? '暂停' : '播放' }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+// 👉 关键修复:添加 onUnmounted 导入
+import { ref, watch, defineProps, defineEmits, computed, onUnmounted } from 'vue'
+
+// 定义属性(明确类型,增加默认值容错)
+const props = defineProps({
+  title: { type: String, default: '时间轴控制' },
+  startTime: { type: Number, required: true, default: 0 },
+  endTime: { type: Number, required: true, default: 0 },
+  currentTime: { type: Number, required: true, default: 0 },
+  step: { type: Number, default: 3600000 },
+  showPlayBtn: { type: Boolean, default: true },
+  playSpeed: { type: Number, default: 1000 }
+});
+
+// 定义事件
+const emit = defineEmits(['timeChange']);
+
+// 计算属性(确保是数字类型,容错NaN)
+const startTimeStamp = computed(() => {
+  return isNaN(props.startTime) ? Date.now() : props.startTime;
+});
+const endTimeStamp = computed(() => {
+  return isNaN(props.endTime) ? Date.now() : props.endTime;
+});
+const currentTimeStamp = computed(() => {
+  return isNaN(props.currentTime) ? Date.now() : props.currentTime;
+});
+
+// 播放状态
+const isPlaying = ref(false);
+let playTimer = null;
+
+// 修复核心:先把时间戳转成Date对象,再格式化(增加容错)
+const formatTime = (timeStamp) => {
+  // 容错:如果不是数字/无效时间戳,返回默认值
+  if (isNaN(timeStamp) || timeStamp <= 0) {
+    return '2025-10-27 00:00:00';
+  }
+  // 时间戳转Date对象
+  const date = new Date(timeStamp);
+  // 格式化:YYYY-MM-DD HH:mm:ss
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
+};
+
+// 滑动条变化
+const handleSliderChange = (e) => {
+  const newTime = Number(e.target.value);
+  emit('timeChange', newTime);
+};
+
+// 播放/暂停(适配10小时步长,到达结束时间停止)
+const togglePlay = () => {
+  if (isPlaying.value) {
+    clearInterval(playTimer);
+    isPlaying.value = false;
+  } else {
+    isPlaying.value = true;
+    playTimer = setInterval(() => {
+      let nextTime = currentTimeStamp.value + props.step;
+      // 到达结束时间后停止播放
+      if (nextTime > endTimeStamp.value) {
+        clearInterval(playTimer);
+        isPlaying.value = false;
+        return;
+      }
+      emit('timeChange', nextTime);
+    }, props.playSpeed);
+  }
+};
+
+// 清理定时器(组件卸载时)
+watch(() => isPlaying.value, (newVal) => {
+  if (!newVal && playTimer) clearInterval(playTimer);
+});
+
+// 组件卸载时清理定时器(已导入 onUnmounted)
+onUnmounted(() => {
+  clearInterval(playTimer);
+  isPlaying.value = false;
+});
+
+defineExpose({
+  stopPlay: () => {
+    clearInterval(playTimer);
+    isPlaying.value = false;
+  }
+});
+</script>
+
+<style scoped>
+.time-slider-container {
+  background: rgba(0, 0, 0, 0.85);
+  color: #fff;
+  padding: 15px;
+  border-radius: 8px;
+  min-width: 300px;
+  box-sizing: border-box;
+}
+
+.time-title {
+  margin: 0 0 10px 0;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.time-info {
+  margin: 10px 0;
+  font-size: 14px;
+  color: #f0f0f0;
+}
+
+.slider {
+  width: 100%;
+  margin: 15px 0;
+  cursor: pointer;
+  accent-color: #409eff;
+}
+
+.time-range {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #ccc;
+  margin-bottom: 10px;
+}
+
+.time-controls {
+  text-align: center;
+}
+
+.play-btn {
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  padding: 4px 12px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: background 0.2s;
+}
+
+.play-btn:hover {
+  background: #66b1ff;
+}
+</style>

+ 10 - 0
WebVue/TaiHufenglang/vite.config.js

@@ -17,4 +17,14 @@ export default defineConfig({
       '@': fileURLToPath(new URL('./src', import.meta.url))
     },
   },
+  server: {
+    // 配置代理转发
+    proxy: {
+      '/geoserver': {
+        target: 'http://127.0.0.1:8080/geoserver', // GeoServer 地址
+        changeOrigin: true, // 开启跨域
+        rewrite: (path) => path.replace(/^\/geoserver/, '') // 重写路径
+      }
+    }
+  }
 })