Cesium(八) 三峡大坝水淹分析,江、湖、水库、大坝水淹决堤分析

前言

水利工程、应急减灾、三维 GIS 可视化 领域,淹没 / 水淹分析是核心刚需功能。通过 WebGL 三维引擎实现大坝、水库、江河、湖泊的水淹决堤模拟,能够直观还原水位上涨、淹没范围变化,为工程推演、应急决策、防汛调度提供可视化支撑。

本篇基于 Vue3 + CesiumJS 实现三峡大坝区域高精度淹没分析 ,封装通用化淹没分析核心类,支持自定义水位参数、实时动画控制、手动拖拽调参,结合天地图高清影像 + 高精度地形,打造极致真实的三维水淹效果。方案通用适配江河、湖泊、水库、大坝决堤、城市内涝等全场景淹没分析需求。

技术栈

  • 前端框架:Vue3 (Composition API / <script setup>)
  • 三维 GIS 引擎:CesiumJS
  • 底图服务:天地图卫星影像 + 注记图层
  • 地形服务:火星地形服务 //data.mars3d.cn/terrain
  • UI 风格:liquid(毛玻璃) 现代化界面
  • 交互:响应式布局 + 垂直高度滑块实时控制

应用场景

  1. 水利工程:大坝、水库水位调度模拟
  2. 应急减灾:洪水淹没、大坝决堤推演
  3. 城市规划:内涝风险区域可视化分析
  4. 地理教学:江河湖泊水文变化三维演示
  5. 防灾预警:淹没范围与水位高度联动展示

核心技术原理

本方案的核心思路是基于 Cesium 多边形拉伸实体 + 动态高度回调 + 地形匹配实现淹没效果:

  1. 区域定义:通过经纬度坐标圈定淹没范围(三峡大坝核心区域);
  2. 实体创建 :使用 Cesium 多边形实体,开启perPositionHeight贴合地形;
  3. 动态水位 :通过CallbackProperty实时更新拉伸高度,模拟水位上涨;
  4. 平滑动画:60fps 帧动画优化,实现流畅的水位升降效果;
  5. 交互联动:UI 参数与三维场景双向绑定,支持自动 / 手动控制水位。

核心模块技术解析

一、通用淹没分析核心类(SubmergenceAnalysis)

这是整个功能的灵魂模块 ,采用面向对象封装,将水体创建、动画控制、坐标转换、回调联动全部封装,支持高度复用、参数自定义,是适配全场景淹没分析的核心。

核心能力

  • 自动适配经纬度坐标(一维 / 二维数组)
  • 支持水位上升 / 下降双模式动画
  • 60fps 平滑动画,无卡顿
  • 实时高度回调、结束回调
  • 实体销毁、暂停、重置

二、Cesium 场景高性能优化

为了保证淹没效果的真实性和流畅度,对 Cesium 场景做了专项优化:

  1. 地形加载 :接入高精度地形,开启requestVertexNormals让地形更立体;
  2. 深度检测globe.depthTestAgainstTerrain = true,让水体贴合地形,不穿透地表;
  3. 雾化效果:开启场景雾化,提升三维空间层次感;
  4. 底图优化:天地图卫星 + 注记双层叠加,无密钥自动兜底离线影像;
  5. 控件精简:隐藏冗余 Cesium 默认控件,提升界面整洁度。

三、水位动态动画实现

摒弃传统生硬的高度修改,采用浏览器 16ms 帧定时器(60fps) + Cesium 回调属性实现平滑动画:

  • 定时器拆分速度参数,让水位上涨更自然;
  • CallbackProperty动态绑定拉伸高度,实时响应数据变化;
  • 自动判断水位目标值,到达后自动停止并触发回调。

四、双向交互控制逻辑

  1. 自动模式:输入起始高度、目标高度、速度,一键启动动画;
  2. 手动模式:垂直滑块拖拽,实时修改水位高度,自动暂停动画;
  3. 状态管理:防重复点击、开始 / 暂停 / 清除状态无缝切换;
  4. 数据联动:UI 实时显示当前水位,滑块与场景高度双向同步。

五、现代化毛玻璃 UI 设计

采用backdrop-filter: blur实现毛玻璃效果,搭配渐变按钮、响应式布局,PC / 移动端自适应,垂直滑块采用电闸风格,贴合水位控制的交互逻辑。

关键代码片段

1. 淹没分析核心类(核心方法)

javascript

运行

