This commit is contained in:
huangchenhao 2025-11-25 16:12:34 +08:00
commit 8d9a0112ce
7 changed files with 475 additions and 71 deletions

View File

@ -13,7 +13,11 @@
<DisasterAnalysis />
</CollapsiblePanel>
<CollapsiblePanel title="快速匹配" subtitle="「力量预置」">
<CollapsiblePanel
title="快速匹配"
subtitle="「力量预置」"
@title-click="handleForcePresetToggle"
>
<ForcePreset />
</CollapsiblePanel>
@ -110,7 +114,7 @@ const handleCloseVideoModal = () => {
}
//
const emit = defineEmits(['start-dispatch', 'view-plan'])
const emit = defineEmits(['start-dispatch', 'view-plan', 'force-preset-toggle'])
/**
* 处理力量调度启动事件向上传递给父组件
@ -125,6 +129,14 @@ const handleStartDispatch = (payload) => {
const handleViewPlan = (plan) => {
emit('view-plan', plan)
}
/**
* 处理快速匹配面板标题点击事件
*/
const handleForcePresetToggle = () => {
console.log('[LeftPanel] 快速匹配标题被点击')
emit('force-preset-toggle')
}
</script>
<style scoped lang="scss">

View File

@ -41,14 +41,23 @@
</template>
<script setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { MAP_TOOLS, DEVICE_WATCH } from "../../constants";
// props
const props = defineProps({
//
activeToolKey: {
type: String,
default: null
}
})
//
const isWatchingDevice = ref(false);
//
const activeTool = ref('modelCompare');
// - 使computedprops
const activeTool = computed(() => props.activeToolKey)
//
const emit = defineEmits(["device-watch", "tool-change"]);
@ -105,15 +114,13 @@ const handleDeviceWatch = () => {
*/
const handleToolClick = (toolKey) => {
//
if (activeTool.value === toolKey) {
activeTool.value = null;
} else {
activeTool.value = toolKey;
}
console.log("切换地图工具:", toolKey);
const newActiveState = activeTool.value === toolKey ? null : toolKey
const isActive = newActiveState === toolKey
console.log("切换地图工具:", toolKey, "激活状态:", isActive);
emit("tool-change", {
tool: toolKey,
active: activeTool.value === toolKey,
active: isActive,
});
};
</script>

View File

@ -7,6 +7,7 @@
<!-- 延迟渲染确保目标元素已存在 -->
<Teleport to="#sa-controls" v-if="isMounted">
<MapControls
:active-tool-key="activeToolKey"
@tool-change="handleToolChange"
@device-watch="handleDeviceWatch"
/>
@ -19,6 +20,15 @@ import { ref, onMounted } from 'vue'
import { MapViewport } from '@/map'
import MapControls from './MapControls.vue'
// props
const props = defineProps({
//
activeToolKey: {
type: String,
default: null
}
})
/**
* 向外抛出的事件
* @event tool-change - 地图工具变化事件包含 { tool: string, active: boolean }

View File

@ -2,8 +2,7 @@
<section class="collapsible-panel" :class="{ 'collapsible-panel--collapsed': isCollapsed }">
<div
class="collapsible-panel__header"
:class="{ 'collapsible-panel__header--clickable': collapsible }"
@click="collapsible && toggle()"
@click="handleHeaderClick"
>
<PanelHeader :title="title" :subtitle="subtitle">
<template #title-icon>
@ -85,7 +84,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:collapsed', 'toggle'])
const emit = defineEmits(['update:collapsed', 'toggle', 'title-click'])
//
const isControlled = computed(() => props.collapsed !== undefined)
@ -108,6 +107,11 @@ const isCollapsed = computed({
}
})
//
function handleHeaderClick() {
emit('title-click')
}
// /
function toggle() {
if (!props.collapsible) return

View File

@ -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,13 +284,33 @@ 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) => {
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
}
}
// 清理

View File

@ -176,7 +176,8 @@ export function useMapMarkers() {
pixelSize: 12,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
})
entities.push(pointEntity)
@ -654,7 +655,8 @@ export function useMapMarkers() {
width: 48,
height: 48,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: resolveBillboardHeightReference(result.samplingSucceeded)
heightReference: resolveBillboardHeightReference(result.samplingSucceeded),
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
properties: {
type,
@ -741,7 +743,8 @@ export function useMapMarkers() {
width: 48,
height: 48,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: resolveBillboardHeightReference(result.samplingSucceeded)
heightReference: resolveBillboardHeightReference(result.samplingSucceeded),
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
properties: {
type: 'station',

View File

@ -24,7 +24,10 @@
<!-- 右侧地图主地图 - 灾后场景 -->
<div class="situational-awareness__right-map">
<MapViewer @tool-change="handleMapToolChange" />
<MapViewer
:active-tool-key="activeToolKey"
@tool-change="handleMapToolChange"
/>
</div>
</div>
@ -57,6 +60,7 @@
<LeftPanel
@start-dispatch="handleStartDispatch"
@view-plan="handleViewPlan"
@force-preset-toggle="handleForcePresetToggle"
/>
</div>
</Transition>
@ -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,14 +543,117 @@ const handleModalStartDispatch = () => {
})
}
/**
* 统一的对比模式状态管理助手
* 职责:
* 1. 同步管理 activeToolKey isCompareMode
* 2. 提供失败回滚机制
* 3. 防止并发操作
*
* @param {boolean} shouldActivate - 是否激活对比模式
* @param {string} source - 调用来源(用于日志追踪)
* @returns {Promise<boolean>} 操作是否成功
*/
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 {
if (tool === COMPARE_TOOL_KEY) {
// : 使
const loadingMessage = ElMessage({
message: active ? '正在启用模型对比...' : '正在关闭模型对比...',
type: 'info',
@ -534,17 +661,18 @@ const handleMapToolChange = async ({ tool, active }) => {
showClose: false,
})
await toggleCompareMode(active, mapStore.viewer)
const success = await setCompareToolState(active, 'map-controls')
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
}
}
// ====================
//
// ====================
@ -694,28 +975,30 @@ const initializeScene = async () => {
},
})
// 3.
const allMockPoints = mockDataService.getAllMockPoints()
allMockPoints.forEach((point) => {
const config =
point.type === 'soldier'
? mockDataService.createPersonnelEntityConfig(point, soldierIcon)
: mockDataService.createDeviceEntityConfig(point, deviceIcon)
viewer.entities.add(config)
})
console.log(`[index.vue] 已添加 ${allMockPoints.length} 个模拟点位`)
// 3. - 10.5
//
// const allMockPoints = mockDataService.getAllMockPoints()
// allMockPoints.forEach((point) => {
// const config =
// point.type === 'soldier'
// ? mockDataService.createPersonnelEntityConfig(point, soldierIcon)
// : mockDataService.createDeviceEntityConfig(point, deviceIcon)
// viewer.entities.add(config)
// })
// console.log(`[index.vue] ${allMockPoints.length} `)
// 4. ""
const allPaths = mockDataService.getAllAnimationPaths()
Object.entries(allPaths).forEach(([pathId, path]) => {
const icon = path.metadata.type === 'soldier' ? soldierIcon : deviceIcon
const config = mockDataService.createPathStartMarkerConfig(
{ ...path, id: pathId },
icon
)
viewer.entities.add(config)
})
console.log('[index.vue] 已添加 3 个路径起点标记')
// 4. ""- 10.5
//
// const allPaths = mockDataService.getAllAnimationPaths()
// Object.entries(allPaths).forEach(([pathId, path]) => {
// const icon = path.metadata.type === 'soldier' ? soldierIcon : deviceIcon
// const config = mockDataService.createPathStartMarkerConfig(
// { ...path, id: pathId },
// icon
// )
// viewer.entities.add(config)
// })
// console.log('[index.vue] 3 ')
// 5.
mapClickHandler.setupClickHandler(viewer, registerEventHandler, registerPostRenderListener)
@ -777,14 +1060,63 @@ 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))
// 10.5.
console.log('[index.vue] 添加模拟点位...')
const allMockPoints = mockDataService.getAllMockPoints()
allMockPoints.forEach((point) => {
const config =
point.type === 'soldier'
? mockDataService.createPersonnelEntityConfig(point, soldierIcon)
: mockDataService.createDeviceEntityConfig(point, deviceIcon)
viewer.entities.add(config)
})
console.log(`[index.vue] 已添加 ${allMockPoints.length} 个模拟点位`)
// ""
const allPaths = mockDataService.getAllAnimationPaths()
Object.entries(allPaths).forEach(([pathId, path]) => {
const icon = path.metadata.type === 'soldier' ? soldierIcon : deviceIcon
const config = mockDataService.createPathStartMarkerConfig(
{ ...path, id: pathId },
icon
)
viewer.entities.add(config)
})
console.log('[index.vue] 已添加 3 个路径起点标记')
// 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)
@ -833,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
// ====================