From 548226263a518cd08514b800e1b1d5ac5714b2ab Mon Sep 17 00:00:00 2001 From: Zzc <1373857752@qq.com> Date: Thu, 13 Nov 2025 18:01:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(cockpit):=20=E6=B7=BB=E5=8A=A0=E7=B4=A7?= =?UTF-8?q?=E6=80=A5=E5=8A=9B=E9=87=8F=E5=9C=B0=E5=9B=BE=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E5=92=8C=E5=B7=A5=E5=85=B7=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加用于获取紧急力量列表和详细信息的 API 函数 - 将地图上的紧急力量标记与切换功能集成 - 实现用于在点击时显示力量详细信息的工具提示组件 - 创建可组合函数以处理地图交互,包括点击事件和位置更新 - 更新驾驶舱布局以支持覆盖图层和工具提示定位 --- .../src/views/cockpit/api/emergencyForce.js | 65 +++ .../cockpit/components/CockpitLayout.vue | 540 +++++++++++++++++- .../components/EmergencyForceTooltip.vue | 296 ++++++++++ .../useEmergencyForceInteraction.js | 468 +++++++++++++++ 4 files changed, 1346 insertions(+), 23 deletions(-) create mode 100644 packages/screen/src/views/cockpit/api/emergencyForce.js create mode 100644 packages/screen/src/views/cockpit/components/EmergencyForceTooltip.vue create mode 100644 packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js diff --git a/packages/screen/src/views/cockpit/api/emergencyForce.js b/packages/screen/src/views/cockpit/api/emergencyForce.js new file mode 100644 index 0000000..5d34c9c --- /dev/null +++ b/packages/screen/src/views/cockpit/api/emergencyForce.js @@ -0,0 +1,65 @@ +/** + * 应急力量相关 API + * @module api/emergencyForce + */ + +import { request } from '@shared/utils/request' + +/** + * 获取应急力量点位列表 + * + * @param {Object} [config={}] - 额外的 axios 配置 + * @param {AbortSignal} [config.signal] - 用于取消请求的信号 + * @param {Object} [config.params] - 查询参数 + * @returns {Promise} 返回应急力量点位数据 + * + * @example + * // 基本用法 + * const data = await fetchEmergencyForceList() + * + * @example + * // 带取消信号 + * const controller = new AbortController() + * const data = await fetchEmergencyForceList({ signal: controller.signal }) + * // 取消请求 + * controller.abort() + */ +export function fetchEmergencyForceList(config = {}) { + return request({ + url: '/snow-ops-platform/xqyjllb/list', + method: 'GET', + ...config + }) +} + +/** + * 根据 rid 获取应急力量详细信息 + * + * @param {string|number} rid - 应急力量记录的唯一标识 + * @param {Object} [config={}] - 额外的 axios 配置 + * @param {AbortSignal} [config.signal] - 用于取消请求的信号 + * @returns {Promise} 返回应急力量详细数据 + * + * @example + * // 基本用法 + * const detail = await fetchEmergencyForceDetail('123') + * + * @example + * // 带取消信号 + * const controller = new AbortController() + * const detail = await fetchEmergencyForceDetail('123', { signal: controller.signal }) + * // 取消请求 + * controller.abort() + */ +export function fetchEmergencyForceDetail(rid, config = {}) { + if (!rid) { + return Promise.reject(new Error('rid 参数不能为空')) + } + + return request({ + url: '/snow-ops-platform/xqyjllb/getById', + method: 'GET', + params: { rid }, + ...config + }) +} diff --git a/packages/screen/src/views/cockpit/components/CockpitLayout.vue b/packages/screen/src/views/cockpit/components/CockpitLayout.vue index 506bfae..4d518c7 100644 --- a/packages/screen/src/views/cockpit/components/CockpitLayout.vue +++ b/packages/screen/src/views/cockpit/components/CockpitLayout.vue @@ -2,25 +2,52 @@
-
- - -
- -
+ +
-
-
- - + + + + +
+
+ + +
+ + + +
+ + +
+ + + + + +
diff --git a/packages/screen/src/views/cockpit/components/EmergencyForceTooltip.vue b/packages/screen/src/views/cockpit/components/EmergencyForceTooltip.vue new file mode 100644 index 0000000..67e531a --- /dev/null +++ b/packages/screen/src/views/cockpit/components/EmergencyForceTooltip.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js b/packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js new file mode 100644 index 0000000..2e6d4ce --- /dev/null +++ b/packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js @@ -0,0 +1,468 @@ +/** + * 应急力量地图交互 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 + } +}