javascript 复制代码
// 淹没分析核心类
class SubmergenceAnalysis {
    constructor(option) {
        this.viewer = option.viewer
        this.waterHeight = option.startHeight || 0
        this.speed = option.speed || 1
        // 动态回调属性,实时更新水位高度
        this.extrudedHeight = new Cesium.CallbackProperty(() => this.waterHeight, false)
        this.createWaterEntity() // 创建水体实体
    }

    // 创建水体多边形实体
    createWaterEntity() {
        this.entity = this.viewer.entities.add({
            polygon: {
                hierarchy: this.coordsTransform(), // 坐标转换
                material: new Cesium.Color.fromBytes(40, 180, 255, 120),
                perPositionHeight: true, // 贴合地形
                extrudedHeight: this.extrudedHeight // 动态拉伸高度
            }
        })
    }

    // 启动平滑水位动画
    startAnimation() {
        this.timer = setInterval(() => {
            this.waterHeight += this.speed / 60
            // 到达目标高度停止
            if (this.waterHeight >= this.targetHeight) {
                this.stopAnimation()
                this.endCallback?.()
            }
            this.speedCallback?.(this.waterHeight)
        }, 16) // 60fps 流畅渲染
    }

    // 坐标转换:经纬度 => Cartesian3
    coordsTransform(coords) {
        return coords.map(([lng, lat]) => Cesium.Cartesian3.fromDegrees(lng, lat))
    }
}

2. Cesium 场景初始化(地形 + 底图)

javascript

运行

javascript 复制代码
// 初始化Cesium场景
const initCesium = async () => {
  viewer = new Cesium.Viewer('mapContainer', {
    imageryProvider: undefined,
    contextOptions: { webgl: { antialias: true } } // 抗锯齿优化
  })
  // 加载天地图卫星+注记图层
  createTDLayer(viewer, 'satellite')
  createTDLayer(viewer, 'label')
  // 加载高精度地形
  await AddTerrain(viewer)
  // 开启地形深度检测,水体不穿透地面
  viewer.scene.globe.depthTestAgainstTerrain = true
  // 相机自动飞行到三峡大坝区域
  flyToSanxia()
}

3. 交互控制逻辑

javascript

运行

javascript 复制代码
// 切换模拟状态:开始/暂停
const toggleSimulation = () => {
  if (!submergenceAnalysis) {
    startAnalysis() // 新建实例并启动
  } else {
    isRunning.value ? submergenceAnalysis.pause() : submergenceAnalysis.start()
    isRunning.value = !isRunning.value
  }
}

// 滑块手动控制水位
const updateHeightFromSlider = () => {
  submergenceAnalysis.pause()
  submergenceAnalysis.setHeight(sliderValue.value) // 直接设置高度
  isRunning.value = false
}

功能效果展示

  1. 场景加载:自动加载天地图高清影像 + 三峡地形,相机定位核心区域;
  2. 参数配置:毛玻璃面板配置起始水位、目标水位、上涨速度;
  3. 自动模拟:一键启动,水位平滑上涨,实时显示当前高度;
  4. 手动控制:右侧垂直滑块拖拽,精准调整水位高度;
  5. 状态控制:支持暂停、继续、清除重置;
  6. 视觉效果:水体半透明贴合地形,边界清晰,三维立体感拉满。

拓展适配(全场景通用)

本方案不局限于三峡大坝 ,仅需修改淹没区域坐标,即可快速适配:

  1. 江河淹没:替换河道经纬度坐标;
  2. 水库模拟:输入水库范围坐标;
  3. 城市内涝:圈定城市低洼区域;
  4. 决堤模拟:调整动画速度,模拟决堤洪水上涨。

同时支持自定义:

  • 水体颜色、透明度
  • 水位上升 / 下降模式
  • 动画速度、最大高度
  • 多区域同步淹没分析

技术亮点总结

  1. 通用化封装:淹没分析类独立封装,零修改适配全场景;
  2. 高性能动画:60fps 帧动画 + Cesium 回调属性,流畅无卡顿;
  3. 真实感渲染:高精度地形 + 深度检测 + 雾化效果,贴合真实地理环境;
  4. 极致交互:自动 + 手动双模式控制,毛玻璃 UI,响应式适配;
  5. 工程化价值:可直接集成到水利、应急、GIS 项目中,开箱即用。

结语

基于 Cesium 的三维淹没分析,是 WebGIS 在水利防灾领域的典型应用。本文实现的三峡大坝水淹模拟,不仅是单一案例,更是一套通用化、可扩展、高性能的淹没分析解决方案。

后续可拓展:实时雨量数据联动、淹没区域面积计算、受影响人口分析等功能,打造完整的防汛减灾可视化系统。

完整代码

