在 WebGIS 开发领域Cesium 是一款功能强大的 3D 地球可视化库结合 React TypeScript 可以高效构建交互式的地球场景应用。本文将详细讲解如何基于React封装 Cesium 组件实现地球场景初始化、鼠标悬浮位置的坐标、自定义视角控制与测量工具栏一、项目环境准备1. 核心依赖React TypeScript基础开发框架提供组件化开发能力Vite构建工具本文示例基于 Vite 构建需配置 Cesium 环境变量Cesium3D 地球可视化核心库2. 环境变量配置在.env.development文件中配置 Cesium Ion Token需前往Cesium 官网申请VITE_CESIUM_ION_TOKEN你的Cesium Ion Token二、核心组件封装CesiumControl.tsx组件是实现整个地球场景的核心包含地球初始化、鼠标移动事件、视角控制工具栏和测量工具方法1. 地球初始化引入Cesium核心库与样式创建Viewer实例import { useEffect, useRef, useState } from react; import * as Cesium from cesium; import { Viewer, Cartesian3, Color, Ion, Entity } from cesium; // 导入 Cesium 样式文件必须引入否则控件样式异常 import cesium/Build/Cesium/Widgets/widgets.css; import ./index.scss; const {VITE_CESIUM_ION_TOKEN} import.meta.env // 使用你的Token令牌 Cesium.Ion.defaultAccessToken VITE_CESIUM_ION_TOKEN const CesiumControl: React.FC{} () { const center [116.40, 39.92, 16000000] as [number, number, number]; const containerRef useRefHTMLDivElement(null); const viewerRef useRefCesium.Viewer | null(null); const [isLoading, setIsLoading] useState(true); const [error, setError] useStatestring | null(null); useEffect(() { if (!containerRef.current) return; try { setIsLoading(true); // 创建 Viewer const viewer new Cesium.Viewer(containerRef.current, { // 控件配置 显示/隐藏 shouldAnimate: false, animation: false, //动画 baseLayerPicker: true, //图层选择器 fullscreenButton: true, //全屏按钮 vrButton: false, //VR效果 homeButton: true, //返回默认视角 infoBox: true, sceneModePicker: true, //场景模式切换2D/3D selectionIndicator: false, timeline: false, //时间轴 navigationHelpButton: true, //帮助说明按钮 navigationInstructionsInitiallyVisible: false, //帮助说明是否默认打开 // 性能优化 targetFrameRate: 60, useBrowserRecommendedResolution: false, }); viewerRef.current viewer; // 视角定位到中国-北京区域 if (viewerRef.current) { viewerRef.current.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], center[2]), duration: 2 // 动画时长秒默认3 }); } setIsLoading(false); } catch (err) { console.error(Failed to initialize Cesium:, err); setError(err instanceof Error ? err.message : Failed to initialize Cesium); setIsLoading(false); } // 清理函数 return () { if (viewerRef.current) { moveHandlerRef.current?.destroy(); viewerRef.current.destroy(); viewerRef.current null; } }; }, []); // 只在组件挂载时初始化一次 return ( div classNamecesium-earth div ref{containerRef} classNamecesium-viewer-container / {isLoading ( div classNameloading-overlay div classNameloading-spinner / p加载地球中.../p /div )} {error ( div classNameerror-overlay p错误: {error}/p button onClick{() window.location.reload()}重试/button /div )} /div ) }; export default CesiumControl;2. 添加鼠标移动监听事件1结构定义div classNamecoords 经度: {coordinates.lon}, 纬度: {coordinates.lat} /div2定义鼠标移动处理器const moveHandlerRef useRefany | null(null); const [coordinates, setCoordinates] useState({ lon: --, lat: -- });3封装鼠标移动事件方法// 添加鼠标移动事件处理器 const addMoveHandler (viewer: Viewer) { moveHandlerRef.current new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); moveHandlerRef.current.setInputAction((movement:any) { const mousePosition movement.endPosition; // 使用 pickEllipsoid 获取地球表面坐标 const cartesian viewer.camera.pickEllipsoid( mousePosition, viewer.scene.globe.ellipsoid ); if (cartesian) { const cartographic Cesium.Cartographic.fromCartesian(cartesian); const longitude Cesium.Math.toDegrees(cartographic.longitude).toFixed(6); const latitude Cesium.Math.toDegrees(cartographic.latitude).toFixed(6); setCoordinates({ lon: longitude, lat: latitude }); } else { setCoordinates({ lon: --, lat: -- }); } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); };4在地球初始化函数中调用并在卸载时销毁useEffect(() { const viewer new Cesium.Viewer(containerRef.current, { ... }); // 监听鼠标移动显示经纬度坐标 addMoveHandler(viewer) // 清理函数 return () { if (viewerRef.current) { moveHandlerRef.current?.destroy(); //也要销毁 viewerRef.current.destroy(); viewerRef.current null; } }; }, []);3. 视角控制工具栏1结构定义div classNametoolbar button onClick{flyToBeijing}飞至北京/button button onClick{flyToShanghai}飞至上海/button button onClick{returnView}返回视角/button button onClick{locate}定位/button MeasureTools viewerRef{viewerRef} / /div2通用方法封装封装flyTo方法通过传入经度、纬度、高度实现相机视角的切换const flyTo (lon: number, lat: number, height 30000) { if (!viewerRef.current) return const destination Cesium.Cartesian3.fromDegrees(lon, lat, height) viewerRef.current.camera.flyTo({ destination, orientation: { heading: viewerRef.current.camera.heading, // 保持原有航向 // pitch: Cesium.Math.toRadians(-45), // 视角倾斜角如果需要的话就加 roll: 0, }, duration: 2, }) }3具体事件方法此处只写了飞至北京、上海作为示例const flyToBeijing () flyTo(116.4074, 39.9042) const flyToShanghai () flyTo(121.4737, 31.2304)返回视角和获取当前定位方法// 返回视角 const returnView () { if (!viewerRef.current) return viewerRef.current.camera.flyHome(1.2) } // 获取当前定位基于浏览器 const locate () { if (!viewerRef.current || !navigator.geolocation) return navigator.geolocation.getCurrentPosition( // 成功函数 (pos) { const { longitude, latitude } pos.coords console.log(当前定位, longitude, latitude); flyTo(longitude, latitude, 15000) }, // 失败函数 () { returnView() }, { enableHighAccuracy: true, timeout: 10000 } ) }4. 测量工具此处的测量工具包含测量距离、测量面积和清除测量。我把它拆成了一个组件可以直接引入使用。1封装测量工具组件MeasureTools.tsx// 测量工具组件 import React, {useState, useEffect, useRef} from react; import * as Cesium from cesium; interface MeasureToolsProps { viewerRef: any; } const MeasureTools:React.FCMeasureToolsProps ({viewerRef}) { const [showMeasureTool, setShowMeasureTool] useStateboolean(false); const [activeTool, setActiveTool] useStatedistance | area | null(null); // 测量相关状态 const distancePointsRef useRefCesium.Cartesian3[]([]); const areaPointsRef useRefCesium.Cartesian3[]([]); const distanceEntitiesRef useRefCesium.Entity[]([]); const areaEntitiesRef useRefCesium.Entity[]([]); const distanceLabelRef useRefCesium.Entity | null(null); const areaLabelRef useRefCesium.Entity | null(null); const floatingPointRef useRefCesium.Entity | null(null); const floatingLabelRef useRefCesium.Entity | null(null); const polylineRef useRefCesium.Entity | null(null); const polygonRef useRefCesium.Entity | null(null); // 事件处理器引用 const distanceHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); const areaHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); const mouseMoveHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); // 生成唯一ID const generateUniqueId () { return entity_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; }; useEffect(() { }, []) // 启动鼠标移动预览仅在绘制时启用 const startMouseMovePreview () { if (!viewerRef.current) return; // 清除旧的处理器 if (mouseMoveHandlerRef.current) { mouseMoveHandlerRef.current.destroy(); } // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); mouseMoveHandlerRef.current handler; handler.setInputAction((movement: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(movement.endPosition, viewerRef.current.scene.globe.ellipsoid); if (cartesian) { updateFloatingPoint(cartesian); } else { clearFloatingPoint(); } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); }; // 停止鼠标移动预览绘制完成后 const stopMouseMovePreview () { if (mouseMoveHandlerRef.current) { mouseMoveHandlerRef.current.destroy(); mouseMoveHandlerRef.current null; } clearFloatingPoint(); }; // 更新浮动点 const updateFloatingPoint (cartesian: Cesium.Cartesian3) { if (!viewerRef.current) return; clearFloatingPoint(); floatingPointRef.current viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 8, color: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2 }, id: generateUniqueId() }); // 显示实时坐标 const cartographic Cesium.Cartographic.fromCartesian(cartesian); const longitude Cesium.Math.toDegrees(cartographic.longitude).toFixed(5); const latitude Cesium.Math.toDegrees(cartographic.latitude).toFixed(5); floatingLabelRef.current viewerRef.current.entities.add({ position: cartesian, label: { text: 经度: ${longitude}\n纬度: ${latitude}, font: 14px sans-serif, fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 清除浮动点 const clearFloatingPoint () { if (!viewerRef.current) return; if (floatingPointRef.current) { viewerRef.current.entities.remove(floatingPointRef.current); floatingPointRef.current null; } if (floatingLabelRef.current) { viewerRef.current.entities.remove(floatingLabelRef.current); floatingLabelRef.current null; } }; // 双击结束当前测量保留结果退出交互模式 const finishCurrentMeasurement () { if (!viewerRef.current) return; // 阻止事件冒泡和默认行为防止相机缩放 if (viewerRef.current.scene.canvas) { // 手动阻止默认行为 const canvas viewerRef.current.scene.canvas; const event new MouseEvent(dblclick, { bubbles: false, cancelable: true, view: window }); canvas.dispatchEvent(event); } // 清除距离测量的事件监听但保留已绘制的点和线 if (distanceHandlerRef.current) { distanceHandlerRef.current.destroy(); distanceHandlerRef.current null; } // 清除面积测量的事件监听但保留已绘制的点和线 if (areaHandlerRef.current) { areaHandlerRef.current.destroy(); areaHandlerRef.current null; } setActiveTool(null); // 停止鼠标移动预览 stopMouseMovePreview(); }; // 清除所有测量内容 const clearAll () { if (!viewerRef.current) return; // 清除距离测量事件监听 if (distanceHandlerRef.current) { distanceHandlerRef.current.destroy(); distanceHandlerRef.current null; } distancePointsRef.current []; distanceEntitiesRef.current.forEach(entity viewerRef.current?.entities.remove(entity)); distanceEntitiesRef.current []; if (distanceLabelRef.current) { viewerRef.current.entities.remove(distanceLabelRef.current); distanceLabelRef.current null; } if (polylineRef.current) { viewerRef.current.entities.remove(polylineRef.current); polylineRef.current null; } // 清除面积测量事件监听 if (areaHandlerRef.current) { areaHandlerRef.current.destroy(); areaHandlerRef.current null; } areaPointsRef.current []; areaEntitiesRef.current.forEach(entity viewerRef.current?.entities.remove(entity)); areaEntitiesRef.current []; if (areaLabelRef.current) { viewerRef.current.entities.remove(areaLabelRef.current); areaLabelRef.current null; } if (polygonRef.current) { viewerRef.current.entities.remove(polygonRef.current); polygonRef.current null; } setActiveTool(null); // 停止鼠标移动预览 stopMouseMovePreview(); }; // 距离测量逻辑 const startDistanceMeasurement () { clearAll(); setActiveTool(distance); if (!viewerRef.current) return; // 启动鼠标移动预览显示坐标 startMouseMovePreview(); // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); distanceHandlerRef.current handler; const clickHandler (click: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(click.position, viewerRef.current.scene.globe.ellipsoid); if (!cartesian) return; // 添加新点到当前距离点列表 distancePointsRef.current.push(cartesian); const pointEntity viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 10, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, id: generateUniqueId() }); distanceEntitiesRef.current.push(pointEntity); if (distancePointsRef.current.length 2) { updateDistancePolylineAndLabel(); } }; // 双击结束测量 const doubleClickHandler () { if (distancePointsRef.current.length 2) { finishCurrentMeasurement(); } }; handler.setInputAction(clickHandler, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction(doubleClickHandler, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); }; // 更新距离折线和标签 const updateDistancePolylineAndLabel () { if (!viewerRef.current) return; const points distancePointsRef.current; if (points.length 2) return; // 更新折线 if (polylineRef.current) viewerRef.current.entities.remove(polylineRef.current); polylineRef.current viewerRef.current.entities.add({ polyline: { positions: points, width: 3, material: Cesium.Color.YELLOW, clampToGround: false }, id: generateUniqueId() }); // 计算总距离 let totalDistance 0; for (let i 0; i points.length - 1; i) { const p1 points[i]; const p2 points[i1]; const distance Cesium.Cartesian3.distance(p1, p2); totalDistance distance; } const lastPoint points[points.length - 1]; const labelPosition points.length 2 ? Cesium.Cartesian3.midpoint(points[0], points[1], new Cesium.Cartesian3()) : lastPoint; const distanceKm totalDistance / 1000; const labelText 总距离: ${distanceKm.toFixed(2)} km; if (distanceLabelRef.current) viewerRef.current.entities.remove(distanceLabelRef.current); distanceLabelRef.current viewerRef.current.entities.add({ position: labelPosition, label: { text: labelText, font: 16px sans-serif, fillColor: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 面积测量逻辑 const startAreaMeasurement () { clearAll(); setActiveTool(area); if (!viewerRef.current) return; // 启动鼠标移动预览显示坐标 startMouseMovePreview(); // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); areaHandlerRef.current handler; const clickHandler (click: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(click.position, viewerRef.current.scene.globe.ellipsoid); if (!cartesian) return; // 添加新点到当前区域点列表 areaPointsRef.current.push(cartesian); const pointEntity viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 10, color: Cesium.Color.BLUE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, id: generateUniqueId() }); areaEntitiesRef.current.push(pointEntity); if (areaPointsRef.current.length 3) { updateAreaPolygonAndLabel(); } }; // 双击结束绘制 const doubleClickHandler () { if (areaPointsRef.current.length 3) { finishCurrentMeasurement(); } }; handler.setInputAction(clickHandler, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction(doubleClickHandler, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); }; const updateAreaPolygonAndLabel () { if (!viewerRef.current) return; const points areaPointsRef.current; if (points.length 3) return; // 更新多边形 if (polygonRef.current) viewerRef.current.entities.remove(polygonRef.current); polygonRef.current viewerRef.current.entities.add({ polygon: { hierarchy: points, material: Cesium.Color.BLUE.withAlpha(0.4), outline: true, outlineColor: Cesium.Color.CYAN, outlineWidth: 2 }, id: generateUniqueId() }); // 计算面积使用多边形面积算法在椭球面上近似 const area computeAreaOnEllipsoid(points); const areaKm2 area / 1e6; // 转换为平方公里 const center computePolygonCenter(points); const labelText 面积: ${areaKm2.toFixed(2)} km²; if (areaLabelRef.current) viewerRef.current.entities.remove(areaLabelRef.current); areaLabelRef.current viewerRef.current.entities.add({ position: center, label: { text: labelText, font: 16px sans-serif, fillColor: Cesium.Color.CYAN, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 辅助函数计算椭球面上多边形面积近似 const computeAreaOnEllipsoid (positions: Cesium.Cartesian3[]): number { if (positions.length 3) return 0; // 将笛卡尔坐标转换为弧度坐标 const radii Cesium.Ellipsoid.WGS84.radii; const cartographicArray positions.map(pos Cesium.Cartographic.fromCartesian(pos)); let area 0; for (let i 0; i cartographicArray.length; i) { const j (i 1) % cartographicArray.length; const p1 cartographicArray[i]; const p2 cartographicArray[j]; area (p2.longitude - p1.longitude) * (2 Math.sin(p1.latitude) Math.sin(p2.latitude)); } area area * radii.x * radii.x / 2; return Math.abs(area); }; const computePolygonCenter (positions: Cesium.Cartesian3[]): Cesium.Cartesian3 { let sumX 0, sumY 0, sumZ 0; positions.forEach(pos { sumX pos.x; sumY pos.y; sumZ pos.z; }); return new Cesium.Cartesian3(sumX / positions.length, sumY / positions.length, sumZ / positions.length); }; // 工具按钮点击处理 const handleDistanceClick () { if (activeTool distance) { clearAll(); } else { startDistanceMeasurement(); } }; const handleAreaClick () { if (activeTool area) { clearAll(); } else { startAreaMeasurement(); } }; const handleClearClick () { clearAll(); }; return ( div className{gj} button classNamegj-btn onClick{() setShowMeasureTool(!showMeasureTool)}测量工具/button { showMeasureTool div classNamemeasure-tool button className{jl} onClick{handleDistanceClick}测量距离/button button className{mj} onClick{handleAreaClick}测量面积/button button classNameqc onClick{handleClearClick}清除标记/button /div } /div ) } export default MeasureTools;2引入使用在 CesiumControl.tsx 组件中引入使用。在第3步中的1结构定义 中使用了组件结构import MeasureTools from ./MeasureTools;5. 样式文件通过定位让工具栏悬浮在左上角经纬度显示在右下角不影响地球视图。样式使用了scss语法需安装sass依赖1安装sass依赖npm i sass sass-loader2样式文件 index.scss.cesium-earth { width: 100%; height: 100vh; position: absolute; overflow: hidden; .toolbar { position: absolute; top: 20px; left: 20px; display: flex; gap: 8px; .gj { position: relative; width: auto; .measure-tool { position: absolute; bottom: -80px; left: 0; button { cursor: pointer; } } } } .coords { position: absolute; bottom: 25px; right: 50px; color: #fff; font-size: 14px; } .error-overlay { display: none; } } .cesium-viewer-container { width: 100%; height: 100%; /** 使用css隐掉控件工具*/ .cesium-viewer-geocoderContainer, .cesium-widget-credits { display: none !important; } } .loading-overlay, .error-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.8); color: white; z-index: 1000; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; } .loading-spinner { width: 40px; height: 40px; border: 3px solid rgba(255, 255, 255, 0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; } keyframes spin { to { transform: rotate(360deg); } } .error-overlay button { margin-top: 15px; padding: 8px 20px; background: #ff4444; border: none; color: white; border-radius: 4px; cursor: pointer; font-size: 14px; } .error-overlay button:hover { background: #ff6666; } /* 响应式调整 */ media (max-width: 768px) { .loading-overlay p, .error-overlay p { font-size: 14px; padding: 0 20px; text-align: center; } }三、组件挂载在 App.vue 中引入并使用 CesiumControl 组件import ./App.css import CesiumControl from ./components/CesiumControl function App() { return ( CesiumControl / ) } export default App四、代码仓库项目的完整代码已上传至Giteecesium-react-demo可直接克隆运行效果图
Cesium基础(二)-- 视角切换与测量工具
在 WebGIS 开发领域Cesium 是一款功能强大的 3D 地球可视化库结合 React TypeScript 可以高效构建交互式的地球场景应用。本文将详细讲解如何基于React封装 Cesium 组件实现地球场景初始化、鼠标悬浮位置的坐标、自定义视角控制与测量工具栏一、项目环境准备1. 核心依赖React TypeScript基础开发框架提供组件化开发能力Vite构建工具本文示例基于 Vite 构建需配置 Cesium 环境变量Cesium3D 地球可视化核心库2. 环境变量配置在.env.development文件中配置 Cesium Ion Token需前往Cesium 官网申请VITE_CESIUM_ION_TOKEN你的Cesium Ion Token二、核心组件封装CesiumControl.tsx组件是实现整个地球场景的核心包含地球初始化、鼠标移动事件、视角控制工具栏和测量工具方法1. 地球初始化引入Cesium核心库与样式创建Viewer实例import { useEffect, useRef, useState } from react; import * as Cesium from cesium; import { Viewer, Cartesian3, Color, Ion, Entity } from cesium; // 导入 Cesium 样式文件必须引入否则控件样式异常 import cesium/Build/Cesium/Widgets/widgets.css; import ./index.scss; const {VITE_CESIUM_ION_TOKEN} import.meta.env // 使用你的Token令牌 Cesium.Ion.defaultAccessToken VITE_CESIUM_ION_TOKEN const CesiumControl: React.FC{} () { const center [116.40, 39.92, 16000000] as [number, number, number]; const containerRef useRefHTMLDivElement(null); const viewerRef useRefCesium.Viewer | null(null); const [isLoading, setIsLoading] useState(true); const [error, setError] useStatestring | null(null); useEffect(() { if (!containerRef.current) return; try { setIsLoading(true); // 创建 Viewer const viewer new Cesium.Viewer(containerRef.current, { // 控件配置 显示/隐藏 shouldAnimate: false, animation: false, //动画 baseLayerPicker: true, //图层选择器 fullscreenButton: true, //全屏按钮 vrButton: false, //VR效果 homeButton: true, //返回默认视角 infoBox: true, sceneModePicker: true, //场景模式切换2D/3D selectionIndicator: false, timeline: false, //时间轴 navigationHelpButton: true, //帮助说明按钮 navigationInstructionsInitiallyVisible: false, //帮助说明是否默认打开 // 性能优化 targetFrameRate: 60, useBrowserRecommendedResolution: false, }); viewerRef.current viewer; // 视角定位到中国-北京区域 if (viewerRef.current) { viewerRef.current.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], center[2]), duration: 2 // 动画时长秒默认3 }); } setIsLoading(false); } catch (err) { console.error(Failed to initialize Cesium:, err); setError(err instanceof Error ? err.message : Failed to initialize Cesium); setIsLoading(false); } // 清理函数 return () { if (viewerRef.current) { moveHandlerRef.current?.destroy(); viewerRef.current.destroy(); viewerRef.current null; } }; }, []); // 只在组件挂载时初始化一次 return ( div classNamecesium-earth div ref{containerRef} classNamecesium-viewer-container / {isLoading ( div classNameloading-overlay div classNameloading-spinner / p加载地球中.../p /div )} {error ( div classNameerror-overlay p错误: {error}/p button onClick{() window.location.reload()}重试/button /div )} /div ) }; export default CesiumControl;2. 添加鼠标移动监听事件1结构定义div classNamecoords 经度: {coordinates.lon}, 纬度: {coordinates.lat} /div2定义鼠标移动处理器const moveHandlerRef useRefany | null(null); const [coordinates, setCoordinates] useState({ lon: --, lat: -- });3封装鼠标移动事件方法// 添加鼠标移动事件处理器 const addMoveHandler (viewer: Viewer) { moveHandlerRef.current new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); moveHandlerRef.current.setInputAction((movement:any) { const mousePosition movement.endPosition; // 使用 pickEllipsoid 获取地球表面坐标 const cartesian viewer.camera.pickEllipsoid( mousePosition, viewer.scene.globe.ellipsoid ); if (cartesian) { const cartographic Cesium.Cartographic.fromCartesian(cartesian); const longitude Cesium.Math.toDegrees(cartographic.longitude).toFixed(6); const latitude Cesium.Math.toDegrees(cartographic.latitude).toFixed(6); setCoordinates({ lon: longitude, lat: latitude }); } else { setCoordinates({ lon: --, lat: -- }); } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); };4在地球初始化函数中调用并在卸载时销毁useEffect(() { const viewer new Cesium.Viewer(containerRef.current, { ... }); // 监听鼠标移动显示经纬度坐标 addMoveHandler(viewer) // 清理函数 return () { if (viewerRef.current) { moveHandlerRef.current?.destroy(); //也要销毁 viewerRef.current.destroy(); viewerRef.current null; } }; }, []);3. 视角控制工具栏1结构定义div classNametoolbar button onClick{flyToBeijing}飞至北京/button button onClick{flyToShanghai}飞至上海/button button onClick{returnView}返回视角/button button onClick{locate}定位/button MeasureTools viewerRef{viewerRef} / /div2通用方法封装封装flyTo方法通过传入经度、纬度、高度实现相机视角的切换const flyTo (lon: number, lat: number, height 30000) { if (!viewerRef.current) return const destination Cesium.Cartesian3.fromDegrees(lon, lat, height) viewerRef.current.camera.flyTo({ destination, orientation: { heading: viewerRef.current.camera.heading, // 保持原有航向 // pitch: Cesium.Math.toRadians(-45), // 视角倾斜角如果需要的话就加 roll: 0, }, duration: 2, }) }3具体事件方法此处只写了飞至北京、上海作为示例const flyToBeijing () flyTo(116.4074, 39.9042) const flyToShanghai () flyTo(121.4737, 31.2304)返回视角和获取当前定位方法// 返回视角 const returnView () { if (!viewerRef.current) return viewerRef.current.camera.flyHome(1.2) } // 获取当前定位基于浏览器 const locate () { if (!viewerRef.current || !navigator.geolocation) return navigator.geolocation.getCurrentPosition( // 成功函数 (pos) { const { longitude, latitude } pos.coords console.log(当前定位, longitude, latitude); flyTo(longitude, latitude, 15000) }, // 失败函数 () { returnView() }, { enableHighAccuracy: true, timeout: 10000 } ) }4. 测量工具此处的测量工具包含测量距离、测量面积和清除测量。我把它拆成了一个组件可以直接引入使用。1封装测量工具组件MeasureTools.tsx// 测量工具组件 import React, {useState, useEffect, useRef} from react; import * as Cesium from cesium; interface MeasureToolsProps { viewerRef: any; } const MeasureTools:React.FCMeasureToolsProps ({viewerRef}) { const [showMeasureTool, setShowMeasureTool] useStateboolean(false); const [activeTool, setActiveTool] useStatedistance | area | null(null); // 测量相关状态 const distancePointsRef useRefCesium.Cartesian3[]([]); const areaPointsRef useRefCesium.Cartesian3[]([]); const distanceEntitiesRef useRefCesium.Entity[]([]); const areaEntitiesRef useRefCesium.Entity[]([]); const distanceLabelRef useRefCesium.Entity | null(null); const areaLabelRef useRefCesium.Entity | null(null); const floatingPointRef useRefCesium.Entity | null(null); const floatingLabelRef useRefCesium.Entity | null(null); const polylineRef useRefCesium.Entity | null(null); const polygonRef useRefCesium.Entity | null(null); // 事件处理器引用 const distanceHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); const areaHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); const mouseMoveHandlerRef useRefCesium.ScreenSpaceEventHandler | null(null); // 生成唯一ID const generateUniqueId () { return entity_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; }; useEffect(() { }, []) // 启动鼠标移动预览仅在绘制时启用 const startMouseMovePreview () { if (!viewerRef.current) return; // 清除旧的处理器 if (mouseMoveHandlerRef.current) { mouseMoveHandlerRef.current.destroy(); } // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); mouseMoveHandlerRef.current handler; handler.setInputAction((movement: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(movement.endPosition, viewerRef.current.scene.globe.ellipsoid); if (cartesian) { updateFloatingPoint(cartesian); } else { clearFloatingPoint(); } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); }; // 停止鼠标移动预览绘制完成后 const stopMouseMovePreview () { if (mouseMoveHandlerRef.current) { mouseMoveHandlerRef.current.destroy(); mouseMoveHandlerRef.current null; } clearFloatingPoint(); }; // 更新浮动点 const updateFloatingPoint (cartesian: Cesium.Cartesian3) { if (!viewerRef.current) return; clearFloatingPoint(); floatingPointRef.current viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 8, color: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2 }, id: generateUniqueId() }); // 显示实时坐标 const cartographic Cesium.Cartographic.fromCartesian(cartesian); const longitude Cesium.Math.toDegrees(cartographic.longitude).toFixed(5); const latitude Cesium.Math.toDegrees(cartographic.latitude).toFixed(5); floatingLabelRef.current viewerRef.current.entities.add({ position: cartesian, label: { text: 经度: ${longitude}\n纬度: ${latitude}, font: 14px sans-serif, fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 清除浮动点 const clearFloatingPoint () { if (!viewerRef.current) return; if (floatingPointRef.current) { viewerRef.current.entities.remove(floatingPointRef.current); floatingPointRef.current null; } if (floatingLabelRef.current) { viewerRef.current.entities.remove(floatingLabelRef.current); floatingLabelRef.current null; } }; // 双击结束当前测量保留结果退出交互模式 const finishCurrentMeasurement () { if (!viewerRef.current) return; // 阻止事件冒泡和默认行为防止相机缩放 if (viewerRef.current.scene.canvas) { // 手动阻止默认行为 const canvas viewerRef.current.scene.canvas; const event new MouseEvent(dblclick, { bubbles: false, cancelable: true, view: window }); canvas.dispatchEvent(event); } // 清除距离测量的事件监听但保留已绘制的点和线 if (distanceHandlerRef.current) { distanceHandlerRef.current.destroy(); distanceHandlerRef.current null; } // 清除面积测量的事件监听但保留已绘制的点和线 if (areaHandlerRef.current) { areaHandlerRef.current.destroy(); areaHandlerRef.current null; } setActiveTool(null); // 停止鼠标移动预览 stopMouseMovePreview(); }; // 清除所有测量内容 const clearAll () { if (!viewerRef.current) return; // 清除距离测量事件监听 if (distanceHandlerRef.current) { distanceHandlerRef.current.destroy(); distanceHandlerRef.current null; } distancePointsRef.current []; distanceEntitiesRef.current.forEach(entity viewerRef.current?.entities.remove(entity)); distanceEntitiesRef.current []; if (distanceLabelRef.current) { viewerRef.current.entities.remove(distanceLabelRef.current); distanceLabelRef.current null; } if (polylineRef.current) { viewerRef.current.entities.remove(polylineRef.current); polylineRef.current null; } // 清除面积测量事件监听 if (areaHandlerRef.current) { areaHandlerRef.current.destroy(); areaHandlerRef.current null; } areaPointsRef.current []; areaEntitiesRef.current.forEach(entity viewerRef.current?.entities.remove(entity)); areaEntitiesRef.current []; if (areaLabelRef.current) { viewerRef.current.entities.remove(areaLabelRef.current); areaLabelRef.current null; } if (polygonRef.current) { viewerRef.current.entities.remove(polygonRef.current); polygonRef.current null; } setActiveTool(null); // 停止鼠标移动预览 stopMouseMovePreview(); }; // 距离测量逻辑 const startDistanceMeasurement () { clearAll(); setActiveTool(distance); if (!viewerRef.current) return; // 启动鼠标移动预览显示坐标 startMouseMovePreview(); // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); distanceHandlerRef.current handler; const clickHandler (click: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(click.position, viewerRef.current.scene.globe.ellipsoid); if (!cartesian) return; // 添加新点到当前距离点列表 distancePointsRef.current.push(cartesian); const pointEntity viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 10, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, id: generateUniqueId() }); distanceEntitiesRef.current.push(pointEntity); if (distancePointsRef.current.length 2) { updateDistancePolylineAndLabel(); } }; // 双击结束测量 const doubleClickHandler () { if (distancePointsRef.current.length 2) { finishCurrentMeasurement(); } }; handler.setInputAction(clickHandler, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction(doubleClickHandler, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); }; // 更新距离折线和标签 const updateDistancePolylineAndLabel () { if (!viewerRef.current) return; const points distancePointsRef.current; if (points.length 2) return; // 更新折线 if (polylineRef.current) viewerRef.current.entities.remove(polylineRef.current); polylineRef.current viewerRef.current.entities.add({ polyline: { positions: points, width: 3, material: Cesium.Color.YELLOW, clampToGround: false }, id: generateUniqueId() }); // 计算总距离 let totalDistance 0; for (let i 0; i points.length - 1; i) { const p1 points[i]; const p2 points[i1]; const distance Cesium.Cartesian3.distance(p1, p2); totalDistance distance; } const lastPoint points[points.length - 1]; const labelPosition points.length 2 ? Cesium.Cartesian3.midpoint(points[0], points[1], new Cesium.Cartesian3()) : lastPoint; const distanceKm totalDistance / 1000; const labelText 总距离: ${distanceKm.toFixed(2)} km; if (distanceLabelRef.current) viewerRef.current.entities.remove(distanceLabelRef.current); distanceLabelRef.current viewerRef.current.entities.add({ position: labelPosition, label: { text: labelText, font: 16px sans-serif, fillColor: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 面积测量逻辑 const startAreaMeasurement () { clearAll(); setActiveTool(area); if (!viewerRef.current) return; // 启动鼠标移动预览显示坐标 startMouseMovePreview(); // 创建新的事件处理器 const handler new Cesium.ScreenSpaceEventHandler(viewerRef.current.scene.canvas); areaHandlerRef.current handler; const clickHandler (click: any) { if (!viewerRef.current) return; const cartesian viewerRef.current.camera.pickEllipsoid(click.position, viewerRef.current.scene.globe.ellipsoid); if (!cartesian) return; // 添加新点到当前区域点列表 areaPointsRef.current.push(cartesian); const pointEntity viewerRef.current.entities.add({ position: cartesian, point: { pixelSize: 10, color: Cesium.Color.BLUE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, id: generateUniqueId() }); areaEntitiesRef.current.push(pointEntity); if (areaPointsRef.current.length 3) { updateAreaPolygonAndLabel(); } }; // 双击结束绘制 const doubleClickHandler () { if (areaPointsRef.current.length 3) { finishCurrentMeasurement(); } }; handler.setInputAction(clickHandler, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction(doubleClickHandler, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); }; const updateAreaPolygonAndLabel () { if (!viewerRef.current) return; const points areaPointsRef.current; if (points.length 3) return; // 更新多边形 if (polygonRef.current) viewerRef.current.entities.remove(polygonRef.current); polygonRef.current viewerRef.current.entities.add({ polygon: { hierarchy: points, material: Cesium.Color.BLUE.withAlpha(0.4), outline: true, outlineColor: Cesium.Color.CYAN, outlineWidth: 2 }, id: generateUniqueId() }); // 计算面积使用多边形面积算法在椭球面上近似 const area computeAreaOnEllipsoid(points); const areaKm2 area / 1e6; // 转换为平方公里 const center computePolygonCenter(points); const labelText 面积: ${areaKm2.toFixed(2)} km²; if (areaLabelRef.current) viewerRef.current.entities.remove(areaLabelRef.current); areaLabelRef.current viewerRef.current.entities.add({ position: center, label: { text: labelText, font: 16px sans-serif, fillColor: Cesium.Color.CYAN, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE, pixelOffset: new Cesium.Cartesian2(0, -20), disableDepthTestDistance: Number.POSITIVE_INFINITY }, id: generateUniqueId() }); }; // 辅助函数计算椭球面上多边形面积近似 const computeAreaOnEllipsoid (positions: Cesium.Cartesian3[]): number { if (positions.length 3) return 0; // 将笛卡尔坐标转换为弧度坐标 const radii Cesium.Ellipsoid.WGS84.radii; const cartographicArray positions.map(pos Cesium.Cartographic.fromCartesian(pos)); let area 0; for (let i 0; i cartographicArray.length; i) { const j (i 1) % cartographicArray.length; const p1 cartographicArray[i]; const p2 cartographicArray[j]; area (p2.longitude - p1.longitude) * (2 Math.sin(p1.latitude) Math.sin(p2.latitude)); } area area * radii.x * radii.x / 2; return Math.abs(area); }; const computePolygonCenter (positions: Cesium.Cartesian3[]): Cesium.Cartesian3 { let sumX 0, sumY 0, sumZ 0; positions.forEach(pos { sumX pos.x; sumY pos.y; sumZ pos.z; }); return new Cesium.Cartesian3(sumX / positions.length, sumY / positions.length, sumZ / positions.length); }; // 工具按钮点击处理 const handleDistanceClick () { if (activeTool distance) { clearAll(); } else { startDistanceMeasurement(); } }; const handleAreaClick () { if (activeTool area) { clearAll(); } else { startAreaMeasurement(); } }; const handleClearClick () { clearAll(); }; return ( div className{gj} button classNamegj-btn onClick{() setShowMeasureTool(!showMeasureTool)}测量工具/button { showMeasureTool div classNamemeasure-tool button className{jl} onClick{handleDistanceClick}测量距离/button button className{mj} onClick{handleAreaClick}测量面积/button button classNameqc onClick{handleClearClick}清除标记/button /div } /div ) } export default MeasureTools;2引入使用在 CesiumControl.tsx 组件中引入使用。在第3步中的1结构定义 中使用了组件结构import MeasureTools from ./MeasureTools;5. 样式文件通过定位让工具栏悬浮在左上角经纬度显示在右下角不影响地球视图。样式使用了scss语法需安装sass依赖1安装sass依赖npm i sass sass-loader2样式文件 index.scss.cesium-earth { width: 100%; height: 100vh; position: absolute; overflow: hidden; .toolbar { position: absolute; top: 20px; left: 20px; display: flex; gap: 8px; .gj { position: relative; width: auto; .measure-tool { position: absolute; bottom: -80px; left: 0; button { cursor: pointer; } } } } .coords { position: absolute; bottom: 25px; right: 50px; color: #fff; font-size: 14px; } .error-overlay { display: none; } } .cesium-viewer-container { width: 100%; height: 100%; /** 使用css隐掉控件工具*/ .cesium-viewer-geocoderContainer, .cesium-widget-credits { display: none !important; } } .loading-overlay, .error-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.8); color: white; z-index: 1000; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; } .loading-spinner { width: 40px; height: 40px; border: 3px solid rgba(255, 255, 255, 0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; } keyframes spin { to { transform: rotate(360deg); } } .error-overlay button { margin-top: 15px; padding: 8px 20px; background: #ff4444; border: none; color: white; border-radius: 4px; cursor: pointer; font-size: 14px; } .error-overlay button:hover { background: #ff6666; } /* 响应式调整 */ media (max-width: 768px) { .loading-overlay p, .error-overlay p { font-size: 14px; padding: 0 20px; text-align: center; } }三、组件挂载在 App.vue 中引入并使用 CesiumControl 组件import ./App.css import CesiumControl from ./components/CesiumControl function App() { return ( CesiumControl / ) } export default App四、代码仓库项目的完整代码已上传至Giteecesium-react-demo可直接克隆运行效果图