/** * 应急力量地图交互 Composable * 处理应急力量标记点的点击事件,使用 HTML Overlay 显示详情信息 * * @module composables/useEmergencyForceInteraction */ import { ref, watch, onBeforeUnmount } from 'vue' import * as Cesium from 'cesium' import { fetchEmergencyForceDetail } from '../api/emergencyForce' /** * 应急力量地图交互 Hook * * @param {Object} mapStore - 地图 Store 实例 * @param {Object} options - 配置选项 * @param {boolean} [options.flyToPoint=false] - 点击时是否飞行到点位 * @param {number} [options.flyDuration=1.5] - 飞行动画时长(秒) * @param {number} [options.flyDistance=500] - 飞行后的相机距离(米) * @returns {Object} 交互状态和方法 */ export function useEmergencyForceInteraction(mapStore, options = {}) { // ==================== 配置选项 ==================== const config = { flyToPoint: options.flyToPoint ?? false, flyDuration: options.flyDuration ?? 1.5, flyDistance: options.flyDistance ?? 500 } // ==================== 状态管理 ==================== /** * 是否启用交互 */ const enabled = ref(false) /** * 当前选中的实体 */ const selectedEntity = ref(null) /** * Tooltip 是否可见 */ const tooltipVisible = ref(false) /** * Tooltip 屏幕位置 * @type {Ref<{x: number, y: number}|null>} */ const tooltipPosition = ref(null) /** * Tooltip 数据 * @type {Ref<{qxmc?: string, yjllpz?: string, wzQtwz?: string}>} */ const tooltipData = ref({}) /** * 加载状态 */ const loading = ref(false) /** * 错误信息 */ const error = ref('') /** * 请求取消控制器 */ const abortController = ref(null) /** * Cesium 事件处理器 */ let clickHandler = null /** * postRender 事件监听器 */ let postRenderListener = null // ==================== 工具函数 ==================== /** * 获取 Cesium Viewer 实例 * @returns {Cesium.Viewer|null} */ const getViewer = () => { if (!mapStore.isReady()) { console.warn('地图尚未就绪') return null } return mapStore.getViewer() } /** * 取消正在进行的请求 */ const cancelRequest = () => { if (abortController.value) { try { abortController.value.abort() } catch (error) { console.warn('取消请求失败', error) } finally { abortController.value = null } } } /** * 更新 Tooltip 位置 * 将 3D 世界坐标转换为屏幕坐标 * 特别处理 clampToGround 实体,确保使用地形高度 */ const updateTooltipPosition = () => { if (!selectedEntity.value || !tooltipVisible.value) { return } const viewer = getViewer() if (!viewer) return try { const position = selectedEntity.value.position?.getValue(Cesium.JulianDate.now()) if (!position) return // 处理 clampToGround 实体 // 对于使用 CLAMP_TO_GROUND 的 billboard,需要获取实际的地形高度 let clampedPosition = position // 转换为地理坐标 const cartographic = Cesium.Cartographic.fromCartesian(position) // 尝试获取地形高度 if (viewer.scene.globe) { const height = viewer.scene.globe.getHeight(cartographic) // 如果成功获取地形高度,使用地形高度重建位置 if (Cesium.defined(height)) { cartographic.height = height clampedPosition = Cesium.Cartographic.toCartesian(cartographic) } else { // 备用方案:尝试使用 clampToHeight const clampedCartesian = viewer.scene.clampToHeight(position) if (clampedCartesian) { clampedPosition = clampedCartesian } } } // 使用修正后的位置进行屏幕坐标转换 const screenPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition) if (screenPosition) { tooltipPosition.value = { x: screenPosition.x, y: screenPosition.y } } } catch (error) { console.error('更新 Tooltip 位置失败:', error) } } /** * 注册 postRender 监听器 * 实时更新 Tooltip 位置(相机移动时) */ const registerPostRenderListener = () => { const viewer = getViewer() if (!viewer || postRenderListener) return postRenderListener = () => { updateTooltipPosition() } viewer.scene.postRender.addEventListener(postRenderListener) } /** * 注销 postRender 监听器 */ const unregisterPostRenderListener = () => { const viewer = getViewer() if (!viewer || !postRenderListener) return try { viewer.scene.postRender.removeEventListener(postRenderListener) postRenderListener = null } catch (error) { console.error('注销 postRender 监听器失败:', error) } } /** * 相机飞行到指定实体 * @param {Cesium.Entity} entity - 目标实体 * @returns {Promise} */ const flyToEntity = async (entity) => { const viewer = getViewer() if (!viewer || !config.flyToPoint) return try { const position = entity.position?.getValue(Cesium.JulianDate.now()) if (!position) return await viewer.camera.flyTo({ destination: position, duration: config.flyDuration, offset: new Cesium.HeadingPitchRange( 0, Cesium.Math.toRadians(-45), config.flyDistance ) }) } catch (error) { console.error('相机飞行失败:', error) } } /** * 显示 Tooltip * @param {Cesium.Entity} entity - 目标实体 * @param {Object} data - 详情数据 */ const showTooltip = (entity, data) => { selectedEntity.value = entity tooltipData.value = data tooltipVisible.value = true updateTooltipPosition() registerPostRenderListener() } /** * 隐藏 Tooltip */ const hideTooltip = () => { tooltipVisible.value = false tooltipPosition.value = null tooltipData.value = {} selectedEntity.value = null error.value = '' unregisterPostRenderListener() } // ==================== 核心逻辑 ==================== /** * 处理应急力量实体点击 * @param {Cesium.Entity} entity - 被点击的实体 */ const handleEmergencyForceClick = async (entity) => { // 取消之前的请求 cancelRequest() // 提取 rid const rid = entity.properties?.rid?.getValue() if (!rid) { console.warn('应急力量实体缺少 rid 属性') error.value = '缺少标识信息' showTooltip(entity, {}) return } // 显示加载状态 loading.value = true error.value = '' showTooltip(entity, {}) // 可选:飞行到点位 await flyToEntity(entity) // 创建取消控制器 const controller = new AbortController() abortController.value = controller try { // 请求详情数据 const response = await fetchEmergencyForceDetail(rid, { signal: controller.signal }) // 检查是否仍然选中该实体 if (selectedEntity.value !== entity) { return } // 解析响应数据 let detailData = response if (response && typeof response === 'object') { if (response.data) { detailData = response.data } } // 更新 Tooltip 数据 tooltipData.value = { qxmc: detailData.qxmc || '', yjllpz: detailData.yjllpz || '', wzQtwz: detailData.wzQtwz || '' } } catch (err) { // 处理请求取消 if (err.name === 'AbortError' || err.name === 'CanceledError') { return } // 处理其他错误 console.error('加载应急力量详情失败:', err) let errorMessage = '加载失败' if (err.response) { errorMessage += `: ${err.response.status}` if (err.response.data?.message) { errorMessage += ` - ${err.response.data.message}` } } else if (err.message) { errorMessage += `: ${err.message}` } error.value = errorMessage } finally { loading.value = false abortController.value = null } } /** * 处理地图点击事件 * @param {Object} click - 点击事件对象 */ const handleMapClick = (click) => { if (!enabled.value) return const viewer = getViewer() if (!viewer) return try { // 拾取点击的对象 const pickedObject = viewer.scene.pick(click.position) // 检查是否点击了应急力量实体 if ( pickedObject && pickedObject.id && pickedObject.id.id && typeof pickedObject.id.id === 'string' && pickedObject.id.id.startsWith('emergencyForce-') ) { // 点击了应急力量标记 handleEmergencyForceClick(pickedObject.id) } else { // 点击了其他地方,隐藏 Tooltip if (tooltipVisible.value) { hideTooltip() } } } catch (error) { console.error('处理地图点击失败:', error) } } /** * 注册点击事件监听器 */ const registerClickHandler = () => { const viewer = getViewer() if (!viewer) { console.warn('无法注册点击事件:地图未就绪') return } // 避免重复注册 if (clickHandler) { console.warn('点击事件已注册') return } try { clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) clickHandler.setInputAction( handleMapClick, Cesium.ScreenSpaceEventType.LEFT_CLICK ) console.log('应急力量点击事件已注册') } catch (error) { console.error('注册点击事件失败', error) } } /** * 注销点击事件监听器 */ const unregisterClickHandler = () => { if (clickHandler) { try { clickHandler.destroy() clickHandler = null console.log('应急力量点击事件已注销') } catch (error) { console.error('注销点击事件失败', error) } } } /** * 清理所有资源 */ const cleanup = () => { cancelRequest() hideTooltip() unregisterClickHandler() unregisterPostRenderListener() loading.value = false } // ==================== 生命周期 ==================== /** * 监听启用状态变化 */ watch(enabled, (newValue) => { if (newValue) { // 启用交互 if (mapStore.isReady()) { registerClickHandler() } else { // 等待地图就绪 mapStore.onReady(() => { if (enabled.value) { registerClickHandler() } }) } } else { // 禁用交互 cleanup() } }) /** * 组件卸载前清理 */ onBeforeUnmount(() => { cleanup() }) // ==================== 返回接口 ==================== return { // 状态 enabled, loading, tooltipVisible, tooltipPosition, tooltipData, error, // 方法 cleanup, hideTooltip } }