html 复制代码
<template>
    <div class="water-analysis-container">
        <div id="mapContainer"></div>
        <!-- 毛玻璃风格控制面板 -->
        <div class="glass-panel">
            <div class="panel-header">
                <h3>三峡大坝淹没分析</h3>
            </div>
            <div class="glass-form">
                <div class="form-grid">
                    <div class="input-group">
                        <label>起始高度 (米)</label>
                        <input v-model="startHeight" type="number" min="0" class="glass-input">
                    </div>
                    <div class="input-group">
                        <label>目标高度 (米)</label>
                        <input v-model="targetHeight" type="number" min="0" class="glass-input">
                    </div>
                    <div class="input-group">
                        <label>速度 (米/秒)</label>
                        <input v-model="speed" type="number" min="1" class="glass-input">
                    </div>
                    <div class="input-group">
                        <label>当前高度 (米)</label>
                        <input v-model="currentHeight" type="text" disabled class="glass-input disabled">
                    </div>
                </div>
            </div>
            <div class="btn-wrapper">
                <button @click="fly()" class="glass-btn">
                    定位
                </button>
                <button @click="toggleSimulation()" class="glass-btn primary">
                    {{ isRunning ? '暂停' : '开始' }}
                </button>
                <button @click="clear()" class="glass-btn secondary">
                    清除
                </button>
            </div>
        </div>
        
        <!-- 垂直进度条 -->
        <div class="vertical-slider-container">
            <div class="slider-label">当前高度</div>
            <input 
                type="range" 
                class="vertical-slider" 
                min="0" 
                :max="maxHeight" 
                v-model.number="sliderValue" 
                @input="updateHeightFromSlider"
            >
            <div class="slider-value">{{ currentHeight }} 米</div>
        </div>
    </div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { TD_MAP_KEY } from '../config.js';

let viewer = undefined;
let submergenceAnalysis = null;

// 响应式数据
const startHeight = ref('0'); // 默认贴地开始
const targetHeight = ref('200'); // 模拟洪水淹没高度
const speed = ref('50');          // 适中的速度
const currentHeight = ref('0');
const isRunning = ref(false);    // 防连点状态
const maxHeight = ref('300');   // 进度条最大值
const sliderValue = ref('0');     // 滑块值

// 淹没分析类
class SubmergenceAnalysis {
    constructor(option) {
        this.viewer = option.viewer;
        this.targetHeight = option.targetHeight || 10;
        this.startHeight = option.startHeight || 0;
        this.waterHeight = option.waterHeight || this.startHeight;
        this.adapCoordi = option.adapCoordi || [];
        this.speed = option.speed || 1;
        // 液态蓝色调,更贴合水的主题
        this.color = option.color || new Cesium.Color.fromBytes(40, 180, 255, 120);
        this.changetype = option.changetype || 'up';
        this.speedCallback = option.speedCallback || (h => {});
        this.endCallback = option.endCallback || (() => {});
        this.polygonEntity = null;
        this.timer = null;
        
        if (this.viewer) {
            this.createEntity();
            this.updatePoly(this.adapCoordi);
        }
    }
    
    // 创建淹没实体
    createEntity() {
        if (this.polygonEntity && this.polygonEntity.length > 0) {
            this.polygonEntity.forEach(entity => this.viewer.entities.remove(entity));
        }
        this.polygonEntity = [];
        
        const nEntity = this.viewer.entities.add({
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy([]),
                material: this.color,
                outline: true,
                outlineColor: Cesium.Color.fromBytes(100, 200, 255, 200),
                outlineWidth: 3,
                // 液态效果:增加透明度渐变
                perPositionHeight: true
            }
        });
        
