272 lines
7.8 KiB
Vue
Raw Normal View History

<template>
<div class="situational-awareness">
<!-- 顶部导航栏 -->
<PageHeader @back="handleBack" />
<!-- 主内容区域 -->
<div class="situational-awareness__main">
<!-- 地图底层 -->
<div class="situational-awareness__map-layer">
<MapViewer />
</div>
<!-- 地图遮罩层 -->
<!-- <div class="situational-awareness__map-mask" aria-hidden="true"></div> -->
<!-- 浮动面板层 -->
<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>
</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>
import { ref, provide, onMounted } from 'vue'
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'
import { useMapStore } from '@/map'
// 使用灾害数据
const disasterData = useDisasterData()
// 提供给子组件使用
provide('disasterData', disasterData)
// 地图 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态势感知地图已就绪')
})
})
// 弹窗状态
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)
// 实际实现:使用 mapStore 的 camera service 飞行到人员位置
// const { camera } = mapStore.services()
// camera.flyTo({ destination: [lon, lat, height] })
showPersonnelDetail.value = false
}
// 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
// }
// })
// })
</script>
<style scoped lang="scss">
@use '@/styles/mixins.scss' as *;
@use './assets/styles/common.scss' as *;
.situational-awareness {
// 容器查询设置,用于嵌入场景的自适应缩放
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-header-height: calc(121 / 1080 * var(--cq-block-100, 100vh)); // Header 高度
--sa-min-width: 1280px;
--sa-min-height: 720px;
position: relative;
width: 100%;
height: 100%;
min-width: var(--sa-min-width);
min-height: var(--sa-min-height);
background-color: var(--bg-dark);
overflow: auto; // 当宿主尺寸 < 最小尺寸时允许滚动
// PageHeader 浮在顶部
> :first-child {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
}
&__main {
position: absolute;
inset: 0; // 铺满整个容器
background: url(./assets/images/main-bg.png) center/cover no-repeat;
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-top: var(--sa-header-height); // 预留 Header 高度
pointer-events: none; // 容器不拦截事件,让中间区域透明
}
// 左右面板列 - 浮动卡片样式
&__panel-column {
display: flex;
flex-direction: column;
gap: var(--sa-gap); // 列内子面板之间的间距
min-width: 0; // 防止在窄容器中溢出
min-height: 0; // 允许 flex 子元素收缩并启用滚动
pointer-events: auto; // 恢复面板的交互能力
}
// 中间占位区域 - 透明且不可交互,点击穿透到地图
&__center-spacer {
pointer-events: none;
}
// 地图控件层 - 高于遮罩和面板,用于放置地图控制工具
&__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;
}
</style>