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
|
|
|
<!-- 地图底层 -->
|
|
|
|
|
<div class="situational-awareness__map-layer">
|
|
|
|
|
<MapViewer />
|
|
|
|
|
</div>
|
2025-11-16 14:43:35 +08:00
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
<!-- 地图遮罩层 -->
|
|
|
|
|
<div class="situational-awareness__map-mask" aria-hidden="true"></div>
|
2025-11-16 14:43:35 +08:00
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
<!-- 浮动面板层 -->
|
|
|
|
|
<div class="situational-awareness__panels-layer">
|
|
|
|
|
<div class="situational-awareness__panel-column situational-awareness__panel-column--left">
|
|
|
|
|
<LeftPanel />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="situational-awareness__center-spacer" aria-hidden="true"></div>
|
|
|
|
|
<div class="situational-awareness__panel-column situational-awareness__panel-column--right">
|
|
|
|
|
<RightPanel />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 地图控件层 - 高于遮罩和面板 -->
|
|
|
|
|
<div class="situational-awareness__controls-layer">
|
|
|
|
|
<div id="sa-controls" class="situational-awareness__controls"></div>
|
|
|
|
|
</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"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-17 11:12:56 +08:00
|
|
|
import { ref, provide, onMounted } from 'vue'
|
2025-11-16 14:43:35 +08:00
|
|
|
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 { useDisasterData } from './composables/useDisasterData'
|
2025-11-17 11:12:56 +08:00
|
|
|
import { useMapStore } from '@/map'
|
2025-11-16 14:43:35 +08:00
|
|
|
|
|
|
|
|
// 使用灾害数据
|
|
|
|
|
const disasterData = useDisasterData()
|
|
|
|
|
|
|
|
|
|
// 提供给子组件使用
|
|
|
|
|
provide('disasterData', disasterData)
|
|
|
|
|
|
2025-11-17 11:12:56 +08:00
|
|
|
// 地图 store
|
|
|
|
|
const mapStore = useMapStore()
|
|
|
|
|
|
|
|
|
|
// 初始化地图
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// 等待地图就绪后配置初始视图
|
|
|
|
|
mapStore.onReady(() => {
|
|
|
|
|
const { camera } = mapStore.services()
|
|
|
|
|
|
|
|
|
|
// 设置初始相机位置(以重庆忠县为例,可根据实际需求调整)
|
|
|
|
|
// 经度: 108.0, 纬度: 30.3, 高度: 50000 米
|
|
|
|
|
camera.setCenter(108.0, 30.3, 50000)
|
|
|
|
|
|
|
|
|
|
console.log('3D态势感知地图已就绪')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2025-11-16 14:43:35 +08:00
|
|
|
// 弹窗状态
|
|
|
|
|
const showPersonnelDetail = ref(false)
|
|
|
|
|
const showCenterDetail = ref(false)
|
|
|
|
|
|
|
|
|
|
// 选中的数据
|
|
|
|
|
const selectedPersonnel = ref({
|
|
|
|
|
name: '张强',
|
|
|
|
|
department: '安全生产部',
|
|
|
|
|
distance: 0.6,
|
|
|
|
|
estimatedArrival: 10,
|
|
|
|
|
avatar: null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const selectedCenter = ref({
|
|
|
|
|
name: '忠县应急中心',
|
|
|
|
|
adminLevel: '国道',
|
|
|
|
|
department: '交通公路部门',
|
|
|
|
|
distance: 0.6,
|
|
|
|
|
image: null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 返回驾驶舱
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
console.log('返回驾驶舱')
|
|
|
|
|
// 实际实现:路由跳转
|
|
|
|
|
// router.push('/cockpit')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理人员联动
|
|
|
|
|
const handlePersonnelLink = (personnel) => {
|
|
|
|
|
console.log('联动人员:', personnel)
|
2025-11-17 11:12:56 +08:00
|
|
|
// 实际实现:使用 mapStore 的 camera service 飞行到人员位置
|
|
|
|
|
// const { camera } = mapStore.services()
|
|
|
|
|
// camera.flyTo({ destination: [lon, lat, height] })
|
2025-11-16 14:43:35 +08:00
|
|
|
showPersonnelDetail.value = false
|
|
|
|
|
}
|
2025-11-17 11:12:56 +08:00
|
|
|
|
|
|
|
|
// TODO: 实现地图实体点击事件监听
|
|
|
|
|
// 当用户点击地图上的标记点时,显示对应的详情弹窗
|
|
|
|
|
// mapStore.onReady(() => {
|
|
|
|
|
// const { query } = mapStore.services()
|
|
|
|
|
// // 监听实体点击事件
|
|
|
|
|
// query.onEntityClick((entity) => {
|
|
|
|
|
// if (entity.type === 'personnel') {
|
|
|
|
|
// selectedPersonnel.value = entity.properties
|
|
|
|
|
// showPersonnelDetail.value = true
|
|
|
|
|
// } else if (entity.type === 'center') {
|
|
|
|
|
// selectedCenter.value = entity.properties
|
|
|
|
|
// showCenterDetail.value = true
|
|
|
|
|
// }
|
|
|
|
|
// })
|
|
|
|
|
// })
|
2025-11-16 14:43:35 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
@use '@/styles/mixins.scss' as *;
|
|
|
|
|
@use './assets/styles/common.scss' as *;
|
|
|
|
|
|
|
|
|
|
.situational-awareness {
|
2025-11-17 11:12:56 +08:00
|
|
|
// 容器查询设置,用于嵌入场景的自适应缩放
|
|
|
|
|
container-name: situational-awareness;
|
|
|
|
|
container-type: size;
|
|
|
|
|
|
|
|
|
|
// 为旧版浏览器提供视口单位回退
|
|
|
|
|
--cq-inline-100: 100vw;
|
|
|
|
|
--cq-block-100: 100vh;
|
|
|
|
|
|
|
|
|
|
// 当支持容器单位时覆盖为容器单位
|
|
|
|
|
@supports (width: 1cqw) {
|
|
|
|
|
--cq-inline-100: 100cqw;
|
|
|
|
|
}
|
|
|
|
|
@supports (height: 1cqh) {
|
|
|
|
|
--cq-block-100: 100cqh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 可配置的布局变量(使用 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));
|
|
|
|
|
--sa-min-width: 1280px;
|
|
|
|
|
--sa-min-height: 720px;
|
|
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
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-17 11:12:56 +08:00
|
|
|
overflow: auto; // 当宿主尺寸 < 最小尺寸时允许滚动
|
2025-11-16 14:43:35 +08:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
&__main {
|
2025-11-17 11:12:56 +08:00
|
|
|
position: relative;
|
2025-11-16 14:43:35 +08:00
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 地图遮罩层 - 覆盖在地图之上,增强视觉效果
|
|
|
|
|
&__map-mask {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
pointer-events: none; // 不阻挡交互
|
|
|
|
|
// 使用 cockpit 的遮罩层图片,保持视觉一致性
|
|
|
|
|
background: url(@/views/cockpit/assets/img/遮罩层.png) center/cover no-repeat;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 浮动面板层 - grid 与 pointer-events 结合保证中间透明
|
|
|
|
|
&__panels-layer {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: var(--sa-left-width) 1fr var(--sa-right-width);
|
|
|
|
|
grid-auto-rows: 1fr;
|
|
|
|
|
gap: var(--sa-gap); // 列之间的间距
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: var(--sa-padding); // 面板与容器边缘的间距
|
|
|
|
|
pointer-events: none; // 容器不拦截事件,让中间区域透明
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 左右面板列 - 浮动卡片样式
|
|
|
|
|
&__panel-column {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--sa-gap); // 列内子面板之间的间距
|
|
|
|
|
min-width: 0; // 防止在窄容器中溢出
|
|
|
|
|
min-height: 0; // 允许 flex 子元素收缩并启用滚动
|
|
|
|
|
pointer-events: auto; // 恢复面板的交互能力
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
// 地图控件层 - 高于遮罩和面板,用于放置地图控制工具
|
|
|
|
|
&__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 的高度
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 窄容器嵌入的紧凑布局(<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>
|