        nEntity.polygon.extrudedHeight = new Cesium.CallbackProperty(() => this.waterHeight, false);
        this.polygonEntity.push(nEntity);
    }
    
    // 更新polygon
    updatePoly(adapCoordi) {
        this.adapCoordi = this.coordsTransformation(adapCoordi);
        if (this.polygonEntity?.length) {
            this.polygonEntity[0].polygon.hierarchy = new Cesium.PolygonHierarchy(this.adapCoordi);
        }
    }
    
    // 坐标转换(适配二维数组)
    coordsTransformation(coords) {
        const c = [];
        // 处理二维数组 [[lng,lat], ...]
        if (Array.isArray(coords[0]) && coords[0].length === 2) {
            coords.forEach(([lng, lat]) => {
                c.push(Cesium.Cartesian3.fromDegrees(lng, lat));
            });
            return c;
        }
        // 兼容一维数组
        if (typeof coords[0] === "number" && typeof coords[1] === "number") {
            return Cesium.Cartesian3.fromDegreesArray(coords);
        }
        return c;
    }

    // 开始淹没
    start() {
        if (this.timer) {
            window.clearInterval(this.timer);
            this.timer = null;
        }
        
        this.timer = window.setInterval(() => {
            const sp = this.speed / 60; // 更平滑的速度
            if (this.changetype === "up") {
                this.waterHeight += sp;
                if (this.waterHeight >= this.targetHeight) {
                    this.waterHeight = this.targetHeight;
                    window.clearInterval(this.timer);
                    this.timer = null;
                    this.endCallback();
                }
            } else {
                this.waterHeight -= sp;
                if (this.waterHeight <= this.targetHeight) {
                    this.waterHeight = this.targetHeight;
                    window.clearInterval(this.timer);
                    this.timer = null;
                    this.endCallback();
                }
            }
            this.speedCallback(this.waterHeight);
        }, 16); // 60fps 更流畅
    }
    
    // 清除淹没
    clear() {
        if (this.timer) {
            window.clearInterval(this.timer);
            this.timer = null;
        }
        this.waterHeight = this.startHeight;
        
        if (this.polygonEntity) {
            this.polygonEntity.forEach(entity => this.viewer.entities.remove(entity));
            this.polygonEntity = null;
        }
    }
    
    // 设置高度
    setHeight(height) {
        this.waterHeight = height;
        this.speedCallback(this.waterHeight);
    }
    
    // 暂停淹没
    pause() {
        if (this.timer) {
            window.clearInterval(this.timer);
            this.timer = null;
        }
    }
}

/**
 * 创建天地图图层
 */
const createTDLayer = (viewer, layerType) => {
  if (!TD_MAP_KEY || typeof TD_MAP_KEY !== 'string' || !TD_MAP_KEY.length) {
    console.warn('天地图密钥无效,使用兜底图层');
    if (layerType === 'satellite') {
      const xyz = new Cesium.UrlTemplateImageryProvider({
        url: '//data.mars3d.cn/tile/img/{z}/{x}/{y}.jpg'
      });
      const layer = viewer.imageryLayers.addImageryProvider(xyz);
      layer.zIndex = 0;
      return layer;
    }
    return null;
  }
  
  const layerConfigs = {
    satellite: {
      url: `https://t{s}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${TD_MAP_KEY}`,
      zIndex: 0
    },
    label: {
      url: `https://t{s}.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=${TD_MAP_KEY}`,
      zIndex: 1
    }
  };

  try {
    const layer = viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
      url: layerConfigs[layerType].url,
      subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
      credit: '天地图',
      maximumLevel: 18,
      minimumLevel: 0
    }));
    layer.zIndex = layerConfigs[layerType].zIndex;
    return layer;
  } catch (error) {
    console.error(`创建${layerType}图层失败:`, error);
    return null;
  }
};

/**
 * 添加地形服务
 */
const AddTerrain = async (viewer) => {
  try {
    viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl('//data.mars3d.cn/terrain', {
      requestWaterMask: false,
      requestVertexNormals: true // 开启法线,地形更真实
    });
    console.log('地形加载成功');
  } catch (e) {
    console.error("加载地形失败", e);
  }
};

/**
 * 计算坐标数组的中心点
 * @param {Array<Array<number>>} coordinates 坐标数组,格式:[[lon, lat], [lon, lat], ...]
 * @returns {Array<number>} 中心点坐标,格式:[lon, lat]
 */
const calculateCenterPoint = (coordinates) => {
  if (!coordinates || coordinates.length === 0) {
    return [0, 0];
  }

  let lonSum = 0;
  let latSum = 0;
  
  for (const coord of coordinates) {
    lonSum += coord[0];
    latSum += coord[1];
  }
  
  return [lonSum / coordinates.length, latSum / coordinates.length];
};

/**
 * 初始化Cesium
 */
