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 给子组件
// ====================