refactor(screen): 重构地图查看器布局并集成真实地图组件

用 MapViewport 组件替换占位符地图,为地图、遮罩、面板和控件添加分层布局。实现控件的传送功能,增加图标加载的错误处理,并使用相机定位初始化地图存储。移除静态标记,转而支持动态实体点击处理(待办)。使用容器查询更新样式以实现响应式设计。
This commit is contained in:
Zzc 2025-11-17 11:12:56 +08:00
parent 21ed5472e4
commit ba76292c53
3 changed files with 213 additions and 162 deletions

View File

@ -42,7 +42,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { MAP_TOOLS, DEVICE_WATCH } from "../../constants"; import { MAP_TOOLS, DEVICE_WATCH } from "../../constants.js";
// //
const isWatchingDevice = ref(false); const isWatchingDevice = ref(false);
@ -53,16 +53,25 @@ const activeTool = ref(null);
// //
const emit = defineEmits(["device-watch", "tool-change"]); const emit = defineEmits(["device-watch", "tool-change"]);
//
console.log('MapControls - MAP_TOOLS:', MAP_TOOLS);
console.log('MapControls - DEVICE_WATCH:', DEVICE_WATCH);
/** /**
* 获取设备图标 * 获取设备图标
*/ */
const getDeviceIcon = () => { const getDeviceIcon = () => {
const iconName = DEVICE_WATCH.icon; const iconName = DEVICE_WATCH.icon;
const state = isWatchingDevice.value ? "点亮" : ""; const state = isWatchingDevice.value ? "点亮" : "";
try {
return new URL( return new URL(
`../../assets/images/MapControls/${iconName}${state}.png`, `../../assets/images/MapControls/${iconName}${state}.png`,
import.meta.url import.meta.url
).href; ).href;
} catch (error) {
console.error('加载设备图标失败:', error);
return '';
}
}; };
/** /**
@ -71,10 +80,15 @@ const getDeviceIcon = () => {
const getToolIcon = (tool) => { const getToolIcon = (tool) => {
const iconName = tool.icon; const iconName = tool.icon;
const state = activeTool.value === tool.key ? "点亮" : ""; const state = activeTool.value === tool.key ? "点亮" : "";
try {
return new URL( return new URL(
`../../assets/images/MapControls/${iconName}${state}.png`, `../../assets/images/MapControls/${iconName}${state}.png`,
import.meta.url import.meta.url
).href; ).href;
} catch (error) {
console.error('加载工具图标失败:', error, tool);
return '';
}
}; };
/** /**
@ -109,11 +123,7 @@ const handleToolClick = (toolKey) => {
@use "../../assets/styles/common.scss" as *; @use "../../assets/styles/common.scss" as *;
.map-controls { .map-controls {
position: absolute; position: relative; //
bottom: vh(30);
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex; display: flex;
gap: vw(14); gap: vw(14);
align-items: flex-end; align-items: flex-end;

View File

@ -1,43 +1,30 @@
<template> <template>
<div class="map-viewer"> <div class="map-viewer">
<div class="map-viewer__container"> <!-- Cesium 地图容器 -->
<!-- 这里放置实际的3D地图组件 Cesium, Mapbox GL JS --> <MapViewport />
<div class="map-placeholder">
</div>
<!-- 地图控制工具 --> <!-- 地图控制工具 - 使用 Teleport 传送到更高层级 -->
<!-- 延迟渲染确保目标元素已存在 -->
<Teleport to="#sa-controls" v-if="isMounted">
<MapControls /> <MapControls />
</Teleport>
<!-- 地图标记点应急人员应急中心等 -->
<div class="map-markers">
<img
src="../../assets/images/SketchPng9eb481bdb1aa555bcf1e817c3db9af492e273f88d5808c989826a8c382c5cb9f.png"
alt="marker"
class="map-marker"
style="top: 40%; left: 45%"
@click="handleMarkerClick('personnel')"
/>
<img
src="../../assets/images/SketchPng3992df008169f438b4eab0a5f08b6d39b14f1387a18c08564067b7845d11b124.png"
alt="center"
class="map-marker"
style="top: 60%; left: 55%"
@click="handleMarkerClick('center')"
/>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'
import { MapViewport } from '@/map'
import MapControls from './MapControls.vue' import MapControls from './MapControls.vue'
const emit = defineEmits(['marker-click']) // Teleport
const isMounted = ref(false)
const handleMarkerClick = (type) => { onMounted(() => {
console.log('点击标记:', type) // 使 nextTick DOM
emit('marker-click', type) setTimeout(() => {
} isMounted.value = true
}, 0)
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -45,83 +32,11 @@ const handleMarkerClick = (type) => {
@use '../../assets/styles/common.scss' as *; @use '../../assets/styles/common.scss' as *;
.map-viewer { .map-viewer {
flex: 1;
height: 100%;
position: relative; position: relative;
&__container {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
background: url(../../assets/images/SketchPng6e145958ea0dbf76e6562cc7965debbb95226caff3271c366ac9b254cbe6e796.png) center/cover no-repeat;
overflow: hidden;
.map-placeholder { // MapViewport
width: 100%; // MapViewport
height: 100%;
background: linear-gradient(135deg, rgba(9, 22, 40, 0.9) 0%, rgba(20, 53, 118, 0.7) 100%);
position: relative;
// 使3D
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url(../../assets/images/SketchPng6e145958ea0dbf76e6562cc7965debbb95226caff3271c366ac9b254cbe6e796.png) center/cover;
opacity: 0.1;
pointer-events: none;
}
.map-watermark {
position: absolute;
top: vh(20);
left: 50%;
transform: translateX(-50%);
padding: vh(8) vw(20);
background: rgba(0, 0, 0, 0.6);
border-radius: vw(6);
color: var(--text-white);
font-size: fs(14);
font-family: SourceHanSansCN-Medium, sans-serif;
}
}
.map-markers {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
.map-marker {
position: absolute;
width: vw(32);
height: vh(36);
cursor: pointer;
pointer-events: all;
transition: transform 0.3s;
animation: markerBounce 2s ease-in-out infinite;
&:hover {
transform: scale(1.2);
animation: none;
}
}
}
}
}
@keyframes markerBounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(vh(-10));
}
} }
</style> </style>

View File

@ -5,15 +5,30 @@
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="situational-awareness__main"> <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 /> <LeftPanel />
</div>
<!-- 中央地图区域 --> <div class="situational-awareness__center-spacer" aria-hidden="true"></div>
<MapViewer @marker-click="handleMarkerClick" /> <div class="situational-awareness__panel-column situational-awareness__panel-column--right">
<!-- 右侧面板 -->
<RightPanel /> <RightPanel />
</div> </div>
</div>
<!-- 地图控件层 - 高于遮罩和面板 -->
<div class="situational-awareness__controls-layer">
<div id="sa-controls" class="situational-awareness__controls"></div>
</div>
</div>
<!-- 弹窗组件 --> <!-- 弹窗组件 -->
<PersonnelDetail <PersonnelDetail
@ -32,7 +47,7 @@
</template> </template>
<script setup> <script setup>
import { ref, provide } from 'vue' import { ref, provide, onMounted } from 'vue'
import PageHeader from './components/PageHeader.vue' import PageHeader from './components/PageHeader.vue'
import LeftPanel from './components/LeftPanel/index.vue' import LeftPanel from './components/LeftPanel/index.vue'
import MapViewer from './components/MapViewer/index.vue' import MapViewer from './components/MapViewer/index.vue'
@ -40,6 +55,7 @@ import RightPanel from './components/RightPanel/index.vue'
import PersonnelDetail from './components/Popups/PersonnelDetail.vue' import PersonnelDetail from './components/Popups/PersonnelDetail.vue'
import EmergencyCenterDetail from './components/Popups/EmergencyCenterDetail.vue' import EmergencyCenterDetail from './components/Popups/EmergencyCenterDetail.vue'
import { useDisasterData } from './composables/useDisasterData' import { useDisasterData } from './composables/useDisasterData'
import { useMapStore } from '@/map'
// 使 // 使
const disasterData = useDisasterData() const disasterData = useDisasterData()
@ -47,6 +63,23 @@ const disasterData = useDisasterData()
// 使 // 使
provide('disasterData', disasterData) 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 showPersonnelDetail = ref(false)
const showCenterDetail = ref(false) const showCenterDetail = ref(false)
@ -75,21 +108,30 @@ const handleBack = () => {
// router.push('/cockpit') // router.push('/cockpit')
} }
//
const handleMarkerClick = (type) => {
if (type === 'personnel') {
showPersonnelDetail.value = true
} else if (type === 'center') {
showCenterDetail.value = true
}
}
// //
const handlePersonnelLink = (personnel) => { const handlePersonnelLink = (personnel) => {
console.log('联动人员:', personnel) console.log('联动人员:', personnel)
// // 使 mapStore camera service
// const { camera } = mapStore.services()
// camera.flyTo({ destination: [lon, lat, height] })
showPersonnelDetail.value = false 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -97,41 +139,125 @@ const handlePersonnelLink = (personnel) => {
@use './assets/styles/common.scss' as *; @use './assets/styles/common.scss' as *;
.situational-awareness { .situational-awareness {
width: 100vw; //
height: 100vh; 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);
background-color: var(--bg-dark); background-color: var(--bg-dark);
overflow: hidden; overflow: auto; // 宿 <
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&__main { &__main {
position: relative;
flex: 1; flex: 1;
display: grid;
grid-template-columns: vw(564) 1fr vw(526);
gap: 0;
min-height: 0; min-height: 0;
background: url(./assets/images/main-bg.png) center/cover no-repeat; background: url(./assets/images/main-bg.png) center/cover no-repeat;
position: relative; overflow: hidden;
}
// // -
&::before { &__map-layer {
content: '';
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0;
right: 0;
bottom: 0;
background: url(../3DSituationalAwarenessCopy/assets/img/SketchPng6e145958ea0dbf76e6562cc7965debbb95226caff3271c366ac9b254cbe6e796.png)
center/cover no-repeat;
opacity: 0.3;
pointer-events: none;
z-index: 0; z-index: 0;
} }
> * { // -
position: relative; &__map-mask {
position: absolute;
inset: 0;
z-index: 1; 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; //
}
// - 穿
&__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> </style>