const initCesium = async () => {
  try {
    if (window.cesiumViewer) {
      window.cesiumViewer.destroy();
      window.cesiumViewer = null;
    }

    viewer = new Cesium.Viewer('mapContainer', {
      timeline: false,
      animation: false,
      homeButton: false,
      sceneModePicker: false,
      navigationHelpButton: false,
      baseLayerPicker: false,
      infoBox: false,
      selectionIndicator: false,
      fullscreenButton: false,
      imageryProvider: undefined,
      geocoder: false,
      // 抗锯齿,画面更清晰
      contextOptions: {
        webgl: {
          antialias: true,
          alpha: true
        }
      }
    });

    window.cesiumViewer = viewer;

    // 创建天地图图层
    createTDLayer(viewer, 'satellite');
    createTDLayer(viewer, 'label');

    // 隐藏默认控件
    const bottomContainer = document.getElementsByClassName("cesium-viewer-bottom")[0];
    if (bottomContainer) bottomContainer.style.display = "none";
    
    // 添加地形
    await AddTerrain(viewer);
    
    // 深度监测 + 雾化效果,更真实
    viewer.scene.globe.depthTestAgainstTerrain = true;
    viewer.scene.fog.enabled = true;
    viewer.scene.fog.density = 0.00005; // 轻微雾化,增加层次感

    // 三峡围城区域绝美初始视角
    // 使用sanxiaCoords计算围城区域中心
    const sanxiaCoords = [
      [111.02086418959858,30.814999676777077],
      [111.03345242496542,30.818319913619924],
      [111.0616814517163,30.83065471232016],
      [111.08196286058784,30.847010459254708],
      [111.09032690713384,30.851358571533403],
      [111.08941451523727,30.859097354922586],
      [111.08985128751604,30.86489194836355],
      [111.07754926471307,30.865906642553313],
      [111.07071726302274,30.87056522934034],
      [111.07374030334265,30.87730594475594],
      [111.05997088365923,30.88205195945251],
      [111.04691618687175,30.8786735678159],
      [111.03135648516805,30.87499845663705],
      [111.00790506454733,30.87628121332698],
      [110.99226820449012,30.8795963545915],
      [110.98210272331553,30.872616157823035],
      [110.95788211720846,30.866535305774985],
      [110.94746745074589,30.862559502013564],
      [110.94015076406508,30.863352191524285],
      [110.92528302666359,30.863440693611718],
      [110.91693796249807,30.851410448406487],
      [110.93297591794753,30.822318912958256],
      [110.94587133848161,30.806478009762124],
      [110.96210609017544,30.791237671320218],
      [110.9924568432671,30.79416243932549],
      [111.02813564063163,30.797039077081255],
      [111.02122137941903,30.812951980562904]
    ];
    
    // 计算中心点
    const centerPoint = calculateCenterPoint(sanxiaCoords);
    
    setTimeout(() => {
        viewer.camera.flyTo({
            // 斜侧视角,高度12000米,能完整看到整个围城区域
            destination: Cesium.Cartesian3.fromDegrees(centerPoint[0], centerPoint[1], 12000),
            orientation: {
                heading: Cesium.Math.toRadians(45),    // 45度航向,斜侧视角
                pitch: Cesium.Math.toRadians(-50),     // 向下50度,能看到地形起伏
                roll: Cesium.Math.toRadians(0)
            },
            duration: 4, // 缓慢飞行,体验更好
            easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT // 平滑缓动
        });
    }, 500);
  } catch (error) {
    console.error('Cesium 初始化失败:', error);
    if (viewer) viewer.destroy();
    viewer = null;
  }
};

/**
 * 飞行到三峡围城区域(优化视角)
 */
const fly = () => {
  if (!viewer) return;
  
  // 三峡围城区域坐标
  const sanxiaCoords = [
    [111.02086418959858,30.814999676777077],
    [111.03345242496542,30.818319913619924],
    [111.0616814517163,30.83065471232016],
    [111.08196286058784,30.847010459254708],
    [111.09032690713384,30.851358571533403],
    [111.08941451523727,30.859097354922586],
    [111.08985128751604,30.86489194836355],
    [111.07754926471307,30.865906642553313],
    [111.07071726302274,30.87056522934034],
    [111.07374030334265,30.87730594475594],
    [111.05997088365923,30.88205195945251],
    [111.04691618687175,30.8786735678159],
    [111.03135648516805,30.87499845663705],
    [111.00790506454733,30.87628121332698],
    [110.99226820449012,30.8795963545915],
    [110.98210272331553,30.872616157823035],
    [110.95788211720846,30.866535305774985],
    [110.94746745074589,30.862559502013564],
    [110.94015076406508,30.863352191524285],
    [110.92528302666359,30.863440693611718],
    [110.91693796249807,30.851410448406487],
    [110.93297591794753,30.822318912958256],
    [110.94587133848161,30.806478009762124],
    [110.96210609017544,30.791237671320218],
    [110.9924568432671,30.79416243932549],
    [111.02813564063163,30.797039077081255],
    [111.02122137941903,30.812951980562904]
  ];
  
  // 计算中心点
  const centerPoint = calculateCenterPoint(sanxiaCoords);
  
  viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(centerPoint[0], centerPoint[1], 10000),
      orientation: {
          heading: Cesium.Math.toRadians(60),    // 调整航向,更优视角
          pitch: Cesium.Math.toRadians(-45),     // 适度向下
          roll: Cesium.Math.toRadians(0)
      },
      duration: 3,
      easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT
  });
};

