This commit is contained in:
huangchenhao 2025-11-19 16:18:51 +08:00
commit 51ba2008ac
37 changed files with 945 additions and 169 deletions

View File

@ -8,6 +8,14 @@ import { onMounted, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import useMapStore from '@/map/stores/mapStore' import useMapStore from '@/map/stores/mapStore'
const props = defineProps({
//
isLoadDefaultBaseMap: {
type: Boolean,
default: true
}
})
const mapStore = useMapStore() const mapStore = useMapStore()
const route = useRoute() const route = useRoute()
let viewer = null let viewer = null
@ -90,10 +98,12 @@ async function initViewer() {
} else if (!skipInitialView) { } else if (!skipInitialView) {
await applyInitialCameraView() await applyInitialCameraView()
} }
if (props.isLoadDefaultBaseMap) {
await loadBaseMap() await loadBaseMap()
} }
}
async function applyInitialCameraView() { async function applyInitialCameraView() {
const { camera } = mapStore.services() const { camera } = mapStore.services()

View File

@ -9,7 +9,7 @@
{ {
"rid": 7, "rid": 7,
"configName": "Extent", "configName": "Extent",
"configValue": "[100.5, 19.9, 109.1, 25.7]", "configValue": "[107.7, 29.9, 108.3, 30.5]",
"configDescrition": "默认地图边界范围", "configDescrition": "默认地图边界范围",
"orgCode": "bdzl" "orgCode": "bdzl"
}, },

View File

@ -224,6 +224,8 @@ export function createLayerService(deps) {
request: 'GetMap', request: 'GetMap',
format: queryParams.get('format') || layerOptions.format || 'image/png', format: queryParams.get('format') || layerOptions.format || 'image/png',
transparent: true, transparent: true,
cql_filter:queryParams.get('cql_filter')||'',
...layerOptions.extraParameters,
...layerOptions.parameters, ...layerOptions.parameters,
}, },
enablePickFeatures: true, enablePickFeatures: true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -150,15 +150,16 @@ const handleStartDispatch = () => {
.force-dispatch__top { .force-dispatch__top {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: vw(12); gap: vw(20);
} }
/* 响应等级卡片 */ /* 响应等级卡片 */
.force-dispatch__level-card { .force-dispatch__level-card {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: vw(12); gap: vw(12);
padding: vh(10) vw(16); padding: vh(5) vw(16);
background: rgba(20, 53, 118, 0.6); background: rgba(20, 53, 118, 0.6);
border: 1px solid rgba(28, 161, 255, 0.3); border: 1px solid rgba(28, 161, 255, 0.3);
border-radius: vw(4); border-radius: vw(4);
@ -166,13 +167,13 @@ const handleStartDispatch = () => {
} }
.force-dispatch__level-label { .force-dispatch__level-label {
font-size: fs(13); font-size: fs(18);
font-family: SourceHanSansCN-Regular, sans-serif; font-family: SourceHanSansCN-Regular, sans-serif;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
.force-dispatch__level-value { .force-dispatch__level-value {
font-size: fs(14); font-size: fs(18);
font-family: SourceHanSansCN-Bold, sans-serif; font-family: SourceHanSansCN-Bold, sans-serif;
font-weight: 600; font-weight: 600;
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);
@ -206,7 +207,7 @@ const handleStartDispatch = () => {
} }
.force-dispatch__plan-text { .force-dispatch__plan-text {
font-size: fs(13); font-size: fs(18);
font-family: SourceHanSansCN-Medium, sans-serif; font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);

View File

@ -1,22 +1,36 @@
<template> <template>
<div class="force-preset"> <div class="force-preset">
<el-dropdown <!-- 自定义下拉框 -->
class="force-preset__filter" <div class="custom-dropdown" v-click-outside="closeDropdown">
trigger="click" <!-- 触发器 -->
@command="handleDistanceChange" <div class="dropdown-trigger" @click="toggleDropdown">
> <span class="trigger-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span>
<div class="filter-content"> <div class="trigger-icon" :class="{ 'is-open': isDropdownOpen }">
<span class="filter-text">距离灾害点{{ forcePreset.searchRadius }}km范围内</span> <img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" />
<img src="../../assets/images/SketchPng8063f445fba047c290a9620343b62ea51d767b8cdcd86769502b5b160998aacc.png" alt="dropdown" class="filter-icon" /> </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> </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"> <div class="force-preset__summary">
<!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> --> <!-- <img src="../../assets/images/SketchPnga96e6ce64e80f6d935217d64400481f3e0361d9e60a7425f6f09c8287716904d.png" alt="background" class="summary-bg" /> -->
@ -68,18 +82,58 @@
</template> </template>
<script setup> <script setup>
import { inject } from 'vue' import { ref, inject } from 'vue'
const { forcePreset } = inject('disasterData') const { forcePreset } = inject('disasterData')
const onDistanceChange = inject('onDistanceChange') 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) { 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> </script>
@ -92,34 +146,117 @@ const handleDistanceChange = (distance) => {
padding: 0; padding: 0;
margin-top: 0; margin-top: 0;
&__filter { //
display: flex; .custom-dropdown {
align-items: center; position: relative;
gap: vw(8); margin-bottom: vh(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%;
} }
.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; flex: 1;
color: var(--text-white); color: var(--text-white);
font-size: fs(14); font-size: fs(15);
font-family: SourceHanSansCN-Medium, sans-serif; font-family: SourceHanSansCN-Medium, sans-serif;
font-weight: 500;
} }
.filter-icon { .trigger-icon {
width: vw(12); width: 16px;
height: vh(12); 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 { &__summary {
@ -205,7 +342,7 @@ const handleDistanceChange = (distance) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: vh(8); gap: vh(8);
max-height: 100px; max-height: vw(120);
overflow-y: auto; overflow-y: auto;
// //
@ -222,9 +359,10 @@ const handleDistanceChange = (distance) => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: vw(12); gap: vw(12);
padding: vh(10) vw(12); padding: vh(0) vw(12);
// background: rgba(20, 53, 118, 0.3); // background: rgba(20, 53, 118, 0.3);
background: url('../../assets/images/文本线条框.png') no-repeat center center; background: url('../../assets/images/文本线条框.png') no-repeat center center;
background-size: 100% 100%;
border-radius: vw(6); border-radius: vw(6);
.station-icon { .station-icon {
@ -238,6 +376,7 @@ const handleDistanceChange = (distance) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: vh(4); gap: vh(4);
padding: vh(5) 0;
.station-name { .station-name {
color: var(--text-white); 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> </style>

View File

@ -2,6 +2,13 @@
<div class="left-panel-wrapper"> <div class="left-panel-wrapper">
<div class="left-panel"> <div class="left-panel">
<CollapsiblePanel title="快速感知" subtitle="「灾害分析」"> <CollapsiblePanel title="快速感知" subtitle="「灾害分析」">
<template #header-right>
<img
src="../../assets/images/摄像头.png"
alt="摄像头"
class="camera-icon"
/>
</template>
<DisasterAnalysis /> <DisasterAnalysis />
</CollapsiblePanel> </CollapsiblePanel>
@ -10,7 +17,7 @@
</CollapsiblePanel> </CollapsiblePanel>
<CollapsiblePanel title="快速响应" subtitle="「力量调度」"> <CollapsiblePanel title="快速响应" subtitle="「力量调度」">
<ForceDispatch /> <ForceDispatch @start-dispatch="handleStartDispatch" />
</CollapsiblePanel> </CollapsiblePanel>
</div> </div>
@ -64,6 +71,16 @@ const isLocationOpen = ref(true)
const toggleLocation = () => { const toggleLocation = () => {
isLocationOpen.value = !isLocationOpen.value isLocationOpen.value = !isLocationOpen.value
} }
//
const emit = defineEmits(['start-dispatch'])
/**
* 处理力量调度启动事件向上传递给父组件
*/
const handleStartDispatch = (payload) => {
emit('start-dispatch', payload)
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -168,4 +185,16 @@ const toggleLocation = () => {
transform: rotate(90deg); 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> </style>

View File

@ -12,7 +12,7 @@
<div class="logo-section"> <div class="logo-section">
<img src="../assets/images/3ad857a9ed044c12b0e3b4345af6be59_mergeImage.png" alt="logo" class="logo-image" /> <img src="../assets/images/3ad857a9ed044c12b0e3b4345af6be59_mergeImage.png" alt="logo" class="logo-image" />
</div> </div>
<h1 class="page-title">渝路智管-公路安全畅通运行管理</h1> <h1 class="page-title">渝路智管-应急保通事件处置</h1>
</div> </div>
<!-- <div class="page-header__right"> <!-- <div class="page-header__right">
@ -44,7 +44,7 @@ const handleBack = () => {
position: relative; position: relative;
width: 100%; width: 100%;
height: vh(111); height: vh(111);
background: url('../assets/images/b149e2d47f8744b5a916eb88fb4115cc_mergeImage.png') no-repeat; background: url('../assets/images/一级标题栏bg.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
display: flex; display: flex;
align-items: center; align-items: center;
@ -123,7 +123,7 @@ const handleBack = () => {
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
font-size: fs(36); font-size: fs(36);
letter-spacing: vw(1.8); letter-spacing: vw(1.8);
font-family: FZLTTHJW--GB1-0, sans-serif; // font-family: FZLTTHJW--GB1-0, sans-serif;
white-space: nowrap; white-space: nowrap;
margin: 0; margin: 0;
} }

View File

@ -11,7 +11,15 @@
<div class="video-monitor-item__content"> <div class="video-monitor-item__content">
<div class="video-placeholder"> <div class="video-placeholder">
<!-- 这里放置实际的视频流组件 --> <!-- 视频播放器 -->
<video
:src="monitor.videoSrc"
autoplay
loop
muted
playsinline
/>
<div class="video-time">{{ currentTime }}</div> <div class="video-time">{{ currentTime }}</div>
<!-- 控制条叠加在视频底部 --> <!-- 控制条叠加在视频底部 -->
@ -164,17 +172,15 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
// margin-bottom: vh(12); // margin-bottom: vh(12);
// //
&::before { video {
content: '';
position: absolute; position: absolute;
top: 50%; top: 0;
left: 50%; left: 0;
transform: translate(-50%, -50%); width: 100%;
width: vw(48); height: 100%;
height: vh(48); object-fit: cover;
background: url(../../assets/images/SketchPngb3b734375de691a8ba794eee7807988d78f942877ab220ebea0aac3bbddccd8b.png) center/contain no-repeat; z-index: 0;
opacity: 0.3;
} }
.video-time { .video-time {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="right-panel"> <div class="right-panel">
<CollapsiblePanel title="现场处置" subtitle="「调度指挥」"> <CollapsiblePanel title="快速处置" subtitle="「调度指挥」">
<DispatchCommand /> <DispatchCommand />
</CollapsiblePanel> </CollapsiblePanel>

View File

@ -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>

View File

@ -218,7 +218,7 @@ function onAfterLeave(el) {
inset: 0; inset: 0;
border-style: solid; border-style: solid;
border-width: vh(25) vw(30); 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-slice: 25 30 25 30 fill;
border-image-width: vh(25) vw(30); border-image-width: vh(25) vw(30);
border-image-repeat: stretch; border-image-repeat: stretch;

View File

@ -47,13 +47,13 @@ const valueClass = computed(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: vw(8); 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: url('../../assets/images/DataField/快速感知_bg.png') no-repeat center center;
background-size: 100% 100%; background-size: 100% 100%;
&__icon { &__icon {
width: vw(24); width: vw(24);
height: vh(30); height: vh(24);
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -268,7 +268,7 @@ const handleClose = () => {
.map-tooltip__background { .map-tooltip__background {
position: relative; position: relative;
padding: vh(14) vw(18); 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%; background-size: 100% 100%;
border-radius: vw(8); border-radius: vw(8);
box-shadow: 0 vw(8) vw(24) rgba(0, 0, 0, 0.5); box-shadow: 0 vw(8) vw(24) rgba(0, 0, 0, 0.5);

View File

@ -31,12 +31,12 @@ defineProps({
justify-content: space-between; justify-content: space-between;
width: vw(400); width: vw(400);
height: vh(43); height: vh(43);
background-image: url('../../assets/images/SketchPng2800be582615dbc26e07b4d56d3fc22a0517aa84065b4d6502827c05f18ca17d.png'); background-image: url('../../assets/images/标题栏bg1.png');
background-position: 0 -1px; background-position: 0 -1px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: vw(400) vh(45); background-size: vw(400) vh(45);
padding: 0 vw(20); padding: 0 vw(20);
margin-bottom: vh(20); margin-bottom: vw(10);
&__content { &__content {
display: flex; display: flex;

View File

@ -6,9 +6,10 @@ import { cesiumDataConfig } from '../config/cesiumData'
import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png' import soldierIcon from '../assets/images/SketchPngfbec927027ff9e49207749ebaafd229429315341fda199251b6dfb1723ff17fb.png'
import deviceIcon from '../assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png' import deviceIcon from '../assets/images/SketchPng860d54f2a31f5f441fc6a88081224f1e98534bf6d5ca1246e420983bdf690380.png'
import emergencyBaseIcon from '../assets/images/应急基地.png' import emergencyBaseIcon from '../assets/images/应急基地.png'
import emergencyCenterIcon from '../assets/images/应急中心.png'
// 默认高度偏移(米) // 默认高度偏移(米)- 与 WuRenJi 保持一致
const DEFAULT_HEIGHT_OFFSET = 10 const DEFAULT_HEIGHT_OFFSET = 100
/** /**
* 地图标记管理 Composable * 地图标记管理 Composable
@ -136,9 +137,18 @@ export function useMapMarkers() {
// 计算中心点 // 计算中心点
const center = Cesium.BoundingSphere.fromPoints(positions).center 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({ const labelEntity = viewer.entities.add({
position: center, position: centerPosition,
label: { label: {
text: '模拟塌陷区域', text: '模拟塌陷区域',
font: '18px "Microsoft YaHei", sans-serif', font: '18px "Microsoft YaHei", sans-serif',
@ -158,7 +168,7 @@ export function useMapMarkers() {
// 添加中心点标记 // 添加中心点标记
const pointEntity = viewer.entities.add({ const pointEntity = viewer.entities.add({
position: center, position: centerPosition,
point: { point: {
color: Cesium.Color.ORANGE, color: Cesium.Color.ORANGE,
pixelSize: 12, pixelSize: 12,
@ -171,6 +181,10 @@ export function useMapMarkers() {
collapseAreaEntities.value = entities collapseAreaEntities.value = entities
console.log('[useMapMarkers] 塌陷区域绘制完成') console.log('[useMapMarkers] 塌陷区域绘制完成')
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return center return center
} }
@ -291,6 +305,10 @@ export function useMapMarkers() {
markerEntities.value.push(...entities) markerEntities.value.push(...entities)
console.log(`[useMapMarkers] 添加固定标记 ${entities.length}`) console.log(`[useMapMarkers] 添加固定标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return entities return entities
} }
@ -387,6 +405,10 @@ export function useMapMarkers() {
markerEntities.value.push(...entities) markerEntities.value.push(...entities)
console.log(`[useMapMarkers] 添加随机标记 ${entities.length}`) console.log(`[useMapMarkers] 添加随机标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
viewer.scene.requestRender()
return entities return entities
} }
@ -604,10 +626,15 @@ export function useMapMarkers() {
false false
) )
// 根据养护站名称选择图标
const stationIcon = station.stationName === '忠县公路交通应急物资储备中心'
? emergencyCenterIcon
: emergencyBaseIcon
const entity = viewer.entities.add({ const entity = viewer.entities.add({
position: result.position, position: result.position,
billboard: { billboard: {
image: emergencyBaseIcon, image: stationIcon,
width: 48, width: 48,
height: 48, height: 48,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
@ -627,6 +654,11 @@ export function useMapMarkers() {
emergencyResourceEntities.value = entities emergencyResourceEntities.value = entities
console.log(`[useMapMarkers] 添加养护站标记 ${entities.length}`) console.log(`[useMapMarkers] 添加养护站标记 ${entities.length}`)
// 强制渲染场景,确保 CLAMP_TO_GROUND 立即生效
if (entities.length > 0) {
viewer.scene.requestRender()
}
} }
return { return {

View File

@ -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
}
}

View File

@ -42,7 +42,9 @@ export const AFTER_3DTILES_CONFIG = {
name: '灾后3D模型', name: '灾后3D模型',
// 3D Tiles 服务 URL // 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, visible: true,

View File

@ -2,6 +2,8 @@
* 3D态势感知常量配置 * 3D态势感知常量配置
*/ */
import { getVideoUrl } from '@shared/utils'
// 视频监控视角类型 // 视频监控视角类型
export const VIDEO_TYPES = { export const VIDEO_TYPES = {
PERSONNEL: 'personnel', // 单兵视角 PERSONNEL: 'personnel', // 单兵视角
@ -16,7 +18,8 @@ export const VIDEO_MONITORS = [
id: 1, id: 1,
type: VIDEO_TYPES.PERSONNEL, type: VIDEO_TYPES.PERSONNEL,
title: '单兵(张三三)设备视角', 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', // 日期范围 dateRange: '2025/9/1-2025/12/1', // 日期范围
hasAudio: true, hasAudio: true,
hasMegaphone: true, hasMegaphone: true,
@ -27,7 +30,8 @@ export const VIDEO_MONITORS = [
id: 2, id: 2,
type: VIDEO_TYPES.DRONE, type: VIDEO_TYPES.DRONE,
title: '无人机(001)视角', 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', dateRange: '2025/9/1-2025/12/1',
hasAudio: false, hasAudio: false,
hasMegaphone: true, hasMegaphone: true,
@ -38,7 +42,8 @@ export const VIDEO_MONITORS = [
id: 3, id: 3,
type: VIDEO_TYPES.VEHICLE_EXTERNAL, type: VIDEO_TYPES.VEHICLE_EXTERNAL,
title: '指挥车外部视角', 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', dateRange: '2025/9/1-2025/12/1',
hasAudio: true, hasAudio: true,
hasMegaphone: true, hasMegaphone: true,
@ -49,7 +54,8 @@ export const VIDEO_MONITORS = [
id: 4, id: 4,
type: VIDEO_TYPES.VEHICLE_MEETING, type: VIDEO_TYPES.VEHICLE_MEETING,
title: '指挥车会议视角', 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', dateRange: '2025/9/1-2025/12/1',
hasAudio: true, hasAudio: true,
hasMegaphone: true, hasMegaphone: true,

View File

@ -26,17 +26,31 @@
<div class="situational-awareness__right-map"> <div class="situational-awareness__right-map">
<MapViewer @tool-change="handleMapToolChange" /> <MapViewer @tool-change="handleMapToolChange" />
</div> </div>
<!-- 场景标签层 -->
<!-- 灾前现场实景标签 - 在中间分割线左侧 -->
<SceneLabel
v-if="isCompareMode"
text="灾前现场实景"
position="center-left"
/>
<!-- 灾后现场实景标签 - 在右侧面板左边 -->
<SceneLabel
text="灾后现场实景"
position="right-left"
/>
</div> </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__panels-layer">
<div <div
class="situational-awareness__panel-column situational-awareness__panel-column--left" class="situational-awareness__panel-column situational-awareness__panel-column--left"
> >
<LeftPanel /> <LeftPanel @start-dispatch="handleStartDispatch" />
</div> </div>
<div <div
class="situational-awareness__center-spacer" class="situational-awareness__center-spacer"
@ -78,6 +92,15 @@
</template> </template>
</MapTooltip> </MapTooltip>
</div> </div>
<!-- 加载动画层 - 一键启动后显示 -->
<div v-if="showLoading" class="situational-awareness__loading-layer">
<img
src="./assets/images/加载gif.gif"
alt="加载中"
class="situational-awareness__loading-gif"
/>
</div>
</div> </div>
<!-- 弹窗组件 --> <!-- 弹窗组件 -->
@ -107,15 +130,21 @@ import RightPanel from "./components/RightPanel/index.vue";
import PersonnelDetail from "./components/Popups/PersonnelDetail.vue"; import PersonnelDetail from "./components/Popups/PersonnelDetail.vue";
import EmergencyCenterDetail from "./components/Popups/EmergencyCenterDetail.vue"; import EmergencyCenterDetail from "./components/Popups/EmergencyCenterDetail.vue";
import MapTooltip from "./components/shared/MapTooltip.vue"; import MapTooltip from "./components/shared/MapTooltip.vue";
import SceneLabel from "./components/SceneLabel.vue";
import { useDisasterData } from "./composables/useDisasterData"; import { useDisasterData } from "./composables/useDisasterData";
import { useDualMapCompare } from "./composables/useDualMapCompare"; import { useDualMapCompare } from "./composables/useDualMapCompare";
import { useMapMarkers } from "./composables/useMapMarkers"; import { useMapMarkers } from "./composables/useMapMarkers";
import { use3DTiles } from "./composables/use3DTiles"; import { use3DTiles } from "./composables/use3DTiles";
import { useMapTooltip } from "./composables/useMapTooltip";
import { useMapStore } from "@/map"; import { useMapStore } from "@/map";
import { request } from "@shared/utils/request"; import { request } from "@shared/utils/request";
// //
import emergencyCenterIcon from "./assets/images/应急中心.png"; 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(); const disasterData = useDisasterData();
@ -127,6 +156,11 @@ const handleDistanceChange = async (newDistance) => {
// //
disasterData.updateSearchRadius(newDistance); disasterData.updateSearchRadius(newDistance);
//
if (mapStore.viewer) {
createOrUpdateRangeCircle(mapStore.viewer, newDistance);
}
// //
await loadEmergencyResources(108.011506, 30.175827); await loadEmergencyResources(108.011506, 30.175827);
}; };
@ -150,9 +184,208 @@ const {
clearEmergencyResourceMarkers, clearEmergencyResourceMarkers,
} = useMapMarkers(); } = useMapMarkers();
// Tooltip
const { tooltipState: mapTooltip, showTooltip, hideTooltip, updateTooltipPosition } = useMapTooltip();
// tooltip
const currentTooltipEntity = ref(null);
//
const showLoading = ref(false);
//
const rangeCircleEntity = ref(null);
// 3D Tiles // 3D Tiles
const { load3DTileset, waitForTilesetReady } = use3DTiles(); 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(() => { onMounted(() => {
// //
@ -180,9 +413,9 @@ onMounted(() => {
0 0
), ),
billboard: { billboard: {
image: emergencyCenterIcon, image: eventIcon,
width: 36, width: 36,
height: 40, height: 36,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: Number.POSITIVE_INFINITY, disableDepthTestDistance: Number.POSITIVE_INFINITY,
@ -190,6 +423,53 @@ onMounted(() => {
}); });
viewer.entities.add(defaultPoint); viewer.entities.add(defaultPoint);
// 1010km
// 1111km13096km
// 10km0.090.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({ // camera.setView({
// ...DEFAULT_CAMERA_VIEW, // ...DEFAULT_CAMERA_VIEW,
// }); // });
@ -197,6 +477,10 @@ onMounted(() => {
...DEFAULT_CAMERA_VIEW, ...DEFAULT_CAMERA_VIEW,
duration: 1, duration: 1,
}); });
// - Tooltip
setupMapClickHandler(viewer);
// 1000ms // 1000ms
// setTimeout(() => { // setTimeout(() => {
// camera.flyTo({ // camera.flyTo({
@ -204,7 +488,7 @@ onMounted(() => {
// duration: 1, // duration: 1,
// }); // });
// }, 5000); // }, 5000);
return; // return;
/** /**
* 设置相机到指定的笛卡尔坐标 * 设置相机到指定的笛卡尔坐标
@ -265,7 +549,7 @@ onMounted(() => {
console.log("[index.vue] 开始初始化地图标记..."); console.log("[index.vue] 开始初始化地图标记...");
const sampledCollapseCenter = await initializeMarkers(viewer, { const sampledCollapseCenter = await initializeMarkers(viewer, {
useSampledHeights: true, // 使 useSampledHeights: true, // 使
heightOffset: 10, // 10 heightOffset: 100, // 100 WuRenJi
}); });
// //
@ -285,6 +569,9 @@ onMounted(() => {
// camera.setView(DEFAULT_CAMERA_VIEW) // camera.setView(DEFAULT_CAMERA_VIEW)
} }
} }
// 使
createOrUpdateRangeCircle(viewer, disasterData.forcePreset.value.searchRadius);
}); });
}); });
@ -409,20 +696,6 @@ const selectedCenter = ref({
image: null, image: null,
}); });
/**
* 地图 Tooltip 状态管理
* 用于显示地图标记点的轻量级信息提示框
*/
const mapTooltip = ref({
visible: false,
x: 0,
y: 0,
title: "",
icon: "",
zIndex: 20, //
data: null, //
});
// //
const handleBack = () => { const handleBack = () => {
console.log("返回驾驶舱"); console.log("返回驾驶舱");
@ -447,6 +720,58 @@ const handleMapTooltipClose = () => {
mapTooltip.value.visible = false; 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 * 在指定屏幕坐标显示地图 Tooltip
* *
@ -578,7 +903,7 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
--sa-gap: calc(16 / 1920 * var(--cq-inline-100, 100vw)); --sa-gap: calc(16 / 1920 * var(--cq-inline-100, 100vw));
--sa-padding: calc(16 / 1920 * var(--cq-inline-100, 100vw)); --sa-padding: calc(16 / 1920 * var(--cq-inline-100, 100vw));
--sa-header-height: calc( --sa-header-height: calc(
121 / 1080 * var(--cq-block-100, 100vh) 131 / 1080 * var(--cq-block-100, 100vh)
); // Header ); // Header
--sa-min-width: 1280px; --sa-min-width: 1280px;
--sa-min-height: 720px; --sa-min-height: 720px;
@ -741,6 +1066,29 @@ const showMapTooltip = ({ x, y, title = "", icon = "", data = null }) => {
z-index: 4; // z-index: 4; //
pointer-events: none; // 穿 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 // Tooltip

View File

@ -1,37 +1,29 @@
import { request } from '@shared/utils/request' import { request } from '@shared/utils/request'
import si from './si.json'
import ddt from './DDT.json'
// 获取业务基础地图 // 获取业务底图
export function getBusinessBaseMapDDT() { export function getBaseMap() {
return [...ddt] // 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() {
return request({ 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', url: '/snow-ops-platform/dataDirectory/queryCatalog',
method: 'get' method: 'GET',
params: {
pcatalog: 'DDT'
}
})
}
// 获取业务图
export function getBusinessMap() {
// return [...si]
return request({
url: '/snow-ops-platform/dataDirectory/queryCatalog',
method: 'GET',
params: {
pcatalog: 'SI'
}
}) })
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="map-center"> <div class="map-center">
<div class="map-container"> <div class="map-container">
<MapViewport /> <MapViewport :isLoadDefaultBaseMap="false"/>
<MapControls /> <MapControls />
</div> </div>
<!-- 顶部功能按钮 --> <!-- 顶部功能按钮 -->

View File

@ -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) => { export const useMapBase = (mapStore) => {
const loadBusinessBaseMapDDT = async () => { // 加载当前业务的底图, 类似于天地图,但是没有使用天地图作为底图,有大的地块的地形纹理,但是缩小范围很小,属于比较粗的图
const loadBaseMap = async () => {
const layerService = mapStore.services().layer const layerService = mapStore.services().layer
const res = await getBusinessBaseMapDDT() const res = await getBaseMap()
const data = [...res] const data = [...res.data]
mapStore.baseMapGroups = data mapStore.baseMapGroups = data
for (const item of data) { for (const item of data) {
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid) const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
@ -18,9 +21,11 @@ export const useMapBase = (mapStore) => {
url: layerConfig.url, url: layerConfig.url,
meta: layerConfig.meta, meta: layerConfig.meta,
options: { options: {
parameters: { // 瓦片方案必传由于cesium版本较老必传
tilingScheme: new Cesium.WebMercatorTilingScheme(),
maximumLevel: 18, // 限制最大级别以匹配 GWC 缓存尺度,避免分辨率不匹配错误
extraParameters: {
srs: 'EPSG:3857', 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 layerService = mapStore.services().layer
const res = await getBusinessBaseMapSI() const res = await getBusinessMap()
const data = [...res] const resData = res.data
mapStore.baseMapGroups = data const data = collectBootLoadLayers(resData)
for (const item of data) { resData[0].Children = data
mapStore.baseMapGroups = resData
for (const item of resData) {
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid) const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
for (const layerConfig of layers) { for (const layerConfig of layers) {
const layer = { const layer = {
@ -51,9 +77,8 @@ export const useMapBase = (mapStore) => {
const loadBaseData = () => { const loadBaseData = () => {
setTimeout(() => { setTimeout(() => {
// loadBusinessBaseMapDDT() loadBaseMap()
// test() loadBusinessMap()
loadBusinessBaseMapLayerSI()
}, 0) }, 0)
} }

View File

@ -16,3 +16,31 @@ export const APP_CONFIG = {
title: '数据大屏', title: '数据大屏',
version: '1.0.0' 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
}
}
}

View File

@ -1,3 +1,5 @@
import { getOssConfig } from '../config'
/** /**
* 格式化日期 * 格式化日期
* @param {Date|string|number} date * @param {Date|string|number} date
@ -73,3 +75,34 @@ export function deepClone(obj) {
} }
return clonedObj 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)
}