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 { 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()
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
|
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 {
|
.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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<CollapsiblePanel title="现场处置" subtitle="「调度指挥」">
|
<CollapsiblePanel title="快速处置" subtitle="「调度指挥」">
|
||||||
<DispatchCommand />
|
<DispatchCommand />
|
||||||
</CollapsiblePanel>
|
</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;
|
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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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模型',
|
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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
// 在默认点附近添加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({
|
// 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 内容字段样式
|
||||||
|
|||||||
@ -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'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
<!-- 顶部功能按钮 -->
|
<!-- 顶部功能按钮 -->
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|||||||