Merge branch 'dev' of http://222.212.85.86:8222/bdzl2/bxztApp into dev
@ -8,6 +8,14 @@ import { onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useMapStore from '@/map/stores/mapStore'
|
||||
|
||||
const props = defineProps({
|
||||
// 是否加载默认底图,也就是天地图
|
||||
isLoadDefaultBaseMap: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const route = useRoute()
|
||||
let viewer = null
|
||||
@ -90,10 +98,12 @@ async function initViewer() {
|
||||
} else if (!skipInitialView) {
|
||||
await applyInitialCameraView()
|
||||
}
|
||||
|
||||
if (props.isLoadDefaultBaseMap) {
|
||||
await loadBaseMap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function applyInitialCameraView() {
|
||||
const { camera } = mapStore.services()
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
{
|
||||
"rid": 7,
|
||||
"configName": "Extent",
|
||||
"configValue": "[100.5, 19.9, 109.1, 25.7]",
|
||||
"configValue": "[107.7, 29.9, 108.3, 30.5]",
|
||||
"configDescrition": "默认地图边界范围",
|
||||
"orgCode": "bdzl"
|
||||
},
|
||||
|
||||
@ -224,6 +224,8 @@ export function createLayerService(deps) {
|
||||
request: 'GetMap',
|
||||
format: queryParams.get('format') || layerOptions.format || 'image/png',
|
||||
transparent: true,
|
||||
cql_filter:queryParams.get('cql_filter')||'',
|
||||
...layerOptions.extraParameters,
|
||||
...layerOptions.parameters,
|
||||
},
|
||||
enablePickFeatures: true,
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
@ -150,15 +150,16 @@ const handleStartDispatch = () => {
|
||||
.force-dispatch__top {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: vw(12);
|
||||
gap: vw(20);
|
||||
}
|
||||
|
||||
/* 响应等级卡片 */
|
||||
.force-dispatch__level-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: vw(12);
|
||||
padding: vh(10) vw(16);
|
||||
padding: vh(5) vw(16);
|
||||
background: rgba(20, 53, 118, 0.6);
|
||||
border: 1px solid rgba(28, 161, 255, 0.3);
|
||||
border-radius: vw(4);
|
||||
@ -166,13 +167,13 @@ const handleStartDispatch = () => {
|
||||
}
|
||||
|
||||
.force-dispatch__level-label {
|
||||
font-size: fs(13);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Regular, sans-serif;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.force-dispatch__level-value {
|
||||
font-size: fs(14);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Bold, sans-serif;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
@ -206,7 +207,7 @@ const handleStartDispatch = () => {
|
||||
}
|
||||
|
||||
.force-dispatch__plan-text {
|
||||
font-size: fs(13);
|
||||
font-size: fs(18);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
<template>
|
||||
<div class="force-preset">
|
||||
<el-dropdown
|
||||
class="force-preset__filter"
|
||||
trigger="click"
|
||||
@command="handleDistanceChange"
|
||||
>
|
||||
<div class="filter-content">
|
||||
<span class="filter-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
|
||||
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" class="filter-icon" />
|
||||
<!-- 自定义下拉框 -->
|
||||
<div class="custom-dropdown" v-click-outside="closeDropdown">
|
||||
<!-- 触发器 -->
|
||||
<div class="dropdown-trigger" @click="toggleDropdown">
|
||||
<span class="trigger-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
|
||||
<div class="trigger-icon" :class="{ 'is-open': isDropdownOpen }">
|
||||
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下拉面板 -->
|
||||
<transition name="dropdown-slide">
|
||||
<div v-if="isDropdownOpen" class="dropdown-panel">
|
||||
<div
|
||||
v-for="option in distanceOptions"
|
||||
:key="option.value"
|
||||
class="dropdown-item"
|
||||
:class="{ 'is-active': forcePreset.searchRadius === option.value }"
|
||||
@click="selectOption(option.value)"
|
||||
>
|
||||
<span class="item-text">{{ option.label }}</span>
|
||||
<div v-if="forcePreset.searchRadius === option.value" class="item-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M8 12L11 15L16 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="10" :class="{ 'is-active': forcePreset.searchRadius === 10 }">10km</el-dropdown-item>
|
||||
<el-dropdown-item :command="30" :class="{ 'is-active': forcePreset.searchRadius === 30 }">30km</el-dropdown-item>
|
||||
<el-dropdown-item :command="50" :class="{ 'is-active': forcePreset.searchRadius === 50 }">50km</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="force-preset__summary">
|
||||
<!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> -->
|
||||
@ -68,18 +82,58 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue'
|
||||
import { ref, inject } from 'vue'
|
||||
|
||||
const { forcePreset } = inject('disasterData')
|
||||
const onDistanceChange = inject('onDistanceChange')
|
||||
|
||||
// 下拉框状态
|
||||
const isDropdownOpen = ref(false)
|
||||
|
||||
// 距离选项
|
||||
const distanceOptions = [
|
||||
{ value: 10, label: '距离灾害点10km范围内' },
|
||||
{ value: 30, label: '距离灾害点30km范围内' },
|
||||
{ value: 50, label: '距离灾害点50km范围内' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 处理距离范围选择变更
|
||||
* @param {number} distance - 选中的距离范围(km)
|
||||
* 切换下拉框显示/隐藏
|
||||
*/
|
||||
const handleDistanceChange = (distance) => {
|
||||
const toggleDropdown = () => {
|
||||
isDropdownOpen.value = !isDropdownOpen.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭下拉框
|
||||
*/
|
||||
const closeDropdown = () => {
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择选项
|
||||
* @param {number} value - 选中的距离值
|
||||
*/
|
||||
const selectOption = (value) => {
|
||||
if (onDistanceChange) {
|
||||
onDistanceChange(distance)
|
||||
onDistanceChange(value)
|
||||
}
|
||||
closeDropdown()
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉框的指令
|
||||
const vClickOutside = {
|
||||
mounted(el, binding) {
|
||||
el.clickOutsideEvent = (event) => {
|
||||
if (!(el === event.target || el.contains(event.target))) {
|
||||
binding.value()
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
unmounted(el) {
|
||||
document.removeEventListener('click', el.clickOutsideEvent)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -92,34 +146,117 @@ const handleDistanceChange = (distance) => {
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
|
||||
&__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(8);
|
||||
padding: vh(10) vw(16);
|
||||
background: rgba(20, 53, 118, 0.5);
|
||||
border-radius: vw(4);
|
||||
margin-bottom: vh(16);
|
||||
cursor: pointer;
|
||||
|
||||
.filter-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(8);
|
||||
width: 100%;
|
||||
// 自定义下拉框
|
||||
.custom-dropdown {
|
||||
position: relative;
|
||||
margin-bottom: vh(8);
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
// 触发器
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: vh(8) vw(16);
|
||||
background: rgba(28, 70, 130, 0.9);
|
||||
border: 1px solid rgba(28, 161, 255, 0.3);
|
||||
border-radius: vw(8);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(28, 70, 130, 1);
|
||||
border-color: rgba(28, 161, 255, 0.5);
|
||||
}
|
||||
|
||||
.trigger-text {
|
||||
flex: 1;
|
||||
color: var(--text-white);
|
||||
font-size: fs(14);
|
||||
font-size: fs(15);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
width: vw(12);
|
||||
height: vh(12);
|
||||
.trigger-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉面板
|
||||
.dropdown-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + vh(4));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(15, 35, 75, 0.98);
|
||||
border: 1px solid rgba(28, 161, 255, 0.3);
|
||||
border-radius: vw(8);
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
box-shadow: 0 vh(4) vh(16) rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
// 下拉项
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: vh(12) vw(16);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(28, 161, 255, 0.1);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(28, 161, 255, 0.15);
|
||||
}
|
||||
|
||||
.item-text {
|
||||
flex: 1;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: fs(15);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: vw(20);
|
||||
height: vh(20);
|
||||
flex-shrink: 0;
|
||||
color: var(--primary-color);
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉动画
|
||||
.dropdown-slide-enter-active,
|
||||
.dropdown-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-slide-enter-from,
|
||||
.dropdown-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(vh(-10));
|
||||
}
|
||||
|
||||
&__summary {
|
||||
@ -205,7 +342,7 @@ const handleDistanceChange = (distance) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: vh(8);
|
||||
max-height: 100px;
|
||||
max-height: vw(120);
|
||||
overflow-y: auto;
|
||||
|
||||
// 滚动条
|
||||
@ -222,9 +359,10 @@ const handleDistanceChange = (distance) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(12);
|
||||
padding: vh(10) vw(12);
|
||||
padding: vh(0) vw(12);
|
||||
// background: rgba(20, 53, 118, 0.3);
|
||||
background: url('../../assets/images/文本线条框.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
border-radius: vw(6);
|
||||
|
||||
.station-icon {
|
||||
@ -238,6 +376,7 @@ const handleDistanceChange = (distance) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: vh(4);
|
||||
padding: vh(5) 0;
|
||||
|
||||
.station-name {
|
||||
color: var(--text-white);
|
||||
@ -257,29 +396,4 @@ const handleDistanceChange = (distance) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown菜单样式覆盖
|
||||
:deep(.el-dropdown-menu) {
|
||||
background: rgba(20, 53, 118, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: vh(4) 0;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
color: var(--text-white);
|
||||
font-size: fs(14);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
padding: vh(8) vw(16);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
<div class="left-panel-wrapper">
|
||||
<div class="left-panel">
|
||||
<CollapsiblePanel title="快速感知" subtitle="「灾害分析」">
|
||||
<template #header-right>
|
||||
<img
|
||||
src="../../assets/images/摄像头.png"
|
||||
alt="摄像头"
|
||||
class="camera-icon"
|
||||
/>
|
||||
</template>
|
||||
<DisasterAnalysis />
|
||||
</CollapsiblePanel>
|
||||
|
||||
@ -10,7 +17,7 @@
|
||||
</CollapsiblePanel>
|
||||
|
||||
<CollapsiblePanel title="快速响应" subtitle="「力量调度」">
|
||||
<ForceDispatch />
|
||||
<ForceDispatch @start-dispatch="handleStartDispatch" />
|
||||
</CollapsiblePanel>
|
||||
</div>
|
||||
|
||||
@ -64,6 +71,16 @@ const isLocationOpen = ref(true)
|
||||
const toggleLocation = () => {
|
||||
isLocationOpen.value = !isLocationOpen.value
|
||||
}
|
||||
|
||||
// 定义对外事件
|
||||
const emit = defineEmits(['start-dispatch'])
|
||||
|
||||
/**
|
||||
* 处理力量调度启动事件,向上传递给父组件
|
||||
*/
|
||||
const handleStartDispatch = (payload) => {
|
||||
emit('start-dispatch', payload)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -168,4 +185,16 @@ const toggleLocation = () => {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.camera-icon {
|
||||
width: vw(24);
|
||||
height: vw(24);
|
||||
margin-right: vw(8);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="logo-section">
|
||||
<img src="../assets/images/3ad857a9ed044c12b0e3b4345af6be59_mergeImage.png" alt="logo" class="logo-image" />
|
||||
</div>
|
||||
<h1 class="page-title">渝路智管-公路安全畅通运行管理</h1>
|
||||
<h1 class="page-title">渝路智管-应急保通事件处置</h1>
|
||||
</div>
|
||||
|
||||
<!-- <div class="page-header__right">
|
||||
@ -44,7 +44,7 @@ const handleBack = () => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: vh(111);
|
||||
background: url('../assets/images/b149e2d47f8744b5a916eb88fb4115cc_mergeImage.png') no-repeat;
|
||||
background: url('../assets/images/一级标题栏bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -123,7 +123,7 @@ const handleBack = () => {
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: fs(36);
|
||||
letter-spacing: vw(1.8);
|
||||
font-family: FZLTTHJW--GB1-0, sans-serif;
|
||||
// font-family: FZLTTHJW--GB1-0, sans-serif;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -11,7 +11,15 @@
|
||||
|
||||
<div class="video-monitor-item__content">
|
||||
<div class="video-placeholder">
|
||||
<!-- 这里放置实际的视频流组件 -->
|
||||
<!-- 视频播放器 -->
|
||||
<video
|
||||
:src="monitor.videoSrc"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
/>
|
||||
|
||||
<div class="video-time">{{ currentTime }}</div>
|
||||
|
||||
<!-- 控制条:叠加在视频底部 -->
|
||||
@ -164,17 +172,15 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
// margin-bottom: vh(12);
|
||||
|
||||
// 这里可以添加实际的视频组件
|
||||
&::before {
|
||||
content: '';
|
||||
// 视频元素
|
||||
video {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: vw(48);
|
||||
height: vh(48);
|
||||
background: url(../../assets/images/SketchPngb3b734375de691a8ba794eee7807988d78f942877ab220ebea0aac3bbddccd8b.png) center/contain no-repeat;
|
||||
opacity: 0.3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.video-time {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="right-panel">
|
||||
<CollapsiblePanel title="现场处置" subtitle="「调度指挥」">
|
||||
<CollapsiblePanel title="快速处置" subtitle="「调度指挥」">
|
||||
<DispatchCommand />
|
||||
</CollapsiblePanel>
|
||||
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="scene-label" :class="labelClass">
|
||||
<div class="scene-label__content">
|
||||
<img :src="iconSrc" alt="scene" class="scene-label__icon" />
|
||||
<span class="scene-label__text">{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import sceneIcon from '../assets/images/SketchPng08621fb3b35614299e29352b8d67ad9c2c7dccf7b9c17d042492671e3bbe19f8.png'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 标签文本
|
||||
*/
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 标签位置:
|
||||
* - 'center-left': 对比模式中间分割线的左侧
|
||||
* - 'right-left': 右侧面板的左边
|
||||
*/
|
||||
position: {
|
||||
type: String,
|
||||
default: 'right-left',
|
||||
validator: (value) => ['center-left', 'right-left'].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
const labelClass = computed(() => {
|
||||
return `scene-label--${props.position}`
|
||||
})
|
||||
|
||||
const iconSrc = sceneIcon
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
|
||||
.scene-label {
|
||||
position: absolute;
|
||||
top: calc(var(--sa-header-height));
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
// 中间分割线左侧(灾前现场实景)
|
||||
&--center-left {
|
||||
left: 50%;
|
||||
transform: translateX(calc(-100% - vw(10)));
|
||||
}
|
||||
|
||||
// 右侧面板左边(灾后现场实景)
|
||||
&--right-left {
|
||||
left: calc(100% - var(--sa-right-width));
|
||||
transform: translateX(calc(-100% - vw(10)));
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(6);
|
||||
padding: vw(5) vw(10);
|
||||
background: rgba(20, 53, 118, 1);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: vw(8);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: vw(32);
|
||||
height: vw(32);
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: var(--text-white);
|
||||
font-size: fs(15);
|
||||
font-family: SourceHanSansCN-Medium, sans-serif;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -218,7 +218,7 @@ function onAfterLeave(el) {
|
||||
inset: 0;
|
||||
border-style: solid;
|
||||
border-width: vh(25) vw(30);
|
||||
border-image-source: url('../../assets/images/面板bg.png');
|
||||
border-image-source: url('../../assets/images/通用卡片bg.png');
|
||||
border-image-slice: 25 30 25 30 fill;
|
||||
border-image-width: vh(25) vw(30);
|
||||
border-image-repeat: stretch;
|
||||
|
||||
@ -47,13 +47,13 @@ const valueClass = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vw(8);
|
||||
padding: vh(8) vw(10);
|
||||
padding: vh(4) vw(10);
|
||||
background: url('../../assets/images/DataField/快速感知_bg.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
|
||||
&__icon {
|
||||
width: vw(24);
|
||||
height: vh(30);
|
||||
height: vh(24);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -268,7 +268,7 @@ const handleClose = () => {
|
||||
.map-tooltip__background {
|
||||
position: relative;
|
||||
padding: vh(14) vw(18);
|
||||
background: url('../../assets/images/Tooltip/tooltipBg.png') no-repeat;
|
||||
background: url('../../assets/images/Tooltip/弹窗bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: vw(8);
|
||||
box-shadow: 0 vw(8) vw(24) rgba(0, 0, 0, 0.5);
|
||||
|
||||
@ -31,12 +31,12 @@ defineProps({
|
||||
justify-content: space-between;
|
||||
width: vw(400);
|
||||
height: vh(43);
|
||||
background-image: url('../../assets/images/SketchPng2800be582615dbc26e07b4d56d3fc22a0517aa84065b4d6502827c05f18ca17d.png');
|
||||
background-image: url('../../assets/images/标题栏bg1.png');
|
||||
background-position: 0 -1px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: vw(400) vh(45);
|
||||
padding: 0 vw(20);
|
||||
margin-bottom: vh(20);
|
||||
margin-bottom: vw(10);
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
|
||||
@ -6,9 +6,10 @@ 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'
|
||||
|
||||
// 默认高度偏移(米)
|
||||
const DEFAULT_HEIGHT_OFFSET = 10
|
||||
// 默认高度偏移(米)- 与 WuRenJi 保持一致
|
||||
const DEFAULT_HEIGHT_OFFSET = 100
|
||||
|
||||
/**
|
||||
* 地图标记管理 Composable
|
||||
@ -136,9 +137,18 @@ export function useMapMarkers() {
|
||||
// 计算中心点
|
||||
const center = Cesium.BoundingSphere.fromPoints(positions).center
|
||||
|
||||
// 将中心点转换为经纬度,然后重新创建位置,确保高度为0
|
||||
// 这样 CLAMP_TO_GROUND 才能正确工作
|
||||
const centerCartographic = Cesium.Cartographic.fromCartesian(center)
|
||||
const centerPosition = Cesium.Cartesian3.fromRadians(
|
||||
centerCartographic.longitude,
|
||||
centerCartographic.latitude,
|
||||
0 // 高度设为0,让 CLAMP_TO_GROUND 自动贴地
|
||||
)
|
||||
|
||||
// 添加标签
|
||||
const labelEntity = viewer.entities.add({
|
||||
position: center,
|
||||
position: centerPosition,
|
||||
label: {
|
||||
text: '模拟塌陷区域',
|
||||
font: '18px "Microsoft YaHei", sans-serif',
|
||||
@ -158,7 +168,7 @@ export function useMapMarkers() {
|
||||
|
||||
// 添加中心点标记
|
||||
const pointEntity = viewer.entities.add({
|
||||
position: center,
|
||||
position: centerPosition,
|
||||
point: {
|
||||
color: Cesium.Color.ORANGE,
|
||||
pixelSize: 12,
|
||||
@ -171,6 +181,10 @@ export function useMapMarkers() {
|
||||
|
||||
collapseAreaEntities.value = entities
|
||||
console.log('[useMapMarkers] 塌陷区域绘制完成')
|
||||
|
||||
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
|
||||
viewer.scene.requestRender()
|
||||
|
||||
return center
|
||||
}
|
||||
|
||||
@ -291,6 +305,10 @@ export function useMapMarkers() {
|
||||
|
||||
markerEntities.value.push(...entities)
|
||||
console.log(`[useMapMarkers] 添加固定标记 ${entities.length} 个`)
|
||||
|
||||
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
|
||||
viewer.scene.requestRender()
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
@ -387,6 +405,10 @@ export function useMapMarkers() {
|
||||
|
||||
markerEntities.value.push(...entities)
|
||||
console.log(`[useMapMarkers] 添加随机标记 ${entities.length} 个`)
|
||||
|
||||
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
|
||||
viewer.scene.requestRender()
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
@ -604,10 +626,15 @@ export function useMapMarkers() {
|
||||
false
|
||||
)
|
||||
|
||||
// 根据养护站名称选择图标
|
||||
const stationIcon = station.stationName === '忠县公路交通应急物资储备中心'
|
||||
? emergencyCenterIcon
|
||||
: emergencyBaseIcon
|
||||
|
||||
const entity = viewer.entities.add({
|
||||
position: result.position,
|
||||
billboard: {
|
||||
image: emergencyBaseIcon,
|
||||
image: stationIcon,
|
||||
width: 48,
|
||||
height: 48,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
@ -627,6 +654,11 @@ export function useMapMarkers() {
|
||||
|
||||
emergencyResourceEntities.value = entities
|
||||
console.log(`[useMapMarkers] 添加养护站标记 ${entities.length} 个`)
|
||||
|
||||
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
|
||||
if (entities.length > 0) {
|
||||
viewer.scene.requestRender()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 地图 Tooltip 状态管理
|
||||
* 用于显示地图标记点的轻量级信息提示框
|
||||
*/
|
||||
export function useMapTooltip() {
|
||||
// Tooltip 状态
|
||||
const tooltipState = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: '',
|
||||
icon: '',
|
||||
zIndex: 20,
|
||||
data: null // 业务数据,用于内容插槽渲染
|
||||
})
|
||||
|
||||
/**
|
||||
* 显示 Tooltip
|
||||
* @param {Object} options - Tooltip 配置选项
|
||||
* @param {number} options.x - 屏幕 X 坐标(像素)
|
||||
* @param {number} options.y - 屏幕 Y 坐标(像素)
|
||||
* @param {string} [options.title=''] - Tooltip 标题文本
|
||||
* @param {string} [options.icon=''] - 标题左侧图标的图片路径
|
||||
* @param {Object} [options.data=null] - 业务数据
|
||||
*/
|
||||
const showTooltip = ({ x, y, title = '', icon = '', data = null }) => {
|
||||
tooltipState.value = {
|
||||
visible: true,
|
||||
x,
|
||||
y,
|
||||
title,
|
||||
icon,
|
||||
zIndex: 20,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏 Tooltip
|
||||
*/
|
||||
const hideTooltip = () => {
|
||||
tooltipState.value.visible = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Tooltip 位置
|
||||
* @param {number} x - 屏幕 X 坐标
|
||||
* @param {number} y - 屏幕 Y 坐标
|
||||
*/
|
||||
const updateTooltipPosition = (x, y) => {
|
||||
tooltipState.value.x = x
|
||||
tooltipState.value.y = y
|
||||
}
|
||||
|
||||
return {
|
||||
tooltipState,
|
||||
showTooltip,
|
||||
hideTooltip,
|
||||
updateTooltipPosition
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,9 @@ export const AFTER_3DTILES_CONFIG = {
|
||||
name: '灾后3D模型',
|
||||
|
||||
// 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/S107/terra_b3dms/tileset.json',
|
||||
url: 'http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/ylzg/zxyj1119/terra_b3dms/tileset.json',
|
||||
|
||||
|
||||
// 默认可见性(初始化时灾后模型默认显示)
|
||||
visible: true,
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
* 3D态势感知常量配置
|
||||
*/
|
||||
|
||||
import { getVideoUrl } from '@shared/utils'
|
||||
|
||||
// 视频监控视角类型
|
||||
export const VIDEO_TYPES = {
|
||||
PERSONNEL: 'personnel', // 单兵视角
|
||||
@ -16,7 +18,8 @@ export const VIDEO_MONITORS = [
|
||||
id: 1,
|
||||
type: VIDEO_TYPES.PERSONNEL,
|
||||
title: '单兵(张三三)设备视角',
|
||||
videoSrc: '/videos/personnel-001.mp4', // 视频源路径
|
||||
// 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', // 日期范围
|
||||
hasAudio: true,
|
||||
hasMegaphone: true,
|
||||
@ -27,7 +30,8 @@ export const VIDEO_MONITORS = [
|
||||
id: 2,
|
||||
type: VIDEO_TYPES.DRONE,
|
||||
title: '无人机(001)视角',
|
||||
videoSrc: '/videos/drone-001.mp4',
|
||||
// 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',
|
||||
hasAudio: false,
|
||||
hasMegaphone: true,
|
||||
@ -38,7 +42,8 @@ export const VIDEO_MONITORS = [
|
||||
id: 3,
|
||||
type: VIDEO_TYPES.VEHICLE_EXTERNAL,
|
||||
title: '指挥车外部视角',
|
||||
videoSrc: '/videos/vehicle-external-001.mp4',
|
||||
// videoSrc: getVideoUrl('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,
|
||||
@ -49,7 +54,8 @@ export const VIDEO_MONITORS = [
|
||||
id: 4,
|
||||
type: VIDEO_TYPES.VEHICLE_MEETING,
|
||||
title: '指挥车会议视角',
|
||||
videoSrc: '/videos/vehicle-meeting-001.mp4',
|
||||
// videoSrc: getVideoUrl('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,
|
||||
|
||||
@ -26,17 +26,31 @@
|
||||
<div class="situational-awareness__right-map">
|
||||
<MapViewer @tool-change="handleMapToolChange" />
|
||||
</div>
|
||||
|
||||
<!-- 场景标签层 -->
|
||||
<!-- 灾前现场实景标签 - 在中间分割线左侧 -->
|
||||
<SceneLabel
|
||||
v-if="isCompareMode"
|
||||
text="灾前现场实景"
|
||||
position="center-left"
|
||||
/>
|
||||
|
||||
<!-- 灾后现场实景标签 - 在右侧面板左边 -->
|
||||
<SceneLabel
|
||||
text="灾后现场实景"
|
||||
position="right-left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 地图遮罩层 -->
|
||||
<!-- <div class="situational-awareness__map-mask" aria-hidden="true"></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 @start-dispatch="handleStartDispatch" />
|
||||
</div>
|
||||
<div
|
||||
class="situational-awareness__center-spacer"
|
||||
@ -78,6 +92,15 @@
|
||||
</template>
|
||||
</MapTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画层 - 一键启动后显示 -->
|
||||
<div v-if="showLoading" class="situational-awareness__loading-layer">
|
||||
<img
|
||||
src="./assets/images/加载gif.gif"
|
||||
alt="加载中"
|
||||
class="situational-awareness__loading-gif"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
@ -107,15 +130,21 @@ import RightPanel from "./components/RightPanel/index.vue";
|
||||
import PersonnelDetail from "./components/Popups/PersonnelDetail.vue";
|
||||
import EmergencyCenterDetail from "./components/Popups/EmergencyCenterDetail.vue";
|
||||
import MapTooltip from "./components/shared/MapTooltip.vue";
|
||||
import SceneLabel from "./components/SceneLabel.vue";
|
||||
import { useDisasterData } from "./composables/useDisasterData";
|
||||
import { useDualMapCompare } from "./composables/useDualMapCompare";
|
||||
import { useMapMarkers } from "./composables/useMapMarkers";
|
||||
import { use3DTiles } from "./composables/use3DTiles";
|
||||
import { useMapTooltip } from "./composables/useMapTooltip";
|
||||
import { useMapStore } from "@/map";
|
||||
import { request } from "@shared/utils/request";
|
||||
|
||||
// 标记点
|
||||
// 标记点图标
|
||||
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";
|
||||
|
||||
// 使用灾害数据
|
||||
const disasterData = useDisasterData();
|
||||
@ -127,6 +156,11 @@ const handleDistanceChange = async (newDistance) => {
|
||||
// 更新搜索半径
|
||||
disasterData.updateSearchRadius(newDistance);
|
||||
|
||||
// 更新范围圈
|
||||
if (mapStore.viewer) {
|
||||
createOrUpdateRangeCircle(mapStore.viewer, newDistance);
|
||||
}
|
||||
|
||||
// 重新加载应急资源数据并更新地图标记
|
||||
await loadEmergencyResources(108.011506, 30.175827);
|
||||
};
|
||||
@ -150,9 +184,208 @@ const {
|
||||
clearEmergencyResourceMarkers,
|
||||
} = useMapMarkers();
|
||||
|
||||
// 地图 Tooltip 功能
|
||||
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
|
||||
|
||||
// 当前显示 tooltip 的实体(用于相机移动时更新位置)
|
||||
const currentTooltipEntity = ref(null);
|
||||
|
||||
// 加载动画状态
|
||||
const showLoading = ref(false);
|
||||
|
||||
// 范围圈实体
|
||||
const rangeCircleEntity = ref(null);
|
||||
|
||||
// 3D Tiles加载功能
|
||||
const { load3DTileset, waitForTilesetReady } = use3DTiles();
|
||||
|
||||
/**
|
||||
* 设置地图点击事件处理器
|
||||
* 当用户点击地图标记点时,显示 Tooltip
|
||||
*/
|
||||
const setupMapClickHandler = (viewer) => {
|
||||
if (!viewer) return;
|
||||
|
||||
// 创建 ScreenSpaceEventHandler 监听点击事件
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
|
||||
handler.setInputAction((click) => {
|
||||
// 获取点击位置的实体
|
||||
const pickedObject = viewer.scene.pick(click.position);
|
||||
|
||||
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
|
||||
const entity = pickedObject.id;
|
||||
|
||||
// 检查实体是否有 properties(标记点才有)
|
||||
if (entity.properties) {
|
||||
const type = entity.properties.type?.getValue();
|
||||
|
||||
// 根据标记类型显示不同的 Tooltip
|
||||
if (type === 'soldier') {
|
||||
showMarkerTooltip(viewer, entity, click.position, soldierIcon);
|
||||
} else if (type === 'device') {
|
||||
showMarkerTooltip(viewer, entity, click.position, deviceIcon);
|
||||
} else if (type === 'emergencyBase' || type === 'station') {
|
||||
// 对于养护站,根据名称判断使用哪个图标
|
||||
const stationName = entity.properties.name?.getValue() || '';
|
||||
const icon = stationName === '忠县公路交通应急物资储备中心'
|
||||
? emergencyCenterIcon
|
||||
: emergencyBaseIcon;
|
||||
showMarkerTooltip(viewer, entity, click.position, icon);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 点击空白区域,隐藏 Tooltip
|
||||
hideTooltip();
|
||||
currentTooltipEntity.value = null;
|
||||
}
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
|
||||
// 监听相机移动事件,更新 tooltip 位置
|
||||
viewer.scene.postRender.addEventListener(() => {
|
||||
if (currentTooltipEntity.value && mapTooltip.value.visible) {
|
||||
updateTooltipPositionForEntity(viewer, currentTooltipEntity.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示标记点 Tooltip
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
* @param {Cesium.Entity} entity - 被点击的实体
|
||||
* @param {Cesium.Cartesian2} screenPosition - 点击的屏幕坐标(备用)
|
||||
* @param {string} icon - 图标路径
|
||||
*/
|
||||
const showMarkerTooltip = (viewer, entity, screenPosition, icon) => {
|
||||
const properties = entity.properties;
|
||||
const type = properties.type?.getValue();
|
||||
|
||||
// 获取实体的 3D 位置
|
||||
const position = entity.position?.getValue(Cesium.JulianDate.now());
|
||||
|
||||
if (!position) {
|
||||
console.warn('[Tooltip] 无法获取实体位置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于使用 CLAMP_TO_GROUND 的 billboard,需要获取实际的地形高度
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position);
|
||||
let clampedPosition = position;
|
||||
|
||||
// 使用 globe.getHeight 获取地形高度
|
||||
if (viewer.scene.globe) {
|
||||
const height = viewer.scene.globe.getHeight(cartographic);
|
||||
if (Cesium.defined(height)) {
|
||||
cartographic.height = height;
|
||||
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
|
||||
}
|
||||
}
|
||||
|
||||
// 将贴地后的 3D 坐标转换为屏幕坐标
|
||||
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
|
||||
|
||||
if (!Cesium.defined(canvasPosition)) {
|
||||
console.warn('[Tooltip] 无法转换坐标到屏幕位置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 Tooltip 数据
|
||||
let title = '';
|
||||
const fields = [];
|
||||
|
||||
if (type === 'soldier') {
|
||||
title = '单兵信息';
|
||||
fields.push(
|
||||
{ label: '姓名', value: properties.name?.getValue() || '-' },
|
||||
{ label: '部门', value: properties.department?.getValue() || '-' },
|
||||
{ label: '位置', value: properties.location?.getValue() || '-' }
|
||||
);
|
||||
} else if (type === 'device') {
|
||||
title = '设备信息';
|
||||
fields.push(
|
||||
{ label: '设备名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
|
||||
{ label: '位置', value: properties.location?.getValue() || '-' }
|
||||
);
|
||||
} else if (type === 'emergencyBase') {
|
||||
title = '应急基地';
|
||||
fields.push(
|
||||
{ label: '名称', value: properties.name?.getValue() || '-' },
|
||||
{ label: '地址', value: properties.address?.getValue() || '-' },
|
||||
{ label: '距离', value: properties.distance?.getValue() || '-' }
|
||||
);
|
||||
} else if (type === 'station') {
|
||||
const stationName = properties.name?.getValue() || '';
|
||||
const distance = properties.distance?.getValue() || 0;
|
||||
|
||||
// 如果是应急中心,显示应急中心信息
|
||||
if (stationName === '忠县公路交通应急物资储备中心') {
|
||||
title = '应急中心';
|
||||
fields.push(
|
||||
{ label: '名称', value: '忠县应急中心' },
|
||||
{ label: '行政等级', value: '国道' },
|
||||
{ label: '隶属单位', value: '交通公路部门' },
|
||||
{ label: '位置信息', value: `目前为止距离现场${distance}公里` }
|
||||
);
|
||||
} else {
|
||||
// 其他养护站显示 tooltip
|
||||
title = '养护站';
|
||||
fields.push(
|
||||
{ label: '名称', value: stationName || '-' },
|
||||
{ label: '距离', value: `${distance}公里` }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示 Tooltip,使用实体的屏幕坐标
|
||||
showTooltip({
|
||||
x: canvasPosition.x,
|
||||
y: canvasPosition.y,
|
||||
title,
|
||||
icon,
|
||||
data: { fields }
|
||||
});
|
||||
|
||||
// 保存当前实体,用于相机移动时更新位置
|
||||
currentTooltipEntity.value = entity;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新 Tooltip 位置(当相机移动时)
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
* @param {Cesium.Entity} entity - 实体对象
|
||||
*/
|
||||
const updateTooltipPositionForEntity = (viewer, entity) => {
|
||||
const position = entity.position?.getValue(Cesium.JulianDate.now());
|
||||
|
||||
if (!position) return;
|
||||
|
||||
// 对于使用 CLAMP_TO_GROUND 的 billboard,需要获取实际的地形高度
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position);
|
||||
let clampedPosition = position;
|
||||
|
||||
// 使用 globe.getHeight 获取地形高度
|
||||
if (viewer.scene.globe) {
|
||||
const height = viewer.scene.globe.getHeight(cartographic);
|
||||
if (Cesium.defined(height)) {
|
||||
cartographic.height = height;
|
||||
clampedPosition = Cesium.Cartographic.toCartesian(cartographic);
|
||||
}
|
||||
}
|
||||
|
||||
// 将贴地后的 3D 坐标转换为屏幕坐标
|
||||
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition);
|
||||
|
||||
// 如果标记点在视野外,隐藏 tooltip
|
||||
if (!Cesium.defined(canvasPosition)) {
|
||||
hideTooltip();
|
||||
currentTooltipEntity.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
updateTooltipPosition(canvasPosition.x, canvasPosition.y);
|
||||
};
|
||||
|
||||
// 初始化地图
|
||||
onMounted(() => {
|
||||
// 等待地图就绪后配置初始视图和模型对比图层
|
||||
@ -180,9 +413,9 @@ onMounted(() => {
|
||||
0
|
||||
),
|
||||
billboard: {
|
||||
image: emergencyCenterIcon,
|
||||
image: eventIcon,
|
||||
width: 36,
|
||||
height: 40,
|
||||
height: 36,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
@ -190,6 +423,53 @@ onMounted(() => {
|
||||
});
|
||||
viewer.entities.add(defaultPoint);
|
||||
|
||||
// 在默认点附近添加10个模拟点位(应急人员和应急装备),分散在10km范围内
|
||||
// 1度纬度约等于111km,1度经度在30度纬度约等于96km
|
||||
// 10km约等于0.09度纬度,0.104度经度
|
||||
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 }
|
||||
];
|
||||
|
||||
simulatedPoints.forEach(point => {
|
||||
const entity = viewer.entities.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, 0),
|
||||
billboard: {
|
||||
image: point.icon,
|
||||
width: 36,
|
||||
height: 40,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
properties: point.type === 'soldier'
|
||||
? {
|
||||
type: 'soldier',
|
||||
name: point.name,
|
||||
department: point.department,
|
||||
location: `目前为止距离现场${point.distance}公里`
|
||||
}
|
||||
: {
|
||||
type: 'device',
|
||||
name: point.name,
|
||||
deviceType: point.deviceType,
|
||||
location: `目前为止距离现场${point.distance}公里`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`[index.vue] 已添加 ${simulatedPoints.length} 个模拟点位`);
|
||||
|
||||
// camera.setView({
|
||||
// ...DEFAULT_CAMERA_VIEW,
|
||||
// });
|
||||
@ -197,6 +477,10 @@ onMounted(() => {
|
||||
...DEFAULT_CAMERA_VIEW,
|
||||
duration: 1,
|
||||
});
|
||||
|
||||
// 设置地图点击事件监听 - 显示 Tooltip
|
||||
setupMapClickHandler(viewer);
|
||||
|
||||
// 延迟 1000ms 后设置相机到默认位置
|
||||
// setTimeout(() => {
|
||||
// camera.flyTo({
|
||||
@ -204,7 +488,7 @@ onMounted(() => {
|
||||
// duration: 1,
|
||||
// });
|
||||
// }, 5000);
|
||||
return;
|
||||
// return;
|
||||
|
||||
/**
|
||||
* 设置相机到指定的笛卡尔坐标
|
||||
@ -265,7 +549,7 @@ onMounted(() => {
|
||||
console.log("[index.vue] 开始初始化地图标记...");
|
||||
const sampledCollapseCenter = await initializeMarkers(viewer, {
|
||||
useSampledHeights: true, // 使用采样高度,确保标记位置准确
|
||||
heightOffset: 10, // 标记相对地面10米
|
||||
heightOffset: 100, // 标记相对地面100米(与 WuRenJi 保持一致)
|
||||
});
|
||||
|
||||
// 如果之前没有设置相机(配置数据缺失),现在再次尝试
|
||||
@ -285,6 +569,9 @@ onMounted(() => {
|
||||
// camera.setView(DEFAULT_CAMERA_VIEW)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建初始范围圈(使用当前搜索半径)
|
||||
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius);
|
||||
});
|
||||
});
|
||||
|
||||
@ -409,20 +696,6 @@ const selectedCenter = ref({
|
||||
image: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* 地图 Tooltip 状态管理
|
||||
* 用于显示地图标记点的轻量级信息提示框
|
||||
*/
|
||||
const mapTooltip = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: "",
|
||||
icon: "",
|
||||
zIndex: 20, // 高于地图控件层,低于全屏弹窗
|
||||
data: null, // 业务数据,用于内容插槽渲染
|
||||
});
|
||||
|
||||
// 返回驾驶舱
|
||||
const handleBack = () => {
|
||||
console.log("返回驾驶舱");
|
||||
@ -447,6 +720,58 @@ const handleMapTooltipClose = () => {
|
||||
mapTooltip.value.visible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理力量调度启动事件
|
||||
* 显示加载动画,3秒后自动隐藏
|
||||
*/
|
||||
const handleStartDispatch = (payload) => {
|
||||
console.log('[index.vue] 启动力量调度:', payload);
|
||||
|
||||
// 显示加载动画
|
||||
showLoading.value = true;
|
||||
|
||||
// 3秒后自动隐藏加载动画
|
||||
setTimeout(() => {
|
||||
showLoading.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建或更新范围圈
|
||||
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
|
||||
* @param {number} radiusKm - 半径(公里)
|
||||
*/
|
||||
const createOrUpdateRangeCircle = (viewer, radiusKm) => {
|
||||
if (!viewer) return;
|
||||
|
||||
const centerLon = 108.011506;
|
||||
const centerLat = 30.175827;
|
||||
const radiusMeters = radiusKm * 1000;
|
||||
|
||||
// 如果已存在范围圈,先移除
|
||||
if (rangeCircleEntity.value) {
|
||||
viewer.entities.remove(rangeCircleEntity.value);
|
||||
rangeCircleEntity.value = null;
|
||||
}
|
||||
|
||||
// 创建新的范围圈
|
||||
rangeCircleEntity.value = viewer.entities.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0),
|
||||
ellipse: {
|
||||
semiMinorAxis: radiusMeters,
|
||||
semiMajorAxis: radiusMeters,
|
||||
height: 0,
|
||||
material: Cesium.Color.fromCssColorString('#1CA1FF').withAlpha(0.2),
|
||||
outline: true,
|
||||
outlineColor: Cesium.Color.fromCssColorString('#1CA1FF').withAlpha(0.8),
|
||||
outlineWidth: 2,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[index.vue] 已创建/更新范围圈: ${radiusKm}km`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 在指定屏幕坐标显示地图 Tooltip
|
||||
*
|
||||
@ -578,7 +903,7 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
--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)
|
||||
131 / 1080 * var(--cq-block-100, 100vh)
|
||||
); // Header 高度
|
||||
--sa-min-width: 1280px;
|
||||
--sa-min-height: 720px;
|
||||
@ -741,6 +1066,29 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
|
||||
z-index: 4; // 高于控件层
|
||||
pointer-events: none; // 容器不拦截事件,点击穿透到地图
|
||||
}
|
||||
|
||||
// 加载动画层 - 一键启动后显示
|
||||
&__loading-layer {
|
||||
position: absolute;
|
||||
top: calc(var(--sa-header-height) + vh(20));
|
||||
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;
|
||||
max-width: 80%;
|
||||
max-height: 60%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip 内容字段样式
|
||||
|
||||
@ -1,37 +1,29 @@
|
||||
import { request } from '@shared/utils/request'
|
||||
import si from './si.json'
|
||||
import ddt from './DDT.json'
|
||||
|
||||
// 获取业务基础地图
|
||||
export function getBusinessBaseMapDDT() {
|
||||
return [...ddt]
|
||||
// return request({
|
||||
// url: '/snow-ops-platform/dataDirectory/queryCatalog',
|
||||
// method: 'GET',
|
||||
// params: {
|
||||
// pcatalog: 'DDT'
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
// 获取业务基础地图
|
||||
export function getBusinessBaseMapSI() {
|
||||
return [...si]
|
||||
// return request({
|
||||
// url: '/snow-ops-platform/dataDirectory/queryCatalog',
|
||||
// method: 'GET',
|
||||
// params: {
|
||||
// pcatalog: 'SI'
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
export function test() {
|
||||
// 获取业务底图
|
||||
export function getBaseMap() {
|
||||
// return [...ddt]
|
||||
return request({
|
||||
url: '/ylzggeoserver/gwc/service/wms?service=WMS&request=GetMap&transparent=true&srs=EPSG%3A3857&format=image%2Fpng&styles=&layers=chongqing_yx&bbox=12053813.612459153%2C3130860.6785608195%2C12210356.646387197%2C3287403.71248886&width=256&height=256',
|
||||
method: 'get'
|
||||
url: '/snow-ops-platform/dataDirectory/queryCatalog',
|
||||
method: 'GET',
|
||||
params: {
|
||||
pcatalog: 'DDT'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取业务图
|
||||
export function getBusinessMap() {
|
||||
// return [...si]
|
||||
return request({
|
||||
url: '/snow-ops-platform/dataDirectory/queryCatalog',
|
||||
method: 'GET',
|
||||
params: {
|
||||
pcatalog: 'SI'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="map-center">
|
||||
<div class="map-container">
|
||||
<MapViewport />
|
||||
<MapViewport :isLoadDefaultBaseMap="false"/>
|
||||
<MapControls />
|
||||
</div>
|
||||
<!-- 顶部功能按钮 -->
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { getBusinessBaseMapDDT, getBusinessBaseMapSI, test } from '@/views/cockpit/api/commonHttp.js'
|
||||
import { getBaseMap, getBusinessMap } from '@/views/cockpit/api/commonHttp.js'
|
||||
import * as Cesium from 'cesium'
|
||||
|
||||
|
||||
// 当前页面的最基础地图服务
|
||||
// 主要是加载地图底图
|
||||
export const useMapBase = (mapStore) => {
|
||||
|
||||
const loadBusinessBaseMapDDT = async () => {
|
||||
// 加载当前业务的底图, 类似于天地图,但是没有使用天地图作为底图,有大的地块的地形纹理,但是缩小范围很小,属于比较粗的图
|
||||
const loadBaseMap = async () => {
|
||||
const layerService = mapStore.services().layer
|
||||
const res = await getBusinessBaseMapDDT()
|
||||
const data = [...res]
|
||||
const res = await getBaseMap()
|
||||
const data = [...res.data]
|
||||
mapStore.baseMapGroups = data
|
||||
for (const item of data) {
|
||||
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
|
||||
@ -18,9 +21,11 @@ export const useMapBase = (mapStore) => {
|
||||
url: layerConfig.url,
|
||||
meta: layerConfig.meta,
|
||||
options: {
|
||||
parameters: {
|
||||
// 瓦片方案,必传,由于cesium版本较老,必传
|
||||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||||
maximumLevel: 18, // 限制最大级别以匹配 GWC 缓存尺度,避免分辨率不匹配错误
|
||||
extraParameters: {
|
||||
srs: 'EPSG:3857',
|
||||
transparent: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,12 +35,33 @@ export const useMapBase = (mapStore) => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadBusinessBaseMapLayerSI = async () => {
|
||||
// 处理启动加载的图层
|
||||
const collectBootLoadLayers = (nodes, layers = [], parent = null) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.Attribute?.servicePath && node.Attribute.bootLoad === 1) {
|
||||
// 确保 bootLoad 图层包含正确的 parentSortIndex
|
||||
const layerWithParentSort = {
|
||||
...node,
|
||||
parentSortIndex: parent?.Attribute?.sortValue
|
||||
};
|
||||
layers.push(layerWithParentSort);
|
||||
}
|
||||
if (node.Children) {
|
||||
collectBootLoadLayers(node.Children, layers, node);
|
||||
}
|
||||
});
|
||||
return layers;
|
||||
};
|
||||
|
||||
// 加载业务地图,业务地图主要是高亮当前业务下的地区的区县,边界都会有高亮线条
|
||||
const loadBusinessMap = async () => {
|
||||
const layerService = mapStore.services().layer
|
||||
const res = await getBusinessBaseMapSI()
|
||||
const data = [...res]
|
||||
mapStore.baseMapGroups = data
|
||||
for (const item of data) {
|
||||
const res = await getBusinessMap()
|
||||
const resData = res.data
|
||||
const data = collectBootLoadLayers(resData)
|
||||
resData[0].Children = data
|
||||
mapStore.baseMapGroups = resData
|
||||
for (const item of resData) {
|
||||
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
|
||||
for (const layerConfig of layers) {
|
||||
const layer = {
|
||||
@ -51,9 +77,8 @@ export const useMapBase = (mapStore) => {
|
||||
|
||||
const loadBaseData = () => {
|
||||
setTimeout(() => {
|
||||
// loadBusinessBaseMapDDT()
|
||||
// test()
|
||||
loadBusinessBaseMapLayerSI()
|
||||
loadBaseMap()
|
||||
loadBusinessMap()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
|
||||
@ -16,3 +16,31 @@ export const APP_CONFIG = {
|
||||
title: '数据大屏',
|
||||
version: '1.0.0'
|
||||
}
|
||||
|
||||
// OSS 配置
|
||||
// 注意: 实际使用时需要从配置中心或环境变量读取,此处为示例配置
|
||||
export const OSS_CONFIG = {
|
||||
// http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/demo/ylzg/%E5%8D%95%E5%85%B5%E8%A7%86%E8%A7%92.mp4
|
||||
// OSS 服务地址
|
||||
url: import.meta.env.VITE_OSS_URL || 'http://222.212.85.86:9000',
|
||||
// OSS bucket
|
||||
bucket: import.meta.env.VITE_OSS_BUCKET || '300bdf2b-a150-406e-be63-d28bd29b409f'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 配置
|
||||
* @returns {{url: string, bucket: string}}
|
||||
*/
|
||||
export const getOssConfig = () => {
|
||||
const { url, bucket } = OSS_CONFIG
|
||||
|
||||
// 确保 URL 包含协议
|
||||
if (url.includes('http://') || url.includes('https://')) {
|
||||
return { url, bucket }
|
||||
} else {
|
||||
return {
|
||||
url: `http://${url}`,
|
||||
bucket
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { getOssConfig } from '../config'
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date|string|number} date
|
||||
@ -73,3 +75,34 @@ export function deepClone(obj) {
|
||||
}
|
||||
return clonedObj
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 资源 URL
|
||||
* @param {string} path - OSS 对象路径,如 'demo/ylzg/单兵视角.mp4'
|
||||
* @returns {string} 完整的 OSS 资源 URL
|
||||
* @example
|
||||
* getAssetUrl('demo/ylzg/单兵视角.mp4')
|
||||
* // => 'http://183.221.225.106:9001/6251daf8-4127-40e0-980d-c86f8a765b20/demo/ylzg/单兵视角.mp4'
|
||||
*/
|
||||
export function getAssetUrl(path) {
|
||||
const { url, bucket } = getOssConfig()
|
||||
return `${url}/${bucket}/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频 URL (getAssetUrl 的别名,用于语义化)
|
||||
* @param {string} path - 视频文件路径
|
||||
* @returns {string} 完整的视频 URL
|
||||
*/
|
||||
export function getVideoUrl(path) {
|
||||
return getAssetUrl(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片 URL (getAssetUrl 的别名,用于语义化)
|
||||
* @param {string} path - 图片文件路径
|
||||
* @returns {string} 完整的图片 URL
|
||||
*/
|
||||
export function getImageUrl(path) {
|
||||
return getAssetUrl(path)
|
||||
}
|
||||
|
||||