/**
 * 开始淹没分析
 */
const start = () => {
  if (!viewer || isRunning.value) return;
  isRunning.value = true;
  
  if (submergenceAnalysis) {
    submergenceAnalysis.clear();
    submergenceAnalysis = null;
  }

  // 三峡围城区域坐标
  const sanxiaCoords = [
    [111.02086418959858,30.814999676777077],
    [111.03345242496542,30.818319913619924],
    [111.0616814517163,30.83065471232016],
    [111.08196286058784,30.847010459254708],
    [111.09032690713384,30.851358571533403],
    [111.08941451523727,30.859097354922586],
    [111.08985128751604,30.86489194836355],
    [111.07754926471307,30.865906642553313],
    [111.07071726302274,30.87056522934034],
    [111.07374030334265,30.87730594475594],
    [111.05997088365923,30.88205195945251],
    [111.04691618687175,30.8786735678159],
    [111.03135648516805,30.87499845663705],
    [111.00790506454733,30.87628121332698],
    [110.99226820449012,30.8795963545915],
    [110.98210272331553,30.872616157823035],
    [110.95788211720846,30.866535305774985],
    [110.94746745074589,30.862559502013564],
    [110.94015076406508,30.863352191524285],
    [110.92528302666359,30.863440693611718],
    [110.91693796249807,30.851410448406487],
    [110.93297591794753,30.822318912958256],
    [110.94587133848161,30.806478009762124],
    [110.96210609017544,30.791237671320218],
    [110.9924568432671,30.79416243932549],
    [111.02813564063163,30.797039077081255],
    [111.02122137941903,30.812951980562904]
  ];

  submergenceAnalysis = new SubmergenceAnalysis({
      viewer: viewer,
      targetHeight: parseFloat(targetHeight.value),
      startHeight: parseFloat(startHeight.value),
      adapCoordi: sanxiaCoords,
      speed: Number(speed.value),
      changetype: "up",
      speedCallback: (h) => {
          currentHeight.value = h.toFixed(2);
          sliderValue.value = h;
      },
      endCallback: () => {
          isRunning.value = false;
          currentHeight.value = targetHeight.value;
          sliderValue.value = parseFloat(targetHeight.value);
      }
  });
  
  // 初始化滑块值
  sliderValue.value = parseFloat(startHeight.value);
  
  submergenceAnalysis.start();
};

/**
 * 清除淹没分析
 */
const clear = () => {
  if (submergenceAnalysis) {
    submergenceAnalysis.clear();
    submergenceAnalysis = null;
    currentHeight.value = '0';
    sliderValue.value = '0';
    isRunning.value = false;
  }
};

/**
 * 切换模拟状态(开始/暂停)
 */
const toggleSimulation = () => {
  if (!submergenceAnalysis) {
    // 如果还没有创建淹没分析实例,先创建并开始
    start();
  } else {
    if (isRunning.value) {
      // 如果正在运行,暂停
      submergenceAnalysis.pause();
      isRunning.value = false;
    } else {
      // 如果已暂停,继续
      submergenceAnalysis.start();
      isRunning.value = true;
    }
  }
};

/**
 * 暂停淹没模拟
 */
const pause = () => {
  if (submergenceAnalysis) {
    submergenceAnalysis.pause();
    isRunning.value = false;
  }
};

/**
 * 通过滑块更新高度
 */
const updateHeightFromSlider = () => {
  if (submergenceAnalysis) {
    // 自动暂停并设置高度
    submergenceAnalysis.pause();
    submergenceAnalysis.setHeight(sliderValue.value);
    isRunning.value = false;
  } else {
    // 如果还没有创建淹没分析实例,先创建但不开始
    createSubmergenceAnalysis();
    submergenceAnalysis.setHeight(sliderValue.value);
  }
};

/**
 * 创建淹没分析实例
 */
