diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDualMapCompare.js b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDualMapCompare.js index 8b8517b..4296828 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDualMapCompare.js +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/composables/useDualMapCompare.js @@ -142,9 +142,11 @@ export function useDualMapCompare() { const enableCompareMode = async (rightViewerInstance, options = {}) => { const { skipLeftModelLoad = false, loadLeftModel = false } = options + // 前置检查: viewer if (!rightViewerInstance) { - console.error('[useDualMapCompare] 右侧主地图Viewer未初始化') - return + const error = new Error('右侧主地图Viewer未初始化') + console.error('[useDualMapCompare]', error.message) + throw error } // 如果只是加载左侧模型(Viewer已存在) @@ -169,8 +171,9 @@ export function useDualMapCompare() { // 查找左侧容器(容器已存在于DOM中) const leftContainer = document.getElementById('leftCesiumContainer') if (!leftContainer) { - console.error('[useDualMapCompare] 找不到左侧容器元素') - return + const error = new Error('找不到左侧容器元素 #leftCesiumContainer') + console.error('[useDualMapCompare]', error.message) + throw error } // 先设置状态,触发CSS动画 @@ -182,9 +185,10 @@ export function useDualMapCompare() { // 初始化左侧Viewer const leftViewerInstance = initLeftViewer(leftContainer) if (!leftViewerInstance) { - console.error('[useDualMapCompare] 左侧Viewer初始化失败') - isCompareMode.value = false - return + const error = new Error('左侧Viewer初始化失败') + console.error('[useDualMapCompare]', error.message) + isCompareMode.value = false // 回滚状态 + throw error } // 立即同步右侧相机的当前位置到左侧 @@ -280,12 +284,32 @@ export function useDualMapCompare() { * @param {boolean} active - true启用,false禁用 * @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer实例(主地图) * @param {Object} options - 配置选项(传递给 enableCompareMode) + * @throws {Error} 当切换失败时抛出错误 */ const toggleCompareMode = async (active, rightViewerInstance, options) => { - if (active) { - await enableCompareMode(rightViewerInstance, options) - } else { - disableCompareMode() + try { + console.log(`[useDualMapCompare] 切换对比模式: ${active}`) + + if (active) { + // 前置检查 + if (!rightViewerInstance) { + throw new Error('右侧Viewer未初始化,无法启用对比模式') + } + + await enableCompareMode(rightViewerInstance, options) + } else { + disableCompareMode() + } + + console.log(`[useDualMapCompare] 对比模式切换成功: ${active}`) + } catch (error) { + console.error('[useDualMapCompare] 切换对比模式失败:', error) + + // 确保状态回滚 + isCompareMode.value = !active + + // 向上层传播错误 + throw error } } diff --git a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue index 145d1d4..93b357b 100644 --- a/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue +++ b/packages/screen/src/views/3DSituationalAwarenessRefactor/index.vue @@ -24,7 +24,10 @@
- +
@@ -57,6 +60,7 @@ @@ -227,7 +231,7 @@ * 4. 用户交互事件处理 */ -import { ref, provide, onMounted, onUnmounted } from 'vue' +import { ref, provide, onMounted, onUnmounted, watch } from 'vue' import * as Cesium from 'cesium' import { ElMessage } from 'element-plus' @@ -320,6 +324,9 @@ const { clearEmergencyResourceMarkers, addReserveCenterMarkers, clearReserveCenterMarkers, + showMarkers, + hideMarkers, + markerEntities, } = useMapMarkers() // 3D Tiles 加载 @@ -337,8 +344,13 @@ const mockDataService = useMockData() // 范围圈管理 const rangeCircleComposable = useRangeCircle() -const { rangeCircleEntity, createOrUpdateRangeCircle, clearRangeCircle } = - rangeCircleComposable +const { + rangeCircleEntity, + createOrUpdateRangeCircle, + clearRangeCircle, + showRangeCircle, + hideRangeCircle, +} = rangeCircleComposable // 路径线管理 const pathLinesComposable = usePathLines() @@ -372,6 +384,18 @@ const { showLoading, startDispatch } = useEmergencyDispatch({ const isLeftPanelCollapsed = ref(false) const isRightPanelCollapsed = ref(false) +// 标记点和范围圈显示状态 +const showMarkersAndRange = ref(false) + +// 地图工具激活状态 - 默认激活模型对比 +const activeToolKey = ref('modelCompare') + +// 工具键常量 +const COMPARE_TOOL_KEY = 'modelCompare' + +// 对比模式切换锁,防止并发操作 +const isCompareTogglePending = ref(false) + // 弹窗状态 const showPersonnelDetail = ref(false) const showCenterDetail = ref(false) @@ -519,32 +543,136 @@ const handleModalStartDispatch = () => { }) } +/** + * 统一的对比模式状态管理助手 + * 职责: + * 1. 同步管理 activeToolKey 和 isCompareMode + * 2. 提供失败回滚机制 + * 3. 防止并发操作 + * + * @param {boolean} shouldActivate - 是否激活对比模式 + * @param {string} source - 调用来源(用于日志追踪) + * @returns {Promise} 操作是否成功 + */ +const setCompareToolState = async (shouldActivate, source = 'unknown') => { + // 1. 并发保护 + if (isCompareTogglePending.value) { + console.warn(`[index.vue] ${source} - 对比模式正在切换中,忽略本次操作`) + return false + } + + if (isCompareMode.value === shouldActivate) { + console.log(`[index.vue] ${source} - 对比模式已是目标状态 (${shouldActivate}),无需操作`) + return true + } + + // 2. 保存当前状态(用于失败回滚) + const prevToolKey = activeToolKey.value + const prevCompareMode = isCompareMode.value + + // 3. 设置锁 + isCompareTogglePending.value = true + + // 4. 乐观更新UI状态(立即响应) + activeToolKey.value = shouldActivate ? COMPARE_TOOL_KEY : null + + try { + // 5. 执行实际的模式切换 + console.log(`[index.vue] ${source} - 开始切换对比模式: ${shouldActivate}`) + await toggleCompareMode(shouldActivate, mapStore.viewer) + + console.log(`[index.vue] ${source} - 对比模式切换成功`) + return true + } catch (error) { + // 6. 失败回滚 + console.error(`[index.vue] ${source} - 切换对比模式失败:`, error) + + activeToolKey.value = prevToolKey + if (isCompareMode.value !== prevCompareMode) { + console.warn(`[index.vue] isCompareMode状态不一致,强制回滚`) + isCompareMode.value = prevCompareMode + } + + ElMessage.error({ + message: `切换对比模式失败: ${error.message || '未知错误'}`, + duration: 3000, + }) + + return false + } finally { + // 7. 释放锁 + isCompareTogglePending.value = false + } +} + +/** + * 处理快速匹配标题点击事件 - 切换显示/隐藏标记和范围圈 + */ +const handleForcePresetToggle = async () => { + console.log('[index.vue] 快速匹配标题点击, 当前状态:', showMarkersAndRange.value ? '已显示' : '已隐藏') + + // 切换显示状态 + if (!showMarkersAndRange.value) { + // 当前隐藏,切换为显示 + + // 1. 关闭地图对比模式(使用统一助手) + if (isCompareMode.value) { + console.log('[index.vue] 快速匹配需要关闭对比模式') + const success = await setCompareToolState(false, 'force-preset') + + if (!success) { + // 如果关闭对比模式失败,不继续执行后续操作 + console.error('[index.vue] 关闭地图对比模式失败,中止快速匹配操作') + return + } + + console.log('[index.vue] 已关闭地图对比模式') + } + + // 2. 显示所有标记和范围圈 + showAllMarkersAndRange() + + // 3. 调整相机到最佳视角 + flyToBestViewForMarkers() + + // 更新状态 + showMarkersAndRange.value = true + } else { + // 当前显示,切换为隐藏 + hideAllMarkersAndRange() + + // 更新状态 + showMarkersAndRange.value = false + } +} + /** * 处理地图工具变化事件 */ const handleMapToolChange = async ({ tool, active }) => { - console.log(`地图工具变化: ${tool}, 激活状态: ${active}`) + console.log(`[index.vue] 地图工具变化: ${tool}, 激活状态: ${active}`) - if (tool === 'modelCompare') { - try { - const loadingMessage = ElMessage({ - message: active ? '正在启用模型对比...' : '正在关闭模型对比...', - type: 'info', - duration: 0, - showClose: false, - }) + if (tool === COMPARE_TOOL_KEY) { + // 模型对比工具: 使用统一助手管理状态 + const loadingMessage = ElMessage({ + message: active ? '正在启用模型对比...' : '正在关闭模型对比...', + type: 'info', + duration: 0, + showClose: false, + }) - await toggleCompareMode(active, mapStore.viewer) + const success = await setCompareToolState(active, 'map-controls') - loadingMessage.close() + loadingMessage.close() + + if (success) { ElMessage.success(active ? '模型对比已启用' : '模型对比已关闭') - } catch (error) { - console.error('切换模型对比模式失败:', error) - ElMessage.error({ - message: `切换模型对比失败: ${error.message || '未知错误'}`, - duration: 3000, - }) } + // 失败消息已在助手函数中处理 + } else { + // 其他工具: 保持原有逻辑 + activeToolKey.value = active ? tool : null + console.log(`[index.vue] 工具 ${tool} ${active ? '激活' : '取消'}`) } } @@ -569,6 +697,103 @@ const handleDistanceChange = async (newDistance) => { await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) } +/** + * 显示所有标记点和范围圈 + */ +const showAllMarkersAndRange = () => { + const viewer = mapStore.viewer + if (!viewer) return + + // 1. 显示模拟点位 + viewer.entities.values.forEach((entity) => { + if (entity.properties) { + const props = entity.properties + if (props.type?.getValue() === 'soldier' || + props.type?.getValue() === 'device' || + props.isPathStartMarker?.getValue()) { + entity.show = true + } + } + }) + + // 2. 显示接口标记 + showMarkers() + + // 3. 显示范围圈 + showRangeCircle() + + console.log('[index.vue] 已显示所有标记点和范围圈') +} + +/** + * 隐藏所有标记点和范围圈 + */ +const hideAllMarkersAndRange = () => { + const viewer = mapStore.viewer + if (!viewer) return + + // 1. 隐藏模拟点位 + viewer.entities.values.forEach((entity) => { + if (entity.properties) { + const props = entity.properties + if (props.type?.getValue() === 'soldier' || + props.type?.getValue() === 'device' || + props.isPathStartMarker?.getValue()) { + entity.show = false + } + } + }) + + // 2. 隐藏接口标记 + hideMarkers() + + // 3. 隐藏范围圈 + hideRangeCircle() + + console.log('[index.vue] 已隐藏所有标记点和范围圈') +} + +/** + * 调整相机到显示范围圈区域 + * 根据当前搜索半径动态调整视角 + */ +const flyToBestViewForMarkers = () => { + const viewer = mapStore.viewer + if (!viewer) return + + const { camera } = mapStore.services() + + // 获取当前搜索半径(公里) + const radiusKm = disasterData.forcePreset.value.searchRadius + const radiusMeters = radiusKm * 1000 + + // 范围圈中心点 + const centerLon = DISASTER_CENTER.lon + const centerLat = DISASTER_CENTER.lat + + // 计算范围圈边界的4个点(东西南北) + // 1度纬度 ≈ 111km + const latOffset = radiusKm / 111.32 + // 1度经度 ≈ 111km * cos(纬度) + const lonOffset = radiusKm / (111.32 * Math.cos(Cesium.Math.toRadians(centerLat))) + + const boundaryPoints = [ + { lon: centerLon, lat: centerLat + latOffset }, // 北 + { lon: centerLon + lonOffset, lat: centerLat }, // 东 + { lon: centerLon, lat: centerLat - latOffset }, // 南 + { lon: centerLon - lonOffset, lat: centerLat }, // 西 + { lon: centerLon, lat: centerLat }, // 中心 + ] + + // 使用智能聚焦方法飞向范围圈区域 + camera.fitBoundsWithTrajectory(boundaryPoints, { + duration: 2, // 2秒飞行时间 + padding: 0.1, // 10%边距,确保范围圈完整显示 + }) + + console.log(`[index.vue] 相机已调整到范围圈视角 (半径: ${radiusKm}km)`) +} + // ==================== // 数据加载函数 // ==================== @@ -654,6 +879,62 @@ const loadReserveCentersAndPresets = async (longitude, latitude) => { } } +/** + * 统计应急基地与预置点、应急装备、应急物资、应急人员的数量 /snow-ops-platform/yhYjll/statistics + */ +const loadEmergencyBaseAndPreset = async (longitude, latitude, maxDistance) => { + try { + const response = await request({ + url: `/snow-ops-platform/yhYjll/statistics`, + method: 'GET', + params: { + longitude, + latitude, + maxDistance: disasterData.forcePreset.value.searchRadius, + }, + }) + if (response?.data) { + console.log('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量加载成功:', response.data) + } else { + console.warn('[index.vue] 应急基地与预置点、应急装备、应急物资、应急人员的数量接口返回数据为空') + } + return response + } catch (error) { + console.error('[index.vue] 加载应急基地与预置点、应急装备、应急物资、应急人员的数量失败:', error) + } + return null +} + +/** + * 查询应急装备和应急物资列表 /snow-ops-platform/yhYjll/listMaterials + */ +const loadEmergencyEquipmentAndMaterial = async (longitude, latitude) => { + try { + const response = await request({ + url: `/snow-ops-platform/yhYjll/listMaterials`, + method: 'GET', + params: { + longitude, + latitude, + maxDistance: disasterData.forcePreset.value.searchRadius, + }, + }) + if (response?.data) { + console.log('[index.vue] 应急装备和应急物资列表加载成功:', response.data) + } else { + console.warn('[index.vue] 应急装备和应急物资列表接口返回数据为空') + } + return response + } catch (error) { + console.error('[index.vue] 查询应急装备和应急物资列表失败:', error) + ElMessage.warning({ + message: '应急装备和应急物资列表数据加载失败', + duration: 3000, + }) + return null + } +} + // ==================== // 场景初始化函数 // ==================== @@ -779,6 +1060,10 @@ const initializeScene = async () => { // 9. 创建范围圈 createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius) + // 默认隐藏范围圈(等待快速匹配激活) + hideRangeCircle() + console.log('[index.vue] 已隐藏范围圈(等待快速匹配激活)') + // 10. 额外等待确保地形完全就绪(避免标记悬浮) console.log('[index.vue] 等待地形完全就绪...') await new Promise(resolve => setTimeout(resolve, 1000)) @@ -810,10 +1095,28 @@ const initializeScene = async () => { // 触发立即渲染,确保 CLAMP_TO_GROUND 生效 viewer.scene.requestRender() + // 默认隐藏模拟点位(等待快速匹配点击时显示) + viewer.entities.values.forEach((entity) => { + if (entity.properties) { + const props = entity.properties + // 隐藏模拟点位(soldier/device)和路径起点标记 + if (props.type?.getValue() === 'soldier' || + props.type?.getValue() === 'device' || + props.isPathStartMarker?.getValue()) { + entity.show = false + } + } + }) + console.log('[index.vue] 已隐藏模拟点位(等待快速匹配激活)') + // 11. 加载应急资源数据(在地形就绪后) console.log('[index.vue] 加载应急资源数据...') await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat) + // 默认隐藏接口标记(等待快速匹配激活) + hideMarkers() + console.log('[index.vue] 已隐藏应急资源标记(等待快速匹配激活)') + // 12. 加载储备中心和预置点数据 console.log('[index.vue] 加载储备中心和预置点数据...') await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat) @@ -862,6 +1165,18 @@ onUnmounted(() => { console.log('[index.vue] 资源清理完成') }) +// ==================== +// 防御性watch - 确保状态始终一致 +// ==================== + +// 作为保险机制,确保即使有新入口直接操作useDualMapCompare,状态也能保持一致 +watch(isCompareMode, (newValue) => { + if (!newValue && activeToolKey.value === COMPARE_TOOL_KEY) { + console.warn('[index.vue] 检测到对比模式被外部关闭,同步更新工具状态') + activeToolKey.value = null + } +}, { immediate: false }) + // ==================== // Provide 给子组件 // ====================