Merge branch 'dev' of http://222.212.85.86:8222/bdzl2/bxztApp into dev
|
Before Width: | Height: | Size: 169 B After Width: | Height: | Size: 166 B |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 143 B |
|
Before Width: | Height: | Size: 137 B After Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@ -66,13 +66,13 @@ const locationInfo = [
|
||||
|
||||
.location-panel {
|
||||
width: 100%;
|
||||
padding: clamp(12px, vh(16), 20px) clamp(16px, vw(20), 28px);
|
||||
padding: clamp(16px, vh(16), 18px) clamp(6px, vw(6), 6px);
|
||||
background: url('../../assets/images/LocationPanel/地理位置内容背景.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
border-radius: vw(8);
|
||||
box-shadow: 0 0 vw(12) rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
min-width: 240px;
|
||||
min-width: 248px;
|
||||
max-width: 100%;
|
||||
// min-height: 160px;
|
||||
}
|
||||
@ -82,7 +82,7 @@ const locationInfo = [
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: clamp(4px, vh(6), 8px) clamp(4px, vw(6), 8px);
|
||||
gap: clamp(2px, vh(4), 6px) 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -4,9 +4,10 @@
|
||||
<CollapsiblePanel title="快速感知" subtitle="「灾害分析」">
|
||||
<template #title-icon>
|
||||
<img
|
||||
src="../../assets/images/摄像头.png"
|
||||
src="../../assets/images/media_dvr_on.png"
|
||||
alt="摄像头"
|
||||
class="camera-icon"
|
||||
@click.stop="handleCameraClick"
|
||||
/>
|
||||
</template>
|
||||
<DisasterAnalysis />
|
||||
@ -55,6 +56,14 @@
|
||||
<DisasterAnalysis />
|
||||
</LocationPanel>
|
||||
</div>
|
||||
|
||||
<!-- 视频弹窗 -->
|
||||
<VideoModal
|
||||
v-if="showVideoModal"
|
||||
:visible="showVideoModal"
|
||||
:monitor="cameraVideoData"
|
||||
@close="handleCloseVideoModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -65,6 +74,7 @@ import DisasterAnalysis from './DisasterAnalysis.vue'
|
||||
import ForcePreset from './ForcePreset.vue'
|
||||
import ForceDispatch from './ForceDispatch.vue'
|
||||
import LocationPanel from './LocationPanel.vue'
|
||||
import VideoModal from '../RightPanel/VideoModal.vue'
|
||||
|
||||
const isLocationOpen = ref(true)
|
||||
|
||||
@ -72,6 +82,30 @@ const toggleLocation = () => {
|
||||
isLocationOpen.value = !isLocationOpen.value
|
||||
}
|
||||
|
||||
// 视频弹窗状态
|
||||
const showVideoModal = ref(false)
|
||||
const cameraVideoData = ref({
|
||||
id: 999,
|
||||
type: 'drone',
|
||||
title: '快速感知',
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/快速感知.mp4', // 预留视频链接,后续补充
|
||||
dateRange: '',
|
||||
hasAudio: true,
|
||||
hasMegaphone: false,
|
||||
hasZoom: false,
|
||||
hasDirectionControl: false
|
||||
})
|
||||
|
||||
// 打开视频弹窗
|
||||
const handleCameraClick = () => {
|
||||
showVideoModal.value = true
|
||||
}
|
||||
|
||||
// 关闭视频弹窗
|
||||
const handleCloseVideoModal = () => {
|
||||
showVideoModal.value = false
|
||||
}
|
||||
|
||||
// 定义对外事件
|
||||
const emit = defineEmits(['start-dispatch'])
|
||||
|
||||
@ -101,6 +135,8 @@ const handleStartDispatch = (payload) => {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-left: vw(40); // 为左侧折叠按钮预留空间
|
||||
padding-top: var(--sa-header-height);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: vw(6);
|
||||
@ -122,7 +158,7 @@ const handleStartDispatch = (payload) => {
|
||||
|
||||
.location-entry {
|
||||
position: absolute;
|
||||
top: vh(32);
|
||||
top: var(--sa-header-height);
|
||||
left: 100%;
|
||||
margin-left: vw(12);
|
||||
display: flex;
|
||||
@ -188,15 +224,21 @@ const handleStartDispatch = (payload) => {
|
||||
}
|
||||
|
||||
.camera-icon {
|
||||
width: vw(20);
|
||||
height: vw(20);
|
||||
margin-left: vw(6);
|
||||
width: vw(30);
|
||||
height: vw(30);
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
vertical-align: middle;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -62,7 +62,7 @@ const handleBack = () => {
|
||||
align-items: center;
|
||||
gap: vw(10);
|
||||
padding: vh(10) vw(24);
|
||||
margin-top: vh(20);
|
||||
margin-top: 50px;
|
||||
background: url('../assets/images/返回按钮.png') 0 -1px no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border: none;
|
||||
|
||||
@ -40,129 +40,132 @@
|
||||
muted
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作台(仅无人机等有方向控制的监控) -->
|
||||
<div v-if="monitor.hasDirectionControl" class="video-modal__console">
|
||||
<img
|
||||
src="../../assets/images/video-control-console.png"
|
||||
alt="操作台背景"
|
||||
class="video-modal__console-bg"
|
||||
/>
|
||||
<!-- 操作台(仅无人机等有方向控制的监控) -->
|
||||
<div v-if="monitor.hasDirectionControl" class="video-modal__console">
|
||||
<!-- 轮盘图标 -->
|
||||
<img
|
||||
src="../../assets/images/video-control-console.png"
|
||||
alt="操作台"
|
||||
class="video-modal__console-bg"
|
||||
/>
|
||||
|
||||
<div class="video-modal__control-bar">
|
||||
<!-- 喊话 -->
|
||||
<div
|
||||
v-if="monitor.hasMegaphone"
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$emit('megaphone', monitor.id)"
|
||||
@keydown.enter.prevent="$emit('megaphone', monitor.id)"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-megaphone.png"
|
||||
alt="喊话"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">喊话</span>
|
||||
</div>
|
||||
<!-- 控制按钮容器 -->
|
||||
<div class="video-modal__control-bar">
|
||||
|
||||
<!-- 声音 -->
|
||||
<div
|
||||
v-if="monitor.hasAudio"
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$emit('audio', monitor.id)"
|
||||
@keydown.enter.prevent="$emit('audio', monitor.id)"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-sound.png"
|
||||
alt="声音"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">声音</span>
|
||||
</div>
|
||||
<!-- 喊话 -->
|
||||
<div
|
||||
v-if="monitor.hasMegaphone"
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$emit('megaphone', monitor.id)"
|
||||
@keydown.enter.prevent="$emit('megaphone', monitor.id)"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-megaphone.png"
|
||||
alt="喊话"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">喊话</span>
|
||||
</div>
|
||||
|
||||
<!-- 视觉左移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('left')"
|
||||
@keydown.enter.prevent="handleMove('left')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-left.png"
|
||||
alt="左移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉左移</span>
|
||||
</div>
|
||||
<!-- 声音 -->
|
||||
<div
|
||||
v-if="monitor.hasAudio"
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="$emit('audio', monitor.id)"
|
||||
@keydown.enter.prevent="$emit('audio', monitor.id)"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-sound.png"
|
||||
alt="声音"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">声音</span>
|
||||
</div>
|
||||
|
||||
<!-- 视觉右移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('right')"
|
||||
@keydown.enter.prevent="handleMove('right')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-right.png"
|
||||
alt="右移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉右移</span>
|
||||
</div>
|
||||
<!-- 视觉左移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('left')"
|
||||
@keydown.enter.prevent="handleMove('left')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-left.png"
|
||||
alt="左移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉左移</span>
|
||||
</div>
|
||||
|
||||
<!-- 视觉上移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('up')"
|
||||
@keydown.enter.prevent="handleMove('up')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-up.png"
|
||||
alt="上移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉上移</span>
|
||||
</div>
|
||||
<!-- 视觉右移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('right')"
|
||||
@keydown.enter.prevent="handleMove('right')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-right.png"
|
||||
alt="右移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉右移</span>
|
||||
</div>
|
||||
|
||||
<!-- 视觉下移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('down')"
|
||||
@keydown.enter.prevent="handleMove('down')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-down.png"
|
||||
alt="下移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉下移</span>
|
||||
</div>
|
||||
<!-- 视觉上移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('up')"
|
||||
@keydown.enter.prevent="handleMove('up')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-up.png"
|
||||
alt="上移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉上移</span>
|
||||
</div>
|
||||
|
||||
<!-- 缩小 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleClose"
|
||||
@keydown.enter.prevent="handleClose"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-shrink.png"
|
||||
alt="缩小"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">缩小</span>
|
||||
<!-- 视觉下移 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleMove('down')"
|
||||
@keydown.enter.prevent="handleMove('down')"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-down.png"
|
||||
alt="下移"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">视觉下移</span>
|
||||
</div>
|
||||
|
||||
<!-- 缩小 -->
|
||||
<div
|
||||
class="video-modal__control-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleClose"
|
||||
@keydown.enter.prevent="handleClose"
|
||||
>
|
||||
<img
|
||||
src="../../assets/images/video-control-shrink.png"
|
||||
alt="缩小"
|
||||
class="video-modal__control-icon"
|
||||
/>
|
||||
<span class="video-modal__control-label">缩小</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -348,6 +351,7 @@ body.body--no-scroll {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: clamp(10px, 1.5vh, 20px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__video {
|
||||
@ -359,49 +363,50 @@ body.body--no-scroll {
|
||||
border-radius: vw(8);
|
||||
}
|
||||
|
||||
// 操作台
|
||||
// 操作台容器
|
||||
&__console {
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: clamp(80px, 10vh, 120px);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: clamp(10px, 1.5vw, 20px);
|
||||
}
|
||||
|
||||
// 轮盘图标
|
||||
&__console-bg {
|
||||
// position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
// width: 100%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
width: clamp(120px, 7vw, 140px);
|
||||
height: clamp(120px, 7vw, 140px);
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 控制按钮容器
|
||||
&__control-bar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: clamp(15px, 2vw, 30px);
|
||||
padding: 0 clamp(20px, 2vw, 40px);
|
||||
gap: clamp(20px, 2.5vw, 40px);
|
||||
padding: clamp(8px, 1vh, 12px) clamp(20px, 2.5vw, 40px);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 50px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&__control-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: clamp(4px, 0.5vh, 8px);
|
||||
gap: clamp(6px, 0.8vw, 10px);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
padding: clamp(4px, 0.5vh, 8px);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.15);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@ -411,22 +416,24 @@ body.body--no-scroll {
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(135, 206, 250, 0.8);
|
||||
outline-offset: 4px;
|
||||
border-radius: vw(4);
|
||||
border-radius: clamp(4px, 0.5vw, 8px);
|
||||
}
|
||||
}
|
||||
|
||||
&__control-icon {
|
||||
width: clamp(24px, 2.5vw, 36px);
|
||||
height: clamp(24px, 2.5vw, 36px);
|
||||
width: clamp(28px, 3vw, 42px);
|
||||
height: clamp(28px, 3vw, 42px);
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
&__control-label {
|
||||
font-size: clamp(11px, 1vw, 13px);
|
||||
font-size: clamp(12px, 1.1vw, 14px);
|
||||
font-family: SourceHanSansCN-Regular, sans-serif;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -225,8 +225,8 @@ onUnmounted(() => {
|
||||
top: -1px;
|
||||
left: -3px;
|
||||
z-index: 2;
|
||||
width: clamp(140px, 10.1vw, 194px);
|
||||
height: clamp(28px, vh(32), 36px);
|
||||
width: clamp(140px, 10.1vw, 154px);
|
||||
height: clamp(18px, vh(18), 18px);
|
||||
background: url("../../assets/images/video-time-bg.png") no-repeat
|
||||
center center;
|
||||
background-size: 100% 100%;
|
||||
@ -275,9 +275,10 @@ onUnmounted(() => {
|
||||
transform-origin: center;
|
||||
|
||||
&__icon {
|
||||
width: clamp(12px, vw(14), 18px);
|
||||
height: clamp(12px, vh(14), 18px);
|
||||
margin-right: clamp(4px, vw(6), 8px);
|
||||
width: clamp(20px, vw(20), 24px);
|
||||
// height: clamp(14px, vw(14), 18px);
|
||||
height: auto;
|
||||
// margin-right: clamp(4px, vw(6), 8px);
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@ -41,6 +41,8 @@ import CollaborationInfo from './CollaborationInfo.vue'
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: vw(40); // 为右侧折叠按钮预留空间
|
||||
padding-top: var(--sa-header-height);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: vw(6);
|
||||
|
||||
@ -60,16 +60,16 @@ const valueClass = computed(() => {
|
||||
&__label {
|
||||
color: var(--text-white);
|
||||
font-size: fs(14);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
// font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: var(--text-white);
|
||||
font-size: fs(14);
|
||||
font-size: fs(15);
|
||||
font-family: PingFangSC-Semibold, sans-serif;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@ -94,6 +94,11 @@
|
||||
<div class="map-tooltip__content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div v-if="$slots.actions" class="map-tooltip__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -280,8 +285,8 @@ const handleClose = () => {
|
||||
*/
|
||||
.map-tooltip__corner {
|
||||
position: absolute;
|
||||
width: vw(20);
|
||||
height: vw(20); // 使用 vw 保持正方形
|
||||
// width: vw(20);
|
||||
// height: vw(20); // 使用 vw 保持正方形
|
||||
pointer-events: none; // 装饰元素不响应鼠标事件
|
||||
z-index: 1; // 确保显示在背景之上
|
||||
|
||||
@ -431,6 +436,19 @@ const handleClose = () => {
|
||||
gap: vh(6);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作按钮区域
|
||||
* 用于显示"连线"、"联动"等交互按钮
|
||||
*/
|
||||
.map-tooltip__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: vh(16);
|
||||
padding-top: vh(12);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 淡入淡出动画
|
||||
* 结合缩放效果,提升视觉体验
|
||||
|
||||
@ -0,0 +1,259 @@
|
||||
import { ref } from 'vue'
|
||||
import * as Cesium from 'cesium'
|
||||
import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png'
|
||||
|
||||
/**
|
||||
* 实体动画管理 Composable
|
||||
* 负责管理地图实体的路径动画,特别是应急人员沿路径移动
|
||||
*/
|
||||
export function useEntityAnimation() {
|
||||
// 动画实体引用
|
||||
const animatedEntity = ref(null)
|
||||
|
||||
// 动画是否正在运行
|
||||
const isAnimating = ref(false)
|
||||
|
||||
/**
|
||||
* 应急人员路径坐标 (Cartesian3 格式)
|
||||
* 这些坐标定义了人员从起点到终点的移动路径
|
||||
*/
|
||||
const PERSONNEL_PATH_COORDINATES = [
|
||||
{ x: -1706079.1327424292, y: 5247893.165552528, z: 3187993.9339800295 },
|
||||
{ x: -1706116.7863268533, y: 5247923.177994122, z: 3187929.297700776 },
|
||||
{ x: -1706131.4939896727, y: 5247956.7916397555, z: 3187865.1250298577 },
|
||||
{ x: -1706117.7768181972, y: 5247999.865521995, z: 3187795.4584125844 },
|
||||
{ x: -1706148.232862157, y: 5248029.100250082, z: 3187735.2203392833 },
|
||||
{ x: -1706129.4638550146, y: 5248073.941490989, z: 3187662.59740559 },
|
||||
{ x: -1706131.3071046746, y: 5248086.057462914, z: 3187643.216358425 },
|
||||
{ x: -1706164.2362053818, y: 5248120.213627388, z: 3187577.1867482658 },
|
||||
{ x: -1706255.3513903276, y: 5248175.916851786, z: 3187422.819624157 },
|
||||
{ x: -1706300.2731912779, y: 5248172.011305182, z: 3187397.8767570513 },
|
||||
{ x: -1706343.1007708232, y: 5248165.925888667, z: 3187382.186124808 }
|
||||
]
|
||||
|
||||
/**
|
||||
* 启动应急人员沿路径移动动画
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {number} [options.duration=36] - 动画总时长(秒)
|
||||
* @param {boolean} [options.trackEntity=false] - 是否相机跟随实体
|
||||
* @param {string} [options.personnelName='应急人员'] - 人员名称
|
||||
* @param {string} [options.department='应急救援队'] - 所属部门
|
||||
* @returns {Cesium.Entity} 返回创建的动画实体
|
||||
*/
|
||||
const startPersonnelMovement = (viewer, options = {}) => {
|
||||
if (!viewer) {
|
||||
console.warn('[useEntityAnimation] startPersonnelMovement: viewer 为空')
|
||||
return null
|
||||
}
|
||||
|
||||
if (isAnimating.value) {
|
||||
console.warn('[useEntityAnimation] 动画已在运行中,请先停止当前动画')
|
||||
return animatedEntity.value
|
||||
}
|
||||
|
||||
const config = {
|
||||
duration: options.duration ?? 60, // 默认 60 秒,让移动更清晰可见
|
||||
trackEntity: options.trackEntity ?? false,
|
||||
personnelName: options.personnelName ?? '应急人员',
|
||||
department: options.department ?? '应急救援队'
|
||||
}
|
||||
|
||||
console.log('[useEntityAnimation] 开始启动人员移动动画...', config)
|
||||
|
||||
// 设置动画时间范围
|
||||
const startTime = Cesium.JulianDate.now()
|
||||
const stopTime = Cesium.JulianDate.addSeconds(
|
||||
startTime,
|
||||
config.duration,
|
||||
new Cesium.JulianDate()
|
||||
)
|
||||
|
||||
// 配置 viewer 时钟
|
||||
viewer.clock.startTime = startTime.clone()
|
||||
viewer.clock.stopTime = stopTime.clone()
|
||||
viewer.clock.currentTime = startTime.clone()
|
||||
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP // 动画结束后停止
|
||||
viewer.clock.multiplier = 1 // 实时速度
|
||||
viewer.clock.shouldAnimate = true
|
||||
|
||||
// 创建 SampledPositionProperty 定义路径
|
||||
const positionProperty = new Cesium.SampledPositionProperty()
|
||||
|
||||
// 计算每个路径点的时间间隔
|
||||
const numberOfPoints = PERSONNEL_PATH_COORDINATES.length
|
||||
const timeInterval = config.duration / (numberOfPoints - 1)
|
||||
|
||||
// 添加路径采样点
|
||||
PERSONNEL_PATH_COORDINATES.forEach((coord, index) => {
|
||||
const time = Cesium.JulianDate.addSeconds(
|
||||
startTime,
|
||||
index * timeInterval,
|
||||
new Cesium.JulianDate()
|
||||
)
|
||||
const position = new Cesium.Cartesian3(coord.x, coord.y, coord.z)
|
||||
positionProperty.addSample(time, position)
|
||||
})
|
||||
|
||||
// 创建脉冲缩放效果 - 让图标闪烁更醒目
|
||||
const pulseScale = new Cesium.CallbackProperty((time) => {
|
||||
const elapsed = Cesium.JulianDate.secondsDifference(time, startTime)
|
||||
// 使用正弦波产生脉冲效果,频率 3Hz,幅度 ±30%
|
||||
return 1.0 + Math.sin(elapsed * 3) * 0.3
|
||||
}, false)
|
||||
|
||||
// 创建动画实体
|
||||
const entity = viewer.entities.add({
|
||||
availability: new Cesium.TimeIntervalCollection([
|
||||
new Cesium.TimeInterval({
|
||||
start: startTime,
|
||||
stop: stopTime
|
||||
})
|
||||
]),
|
||||
position: positionProperty,
|
||||
orientation: new Cesium.VelocityOrientationProperty(positionProperty), // 自动朝向移动方向
|
||||
billboard: {
|
||||
image: soldierIcon,
|
||||
width: 48, // 增大尺寸,从 36 增加到 48
|
||||
height: 56, // 从 40 增加到 56
|
||||
scale: pulseScale, // 使用脉冲缩放效果
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
// 添加距离缩放,让图标在不同距离下都清晰可见
|
||||
scaleByDistance: new Cesium.NearFarScalar(
|
||||
1000, 1.5, // 近距离(1000米)放大到 1.5 倍
|
||||
50000, 0.8 // 远距离(50000米)缩小到 0.8 倍
|
||||
)
|
||||
},
|
||||
// 添加发光轨迹线
|
||||
path: {
|
||||
resolution: 1,
|
||||
material: new Cesium.PolylineGlowMaterialProperty({
|
||||
glowPower: 0.4, // 发光强度
|
||||
taperPower: 0.5, // 渐变效果
|
||||
color: Cesium.Color.CYAN // 青色轨迹
|
||||
}),
|
||||
width: 8,
|
||||
leadTime: 0, // 前方不显示轨迹
|
||||
trailTime: config.duration // 后方显示完整轨迹
|
||||
},
|
||||
properties: {
|
||||
type: 'animatedSoldier',
|
||||
name: config.personnelName,
|
||||
department: config.department,
|
||||
isAnimating: true
|
||||
}
|
||||
})
|
||||
|
||||
animatedEntity.value = entity
|
||||
isAnimating.value = true
|
||||
|
||||
// 可选: 相机跟随实体
|
||||
if (config.trackEntity) {
|
||||
viewer.trackedEntity = entity
|
||||
// 设置相机视角偏移
|
||||
entity.viewFrom = new Cesium.Cartesian3(-100.0, -100.0, 50.0)
|
||||
}
|
||||
|
||||
console.log('[useEntityAnimation] 人员移动动画已启动')
|
||||
|
||||
// 监听动画结束事件
|
||||
const removeListener = viewer.clock.onStop.addEventListener(() => {
|
||||
console.log('[useEntityAnimation] 动画已结束')
|
||||
isAnimating.value = false
|
||||
removeListener() // 移除监听器
|
||||
})
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止人员移动动画
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
* @param {boolean} [removeEntity=true] - 是否移除动画实体
|
||||
*/
|
||||
const stopPersonnelMovement = (viewer, removeEntity = true) => {
|
||||
if (!viewer) {
|
||||
console.warn('[useEntityAnimation] stopPersonnelMovement: viewer 为空')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[useEntityAnimation] 停止人员移动动画')
|
||||
|
||||
// 停止时钟动画
|
||||
viewer.clock.shouldAnimate = false
|
||||
|
||||
// 取消相机跟随
|
||||
if (viewer.trackedEntity === animatedEntity.value) {
|
||||
viewer.trackedEntity = undefined
|
||||
}
|
||||
|
||||
// 移除实体
|
||||
if (removeEntity && animatedEntity.value) {
|
||||
viewer.entities.remove(animatedEntity.value)
|
||||
animatedEntity.value = null
|
||||
}
|
||||
|
||||
isAnimating.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停动画
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
*/
|
||||
const pauseAnimation = (viewer) => {
|
||||
if (!viewer) return
|
||||
viewer.clock.shouldAnimate = false
|
||||
console.log('[useEntityAnimation] 动画已暂停')
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复动画
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
*/
|
||||
const resumeAnimation = (viewer) => {
|
||||
if (!viewer) return
|
||||
if (isAnimating.value) {
|
||||
viewer.clock.shouldAnimate = true
|
||||
console.log('[useEntityAnimation] 动画已恢复')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径的起点坐标 (用于初始标记点)
|
||||
* @returns {Cesium.Cartesian3} 起点坐标
|
||||
*/
|
||||
const getStartPosition = () => {
|
||||
const firstCoord = PERSONNEL_PATH_COORDINATES[0]
|
||||
return new Cesium.Cartesian3(firstCoord.x, firstCoord.y, firstCoord.z)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Cartesian3 坐标转换为经纬度
|
||||
* @param {Cesium.Cartesian3} cartesian - Cartesian3 坐标
|
||||
* @returns {Object} { longitude, latitude, height }
|
||||
*/
|
||||
const cartesianToLonLat = (cartesian) => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
|
||||
return {
|
||||
longitude: Cesium.Math.toDegrees(cartographic.longitude),
|
||||
latitude: Cesium.Math.toDegrees(cartographic.latitude),
|
||||
height: cartographic.height
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
animatedEntity,
|
||||
isAnimating,
|
||||
startPersonnelMovement,
|
||||
stopPersonnelMovement,
|
||||
pauseAnimation,
|
||||
resumeAnimation,
|
||||
getStartPosition,
|
||||
cartesianToLonLat,
|
||||
PERSONNEL_PATH_COORDINATES
|
||||
}
|
||||
}
|
||||
|
||||
export default useEntityAnimation
|
||||
@ -6,7 +6,8 @@ import { cesiumDataConfig } from '../config/cesiumData'
|
||||
import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png'
|
||||
import deviceIcon from '../assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png'
|
||||
import emergencyBaseIcon from '../assets/images/应急基地.png'
|
||||
import emergencyCenterIcon from '../assets/images/应急中心.png'
|
||||
import emergencyCenterIcon from '../assets/images/应急中心icon定位.png'
|
||||
import reserveCenterIcon from '../assets/images/储备中心.png'
|
||||
|
||||
// 默认高度偏移(米)- 与 WuRenJi 保持一致
|
||||
const DEFAULT_HEIGHT_OFFSET = 100
|
||||
@ -19,6 +20,7 @@ export function useMapMarkers() {
|
||||
const collapseAreaEntities = ref([])
|
||||
const markerEntities = ref([])
|
||||
const emergencyResourceEntities = ref([]) // 应急资源标记(由API数据动态生成)
|
||||
const reserveCenterEntities = ref([]) // 储备中心和预置点标记
|
||||
|
||||
/**
|
||||
* 获取塌陷区域的所有位置点
|
||||
@ -574,6 +576,108 @@ export function useMapMarkers() {
|
||||
emergencyResourceEntities.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除储备中心和预置点标记
|
||||
* @param {Cesium.Viewer} viewer
|
||||
*/
|
||||
const clearReserveCenterMarkers = (viewer) => {
|
||||
if (!viewer) return
|
||||
|
||||
console.log(`[useMapMarkers] 清除 ${reserveCenterEntities.value.length} 个储备中心/预置点标记`)
|
||||
|
||||
reserveCenterEntities.value.forEach(entity => {
|
||||
if (entity) viewer.entities.remove(entity)
|
||||
})
|
||||
reserveCenterEntities.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据API数据添加储备中心和预置点标记
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {Array} reserveData - API返回的储备中心和预置点数据列表
|
||||
* @param {string} reserveData[].gl1Id - ID
|
||||
* @param {string} reserveData[].gl1Yjllmc - 名称
|
||||
* @param {number} reserveData[].gl1Lng - 经度
|
||||
* @param {number} reserveData[].gl1Lat - 纬度
|
||||
* @param {string} reserveData[].gl1Qxmc - 区县名称
|
||||
* @param {string} reserveData[].gl1Rysl - 人员数量
|
||||
* @param {string} reserveData[].gl1Zdmj - 占地面积
|
||||
* @param {string} reserveData[].gl1Lx - 类型 (2=储备中心, 3=预置点)
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {number} [options.heightOffset=10] - 相对地面的高度偏移(米)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const addReserveCenterMarkers = async (viewer, reserveData, options = {}) => {
|
||||
if (!viewer) {
|
||||
console.warn('[useMapMarkers] addReserveCenterMarkers: viewer 为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(reserveData) || reserveData.length === 0) {
|
||||
console.warn('[useMapMarkers] addReserveCenterMarkers: reserveData 为空或不是数组')
|
||||
return
|
||||
}
|
||||
|
||||
const markerOptions = {
|
||||
heightOffset: options.heightOffset ?? DEFAULT_HEIGHT_OFFSET
|
||||
}
|
||||
|
||||
console.log('[useMapMarkers] 开始添加储备中心和预置点标记...', reserveData.length)
|
||||
|
||||
const entities = []
|
||||
|
||||
for (const item of reserveData) {
|
||||
if (!item.gl1Lat || !item.gl1Lng) {
|
||||
console.warn('[useMapMarkers] 数据缺少坐标信息:', item)
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await resolveCartesianFromDegrees(
|
||||
viewer,
|
||||
item.gl1Lng,
|
||||
item.gl1Lat,
|
||||
markerOptions.heightOffset,
|
||||
false
|
||||
)
|
||||
|
||||
// 根据类型选择图标和类型标识
|
||||
// gl1Lx: 2=储备中心, 3=预置点
|
||||
const itemType = String(item.gl1Lx).trim()
|
||||
const isReserveCenter = itemType === '2'
|
||||
const icon = isReserveCenter ? reserveCenterIcon : emergencyBaseIcon
|
||||
const type = isReserveCenter ? 'reserveCenter' : 'presetPoint'
|
||||
|
||||
const entity = viewer.entities.add({
|
||||
position: result.position,
|
||||
billboard: {
|
||||
image: icon,
|
||||
width: 48,
|
||||
height: 48,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
heightReference: resolveBillboardHeightReference(result.samplingSucceeded),
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
||||
},
|
||||
properties: {
|
||||
type,
|
||||
id: item.gl1Id,
|
||||
name: item.gl1Yjllmc || (isReserveCenter ? '储备中心' : '预置点'),
|
||||
district: item.gl1Qxmc || '-',
|
||||
personnelCount: item.gl1Rysl || '0',
|
||||
area: item.gl1Zdmj || '-'
|
||||
}
|
||||
})
|
||||
entities.push(entity)
|
||||
}
|
||||
|
||||
reserveCenterEntities.value = entities
|
||||
console.log(`[useMapMarkers] 添加储备中心/预置点标记 ${entities.length} 个`)
|
||||
|
||||
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
|
||||
if (entities.length > 0) {
|
||||
viewer.scene.requestRender()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据API数据添加应急资源标记(养护站)
|
||||
* @param {Cesium.Viewer} viewer
|
||||
@ -665,6 +769,7 @@ export function useMapMarkers() {
|
||||
collapseAreaEntities,
|
||||
markerEntities,
|
||||
emergencyResourceEntities,
|
||||
reserveCenterEntities,
|
||||
initializeMarkers,
|
||||
clearMarkers,
|
||||
setMarkersSplitDirection,
|
||||
@ -675,6 +780,8 @@ export function useMapMarkers() {
|
||||
addRandomMarkers,
|
||||
addEmergencyResourceMarkers,
|
||||
clearEmergencyResourceMarkers,
|
||||
addReserveCenterMarkers,
|
||||
clearReserveCenterMarkers,
|
||||
getCollapseCenter: calculateCollapseCenter // 提前获取中心点,不依赖标记初始化
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export const AFTER_3DTILES_CONFIG = {
|
||||
|
||||
// 3D Tiles 服务 URL
|
||||
// url: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/S107/terra_b3dms/tileset.json',
|
||||
url: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/ylzg/zxyj1119/terra_b3dms/tileset.json',
|
||||
url: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/ylzg/zxyj1120/terra_b3dms/tileset.json',
|
||||
|
||||
|
||||
// 默认可见性(初始化时灾后模型默认显示)
|
||||
|
||||
@ -17,7 +17,7 @@ export const VIDEO_MONITORS = [
|
||||
{
|
||||
id: 1,
|
||||
type: VIDEO_TYPES.PERSONNEL,
|
||||
title: '单兵(张三三)设备视角',
|
||||
title: '单兵(张维)设备视角',
|
||||
// videoSrc: getVideoUrl('demo/ylzg/单兵视角.mp4'), // 从 OSS 获取视频 URL
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/单兵视角.mp4',
|
||||
dateRange: '2025/9/1-2025/12/1', // 日期范围
|
||||
@ -43,7 +43,7 @@ export const VIDEO_MONITORS = [
|
||||
type: VIDEO_TYPES.VEHICLE_EXTERNAL,
|
||||
title: '指挥车外部视角',
|
||||
// videoSrc: getVideoUrl('demo/ylzg/单兵视角.mp4'), // 暂时使用单兵视角视频
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/单兵视角.mp4',
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/指挥车外部视角.mp4',
|
||||
dateRange: '2025/9/1-2025/12/1',
|
||||
hasAudio: true,
|
||||
hasMegaphone: true,
|
||||
@ -55,7 +55,7 @@ export const VIDEO_MONITORS = [
|
||||
type: VIDEO_TYPES.VEHICLE_MEETING,
|
||||
title: '指挥车会议视角',
|
||||
// videoSrc: getVideoUrl('demo/ylzg/无人机视角.mp4'), // 暂时使用无人机视角视频
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/无人机视角.mp4',
|
||||
videoSrc: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/指挥车会议视角.mp4',
|
||||
dateRange: '2025/9/1-2025/12/1',
|
||||
hasAudio: true,
|
||||
hasMegaphone: true,
|
||||
|
||||
@ -126,6 +126,18 @@
|
||||
<span class="tooltip-field-value">{{ field.value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮插槽 -->
|
||||
<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>
|
||||
</MapTooltip>
|
||||
</div>
|
||||
|
||||
@ -172,6 +184,7 @@ import { useDualMapCompare } from "./composables/useDualMapCompare";
|
||||
import { useMapMarkers } from "./composables/useMapMarkers";
|
||||
import { use3DTiles } from "./composables/use3DTiles";
|
||||
import { useMapTooltip } from "./composables/useMapTooltip";
|
||||
import { useEntityAnimation } from "./composables/useEntityAnimation";
|
||||
import { useMapStore } from "@/map";
|
||||
import { request } from "@shared/utils/request";
|
||||
|
||||
@ -181,6 +194,7 @@ 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";
|
||||
|
||||
// 折叠按钮图标
|
||||
import collapseLeftArrow from "./assets/images/折叠面板左箭头.png";
|
||||
@ -203,6 +217,9 @@ const handleDistanceChange = async (newDistance) => {
|
||||
|
||||
// 重新加载应急资源数据并更新地图标记
|
||||
await loadEmergencyResources(108.011506, 30.175827);
|
||||
|
||||
// 重新加载储备中心和预置点数据并更新地图标记
|
||||
await loadReserveCentersAndPresets(108.011506, 30.175827);
|
||||
};
|
||||
|
||||
// 提供给子组件使用
|
||||
@ -222,11 +239,16 @@ const {
|
||||
getCollapseCenter,
|
||||
addEmergencyResourceMarkers,
|
||||
clearEmergencyResourceMarkers,
|
||||
addReserveCenterMarkers,
|
||||
clearReserveCenterMarkers,
|
||||
} = useMapMarkers();
|
||||
|
||||
// 地图 Tooltip 功能
|
||||
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
|
||||
|
||||
// 实体动画功能
|
||||
const { startPersonnelMovement, stopPersonnelMovement, isAnimating } = useEntityAnimation();
|
||||
|
||||
// 当前显示 tooltip 的实体(用于相机移动时更新位置)
|
||||
const currentTooltipEntity = ref(null);
|
||||
|
||||
@ -300,6 +322,11 @@ const setupMapClickHandler = (viewer) => {
|
||||
showMarkerTooltip(viewer, markerEntity, click.position, icon);
|
||||
foundMarker = true;
|
||||
break;
|
||||
} else if (type === 'reserveCenter' || type === 'presetPoint') {
|
||||
const icon = type === 'reserveCenter' ? reserveCenterIcon : emergencyBaseIcon;
|
||||
showMarkerTooltip(viewer, markerEntity, click.position, icon);
|
||||
foundMarker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -328,6 +355,10 @@ const setupMapClickHandler = (viewer) => {
|
||||
? emergencyCenterIcon
|
||||
: emergencyBaseIcon;
|
||||
showMarkerTooltip(viewer, entity, click.position, icon);
|
||||
} else if (type === 'reserveCenter' || type === 'presetPoint') {
|
||||
// 储备中心和预置点
|
||||
const icon = type === 'reserveCenter' ? reserveCenterIcon : emergencyBaseIcon;
|
||||
showMarkerTooltip(viewer, entity, click.position, icon);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -388,28 +419,45 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
|
||||
// 构建 Tooltip 数据
|
||||
let title = '';
|
||||
const fields = [];
|
||||
const actions = [];
|
||||
|
||||
if (type === 'soldier') {
|
||||
title = '单兵信息';
|
||||
// 应急人员
|
||||
title = '应急人员';
|
||||
fields.push(
|
||||
{ label: '姓名', value: properties.name?.getValue() || '-' },
|
||||
{ label: '部门', value: properties.department?.getValue() || '-' },
|
||||
{ label: '位置', value: properties.location?.getValue() || '-' }
|
||||
{ label: '位置信息', value: properties.location?.getValue() || '-' },
|
||||
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
|
||||
);
|
||||
actions.push({
|
||||
label: '联动',
|
||||
type: 'link',
|
||||
data: entity
|
||||
});
|
||||
} else if (type === 'device') {
|
||||
title = '设备信息';
|
||||
// 应急装备
|
||||
title = '应急装备';
|
||||
fields.push(
|
||||
{ label: '设备名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
|
||||
{ label: '位置', value: properties.location?.getValue() || '-' }
|
||||
{ label: '位置信息', value: properties.location?.getValue() || '-' },
|
||||
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
|
||||
);
|
||||
} else if (type === 'emergencyBase') {
|
||||
// 应急基地
|
||||
title = '应急基地';
|
||||
fields.push(
|
||||
{ label: '名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '地址', value: properties.address?.getValue() || '-' },
|
||||
{ label: '距离', value: properties.distance?.getValue() || '-' }
|
||||
{ label: '基地名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '路线编号', value: properties.routeNumber?.getValue() || '-' },
|
||||
{ label: '隶属单位', value: properties.department?.getValue() || '-' },
|
||||
{ label: '位置信息', value: properties.location?.getValue() || '-' }
|
||||
);
|
||||
actions.push({
|
||||
label: '连线',
|
||||
type: 'connect',
|
||||
data: entity
|
||||
});
|
||||
} else if (type === 'station') {
|
||||
const stationName = properties.name?.getValue() || '';
|
||||
const distance = properties.distance?.getValue() || 0;
|
||||
@ -431,6 +479,20 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
|
||||
{ label: '距离', value: `${distance}公里` }
|
||||
);
|
||||
}
|
||||
} else if (type === 'reserveCenter') {
|
||||
// 储备中心
|
||||
title = '储备中心';
|
||||
fields.push(
|
||||
{ label: '名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '位置信息', value: properties.location?.getValue() || '-' }
|
||||
);
|
||||
} else if (type === 'presetPoint') {
|
||||
// 预置点
|
||||
title = '预置点';
|
||||
fields.push(
|
||||
{ label: '名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '位置信息', value: properties.location?.getValue() || '-' }
|
||||
);
|
||||
}
|
||||
|
||||
// 显示 Tooltip,使用实体的屏幕坐标
|
||||
@ -439,7 +501,7 @@ const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
|
||||
y: canvasPosition.y,
|
||||
title,
|
||||
icon,
|
||||
data: { fields }
|
||||
data: { fields, actions: actions.length > 0 ? actions : undefined }
|
||||
});
|
||||
|
||||
// 保存当前实体,用于相机移动时更新位置
|
||||
@ -519,22 +581,20 @@ onMounted(() => {
|
||||
});
|
||||
viewer.entities.add(defaultPoint);
|
||||
|
||||
// 在默认点附近添加10个模拟点位(应急人员和应急装备),分散在10km范围内
|
||||
// 1度纬度约等于111km,1度经度在30度纬度约等于96km
|
||||
// 10km约等于0.09度纬度,0.104度经度
|
||||
// 在默认点附近添加10个模拟点位(应急人员和应急装备)
|
||||
const simulatedPoints = [
|
||||
// 应急人员 - 分散在不同方向
|
||||
{ type: 'soldier', name: '张三', department: '应急救援队', lon: 108.051, lat: 30.205, distance: 4.2, icon: soldierIcon },
|
||||
{ type: 'soldier', name: '李四', department: '消防队', lon: 107.975, lat: 30.195, distance: 5.8, icon: soldierIcon },
|
||||
{ type: 'soldier', name: '王五', department: '医疗队', lon: 108.025, lat: 30.155, distance: 3.5, icon: soldierIcon },
|
||||
{ type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.085, lat: 30.168, distance: 7.2, icon: soldierIcon },
|
||||
{ type: 'soldier', name: '刘七', department: '消防队', lon: 107.945, lat: 30.182, distance: 8.5, icon: soldierIcon },
|
||||
// 应急装备 - 分散在不同方向
|
||||
{ type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 108.065, lat: 30.185, distance: 6.3, icon: deviceIcon },
|
||||
{ type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.960, lat: 30.165, distance: 6.8, icon: deviceIcon },
|
||||
{ type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.035, lat: 30.225, distance: 5.5, icon: deviceIcon },
|
||||
{ type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.095, lat: 30.195, distance: 9.2, icon: deviceIcon },
|
||||
{ type: 'device', name: '通讯设备', deviceType: '卫星电话', lon: 107.930, lat: 30.175, distance: 9.8, icon: deviceIcon }
|
||||
// 应急人员 (6个)
|
||||
{ type: 'soldier', name: '张三', department: '应急救援队', lon: 107.97, lat: 30.25, distance: 2.5, estimatedArrival: '10分钟', icon: soldierIcon },
|
||||
{ type: 'soldier', name: '李四', department: '消防队', lon: 107.971901, lat: 30.251428, distance: 2.3, estimatedArrival: '8分钟', icon: soldierIcon },
|
||||
{ type: 'soldier', name: '王五', department: '医疗队', lon: 107.974901, lat: 30.241428, distance: 3.1, estimatedArrival: '12分钟', icon: soldierIcon },
|
||||
{ type: 'soldier', name: '赵六', department: '应急救援队', lon: 108.047344, lat: 30.164313, distance: 4.2, estimatedArrival: '15分钟', icon: soldierIcon },
|
||||
{ type: 'soldier', name: '刘七', department: '消防队', lon: 108.046344, lat: 30.168313, distance: 3.8, estimatedArrival: '14分钟', icon: soldierIcon },
|
||||
{ type: 'soldier', name: '陈八', department: '医疗队', lon: 108.050344, lat: 30.170313, distance: 4.5, estimatedArrival: '16分钟', icon: soldierIcon },
|
||||
// 应急装备 (4个)
|
||||
{ type: 'device', name: '救援车辆A', deviceType: '消防车', lon: 107.98088, lat: 30.2487, distance: 2.8, estimatedArrival: '11分钟', icon: deviceIcon },
|
||||
{ type: 'device', name: '救援车辆B', deviceType: '救护车', lon: 107.97898, lat: 30.2502, distance: 2.6, estimatedArrival: '9分钟', icon: deviceIcon },
|
||||
{ type: 'device', name: '无人机A', deviceType: 'DJI', lon: 108.049344, lat: 30.160313, distance: 4.8, estimatedArrival: '17分钟', icon: deviceIcon },
|
||||
{ type: 'device', name: '无人机B', deviceType: 'DJI', lon: 108.043344, lat: 30.169313, distance: 3.6, estimatedArrival: '13分钟', icon: deviceIcon }
|
||||
];
|
||||
|
||||
simulatedPoints.forEach(point => {
|
||||
@ -553,13 +613,15 @@ onMounted(() => {
|
||||
type: 'soldier',
|
||||
name: point.name,
|
||||
department: point.department,
|
||||
location: `目前为止距离现场${point.distance}公里`
|
||||
location: `目前为止距离现场${point.distance}公里`,
|
||||
estimatedArrival: point.estimatedArrival
|
||||
}
|
||||
: {
|
||||
type: 'device',
|
||||
name: point.name,
|
||||
deviceType: point.deviceType,
|
||||
location: `目前为止距离现场${point.distance}公里`
|
||||
location: `目前为止距离现场${point.distance}公里`,
|
||||
estimatedArrival: point.estimatedArrival
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -721,10 +783,60 @@ const loadEmergencyResources = async (longitude, latitude) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载储备中心和预置点数据
|
||||
const loadReserveCentersAndPresets = async (longitude, latitude) => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/snow-ops-platform/yhYjll/list`,
|
||||
method: "GET",
|
||||
params: {
|
||||
longitude,
|
||||
latitude,
|
||||
maxDistance: disasterData.forcePreset.value.searchRadius,
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.data && Array.isArray(response.data)) {
|
||||
console.log("[index.vue] 储备中心和预置点数据加载成功:", response.data);
|
||||
|
||||
// 更新地图标记
|
||||
if (mapStore.viewer) {
|
||||
console.log("[index.vue] 添加储备中心和预置点地图标记...");
|
||||
|
||||
// 清除旧的标记
|
||||
clearReserveCenterMarkers(mapStore.viewer);
|
||||
|
||||
// 添加新的标记
|
||||
await addReserveCenterMarkers(
|
||||
mapStore.viewer,
|
||||
response.data,
|
||||
{ heightOffset: 10 }
|
||||
);
|
||||
} else {
|
||||
console.warn("[index.vue] 地图viewer未就绪,跳过标记更新");
|
||||
}
|
||||
} else {
|
||||
console.warn("[index.vue] 储备中心和预置点接口返回数据为空");
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("[index.vue] 加载储备中心和预置点数据失败:", error);
|
||||
ElMessage.warning({
|
||||
message: "储备中心和预置点数据加载失败",
|
||||
duration: 3000,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载应急资源数据(使用默认灾害点坐标)
|
||||
// await loadEmergencyResources(108.011506, 30.175827);
|
||||
const response = await loadEmergencyResources(108.011506, 30.175827);
|
||||
|
||||
// 加载储备中心和预置点数据(使用相同的坐标)
|
||||
await loadReserveCentersAndPresets(108.011506, 30.175827);
|
||||
});
|
||||
|
||||
/**
|
||||
@ -808,6 +920,55 @@ const handlePersonnelLink = (personnel) => {
|
||||
showPersonnelDetail.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理 Tooltip 操作按钮点击事件
|
||||
* @param {Object} action - 操作对象,包含 type 和 data
|
||||
*/
|
||||
const handleTooltipAction = (action) => {
|
||||
console.log('[index.vue] Tooltip 操作按钮点击:', action);
|
||||
|
||||
if (action.type === 'link') {
|
||||
// 应急人员的"联动"操作
|
||||
const entity = action.data;
|
||||
const properties = entity.properties;
|
||||
|
||||
console.log('[index.vue] 应急人员联动:', {
|
||||
name: properties.name?.getValue(),
|
||||
department: properties.department?.getValue()
|
||||
});
|
||||
|
||||
// 可以在这里实现联动功能,比如:
|
||||
// 1. 飞行到人员位置
|
||||
// 2. 打开详情弹窗
|
||||
// 3. 高亮显示相关信息
|
||||
ElMessage.success(`已联动应急人员: ${properties.name?.getValue() || '未知'}`);
|
||||
|
||||
// 关闭 tooltip
|
||||
hideTooltip();
|
||||
currentTooltipEntity.value = null;
|
||||
|
||||
} else if (action.type === 'connect') {
|
||||
// 应急基地的"连线"操作
|
||||
const entity = action.data;
|
||||
const properties = entity.properties;
|
||||
|
||||
console.log('[index.vue] 应急基地连线:', {
|
||||
name: properties.name?.getValue(),
|
||||
routeNumber: properties.routeNumber?.getValue()
|
||||
});
|
||||
|
||||
// 可以在这里实现连线功能,比如:
|
||||
// 1. 在地图上绘制线路
|
||||
// 2. 显示路径规划
|
||||
// 3. 计算距离和时间
|
||||
ElMessage.success(`已连线应急基地: ${properties.name?.getValue() || '未知'}`);
|
||||
|
||||
// 关闭 tooltip
|
||||
hideTooltip();
|
||||
currentTooltipEntity.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭地图 Tooltip
|
||||
* 统一的关闭入口,便于后续扩展埋点或联动逻辑
|
||||
@ -816,9 +977,66 @@ const handleMapTooltipClose = () => {
|
||||
mapTooltip.value.visible = false;
|
||||
};
|
||||
|
||||
// 路径线实体引用
|
||||
const pathLineEntity = ref(null);
|
||||
|
||||
/**
|
||||
* 绘制红色路径线
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
*/
|
||||
const drawRedPathLine = (viewer) => {
|
||||
if (!viewer) {
|
||||
console.warn('[index.vue] drawRedPathLine: viewer 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已存在路径线,先移除
|
||||
if (pathLineEntity.value) {
|
||||
viewer.entities.remove(pathLineEntity.value);
|
||||
pathLineEntity.value = null;
|
||||
}
|
||||
|
||||
// 路径坐标点
|
||||
const pathCoordinates = [
|
||||
{ x: -1706079.1327424292, y: 5247893.165552528, z: 3187993.9339800295 },
|
||||
{ x: -1706116.7863268533, y: 5247923.177994122, z: 3187929.297700776 },
|
||||
{ x: -1706131.4939896727, y: 5247956.7916397555, z: 3187865.1250298577 },
|
||||
{ x: -1706117.7768181972, y: 5247999.865521995, z: 3187795.4584125844 },
|
||||
{ x: -1706148.232862157, y: 5248029.100250082, z: 3187735.2203392833 },
|
||||
{ x: -1706129.4638550146, y: 5248073.941490989, z: 3187662.59740559 },
|
||||
{ x: -1706131.3071046746, y: 5248086.057462914, z: 3187643.216358425 },
|
||||
{ x: -1706164.2362053818, y: 5248120.213627388, z: 3187577.1867482658 },
|
||||
{ x: -1706255.3513903276, y: 5248175.916851786, z: 3187422.819624157 },
|
||||
{ x: -1706300.2731912779, y: 5248172.011305182, z: 3187397.8767570513 },
|
||||
{ x: -1706343.1007708232, y: 5248165.925888667, z: 3187382.186124808 }
|
||||
];
|
||||
|
||||
// 将坐标点转换为 Cartesian3 数组
|
||||
const positions = pathCoordinates.map(coord =>
|
||||
new Cesium.Cartesian3(coord.x, coord.y, coord.z)
|
||||
);
|
||||
|
||||
// 创建红色路径线实体
|
||||
pathLineEntity.value = viewer.entities.add({
|
||||
polyline: {
|
||||
positions: positions,
|
||||
width: 5,
|
||||
material: Cesium.Color.RED,
|
||||
clampToGround: true,
|
||||
// 添加发光效果使路径更醒目
|
||||
// material: new Cesium.PolylineGlowMaterialProperty({
|
||||
// glowPower: 0.3,
|
||||
// color: Cesium.Color.RED
|
||||
// })
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[index.vue] 红色路径线已绘制');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理力量调度启动事件
|
||||
* 显示加载动画,3秒后自动隐藏
|
||||
* 显示加载动画,3秒后自动隐藏,然后启动人员移动动画
|
||||
*/
|
||||
const handleStartDispatch = (payload) => {
|
||||
console.log('[index.vue] 启动力量调度:', payload);
|
||||
@ -826,9 +1044,27 @@ const handleStartDispatch = (payload) => {
|
||||
// 显示加载动画
|
||||
showLoading.value = true;
|
||||
|
||||
// 3秒后自动隐藏加载动画
|
||||
// 绘制红色路径线
|
||||
if (mapStore.viewer) {
|
||||
drawRedPathLine(mapStore.viewer);
|
||||
}
|
||||
|
||||
// 3秒后隐藏加载动画并启动人员移动
|
||||
setTimeout(() => {
|
||||
showLoading.value = false;
|
||||
|
||||
// 启动应急人员沿路径移动动画
|
||||
if (mapStore.viewer) {
|
||||
console.log('[index.vue] 启动应急人员移动动画...');
|
||||
startPersonnelMovement(mapStore.viewer, {
|
||||
duration: 60, // 60秒完成整个路径,移动更清晰可见
|
||||
trackEntity: false, // 不自动跟随相机(可设为 true 启用跟随)
|
||||
personnelName: '应急救援人员',
|
||||
department: '应急救援队'
|
||||
});
|
||||
} else {
|
||||
console.warn('[index.vue] 地图viewer未就绪,无法启动动画');
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
@ -1134,8 +1370,8 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
// 左右面板列 - 浮动卡片样式
|
||||
&__panel-column {
|
||||
position: absolute;
|
||||
top: var(--sa-header-height); // 从 header 下方开始
|
||||
bottom: 0;
|
||||
// top: var(--sa-header-height); // 从 header 下方开始
|
||||
// bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sa-gap); // 列内子面板之间的间距
|
||||
@ -1274,7 +1510,7 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
// 加载动画层 - 一键启动后显示
|
||||
&__loading-layer {
|
||||
position: absolute;
|
||||
top: calc(var(--sa-header-height) + vh(20));
|
||||
top: calc(var(--sa-header-height) + vh(40));
|
||||
left: 0;
|
||||
right: 0;
|
||||
// bottom: 0;
|
||||
@ -1289,8 +1525,8 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
&__loading-gif {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 80%;
|
||||
max-height: 60%;
|
||||
max-width: 320px;
|
||||
max-height: 55px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
@ -1321,6 +1557,32 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 窄容器嵌入的紧凑布局(<1100px 宽度)
|
||||
.situational-awareness.is-compact {
|
||||
--sa-left-width: calc(380 / 1920 * var(--cq-inline-100, 100vw));
|
||||
|
||||