const createSubmergenceAnalysis = () => {
  // 三峡围城区域坐标
  const sanxiaCoords = [
    [111.02086418959858,30.814999676777077],
    [111.03345242496542,30.818319913619924],
    [111.0616814517163,30.83065471232016],
    [111.08196286058784,30.847010459254708],
    [111.09032690713384,30.851358571533403],
    [111.08941451523727,30.859097354922586],
    [111.08985128751604,30.86489194836355],
    [111.07754926471307,30.865906642553313],
    [111.07071726302274,30.87056522934034],
    [111.07374030334265,30.87730594475594],
    [111.05997088365923,30.88205195945251],
    [111.04691618687175,30.8786735678159],
    [111.03135648516805,30.87499845663705],
    [111.00790506454733,30.87628121332698],
    [110.99226820449012,30.8795963545915],
    [110.98210272331553,30.872616157823035],
    [110.95788211720846,30.866535305774985],
    [110.94746745074589,30.862559502013564],
    [110.94015076406508,30.863352191524285],
    [110.92528302666359,30.863440693611718],
    [110.91693796249807,30.851410448406487],
    [110.93297591794753,30.822318912958256],
    [110.94587133848161,30.806478009762124],
    [110.96210609017544,30.791237671320218],
    [110.9924568432671,30.79416243932549],
    [111.02813564063163,30.797039077081255],
    [111.02122137941903,30.812951980562904]
  ];

  submergenceAnalysis = new SubmergenceAnalysis({
      viewer: viewer,
      targetHeight: parseFloat(targetHeight.value),
      startHeight: parseFloat(startHeight.value),
      adapCoordi: sanxiaCoords,
      speed: Number(speed.value),
      changetype: "up",
      speedCallback: (h) => {
          currentHeight.value = h.toFixed(2);
          sliderValue.value = h;
      },
      endCallback: () => {
          isRunning.value = false;
          currentHeight.value = targetHeight.value;
          sliderValue.value = parseFloat(targetHeight.value);
      }
  });
  
  // 初始化滑块值
  sliderValue.value = parseFloat(startHeight.value);
};

// 生命周期
onMounted(() => {
  initCesium();
});

onUnmounted(() => {
  if (viewer) {
    if (submergenceAnalysis) {
        submergenceAnalysis.clear();
        submergenceAnalysis = null;
    }
    viewer.destroy();
    viewer = null;
    window.cesiumViewer = null;
  }
});
</script>

<style scoped>
/* 基础容器 */
.water-analysis-container {
    position: relative;
    width: 100%;
    height: 100vh;
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

#mapContainer {
    width: 100%;
    height: 100%;
    position: relative;
}

/* 毛玻璃风格控制面板 */
.glass-panel {
    position: absolute;
    top: 20px;
    left: 20px;
    z-index: 1000;
    /* 毛玻璃效果 */
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
    /* 边框和阴影 */
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 12px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    /* 内边距 */
    padding: 16px;
    /*width: 300px;*/
    max-width: 90vw;
}

/* 面板头部 */
.panel-header {
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.panel-header h3 {
    color: white;
    font-size: 18px;
    font-weight: 600;
    margin: 0;
    text-align: center;
    letter-spacing: 0.5px;
}

/* 毛玻璃表单 */
.glass-form {
    margin-bottom: 16px;
}

.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.form-row {
    margin-bottom: 12px;
}

.input-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.input-group label {
    color: rgba(255, 255, 255, 0.9);
    font-size: 13px;
    font-weight: 500;
}

/* 毛玻璃输入框 */
.glass-input {
    padding: 10px 12px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 8px;
    color: white;
    font-size: 14px;
    outline: none;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.glass-input:focus {
    border-color: rgba(255, 255, 255, 0.3);
    background: rgba(255, 255, 255, 0.12);
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
}

.glass-input.disabled {
    background: rgba(255, 255, 255, 0.05);
    color: rgba(255, 255, 255, 0.6);
    cursor: not-allowed;
    border-color: rgba(255, 255, 255, 0.1);
}

/* 按钮组 */
.btn-wrapper {
    display: flex;
    flex-direction: row;
    gap: 8px;
    justify-content: space-between;
}

/* 毛玻璃按钮 */
.glass-btn {
    padding: 8px 12px;
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 8px;
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05));
    color: white;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    text-align: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    backdrop-filter: blur(5px);
    flex: 1;
}

.glass-btn:hover:not(:disabled) {
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));
    border-color: rgba(255, 255, 255, 0.3);
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}

.glass-btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

