2025-11-16 14:43:35 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="situational-awareness">
|
|
|
|
|
|
<!-- 顶部导航栏 -->
|
|
|
|
|
|
<PageHeader @back="handleBack" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主内容区域 -->
|
|
|
|
|
|
<div class="situational-awareness__main">
|
2025-11-17 11:12:56 +08:00
|
|
|
|
<!-- 地图底层 -->
|
2025-11-18 21:24:31 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="situational-awareness__map-layer"
|
|
|
|
|
|
:class="{ 'is-compare-mode': isCompareMode }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 左侧地图容器(对比模式下显示灾前场景) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
id="leftCesiumContainer"
|
|
|
|
|
|
class="situational-awareness__left-map"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 中间分割线 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="isCompareMode"
|
|
|
|
|
|
class="situational-awareness__center-divider"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧地图(主地图 - 灾后场景) -->
|
|
|
|
|
|
<div class="situational-awareness__right-map">
|
2025-11-25 14:46:53 +08:00
|
|
|
|
<MapViewer
|
|
|
|
|
|
:active-tool-key="activeToolKey"
|
|
|
|
|
|
@tool-change="handleMapToolChange"
|
|
|
|
|
|
/>
|
2025-11-18 21:24:31 +08:00
|
|
|
|
</div>
|
2025-11-19 17:06:05 +08:00
|
|
|
|
</div>
|
2025-11-19 14:04:48 +08:00
|
|
|
|
|
2025-11-19 17:06:05 +08:00
|
|
|
|
<!-- 地图遮罩层 -->
|
|
|
|
|
|
<div class="situational-awareness__map-mask" aria-hidden="true"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 场景标签层 - 独立层级,显示在遮罩层之上 -->
|
|
|
|
|
|
<div class="situational-awareness__scene-labels-layer">
|
2025-11-19 14:04:48 +08:00
|
|
|
|
<!-- 灾前现场实景标签 - 在中间分割线左侧 -->
|
|
|
|
|
|
<SceneLabel
|
|
|
|
|
|
v-if="isCompareMode"
|
|
|
|
|
|
text="灾前现场实景"
|
|
|
|
|
|
position="center-left"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 灾后现场实景标签 - 在右侧面板左边 -->
|
|
|
|
|
|
<SceneLabel
|
|
|
|
|
|
text="灾后现场实景"
|
|
|
|
|
|
position="right-left"
|
|
|
|
|
|
/>
|
2025-11-17 11:12:56 +08:00
|
|
|
|
</div>
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
|
<!-- 浮动面板层 -->
|
|
|
|
|
|
<div class="situational-awareness__panels-layer">
|
2025-11-19 17:06:05 +08:00
|
|
|
|
<Transition name="panel-slide-left">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-show="!isLeftPanelCollapsed"
|
|
|
|
|
|
class="situational-awareness__panel-column situational-awareness__panel-column--left"
|
|
|
|
|
|
>
|
2025-11-21 17:00:10 +08:00
|
|
|
|
<LeftPanel
|
|
|
|
|
|
@start-dispatch="handleStartDispatch"
|
|
|
|
|
|
@view-plan="handleViewPlan"
|
2025-11-25 14:46:53 +08:00
|
|
|
|
@force-preset-toggle="handleForcePresetToggle"
|
2025-11-26 18:05:35 +08:00
|
|
|
|
@quick-response-toggle="handleQuickResponseToggle"
|
2025-11-21 17:00:10 +08:00
|
|
|
|
/>
|
2025-11-19 17:06:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
|
|
|
|
<Transition name="panel-slide-right">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-show="!isRightPanelCollapsed"
|
|
|
|
|
|
class="situational-awareness__panel-column situational-awareness__panel-column--right"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RightPanel />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 折叠按钮层 -->
|
|
|
|
|
|
<div class="situational-awareness__collapse-buttons-layer">
|
|
|
|
|
|
<!-- 左侧折叠按钮 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="situational-awareness__collapse-btn situational-awareness__collapse-btn--left"
|
|
|
|
|
|
:class="{ 'is-collapsed': isLeftPanelCollapsed }"
|
|
|
|
|
|
@click="toggleLeftPanel"
|
|
|
|
|
|
:aria-label="isLeftPanelCollapsed ? '展开左侧面板' : '收起左侧面板'"
|
2025-11-18 21:24:31 +08:00
|
|
|
|
>
|
2025-11-19 17:06:05 +08:00
|
|
|
|
<img
|
|
|
|
|
|
:src="isLeftPanelCollapsed ? collapseRightArrow : collapseLeftArrow"
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
class="collapse-arrow"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧折叠按钮 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="situational-awareness__collapse-btn situational-awareness__collapse-btn--right"
|
|
|
|
|
|
:class="{ 'is-collapsed': isRightPanelCollapsed }"
|
|
|
|
|
|
@click="toggleRightPanel"
|
|
|
|
|
|
:aria-label="isRightPanelCollapsed ? '展开右侧面板' : '收起右侧面板'"
|
2025-11-18 21:24:31 +08:00
|
|
|
|
>
|
2025-11-19 17:06:05 +08:00
|
|
|
|
<img
|
|
|
|
|
|
:src="isRightPanelCollapsed ? collapseLeftArrow : collapseRightArrow"
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
class="collapse-arrow"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
2025-11-17 11:12:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 地图控件层 - 高于遮罩和面板 -->
|
|
|
|
|
|
<div class="situational-awareness__controls-layer">
|
|
|
|
|
|
<div id="sa-controls" class="situational-awareness__controls"></div>
|
|
|
|
|
|
</div>
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 地图 Tooltip 层 - 用于显示地图标记点的轻量级信息提示框 -->
|
|
|
|
|
|
<div class="situational-awareness__tooltip-layer">
|
|
|
|
|
|
<MapTooltip
|
2025-11-24 16:50:43 +08:00
|
|
|
|
:visible="mapTooltip.visible"
|
2025-11-18 21:24:31 +08:00
|
|
|
|
:x="mapTooltip.x"
|
|
|
|
|
|
:y="mapTooltip.y"
|
|
|
|
|
|
:title="mapTooltip.title"
|
|
|
|
|
|
:icon="mapTooltip.icon"
|
|
|
|
|
|
:z-index="mapTooltip.zIndex"
|
2025-11-24 13:47:51 +08:00
|
|
|
|
:show-video-icon="mapTooltip.data && mapTooltip.data.supportVideo"
|
|
|
|
|
|
:video-icon-src="mediaIcon"
|
|
|
|
|
|
@video-icon-click="handleVideoIconClick"
|
2025-11-18 21:24:31 +08:00
|
|
|
|
@close="handleMapTooltipClose"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- Tooltip 内容插槽 - 根据实际业务数据渲染 -->
|
|
|
|
|
|
<template v-if="mapTooltip.data && mapTooltip.data.fields">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(field, index) in mapTooltip.data.fields"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="tooltip-field-item"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="tooltip-field-label">{{ field.label }}:</span>
|
|
|
|
|
|
<span class="tooltip-field-value">{{ field.value }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-11-20 18:09:04 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 操作按钮插槽 -->
|
|
|
|
|
|
<template v-if="mapTooltip.data && mapTooltip.data.actions" #actions>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(action, index) in mapTooltip.data.actions"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="tooltip-action-btn"
|
|
|
|
|
|
@click="handleTooltipAction(action)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ action.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
2025-11-18 21:24:31 +08:00
|
|
|
|
</MapTooltip>
|
|
|
|
|
|
</div>
|
2025-11-19 14:04:48 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 加载动画层 - 一键启动后显示 -->
|
|
|
|
|
|
<div v-if="showLoading" class="situational-awareness__loading-layer">
|
|
|
|
|
|
<img
|
2025-11-25 10:09:59 +08:00
|
|
|
|
src="./assets/images/加载3.gif"
|
2025-11-19 14:04:48 +08:00
|
|
|
|
alt="加载中"
|
|
|
|
|
|
class="situational-awareness__loading-gif"
|
|
|
|
|
|
/>
|
2025-11-25 09:46:56 +08:00
|
|
|
|
<!-- <img
|
|
|
|
|
|
src="./assets/images/加载.gif"
|
|
|
|
|
|
alt="加载中"
|
|
|
|
|
|
class="situational-awareness__loading-gif"
|
|
|
|
|
|
/> -->
|
2025-11-19 14:04:48 +08:00
|
|
|
|
</div>
|
2025-11-16 14:43:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 弹窗组件 -->
|
|
|
|
|
|
<PersonnelDetail
|
|
|
|
|
|
:visible="showPersonnelDetail"
|
|
|
|
|
|
:personnel-data="selectedPersonnel"
|
|
|
|
|
|
@close="showPersonnelDetail = false"
|
|
|
|
|
|
@link="handlePersonnelLink"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<EmergencyCenterDetail
|
|
|
|
|
|
:visible="showCenterDetail"
|
|
|
|
|
|
:center-data="selectedCenter"
|
|
|
|
|
|
@close="showCenterDetail = false"
|
|
|
|
|
|
/>
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-24 13:47:51 +08:00
|
|
|
|
<!-- 视频监控弹窗 -->
|
|
|
|
|
|
<VideoModal
|
|
|
|
|
|
v-if="showVideoModal"
|
|
|
|
|
|
:visible="showVideoModal"
|
|
|
|
|
|
:monitor="selectedVideoMonitor"
|
|
|
|
|
|
@close="handleVideoModalClose"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-11-21 17:00:10 +08:00
|
|
|
|
<!-- 智能应急方案弹窗 -->
|
|
|
|
|
|
<StretchableModal
|
2025-11-24 16:50:43 +08:00
|
|
|
|
:visible="showStretchableModal"
|
2025-11-21 17:00:10 +08:00
|
|
|
|
title="公路灾害现场处置推荐方案"
|
|
|
|
|
|
:close-on-click-modal="true"
|
|
|
|
|
|
:close-on-press-escape="true"
|
|
|
|
|
|
:show-close="true"
|
2025-11-25 10:05:30 +08:00
|
|
|
|
@close="showStretchableModal = false"
|
2025-11-21 17:00:10 +08:00
|
|
|
|
width="clamp(100px, 50vw, 1400px)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 使用应急方案内容组件 -->
|
2025-11-21 18:16:29 +08:00
|
|
|
|
<EmergencyPlanContent :stations="disasterData.forcePreset.value.stations" />
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 底部一键启动按钮 -->
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<ActionButton
|
2025-11-25 11:34:31 +08:00
|
|
|
|
text="响应调度"
|
2025-11-21 17:00:10 +08:00
|
|
|
|
type="primary"
|
|
|
|
|
|
size="medium"
|
|
|
|
|
|
@click="handleModalStartDispatch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</StretchableModal>
|
2025-11-16 14:43:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 3D 态势感知主页面(重构版)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 架构说明:
|
|
|
|
|
|
* - 使用 Composition API 和 Composable 模式
|
|
|
|
|
|
* - 生命周期管理:useCesiumLifecycle 统一管理资源
|
|
|
|
|
|
* - 功能模块化:每个功能由独立的 composable 管理
|
|
|
|
|
|
* - 数据层分离:硬编码数据移至 constants 目录
|
|
|
|
|
|
*
|
|
|
|
|
|
* 主要职责:
|
|
|
|
|
|
* 1. 组件布局和模板渲染
|
|
|
|
|
|
* 2. 子组件编排和事件转发
|
|
|
|
|
|
* 3. 生命周期入口(初始化和清理)
|
|
|
|
|
|
* 4. 用户交互事件处理
|
|
|
|
|
|
*/
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
import { ref, provide, onMounted, onUnmounted, watch } from 'vue'
|
2025-11-25 09:35:28 +08:00
|
|
|
|
import * as Cesium from 'cesium'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 组件导入 ==========
|
|
|
|
|
|
import PageHeader from './components/PageHeader.vue'
|
|
|
|
|
|
import LeftPanel from './components/LeftPanel/index.vue'
|
|
|
|
|
|
import MapViewer from './components/MapViewer/index.vue'
|
|
|
|
|
|
import RightPanel from './components/RightPanel/index.vue'
|
|
|
|
|
|
import PersonnelDetail from './components/Popups/PersonnelDetail.vue'
|
|
|
|
|
|
import EmergencyCenterDetail from './components/Popups/EmergencyCenterDetail.vue'
|
|
|
|
|
|
import VideoModal from './components/RightPanel/VideoModal.vue'
|
|
|
|
|
|
import MapTooltip from './components/shared/MapTooltip.vue'
|
|
|
|
|
|
import SceneLabel from './components/SceneLabel.vue'
|
|
|
|
|
|
import StretchableModal from './components/shared/StretchableModal.vue'
|
|
|
|
|
|
import ActionButton from './components/shared/ActionButton.vue'
|
|
|
|
|
|
import EmergencyPlanContent from './components/shared/EmergencyPlanContent.vue'
|
|
|
|
|
|
|
|
|
|
|
|
// ========== Composables 导入 ==========
|
|
|
|
|
|
import { useDisasterData } from './composables/useDisasterData'
|
|
|
|
|
|
import { useDualMapCompare } from './composables/useDualMapCompare'
|
|
|
|
|
|
import { useMapMarkers } from './composables/useMapMarkers'
|
|
|
|
|
|
import { use3DTiles } from './composables/use3DTiles'
|
|
|
|
|
|
import { useEntityAnimation } from './composables/useEntityAnimation'
|
|
|
|
|
|
|
|
|
|
|
|
// 新的 composables
|
|
|
|
|
|
import { useCesiumLifecycle } from './composables/useCesiumLifecycle'
|
|
|
|
|
|
import { useMapTooltip } from './composables/useMapTooltip'
|
|
|
|
|
|
import { useMockData } from './composables/useMockData'
|
|
|
|
|
|
import { useMapClickHandler } from './composables/useMapClickHandler'
|
|
|
|
|
|
import { useRangeCircle } from './composables/useRangeCircle'
|
|
|
|
|
|
import { usePathLines } from './composables/usePathLines'
|
|
|
|
|
|
import { useEmergencyDispatch } from './composables/useEmergencyDispatch'
|
2025-11-26 18:05:35 +08:00
|
|
|
|
import { useEmergencyRouteSelection } from './composables/useEmergencyRouteSelection'
|
|
|
|
|
|
import { useSimulatedMarkers } from './composables/useSimulatedMarkers'
|
2025-11-25 09:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 工具和常量导入 ==========
|
|
|
|
|
|
import { useMapStore } from '@/map'
|
|
|
|
|
|
import { request } from '@shared/utils/request'
|
|
|
|
|
|
import {
|
|
|
|
|
|
DISASTER_CENTER,
|
|
|
|
|
|
DEFAULT_CAMERA_CARTESIAN,
|
|
|
|
|
|
DEFAULT_CAMERA_POSITION,
|
|
|
|
|
|
DEFAULT_CAMERA_VIEW,
|
|
|
|
|
|
DEFAULT_SEARCH_RADIUS,
|
|
|
|
|
|
MARKER_ICON_SIZE,
|
|
|
|
|
|
} from './constants'
|
2025-11-26 18:05:35 +08:00
|
|
|
|
import { calculateDistance, isValidCoordinate } from './utils/geoUtils'
|
2025-11-25 09:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 图标资源导入 ==========
|
|
|
|
|
|
import emergencyCenterIcon from './assets/images/应急中心.png'
|
|
|
|
|
|
import eventIcon from './assets/images/事件icon.png'
|
|
|
|
|
|
import soldierIcon from './assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png'
|
|
|
|
|
|
import deviceIcon from './assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png'
|
|
|
|
|
|
import emergencyBaseIcon from './assets/images/应急基地.png'
|
|
|
|
|
|
import reserveCenterIcon from './assets/images/储备中心.png'
|
2025-11-25 19:21:38 +08:00
|
|
|
|
import cityEmergencyIcon from './assets/images/市应急点.png'
|
|
|
|
|
|
import districtEmergencyIcon from './assets/images/区县应急点.png'
|
|
|
|
|
|
import otherEmergencyIcon from './assets/images/其他应急点.png'
|
2025-11-25 09:35:28 +08:00
|
|
|
|
import mediaIcon from './assets/images/media.png'
|
|
|
|
|
|
import collapseLeftArrow from './assets/images/折叠面板左箭头.png'
|
|
|
|
|
|
import collapseRightArrow from './assets/images/折叠面板右箭头.png'
|
|
|
|
|
|
|
|
|
|
|
|
// ====================
|
|
|
|
|
|
// 数据层
|
|
|
|
|
|
// ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 使用灾害数据 composable
|
|
|
|
|
|
const disasterData = useDisasterData()
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 地图 store
|
|
|
|
|
|
const mapStore = useMapStore()
|
2025-11-20 16:59:33 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 生命周期管理
|
|
|
|
|
|
// ====================
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// Cesium 资源生命周期管理
|
|
|
|
|
|
const {
|
|
|
|
|
|
registerEventHandler,
|
|
|
|
|
|
registerPostRenderListener,
|
|
|
|
|
|
registerTimeout,
|
|
|
|
|
|
cleanup,
|
|
|
|
|
|
} = useCesiumLifecycle()
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 地图功能模块
|
|
|
|
|
|
// ====================
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 双地图对比功能
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const { isCompareMode, toggleCompareMode } = useDualMapCompare()
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 地图标记功能
|
|
|
|
|
|
const {
|
|
|
|
|
|
initializeMarkers,
|
|
|
|
|
|
addEmergencyResourceMarkers,
|
|
|
|
|
|
clearEmergencyResourceMarkers,
|
2025-11-20 16:59:33 +08:00
|
|
|
|
addReserveCenterMarkers,
|
|
|
|
|
|
clearReserveCenterMarkers,
|
2025-11-25 14:46:53 +08:00
|
|
|
|
showMarkers,
|
|
|
|
|
|
hideMarkers,
|
|
|
|
|
|
markerEntities,
|
2025-11-25 18:16:46 +08:00
|
|
|
|
reserveCenterEntities,
|
|
|
|
|
|
emergencyResourceEntities,
|
2025-11-25 19:21:38 +08:00
|
|
|
|
drawCollapseBoundary,
|
|
|
|
|
|
drawCollapseBoundaryLeft,
|
|
|
|
|
|
clearCollapseBoundary,
|
|
|
|
|
|
showCollapseBoundary,
|
|
|
|
|
|
hideCollapseBoundary,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
} = useMapMarkers()
|
|
|
|
|
|
|
|
|
|
|
|
// 3D Tiles 加载
|
|
|
|
|
|
const { load3DTileset, waitForTilesetReady } = use3DTiles()
|
|
|
|
|
|
|
|
|
|
|
|
// 地图 Tooltip
|
|
|
|
|
|
const tooltipComposable = useMapTooltip()
|
|
|
|
|
|
const { tooltipState: mapTooltip, showTooltip, hideTooltip } = tooltipComposable
|
|
|
|
|
|
|
|
|
|
|
|
// 实体动画
|
|
|
|
|
|
const entityAnimationComposable = useEntityAnimation()
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟数据服务
|
|
|
|
|
|
const mockDataService = useMockData()
|
|
|
|
|
|
|
|
|
|
|
|
// 范围圈管理
|
|
|
|
|
|
const rangeCircleComposable = useRangeCircle()
|
2025-11-25 14:46:53 +08:00
|
|
|
|
const {
|
|
|
|
|
|
rangeCircleEntity,
|
|
|
|
|
|
createOrUpdateRangeCircle,
|
|
|
|
|
|
clearRangeCircle,
|
|
|
|
|
|
showRangeCircle,
|
|
|
|
|
|
hideRangeCircle,
|
|
|
|
|
|
} = rangeCircleComposable
|
2025-11-25 09:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 路径线管理
|
|
|
|
|
|
const pathLinesComposable = usePathLines()
|
|
|
|
|
|
|
|
|
|
|
|
// 地图点击事件处理
|
|
|
|
|
|
const mapClickHandler = useMapClickHandler({
|
|
|
|
|
|
tooltipComposable,
|
|
|
|
|
|
icons: {
|
|
|
|
|
|
soldierIcon,
|
|
|
|
|
|
deviceIcon,
|
|
|
|
|
|
emergencyCenterIcon,
|
|
|
|
|
|
emergencyBaseIcon,
|
|
|
|
|
|
reserveCenterIcon,
|
2025-11-25 19:21:38 +08:00
|
|
|
|
cityEmergencyIcon,
|
|
|
|
|
|
districtEmergencyIcon,
|
|
|
|
|
|
otherEmergencyIcon,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
},
|
|
|
|
|
|
rangeCircleEntity,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 应急调度流程
|
2025-11-26 18:05:35 +08:00
|
|
|
|
const { showLoading, loadingMessage, startDispatch, startDispatchWithRouting } = useEmergencyDispatch({
|
2025-11-25 09:35:28 +08:00
|
|
|
|
pathLinesComposable,
|
|
|
|
|
|
entityAnimationComposable,
|
|
|
|
|
|
mapStore,
|
|
|
|
|
|
registerTimeoutFn: registerTimeout,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 应急点选择
|
|
|
|
|
|
const { selectByType } = useEmergencyRouteSelection()
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟标记管理
|
|
|
|
|
|
const { createSimulatedMarkers, clearSimulatedMarkers, hideEmergencyMarkers, getEmergencyMarkerData } =
|
|
|
|
|
|
useSimulatedMarkers()
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// UI 状态
|
|
|
|
|
|
// ====================
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 面板折叠状态
|
|
|
|
|
|
const isLeftPanelCollapsed = ref(false)
|
|
|
|
|
|
const isRightPanelCollapsed = ref(false)
|
2025-11-20 18:09:04 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 标记点和范围圈显示状态
|
|
|
|
|
|
const showMarkersAndRange = ref(false)
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 快速响应执行状态
|
|
|
|
|
|
const quickResponseExecuted = ref(false)
|
|
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 地图工具激活状态 - 默认激活模型对比
|
|
|
|
|
|
const activeToolKey = ref('modelCompare')
|
|
|
|
|
|
|
|
|
|
|
|
// 工具键常量
|
|
|
|
|
|
const COMPARE_TOOL_KEY = 'modelCompare'
|
|
|
|
|
|
|
|
|
|
|
|
// 对比模式切换锁,防止并发操作
|
|
|
|
|
|
const isCompareTogglePending = ref(false)
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 弹窗状态
|
|
|
|
|
|
const showPersonnelDetail = ref(false)
|
|
|
|
|
|
const showCenterDetail = ref(false)
|
|
|
|
|
|
const showStretchableModal = ref(false)
|
|
|
|
|
|
const showVideoModal = ref(false)
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 选中的数据
|
|
|
|
|
|
const selectedPersonnel = ref({
|
|
|
|
|
|
name: '张强',
|
|
|
|
|
|
department: '安全生产部',
|
|
|
|
|
|
distance: 0.6,
|
|
|
|
|
|
estimatedArrival: 10,
|
|
|
|
|
|
avatar: null,
|
|
|
|
|
|
})
|
2025-11-19 14:04:48 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const selectedCenter = ref({
|
|
|
|
|
|
name: '忠县应急中心',
|
|
|
|
|
|
adminLevel: '国道',
|
|
|
|
|
|
department: '交通公路部门',
|
|
|
|
|
|
distance: 0.6,
|
|
|
|
|
|
image: null,
|
|
|
|
|
|
})
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const selectedVideoMonitor = ref({
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
videoSrc: '',
|
|
|
|
|
|
dateRange: '',
|
|
|
|
|
|
hasMegaphone: false,
|
|
|
|
|
|
hasAudio: true,
|
|
|
|
|
|
hasDirectionControl: false,
|
|
|
|
|
|
})
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 事件处理函数
|
|
|
|
|
|
// ====================
|
2025-11-19 17:06:05 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 切换左侧面板
|
|
|
|
|
|
*/
|
2025-11-19 17:06:05 +08:00
|
|
|
|
const toggleLeftPanel = () => {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
isLeftPanelCollapsed.value = !isLeftPanelCollapsed.value
|
|
|
|
|
|
}
|
2025-11-19 17:06:05 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 切换右侧面板
|
|
|
|
|
|
*/
|
2025-11-19 17:06:05 +08:00
|
|
|
|
const toggleRightPanel = () => {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
isRightPanelCollapsed.value = !isRightPanelCollapsed.value
|
|
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
|
2025-11-19 11:13:29 +08:00
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 返回驾驶舱
|
2025-11-19 11:13:29 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const handleBack = () => {
|
|
|
|
|
|
console.log('返回驾驶舱')
|
|
|
|
|
|
// 实际实现:路由跳转
|
|
|
|
|
|
// router.push('/cockpit')
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 处理人员联动
|
2025-11-19 11:13:29 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const handlePersonnelLink = (personnel) => {
|
|
|
|
|
|
console.log('联动人员:', personnel)
|
|
|
|
|
|
showPersonnelDetail.value = false
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 Tooltip 操作按钮点击事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleTooltipAction = (action) => {
|
|
|
|
|
|
console.log('[index.vue] Tooltip 操作按钮点击:', action)
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
if (action.type === 'link') {
|
|
|
|
|
|
// 应急人员的"联动"操作
|
|
|
|
|
|
ElMessage.success(`已联动应急人员: ${action.data.name?.getValue() || '未知'}`)
|
|
|
|
|
|
hideTooltip()
|
|
|
|
|
|
} else if (action.type === 'connect') {
|
|
|
|
|
|
// 应急基地的"连线"操作
|
|
|
|
|
|
ElMessage.success(`已连线应急基地: ${action.data.name?.getValue() || '未知'}`)
|
|
|
|
|
|
hideTooltip()
|
2025-11-19 11:13:29 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 关闭地图 Tooltip
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleMapTooltipClose = () => {
|
|
|
|
|
|
mapTooltip.value.visible = false
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理视频图标点击事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleVideoIconClick = () => {
|
|
|
|
|
|
const { data } = mapTooltip.value
|
|
|
|
|
|
if (data && data.supportVideo) {
|
|
|
|
|
|
selectedVideoMonitor.value = {
|
|
|
|
|
|
id: Date.now().toString(),
|
|
|
|
|
|
title: data.videoTitle || '视频监控',
|
|
|
|
|
|
videoSrc: data.videoSrc || '',
|
|
|
|
|
|
dateRange: new Date().toLocaleDateString(),
|
|
|
|
|
|
hasMegaphone: false,
|
|
|
|
|
|
hasAudio: true,
|
|
|
|
|
|
hasDirectionControl: false,
|
2025-11-19 11:13:29 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
showVideoModal.value = true
|
2025-11-19 11:13:29 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理视频弹窗关闭事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleVideoModalClose = () => {
|
|
|
|
|
|
showVideoModal.value = false
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理力量调度启动事件
|
|
|
|
|
|
*/
|
2025-11-26 18:05:35 +08:00
|
|
|
|
const handleStartDispatch = async (payload) => {
|
|
|
|
|
|
console.log('[index.vue] 启动响应调度')
|
|
|
|
|
|
|
|
|
|
|
|
// Check if Quick Response has been executed
|
|
|
|
|
|
if (!quickResponseExecuted.value) {
|
|
|
|
|
|
console.log('[index.vue] 快速响应未执行,自动执行...')
|
|
|
|
|
|
await executeQuickResponse()
|
|
|
|
|
|
quickResponseExecuted.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get emergency marker data
|
|
|
|
|
|
const emergencyMarkerData = getEmergencyMarkerData()
|
|
|
|
|
|
|
|
|
|
|
|
if (emergencyMarkerData.length === 0) {
|
|
|
|
|
|
ElMessage.error('未找到应急点数据')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start dispatch with routing using emergency points
|
|
|
|
|
|
startDispatchWithRouting({
|
|
|
|
|
|
emergencyPoints: emergencyMarkerData.map(marker => ({
|
|
|
|
|
|
lon: marker.markerCoords.lon,
|
|
|
|
|
|
lat: marker.markerCoords.lat,
|
|
|
|
|
|
type: marker.type === 'emergencyPersonnel' ? 'personnel' : 'equipment',
|
|
|
|
|
|
name: marker.name
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理查看智能应急方案事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleViewPlan = (plan) => {
|
|
|
|
|
|
console.log('[index.vue] 查看智能应急方案:', plan)
|
|
|
|
|
|
showStretchableModal.value = true
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理弹窗中的一键启动按钮点击事件
|
|
|
|
|
|
*/
|
2025-11-26 18:05:35 +08:00
|
|
|
|
const handleModalStartDispatch = async () => {
|
|
|
|
|
|
console.log('[index.vue] 弹窗中点击一键启动 - 使用智能路径规划')
|
2025-11-25 09:35:28 +08:00
|
|
|
|
showStretchableModal.value = false
|
2025-11-26 18:05:35 +08:00
|
|
|
|
|
|
|
|
|
|
// Check if Quick Response has been executed
|
|
|
|
|
|
if (!quickResponseExecuted.value) {
|
|
|
|
|
|
console.log('[index.vue] 快速响应未执行,自动执行...')
|
|
|
|
|
|
await executeQuickResponse()
|
|
|
|
|
|
quickResponseExecuted.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get emergency marker data
|
|
|
|
|
|
const emergencyMarkerData = getEmergencyMarkerData()
|
|
|
|
|
|
|
|
|
|
|
|
if (emergencyMarkerData.length === 0) {
|
|
|
|
|
|
ElMessage.error('未找到应急点数据')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Use emergency marker data for routing
|
|
|
|
|
|
startDispatchWithRouting({
|
|
|
|
|
|
emergencyPoints: emergencyMarkerData.map(marker => ({
|
|
|
|
|
|
lon: marker.markerCoords.lon,
|
|
|
|
|
|
lat: marker.markerCoords.lat,
|
|
|
|
|
|
type: marker.type === 'emergencyPersonnel' ? 'personnel' : 'equipment',
|
|
|
|
|
|
name: marker.name
|
|
|
|
|
|
}))
|
2025-11-25 09:35:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 统一的对比模式状态管理助手
|
|
|
|
|
|
* 职责:
|
|
|
|
|
|
* 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] 已关闭地图对比模式')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 2. 显示所有标记和范围圈(现在是异步函数)
|
|
|
|
|
|
await showAllMarkersAndRange()
|
2025-11-25 14:46:53 +08:00
|
|
|
|
|
|
|
|
|
|
// 3. 调整相机到最佳视角
|
|
|
|
|
|
flyToBestViewForMarkers()
|
|
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
|
showMarkersAndRange.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 当前显示,切换为隐藏
|
|
|
|
|
|
hideAllMarkersAndRange()
|
|
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
|
showMarkersAndRange.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理快速响应标题点击事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleQuickResponseToggle = async () => {
|
|
|
|
|
|
console.log('[index.vue] 快速响应标题点击')
|
|
|
|
|
|
|
|
|
|
|
|
if (!quickResponseExecuted.value) {
|
|
|
|
|
|
// Execute Quick Response
|
|
|
|
|
|
await executeQuickResponse()
|
|
|
|
|
|
quickResponseExecuted.value = true
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Hide Quick Response markers
|
|
|
|
|
|
hideQuickResponse()
|
|
|
|
|
|
quickResponseExecuted.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 辅助函数:判断是否为应急点标记
|
|
|
|
|
|
*/
|
|
|
|
|
|
const isEmergencyPointMarker = (type) => {
|
|
|
|
|
|
return type === 'cityEmergency' ||
|
|
|
|
|
|
type === 'districtEmergency' ||
|
|
|
|
|
|
type === 'otherEmergency'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 执行快速响应逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
const executeQuickResponse = async () => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 执行快速响应...')
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Hide all other markers
|
|
|
|
|
|
viewer.entities.values.forEach((entity) => {
|
|
|
|
|
|
if (entity.properties) {
|
|
|
|
|
|
const type = entity.properties.type?.getValue()
|
|
|
|
|
|
const isSimulated = entity.properties.isSimulated?.getValue()
|
|
|
|
|
|
|
|
|
|
|
|
// Hide non-simulated, non-emergency-point markers
|
|
|
|
|
|
if (!isSimulated && !isEmergencyPointMarker(type)) {
|
|
|
|
|
|
entity.show = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Hide养护站等 emergency resource markers
|
|
|
|
|
|
hideMarkers()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Load emergency points within range
|
|
|
|
|
|
const rangeResponse = await request({
|
|
|
|
|
|
url: `/snow-ops-platform/yhYjll/list`,
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
params: {
|
|
|
|
|
|
longitude: DISASTER_CENTER.lon,
|
|
|
|
|
|
latitude: DISASTER_CENTER.lat,
|
|
|
|
|
|
maxDistance: disasterData.forcePreset.value.searchRadius, // 30 or 50 km
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!rangeResponse?.data || !Array.isArray(rangeResponse.data)) {
|
|
|
|
|
|
ElMessage.error('加载应急点失败')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Display emergency points on map
|
|
|
|
|
|
await displayEmergencyPoints(rangeResponse.data)
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Select 2 nearest emergency points
|
|
|
|
|
|
const emergencyPoints = rangeResponse.data
|
|
|
|
|
|
const pointsWithDistance = emergencyPoints.map(point => ({
|
|
|
|
|
|
...point,
|
|
|
|
|
|
distance: calculateDistance(
|
|
|
|
|
|
{ lon: DISASTER_CENTER.lon, lat: DISASTER_CENTER.lat },
|
|
|
|
|
|
{ lon: point.gl1Lng, lat: point.gl1Lat }
|
|
|
|
|
|
)
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const sortedPoints = pointsWithDistance.sort((a, b) => a.distance - b.distance)
|
|
|
|
|
|
const nearest2Points = sortedPoints.slice(0, 2)
|
|
|
|
|
|
|
|
|
|
|
|
if (nearest2Points.length < 2) {
|
|
|
|
|
|
ElMessage.warning('应急点数量不足2个')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 选择最近的2个应急点:', nearest2Points.map(p => p.gl1Yjllmc))
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Create 6 simulated markers
|
|
|
|
|
|
createSimulatedMarkers(viewer, DISASTER_CENTER, nearest2Points)
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Force render
|
|
|
|
|
|
viewer.scene.requestRender()
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 快速响应执行完成')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示应急点标记
|
|
|
|
|
|
*/
|
|
|
|
|
|
const displayEmergencyPoints = async (emergencyPoints) => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
|
|
|
|
|
|
|
|
|
|
|
// Clear existing emergency point markers
|
|
|
|
|
|
viewer.entities.values.forEach(entity => {
|
|
|
|
|
|
const type = entity.properties?.type?.getValue()
|
|
|
|
|
|
if (isEmergencyPointMarker(type)) {
|
|
|
|
|
|
viewer.entities.remove(entity)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Add new emergency point markers
|
|
|
|
|
|
emergencyPoints.forEach(point => {
|
|
|
|
|
|
// Determine icon based on level
|
|
|
|
|
|
const levelString = String(point.gl1Lx || '').trim()
|
|
|
|
|
|
let icon = otherEmergencyIcon
|
|
|
|
|
|
let type = 'otherEmergency'
|
|
|
|
|
|
let levelName = '其他'
|
|
|
|
|
|
|
|
|
|
|
|
if (levelString.startsWith('1') || levelString === '1') {
|
|
|
|
|
|
icon = cityEmergencyIcon
|
|
|
|
|
|
type = 'cityEmergency'
|
|
|
|
|
|
levelName = '市级'
|
|
|
|
|
|
} else if (levelString.startsWith('2') || levelString === '2') {
|
|
|
|
|
|
icon = districtEmergencyIcon
|
|
|
|
|
|
type = 'districtEmergency'
|
|
|
|
|
|
levelName = '区县级'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
position: Cesium.Cartesian3.fromDegrees(point.gl1Lng, point.gl1Lat, 0),
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: icon,
|
|
|
|
|
|
width: 29,
|
|
|
|
|
|
height: 32,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: new Cesium.PropertyBag({
|
|
|
|
|
|
type: type,
|
|
|
|
|
|
name: point.gl1Yjllmc,
|
|
|
|
|
|
level: levelName,
|
|
|
|
|
|
district: point.gl1Qxmc,
|
|
|
|
|
|
personnel: point.gl1Rysl,
|
|
|
|
|
|
area: point.gl1Zdmj
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[index.vue] 显示 ${emergencyPoints.length} 个应急点`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 隐藏快速响应标记
|
|
|
|
|
|
*/
|
|
|
|
|
|
const hideQuickResponse = () => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 隐藏快速响应标记...')
|
|
|
|
|
|
|
|
|
|
|
|
// Hide emergency point markers
|
|
|
|
|
|
viewer.entities.values.forEach(entity => {
|
|
|
|
|
|
const type = entity.properties?.type?.getValue()
|
|
|
|
|
|
if (isEmergencyPointMarker(type)) {
|
|
|
|
|
|
entity.show = false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Clear simulated markers
|
|
|
|
|
|
clearSimulatedMarkers(viewer)
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 已隐藏快速响应标记')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 11:13:29 +08:00
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 处理地图工具变化事件
|
2025-11-19 11:13:29 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const handleMapToolChange = async ({ tool, active }) => {
|
2025-11-25 14:46:53 +08:00
|
|
|
|
console.log(`[index.vue] 地图工具变化: ${tool}, 激活状态: ${active}`)
|
|
|
|
|
|
|
|
|
|
|
|
if (tool === COMPARE_TOOL_KEY) {
|
|
|
|
|
|
// 模型对比工具: 使用统一助手管理状态
|
|
|
|
|
|
const loadingMessage = ElMessage({
|
|
|
|
|
|
message: active ? '正在启用模型对比...' : '正在关闭模型对比...',
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
duration: 0,
|
|
|
|
|
|
showClose: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const success = await setCompareToolState(active, 'map-controls')
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
loadingMessage.close()
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
if (success) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
ElMessage.success(active ? '模型对比已启用' : '模型对比已关闭')
|
2025-11-19 11:13:29 +08:00
|
|
|
|
}
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 失败消息已在助手函数中处理
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他工具: 保持原有逻辑
|
|
|
|
|
|
activeToolKey.value = active ? tool : null
|
|
|
|
|
|
console.log(`[index.vue] 工具 ${tool} ${active ? '激活' : '取消'}`)
|
2025-11-19 11:13:29 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-19 11:13:29 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理距离范围变更
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleDistanceChange = async (newDistance) => {
|
|
|
|
|
|
console.log(`[index.vue] 距离范围改变为: ${newDistance}km`)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 1. 更新搜索半径
|
2025-11-25 09:35:28 +08:00
|
|
|
|
disasterData.updateSearchRadius(newDistance)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 2. 更新范围圈
|
2025-11-25 09:35:28 +08:00
|
|
|
|
if (mapStore.viewer) {
|
|
|
|
|
|
createOrUpdateRangeCircle(mapStore.viewer, newDistance)
|
|
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 3. 重新加载应急资源数据并更新地图标记
|
|
|
|
|
|
// await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 4. 重新加载储备中心和预置点数据并更新地图标记
|
2025-11-25 09:35:28 +08:00
|
|
|
|
await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 5. 重新加载应急统计数据 (新增)
|
|
|
|
|
|
await loadEmergencyBaseAndPreset(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 显示所有标记点和范围圈
|
|
|
|
|
|
*/
|
2025-11-25 19:21:38 +08:00
|
|
|
|
const showAllMarkersAndRange = async () => {
|
2025-11-25 14:46:53 +08:00
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
// })
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 2. 显示接口标记
|
|
|
|
|
|
// hideMarkers()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 加载全部储备中心/预置点到地图(不限制距离)
|
|
|
|
|
|
console.log('[index.vue] 加载全部储备中心/预置点到地图...')
|
|
|
|
|
|
await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat, true)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 显示范围圈
|
|
|
|
|
|
showRangeCircle()
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[index.vue] 已显示所有标记点和范围圈')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 隐藏所有标记点和范围圈
|
|
|
|
|
|
*/
|
|
|
|
|
|
const hideAllMarkersAndRange = () => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 隐藏模拟点位
|
2025-11-25 14:46:53 +08:00
|
|
|
|
viewer.entities.values.forEach((entity) => {
|
|
|
|
|
|
if (entity.properties) {
|
|
|
|
|
|
const props = entity.properties
|
2025-11-26 18:05:35 +08:00
|
|
|
|
if (props.type?.getValue() === 'soldier' ||
|
|
|
|
|
|
props.type?.getValue() === 'device' ||
|
2025-11-25 14:46:53 +08:00
|
|
|
|
props.isPathStartMarker?.getValue()) {
|
2025-11-25 18:16:46 +08:00
|
|
|
|
entity.show = false
|
2025-11-25 14:46:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 2. 隐藏接口标记
|
2025-11-25 18:16:46 +08:00
|
|
|
|
hideMarkers()
|
|
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 3. 隐藏范围圈
|
|
|
|
|
|
hideRangeCircle()
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
console.log('[index.vue] 已隐藏所有标记点和范围圈')
|
|
|
|
|
|
}
|
2025-11-25 19:21:38 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 在选中的应急点位置显示人员/装备初始标记
|
|
|
|
|
|
* @param {Array} personnelPoints - 选中的人员应急点
|
|
|
|
|
|
* @param {Array} equipmentPoints - 选中的装备应急点
|
|
|
|
|
|
*/
|
|
|
|
|
|
const showEmergencyPointMarkers = (personnelPoints, equipmentPoints) => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) return
|
2025-11-25 19:21:38 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
console.log('🔥🔥🔥 [NEW CODE] 在应急点位置添加人员/装备标记 🔥🔥🔥')
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 清除旧的应急点标记
|
|
|
|
|
|
viewer.entities.values.forEach((entity) => {
|
|
|
|
|
|
if (entity.properties) {
|
|
|
|
|
|
const type = entity.properties.type?.getValue()
|
|
|
|
|
|
if (type === 'emergencyPersonnel' || type === 'emergencyEquipment') {
|
|
|
|
|
|
entity.show = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 添加人员标记
|
|
|
|
|
|
personnelPoints.forEach((point, index) => {
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, 0),
|
2025-11-25 18:16:46 +08:00
|
|
|
|
billboard: {
|
|
|
|
|
|
image: soldierIcon,
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: new Cesium.PropertyBag({
|
2025-11-26 18:05:35 +08:00
|
|
|
|
type: 'emergencyPersonnel',
|
|
|
|
|
|
name: point.name || `应急人员${index + 1}`,
|
|
|
|
|
|
emergencyPointId: point.id
|
2025-11-25 18:16:46 +08:00
|
|
|
|
}),
|
|
|
|
|
|
show: true
|
|
|
|
|
|
})
|
2025-11-26 18:05:35 +08:00
|
|
|
|
console.log(`[index.vue] 已添加人员标记: ${point.name}`)
|
|
|
|
|
|
})
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
// 添加装备标记
|
|
|
|
|
|
equipmentPoints.forEach((point, index) => {
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, 0),
|
2025-11-25 18:16:46 +08:00
|
|
|
|
billboard: {
|
|
|
|
|
|
image: deviceIcon,
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
|
|
|
|
},
|
|
|
|
|
|
properties: new Cesium.PropertyBag({
|
2025-11-26 18:05:35 +08:00
|
|
|
|
type: 'emergencyEquipment',
|
|
|
|
|
|
name: point.name || `应急装备${index + 1}`,
|
|
|
|
|
|
emergencyPointId: point.id
|
2025-11-25 18:16:46 +08:00
|
|
|
|
}),
|
|
|
|
|
|
show: true
|
|
|
|
|
|
})
|
2025-11-26 18:05:35 +08:00
|
|
|
|
console.log(`[index.vue] 已添加装备标记: ${point.name}`)
|
2025-11-25 14:46:53 +08:00
|
|
|
|
})
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-26 18:05:35 +08:00
|
|
|
|
viewer.scene.requestRender()
|
|
|
|
|
|
console.log('[index.vue] 应急点标记显示完成')
|
2025-11-25 14:46:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 调整相机到显示范围圈区域
|
|
|
|
|
|
* 根据当前搜索半径动态调整视角
|
|
|
|
|
|
*/
|
|
|
|
|
|
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)`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 数据加载函数
|
|
|
|
|
|
// ====================
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载养护站数据
|
|
|
|
|
|
*/
|
2025-11-18 21:24:31 +08:00
|
|
|
|
const loadEmergencyResources = async (longitude, latitude) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await request({
|
|
|
|
|
|
url: `/snow-ops-platform/disaster/matchEmergencyResources`,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
method: 'GET',
|
2025-11-18 21:24:31 +08:00
|
|
|
|
params: {
|
|
|
|
|
|
longitude,
|
|
|
|
|
|
latitude,
|
|
|
|
|
|
maxDistance: disasterData.forcePreset.value.searchRadius,
|
|
|
|
|
|
},
|
2025-11-25 09:35:28 +08:00
|
|
|
|
})
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
if (response?.data) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
disasterData.updateForcePreset(response.data)
|
|
|
|
|
|
console.log('[index.vue] 应急资源数据加载成功:', response.data)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
if (mapStore.viewer) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.log('[index.vue] 更新地图应急资源标记...')
|
|
|
|
|
|
clearEmergencyResourceMarkers(mapStore.viewer)
|
|
|
|
|
|
await addEmergencyResourceMarkers(mapStore.viewer, response.data, { longitude, latitude }, { heightOffset: 10 })
|
2025-11-18 21:24:31 +08:00
|
|
|
|
} else {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.warn('[index.vue] 地图viewer未就绪,跳过标记更新')
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.warn('[index.vue] 应急资源接口返回数据为空')
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
return response
|
2025-11-18 21:24:31 +08:00
|
|
|
|
} catch (error) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.error('[index.vue] 加载应急资源数据失败:', error)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
ElMessage.warning({
|
2025-11-25 09:35:28 +08:00
|
|
|
|
message: '应急资源数据加载失败,使用默认数据',
|
2025-11-18 21:24:31 +08:00
|
|
|
|
duration: 3000,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
return null
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载储备中心和预置点数据
|
2025-11-25 19:21:38 +08:00
|
|
|
|
* @param {number} longitude - 经度(可选,不传则查询全部)
|
|
|
|
|
|
* @param {number} latitude - 纬度(可选,不传则查询全部)
|
|
|
|
|
|
* @param {boolean} loadAllForMap - 是否加载全部点位到地图(true时不限制距离)
|
2025-11-25 09:35:28 +08:00
|
|
|
|
*/
|
2025-11-25 19:21:38 +08:00
|
|
|
|
const loadReserveCentersAndPresets = async (longitude, latitude, loadAllForMap = false) => {
|
2025-11-20 16:59:33 +08:00
|
|
|
|
try {
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 构建请求参数
|
|
|
|
|
|
const params = {}
|
|
|
|
|
|
|
|
|
|
|
|
if (!loadAllForMap && longitude !== undefined && latitude !== undefined) {
|
|
|
|
|
|
// 正常模式:传递经纬度和距离限制
|
|
|
|
|
|
params.longitude = longitude
|
|
|
|
|
|
params.latitude = latitude
|
|
|
|
|
|
params.maxDistance = disasterData.forcePreset.value.searchRadius
|
|
|
|
|
|
}
|
|
|
|
|
|
// loadAllForMap 为 true 时,不传任何参数,获取全部数据
|
|
|
|
|
|
|
2025-11-20 16:59:33 +08:00
|
|
|
|
const response = await request({
|
|
|
|
|
|
url: `/snow-ops-platform/yhYjll/list`,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
method: 'GET',
|
2025-11-25 19:21:38 +08:00
|
|
|
|
params,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
})
|
2025-11-20 16:59:33 +08:00
|
|
|
|
|
|
|
|
|
|
if (response?.data && Array.isArray(response.data)) {
|
2025-11-25 19:21:38 +08:00
|
|
|
|
console.log('[index.vue] 储备中心和预置点数据加载成功:', response.data.length, '个点位')
|
2025-11-20 16:59:33 +08:00
|
|
|
|
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 1. 转换数据为标准 stations 格式
|
2025-11-25 18:16:46 +08:00
|
|
|
|
const transformedStations = disasterData.transformReserveDataToStations(
|
|
|
|
|
|
response.data,
|
|
|
|
|
|
{ longitude, latitude }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 2. 仅在非"全部加载"模式下更新 forcePreset.stations(用于 station-list 显示)
|
|
|
|
|
|
if (!loadAllForMap && transformedStations.length > 0) {
|
2025-11-25 18:16:46 +08:00
|
|
|
|
disasterData.forcePreset.value.stations = transformedStations
|
2025-11-25 19:21:38 +08:00
|
|
|
|
console.log('[index.vue] 已更新 forcePreset.stations:', transformedStations.length, '个')
|
2025-11-25 18:16:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 3. 添加地图标记(全部或范围内)
|
2025-11-20 16:59:33 +08:00
|
|
|
|
if (mapStore.viewer) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.log('[index.vue] 添加储备中心和预置点地图标记...')
|
|
|
|
|
|
clearReserveCenterMarkers(mapStore.viewer)
|
|
|
|
|
|
await addReserveCenterMarkers(mapStore.viewer, response.data, { heightOffset: 10 })
|
2025-11-20 16:59:33 +08:00
|
|
|
|
} else {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.warn('[index.vue] 地图viewer未就绪,跳过标记更新')
|
2025-11-20 16:59:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.warn('[index.vue] 储备中心和预置点接口返回数据为空')
|
2025-11-20 16:59:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
return response
|
2025-11-20 16:59:33 +08:00
|
|
|
|
} catch (error) {
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.error('[index.vue] 加载储备中心和预置点数据失败:', error)
|
2025-11-20 16:59:33 +08:00
|
|
|
|
ElMessage.warning({
|
2025-11-25 09:35:28 +08:00
|
|
|
|
message: '储备中心和预置点数据加载失败',
|
2025-11-20 16:59:33 +08:00
|
|
|
|
duration: 3000,
|
2025-11-25 09:35:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
return null
|
2025-11-20 16:59:33 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-20 16:59:33 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
/**
|
2025-11-25 18:16:46 +08:00
|
|
|
|
* 加载应急基地与预置点、应急装备、应急物资、应急人员的统计数量
|
|
|
|
|
|
* /snow-ops-platform/yhYjll/statistics
|
2025-11-25 14:46:53 +08:00
|
|
|
|
*/
|
2025-11-25 18:16:46 +08:00
|
|
|
|
const loadEmergencyBaseAndPreset = async (longitude, latitude) => {
|
2025-11-25 14:46:53 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await request({
|
|
|
|
|
|
url: `/snow-ops-platform/yhYjll/statistics`,
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
params: {
|
|
|
|
|
|
longitude,
|
|
|
|
|
|
latitude,
|
|
|
|
|
|
maxDistance: disasterData.forcePreset.value.searchRadius,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
if (response?.data) {
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 保存统计数据到状态 (仅更新统计,不更新stations)
|
|
|
|
|
|
disasterData.updateForcePreset(response.data, { onlyStatistics: true })
|
|
|
|
|
|
console.log('[index.vue] 应急统计数据加载成功:', response.data)
|
2025-11-25 14:46:53 +08:00
|
|
|
|
} else {
|
2025-11-25 18:16:46 +08:00
|
|
|
|
console.warn('[index.vue] 应急统计数据接口返回数据为空')
|
2025-11-25 14:46:53 +08:00
|
|
|
|
}
|
2025-11-25 18:16:46 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
return response
|
|
|
|
|
|
} catch (error) {
|
2025-11-25 18:16:46 +08:00
|
|
|
|
console.error('[index.vue] 加载应急统计数据失败:', error)
|
|
|
|
|
|
ElMessage.warning({
|
|
|
|
|
|
message: '应急统计数据加载失败',
|
|
|
|
|
|
duration: 3000,
|
|
|
|
|
|
})
|
|
|
|
|
|
return null
|
2025-11-25 14:46:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询应急装备和应急物资列表 /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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-25 18:16:46 +08:00
|
|
|
|
loadEmergencyEquipmentAndMaterial(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
console.log('[index.vue] 应急装备和应急物资列表加载成功:', response.data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('[index.vue] 查询应急装备和应急物资列表失败:', error)
|
|
|
|
|
|
})
|
2025-11-25 14:46:53 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 场景初始化函数
|
|
|
|
|
|
// ====================
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 初始化场景
|
2025-11-18 21:24:31 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const initializeScene = async () => {
|
|
|
|
|
|
const viewer = mapStore.viewer
|
|
|
|
|
|
if (!viewer) {
|
|
|
|
|
|
console.error('[index.vue] viewer 未就绪')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.log('[index.vue] 开始初始化场景...')
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 1. 启用模型对比功能(界面立即呈现,模型延迟加载)
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[index.vue] 启用模型对比功能(不加载左侧模型)...')
|
|
|
|
|
|
await toggleCompareMode(true, viewer, { skipLeftModelLoad: true })
|
|
|
|
|
|
console.log('[index.vue] 模型对比界面已启用')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[index.vue] 启用模型对比功能失败:', error)
|
|
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 2. 定位相机到目标位置
|
|
|
|
|
|
console.log('[index.vue] 设置相机到目标位置...')
|
|
|
|
|
|
viewer.camera.setView({
|
|
|
|
|
|
destination: Cesium.Cartesian3.fromDegrees(
|
|
|
|
|
|
DEFAULT_CAMERA_POSITION.lon,
|
|
|
|
|
|
DEFAULT_CAMERA_POSITION.lat,
|
|
|
|
|
|
DEFAULT_CAMERA_VIEW.height
|
|
|
|
|
|
),
|
|
|
|
|
|
orientation: {
|
|
|
|
|
|
heading: Cesium.Math.toRadians(DEFAULT_CAMERA_VIEW.heading),
|
|
|
|
|
|
pitch: Cesium.Math.toRadians(DEFAULT_CAMERA_VIEW.pitch),
|
|
|
|
|
|
roll: DEFAULT_CAMERA_VIEW.roll,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-25 11:40:36 +08:00
|
|
|
|
// 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. 添加路径起点标记(用于"一键启动")- 已移至步骤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 个路径起点标记')
|
2025-11-25 09:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 5. 设置地图点击事件监听
|
|
|
|
|
|
mapClickHandler.setupClickHandler(viewer, registerEventHandler, registerPostRenderListener)
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 加载右侧 3D Tiles(灾后模型)
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[index.vue] 开始加载3D模型...')
|
|
|
|
|
|
const afterTileset = await load3DTileset(viewer, 'after', false)
|
|
|
|
|
|
|
|
|
|
|
|
if (afterTileset) {
|
|
|
|
|
|
console.log('[index.vue] 等待3D模型完全就绪...')
|
|
|
|
|
|
await waitForTilesetReady(afterTileset)
|
|
|
|
|
|
console.log('[index.vue] 3D模型已完全就绪')
|
|
|
|
|
|
|
|
|
|
|
|
// 等待地形数据准备就绪
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
|
|
|
|
|
|
|
|
|
|
// 添加中心点标记
|
|
|
|
|
|
console.log('[index.vue] 添加中心点标记...')
|
|
|
|
|
|
console.log('[index.vue] 中心点标记位置:', DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
|
|
|
|
|
viewer.entities.add({
|
|
|
|
|
|
position: Cesium.Cartesian3.fromDegrees(DISASTER_CENTER.lon, DISASTER_CENTER.lat, 0),
|
|
|
|
|
|
billboard: {
|
|
|
|
|
|
image: eventIcon,
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
|
|
|
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
|
|
|
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log('[index.vue] 中心点标记已添加')
|
2025-11-25 19:21:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 绘制坍塌边界蓝色实线
|
|
|
|
|
|
console.log('[index.vue] 绘制坍塌边界蓝色实线...')
|
|
|
|
|
|
drawCollapseBoundary(viewer)
|
|
|
|
|
|
console.log('[index.vue] 坍塌边界蓝色实线已添加')
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[index.vue] 3D模型加载失败:', error)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 7. 延迟加载左侧 3D Tiles(灾前模型)
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[index.vue] 开始加载左侧灾前模型...')
|
|
|
|
|
|
await toggleCompareMode(true, viewer, { loadLeftModel: true })
|
|
|
|
|
|
console.log('[index.vue] 左侧灾前模型加载完成')
|
2025-11-25 19:21:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 在左侧地图绘制蓝色虚线边界
|
|
|
|
|
|
const leftContainer = document.getElementById('leftCesiumContainer')
|
|
|
|
|
|
if (leftContainer) {
|
|
|
|
|
|
console.log('[index.vue] 绘制左侧坍塌边界蓝色虚线...')
|
|
|
|
|
|
drawCollapseBoundaryLeft(leftContainer)
|
|
|
|
|
|
console.log('[index.vue] 左侧坍塌边界蓝色虚线已添加')
|
|
|
|
|
|
}
|
2025-11-25 09:35:28 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[index.vue] 左侧模型加载失败:', error)
|
2025-11-20 18:09:04 +08:00
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 8. 初始化地图标记
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('[index.vue] 开始初始化地图标记...')
|
|
|
|
|
|
await initializeMarkers(viewer, {
|
|
|
|
|
|
useSampledHeights: true,
|
|
|
|
|
|
heightOffset: 100,
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log('[index.vue] 地图标记初始化完成')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[index.vue] 地图标记初始化失败:', error)
|
2025-11-24 13:47:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 9. 创建范围圈
|
|
|
|
|
|
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius)
|
2025-11-24 13:47:51 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 默认隐藏范围圈(等待快速匹配激活)
|
|
|
|
|
|
hideRangeCircle()
|
|
|
|
|
|
console.log('[index.vue] 已隐藏范围圈(等待快速匹配激活)')
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 10. 额外等待确保地形完全就绪(避免标记悬浮)
|
|
|
|
|
|
console.log('[index.vue] 等待地形完全就绪...')
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
2025-11-21 11:09:22 +08:00
|
|
|
|
|
2025-11-25 11:40:36 +08:00
|
|
|
|
// 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()
|
|
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 默认隐藏模拟点位(等待快速匹配点击时显示)
|
|
|
|
|
|
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] 已隐藏模拟点位(等待快速匹配激活)')
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 11. 加载应急资源数据(在地形就绪后)
|
|
|
|
|
|
console.log('[index.vue] 加载应急资源数据...')
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// await loadEmergencyResources(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
2025-11-21 11:09:22 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// 默认隐藏接口标记(等待快速匹配激活)
|
|
|
|
|
|
hideMarkers()
|
|
|
|
|
|
console.log('[index.vue] 已隐藏应急资源标记(等待快速匹配激活)')
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 12. 加载储备中心和预置点数据
|
|
|
|
|
|
console.log('[index.vue] 加载储备中心和预置点数据...')
|
|
|
|
|
|
await loadReserveCentersAndPresets(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-25 18:16:46 +08:00
|
|
|
|
// 13. 加载应急统计数据
|
|
|
|
|
|
console.log('[index.vue] 加载应急统计数据...')
|
|
|
|
|
|
await loadEmergencyBaseAndPreset(DISASTER_CENTER.lon, DISASTER_CENTER.lat)
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.log('[index.vue] 场景初始化完成')
|
|
|
|
|
|
}
|
2025-11-21 11:09:22 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 生命周期钩子
|
|
|
|
|
|
// ====================
|
2025-11-21 11:09:22 +08:00
|
|
|
|
|
2025-11-19 14:04:48 +08:00
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 组件挂载
|
|
|
|
|
|
* 统一的初始化入口
|
2025-11-19 14:04:48 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// 等待地图就绪后初始化场景(数据加载已在 initializeScene 中完成)
|
|
|
|
|
|
mapStore.onReady(async () => {
|
|
|
|
|
|
console.log('[index.vue] 3D态势感知地图已就绪')
|
2025-11-19 14:04:48 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 初始化场景(包含所有数据加载)
|
|
|
|
|
|
await initializeScene()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-25 09:35:28 +08:00
|
|
|
|
* 组件卸载
|
|
|
|
|
|
* 清理所有资源
|
2025-11-21 17:00:10 +08:00
|
|
|
|
*/
|
2025-11-25 09:35:28 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
console.log('[index.vue] 组件卸载,开始清理资源...')
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
const viewer = mapStore.viewer
|
2025-11-25 19:21:38 +08:00
|
|
|
|
const leftContainer = document.getElementById('leftCesiumContainer')
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 清理范围圈
|
|
|
|
|
|
if (viewer) {
|
|
|
|
|
|
clearRangeCircle(viewer)
|
2025-11-25 19:21:38 +08:00
|
|
|
|
// 清理坍塌边界线(包括左右两侧)
|
|
|
|
|
|
clearCollapseBoundary(viewer, leftContainer)
|
2025-11-25 09:35:28 +08:00
|
|
|
|
}
|
2025-11-21 17:00:10 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 清理地图点击处理器
|
|
|
|
|
|
mapClickHandler.destroy()
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// 清理所有 Cesium 资源(由 useCesiumLifecycle 自动执行)
|
|
|
|
|
|
cleanup()
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
console.log('[index.vue] 资源清理完成')
|
|
|
|
|
|
})
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 14:46:53 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// 防御性watch - 确保状态始终一致
|
|
|
|
|
|
// ====================
|
|
|
|
|
|
|
|
|
|
|
|
// 作为保险机制,确保即使有新入口直接操作useDualMapCompare,状态也能保持一致
|
|
|
|
|
|
watch(isCompareMode, (newValue) => {
|
|
|
|
|
|
if (!newValue && activeToolKey.value === COMPARE_TOOL_KEY) {
|
|
|
|
|
|
console.warn('[index.vue] 检测到对比模式被外部关闭,同步更新工具状态')
|
|
|
|
|
|
activeToolKey.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { immediate: false })
|
|
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
// ====================
|
|
|
|
|
|
// Provide 给子组件
|
|
|
|
|
|
// ====================
|
2025-11-19 15:23:17 +08:00
|
|
|
|
|
2025-11-25 09:35:28 +08:00
|
|
|
|
provide('disasterData', disasterData)
|
|
|
|
|
|
provide('onDistanceChange', handleDistanceChange)
|
2025-11-19 17:06:05 +08:00
|
|
|
|
|
2025-11-16 14:43:35 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2025-11-18 21:24:31 +08:00
|
|
|
|
@use "@/styles/mixins.scss" as *;
|
|
|
|
|
|
@use "./assets/styles/common.scss" as *;
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
|
|
|
|
|
.situational-awareness {
|
2025-11-17 11:12:56 +08:00
|
|
|
|
// 容器查询设置,用于嵌入场景的自适应缩放
|
|
|
|
|
|
container-name: situational-awareness;
|
|
|
|
|
|
container-type: size;
|
|
|
|
|
|
|
2025-11-19 09:13:06 +08:00
|
|
|
|
// 为旧版浏览器提供视口单位回退,封顶 1920×1080
|
|
|
|
|
|
--cq-inline-100: clamp(0px, 100vw, 1920px);
|
|
|
|
|
|
--cq-block-100: clamp(0px, 100vh, 1080px);
|
2025-11-17 11:12:56 +08:00
|
|
|
|
|
2025-11-19 09:13:06 +08:00
|
|
|
|
// 当支持容器单位时覆盖为容器单位,同样封顶
|
2025-11-17 11:12:56 +08:00
|
|
|
|
@supports (width: 1cqw) {
|
2025-11-19 09:13:06 +08:00
|
|
|
|
--cq-inline-100: min(100cqw, 1920px);
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
@supports (height: 1cqh) {
|
2025-11-19 09:13:06 +08:00
|
|
|
|
--cq-block-100: min(100cqh, 1080px);
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 可配置的布局变量(使用 calc 直接计算,避免函数嵌套)
|
|
|
|
|
|
--sa-left-width: calc(464 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-right-width: calc(486 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-gap: calc(16 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-padding: calc(16 / 1920 * var(--cq-inline-100, 100vw));
|
2025-11-18 21:24:31 +08:00
|
|
|
|
--sa-header-height: calc(
|
2025-11-19 14:04:48 +08:00
|
|
|
|
131 / 1080 * var(--cq-block-100, 100vh)
|
2025-11-18 21:24:31 +08:00
|
|
|
|
); // Header 高度
|
2025-11-17 11:12:56 +08:00
|
|
|
|
--sa-min-width: 1280px;
|
|
|
|
|
|
--sa-min-height: 720px;
|
|
|
|
|
|
|
2025-11-17 17:59:25 +08:00
|
|
|
|
position: relative;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2025-11-19 09:13:06 +08:00
|
|
|
|
// max-width: 1920px;
|
|
|
|
|
|
// max-height: 1080px;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
min-width: var(--sa-min-width);
|
|
|
|
|
|
min-height: var(--sa-min-height);
|
2025-11-16 14:43:35 +08:00
|
|
|
|
background-color: var(--bg-dark);
|
2025-11-19 09:13:06 +08:00
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
overflow-y: auto; // 当宿主尺寸 < 最小尺寸时允许滚动,达到上限时不放大
|
2025-11-17 17:59:25 +08:00
|
|
|
|
|
|
|
|
|
|
// PageHeader 浮在顶部
|
|
|
|
|
|
> :first-child {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
|
|
|
|
|
&__main {
|
2025-11-17 17:59:25 +08:00
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0; // 铺满整个容器
|
2025-11-16 14:43:35 +08:00
|
|
|
|
background: url(./assets/images/main-bg.png) center/cover no-repeat;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 地图底层 - 填满整个容器
|
|
|
|
|
|
&__map-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 0;
|
2025-11-18 21:24:31 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
|
|
// 默认单地图模式
|
|
|
|
|
|
&:not(.is-compare-mode) {
|
|
|
|
|
|
.situational-awareness__right-map {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.situational-awareness__left-map {
|
|
|
|
|
|
width: 0;
|
2025-11-19 17:06:05 +08:00
|
|
|
|
opacity: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
overflow: hidden;
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 双地图对比模式
|
|
|
|
|
|
&.is-compare-mode {
|
|
|
|
|
|
.situational-awareness__left-map {
|
|
|
|
|
|
width: 50%;
|
2025-11-19 17:06:05 +08:00
|
|
|
|
opacity: 1;
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
transition: width 0.3s ease, opacity 0.3s ease;
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.situational-awareness__right-map {
|
|
|
|
|
|
width: 50%;
|
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 左侧地图容器(灾前场景 - 对比时显示)
|
|
|
|
|
|
&__left-map {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 右侧地图容器(灾后场景 - 主地图)
|
|
|
|
|
|
&__right-map {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 中间分割线
|
|
|
|
|
|
&__center-divider {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
to bottom,
|
|
|
|
|
|
rgba(255, 255, 255, 0.1),
|
|
|
|
|
|
rgba(255, 255, 255, 0.5),
|
|
|
|
|
|
rgba(255, 255, 255, 0.1)
|
|
|
|
|
|
);
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
pointer-events: none;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 地图遮罩层 - 覆盖在地图之上,增强视觉效果
|
|
|
|
|
|
&__map-mask {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
pointer-events: none; // 不阻挡交互
|
|
|
|
|
|
// 使用 cockpit 的遮罩层图片,保持视觉一致性
|
2025-11-18 21:24:31 +08:00
|
|
|
|
background: url(@/views/cockpit/assets/img/遮罩层.png) center/cover
|
|
|
|
|
|
no-repeat;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 17:06:05 +08:00
|
|
|
|
// 场景标签层 - 显示在遮罩层和面板层之上
|
|
|
|
|
|
&__scene-labels-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
pointer-events: none; // 标签不阻挡交互
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 浮动面板层 - 使用绝对定位固定面板位置
|
2025-11-17 11:12:56 +08:00
|
|
|
|
&__panels-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
pointer-events: none; // 容器不拦截事件,让中间区域透明
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 左右面板列 - 浮动卡片样式
|
|
|
|
|
|
&__panel-column {
|
2025-11-19 17:06:05 +08:00
|
|
|
|
position: absolute;
|
2025-11-20 16:59:33 +08:00
|
|
|
|
// top: var(--sa-header-height); // 从 header 下方开始
|
|
|
|
|
|
// bottom: 0;
|
2025-11-17 11:12:56 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--sa-gap); // 列内子面板之间的间距
|
|
|
|
|
|
min-width: 0; // 防止在窄容器中溢出
|
|
|
|
|
|
min-height: 0; // 允许 flex 子元素收缩并启用滚动
|
|
|
|
|
|
pointer-events: auto; // 恢复面板的交互能力
|
2025-11-25 16:12:33 +08:00
|
|
|
|
height: 100%;
|
2025-11-19 17:06:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 左侧面板固定在左边
|
|
|
|
|
|
&--left {
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: var(--sa-left-width);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 右侧面板固定在右边
|
|
|
|
|
|
&--right {
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
width: var(--sa-right-width);
|
|
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
|
// 中间占位区域 - 透明且不可交互,点击穿透到地图
|
|
|
|
|
|
&__center-spacer {
|
|
|
|
|
|
pointer-events: none;
|
2025-11-16 14:43:35 +08:00
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
|
2025-11-19 17:06:05 +08:00
|
|
|
|
// 折叠按钮层 - 独立层级,放置在屏幕两侧
|
|
|
|
|
|
&__collapse-buttons-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 11; // 高于场景标签层
|
|
|
|
|
|
pointer-events: none; // 容器不拦截事件
|
|
|
|
|
|
padding-top: var(--sa-header-height); // 预留 Header 高度
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 折叠按钮
|
|
|
|
|
|
&__collapse-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
z-index: 11;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: translateY(-50%) scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collapse-arrow {
|
|
|
|
|
|
width: vw(30);
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 左侧按钮 - 固定在屏幕最左侧
|
|
|
|
|
|
&--left {
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 右侧按钮 - 固定在屏幕最右侧
|
|
|
|
|
|
&--right {
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 面板滑动动画 - 左侧
|
|
|
|
|
|
.panel-slide-left-enter-active,
|
|
|
|
|
|
.panel-slide-left-leave-active {
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-slide-left-enter-from {
|
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-slide-left-leave-to {
|
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 面板滑动动画 - 右侧
|
|
|
|
|
|
.panel-slide-right-enter-active,
|
|
|
|
|
|
.panel-slide-right-leave-active {
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-slide-right-enter-from {
|
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-slide-right-leave-to {
|
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
|
// 地图控件层 - 高于遮罩和面板,用于放置地图控制工具
|
|
|
|
|
|
&__controls-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 3;
|
|
|
|
|
|
pointer-events: none; // 容器不拦截事件
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
padding-bottom: 30px; // 临时使用固定值,确保控件显示
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 控件容器 - 恢复交互能力
|
|
|
|
|
|
&__controls {
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
// 调试:确保控件容器可见
|
|
|
|
|
|
min-height: 56px; // MapControls 的高度
|
|
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 地图 Tooltip 层 - 覆盖地图和面板,仅 Tooltip 自身可交互
|
|
|
|
|
|
&__tooltip-layer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 4; // 高于控件层
|
|
|
|
|
|
pointer-events: none; // 容器不拦截事件,点击穿透到地图
|
|
|
|
|
|
}
|
2025-11-19 14:04:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载动画层 - 一键启动后显示
|
|
|
|
|
|
&__loading-layer {
|
|
|
|
|
|
position: absolute;
|
2025-11-20 16:59:33 +08:00
|
|
|
|
top: calc(var(--sa-header-height) + vh(40));
|
2025-11-19 14:04:48 +08:00
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
// bottom: 0;
|
|
|
|
|
|
z-index: 5; // 高于 Tooltip 层
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
pointer-events: none; // 不阻止点击穿透
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载 GIF 图片
|
|
|
|
|
|
&__loading-gif {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
height: auto;
|
2025-11-21 11:09:22 +08:00
|
|
|
|
max-width: 320px;
|
|
|
|
|
|
max-height: 55px;
|
2025-11-19 14:04:48 +08:00
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
2025-11-18 21:24:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tooltip 内容字段样式
|
|
|
|
|
|
// 用于在 Tooltip 插槽中展示字段列表
|
|
|
|
|
|
.tooltip-field-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: vw(8);
|
|
|
|
|
|
padding: vh(6) 0;
|
|
|
|
|
|
|
|
|
|
|
|
.tooltip-field-label {
|
|
|
|
|
|
color: var(--text-gray);
|
|
|
|
|
|
font-size: fs(13);
|
|
|
|
|
|
font-family: SourceHanSansCN-Regular, sans-serif;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tooltip-field-value {
|
|
|
|
|
|
color: var(--text-white);
|
|
|
|
|
|
font-size: fs(13);
|
|
|
|
|
|
font-family: SourceHanSansCN-Medium, sans-serif;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 18:09:04 +08:00
|
|
|
|
// Tooltip 操作按钮样式
|
|
|
|
|
|
// 用于"连线"、"联动"等交互按钮
|
|
|
|
|
|
.tooltip-action-btn {
|
|
|
|
|
|
min-width: vw(100);
|
|
|
|
|
|
height: vh(36);
|
|
|
|
|
|
padding: 0 vw(24);
|
|
|
|
|
|
background: url('./assets/images/地图tooltip-button.png') no-repeat center/100% 100%;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--text-white);
|
|
|
|
|
|
font-size: fs(14);
|
|
|
|
|
|
font-family: SourceHanSansCN-Medium, sans-serif;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
filter: brightness(1.2);
|
|
|
|
|
|
transform: translateY(vh(-2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
filter: brightness(0.9);
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
|
// 窄容器嵌入的紧凑布局(<1100px 宽度)
|
|
|
|
|
|
.situational-awareness.is-compact {
|
|
|
|
|
|
--sa-left-width: calc(380 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-right-width: calc(400 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-gap: calc(12 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
--sa-padding: calc(12 / 1920 * var(--cq-inline-100, 100vw));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 嵌入模式 - 使用更保守的最小尺寸
|
|
|
|
|
|
.situational-awareness.is-embedded {
|
|
|
|
|
|
--sa-min-width: 1024px;
|
|
|
|
|
|
--sa-min-height: 600px;
|
2025-11-16 14:43:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|