/* 主按钮样式 */
.glass-btn.primary {
    background: linear-gradient(135deg, #007aff, #0056b3);
    border-color: rgba(0, 122, 255, 0.5);
}

.glass-btn.primary:hover:not(:disabled) {
    background: linear-gradient(135deg, #0056b3, #007aff);
    box-shadow: 0 6px 16px rgba(0, 122, 255, 0.4);
}

/* 次要按钮样式 */
.glass-btn.secondary {
    background: linear-gradient(135deg, #ff3b30, #cc0000);
    border-color: rgba(255, 59, 48, 0.5);
}

.glass-btn.secondary:hover:not(:disabled) {
    background: linear-gradient(135deg, #cc0000, #ff3b30);
    box-shadow: 0 6px 16px rgba(255, 59, 48, 0.4);
}

/* 垂直进度条容器 */
.vertical-slider-container {
    position: absolute;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    z-index: 1000;
    /* 毛玻璃效果 */
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
    /* 边框和阴影 */
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 12px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
    /* 内边距 */
    padding: 16px;
}

/* 进度条标签 */
.slider-label {
    color: white;
    font-size: 14px;
    font-weight: 500;
    text-align: center;
}

/* 垂直进度条 */
.vertical-slider {
    -webkit-appearance: none;
    appearance: none;
    width: 8px;
    height: 200px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 4px;
    outline: none;
    writing-mode: bt-lr;
    -webkit-appearance: slider-vertical;
}

/* 进度条轨道 */
.vertical-slider::-webkit-slider-runnable-track {
    width: 8px;
    height: 200px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 4px;
}

/* 进度条滑块 - 电闸风格 */
.vertical-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 24px;
    height: 32px;
    background: linear-gradient(135deg, #4CAF50, #45a049);
    border: 2px solid white;
    border-radius: 2px;
    cursor: pointer;
    margin-top: -10px;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
    position: relative;
}

.vertical-slider::-webkit-slider-thumb::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 8px;
    height: 16px;
    background: rgba(255, 255, 255, 0.8);
    border-radius: 1px;
}

.vertical-slider::-webkit-slider-thumb:hover {
    background: linear-gradient(135deg, #45a049, #4CAF50);
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
}

.vertical-slider:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.vertical-slider:disabled::-webkit-slider-thumb {
    background: rgba(100, 100, 100, 0.5);
    cursor: not-allowed;
}

/* Firefox 滑块样式 */
.vertical-slider::-moz-range-thumb {
    width: 24px;
    height: 32px;
    background: linear-gradient(135deg, #4CAF50, #45a049);
    border: 2px solid white;
    border-radius: 2px;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
    position: relative;
}

.vertical-slider::-moz-range-thumb:hover {
    background: linear-gradient(135deg, #45a049, #4CAF50);
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
}

.vertical-slider:disabled::-moz-range-thumb {
    background: rgba(100, 100, 100, 0.5);
    cursor: not-allowed;
}

/* 进度条值 */
.slider-value {
    color: white;
    font-size: 14px;
    font-weight: 500;
    text-align: center;
    min-width: 80px;
}

/* 响应式适配 */
@media (max-width: 768px) {
    .glass-panel {
        width: calc(100% - 40px);
        max-width: none;
    }
    
    .vertical-slider-container {
        right: 10px;
        top: 20px;
        transform: none;
        flex-direction: row;
        width: calc(100% - 60px);
        height: 60px;
    }
    
    .vertical-slider {
        width: 200px;
        height: 8px;
        writing-mode: lr-tb;
        -webkit-appearance: slider-horizontal;
    }
    
    .vertical-slider::-webkit-slider-runnable-track {
        width: 200px;
        height: 8px;
    }
    
    .vertical-slider::-webkit-slider-thumb {
        margin-top: -6px;
    }
}
</style>
相关推荐
数字冰雹2 小时前
数字孪生携手AIGC:一个指令,一座智慧城市的全景智能即刻生成
人工智能·ai·aigc·智慧城市·数字孪生·数据可视化
wzl202612132 小时前
基于企微API与数据可视化,构建私域运营的监控与ROI分析体系
信息可视化·自动化·企业微信·rpa
m0_738820202 小时前
Vue 3 + ECharts 响应式图表开发实战笔记
信息可视化
Highcharts.js2 小时前
Highcharts Grid Lite:企业免费表格数据的基本工具
前端·javascript·信息可视化·免费·highcharts·表格工具
计算机学姐2 小时前
基于SpringBoot+Vue的家政服务预约系统【个性化推荐+数据可视化】
java·vue.js·spring boot·后端·mysql·信息可视化·java-ee
书到用时方恨少!2 小时前
基于 Three.js 的 3D 地球可视化项目
开发语言·javascript·3d
平行云PVT11 小时前
数字孪生信创云渲染技术解析:从混合信创到全国产化架构
linux·unity·云原生·ue5·图形渲染·webgl·gpu算力
出门吃三碗饭14 小时前
3DGS场景优化 8*A100GPU实战
3d
BJ-Giser14 小时前
Cesium 基于EZ-Tree的植被效果
前端·可视化·cesium