feat: 冰雪阻断大屏新增聚焦功能

This commit is contained in:
nightdays 2025-11-20 11:08:08 +08:00
parent 0802468c75
commit 09dc7a6119
4 changed files with 61 additions and 406 deletions

View File

@ -9,17 +9,18 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.18",
"vue-router": "^4.6.3",
"pinia": "^3.0.3",
"element-plus": "^2.11.5",
"@element-plus/icons-vue": "^2.3.2",
"echarts": "^6.0.0",
"vue-echarts": "^8.0.1",
"cesium": "^1.135.0",
"axios": "^1.13.2",
"@h5/shared": "workspace:*",
"@turf/turf": "^7.3.0",
"@vueuse/core": "^14.0.0",
"@h5/shared": "workspace:*"
"axios": "^1.13.2",
"cesium": "^1.135.0",
"echarts": "^6.0.0",
"element-plus": "^2.11.5",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-echarts": "^8.0.1",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",

View File

@ -433,12 +433,19 @@ const renderEmergencyForcePoints = async (entityService, points, markerIcon) =>
* 应急资源表格行被点击时触发
*/
const handleEmergencyResourceClick = (resource) => {
let focusKey = null
//
const filterList = filterMark.value.serviceFacility
const index = filterList.findIndex((item) => item == resource.qxmc)
//
if (index > -1) filterList.splice(index, 1)
else filterList.push(resource.qxmc)
if (index > -1) {
filterList.splice(index, 1)
}
else {
filterList.push(resource.qxmc)
focusKey = resource.qxmc
}
// legend
const legendIndex = legendToolActiveItem.value.indexOf('serviceFacility')
@ -451,9 +458,16 @@ const handleEmergencyResourceClick = (resource) => {
legendToolActiveItem.value.splice(legendIndex, 1)
}
mapImageMarkHook.filterYHZMark("serviceFacility", (item) => {
return filterList.find((filterItem) => filterItem == item.qxmc)
})
// hook
mapImageMarkHook.filterYHZMark(
"serviceFacility",
(item) => {
return filterList.find((filterItem) => filterItem == item.qxmc)
},
(item) => {
return item.qxmc == focusKey
}
)
}

View File

@ -1,391 +0,0 @@
<template>
<transition name="fade">
<div v-if="visible && position" class="common-tooltip" :style="{
left: `${position.x}px`,
top: `${position.y - 20}px`
}">
<span class="title">{{ title }}</span>
<!-- 关闭按钮 -->
<button class="close-button" type="button" aria-label="关闭" @click="handleClose">
×
</button>
<!-- 内容区域 -->
<div class="tooltip-content" v-if="!loading && detail">
<div class="info-item">
<span class="label">名称:</span>
<span class="value">{{ detail.mc }}</span>
</div>
<div class="info-item">
<span class="label">所属区县</span>
<span class="value">{{ detail.qxmc }}</span>
</div>
<div class="info-item">
<span class="label">应急设备</span>
<span class="value">{{ detail.sbsl }}</span>
</div>
<div class="info-item">
<span class="label">应急物资</span>
<span class="value">{{ detail.wzsl }}</span>
</div>
<div class="info-item">
<span class="label">应急人员</span>
<span class="value">{{ detail.rysl }}</span>
</div>
<!-- 如果没有数据 -->
<div v-if="!hasData" class="no-data">
暂无详细信息
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</transition>
</template>
<script setup>
import { computed, ref } from 'vue'
import { getYHZDetail } from '@/views/cockpit/api/commonHttp.js'
/**
* 应急力量详情提示框组件
* 使用 HTML Overlay 方式显示在地图标记点上方
*/
// ==================== Props ====================
const props = defineProps({
/**
* 是否显示提示框
*/
visible: {
type: Boolean,
default: false
},
/**
* 提示框位置屏幕坐标
* @type {{ x: number, y: number }}
*/
position: {
type: Object,
default: null
},
/**
* 详情数据
* @type {{ qxmc?: string, yjllpz?: string, wzQtwz?: string }}
*/
data: {
type: Object,
default: () => ({})
},
/**
* 错误信息
*/
error: {
type: String,
default: ''
}
})
// ==================== State ====================
const title = ref('养护站')
const detail = ref(null)
const loading = ref(true)
// ==================== Emits ====================
const emit = defineEmits(['close'])
// ==================== Computed ====================
/**
* 是否有有效数据
*/
const hasData = computed(() => {
return !!props.data
})
// ==================== Methods ====================
// index.js
const init = () => {
getDetail()
}
const getDetail = async () => {
loading.value = true
const res = await getYHZDetail({
id: props.data.id
})
if (res.success) {
detail.value = res.data
loading.value = false
}
}
/**
* 处理关闭按钮点击
*/
const handleClose = () => {
emit('close')
}
defineExpose({
init
})
</script>
<style scoped lang="scss">
@use '@/styles/mixins.scss' as *;
.flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.common-tooltip {
// CSS
// PNG
--tooltip-top-height: 68px;
--tooltip-bottom-height: 68px;
--tooltip-side-padding: 24px;
--tooltip-body-padding: 16px;
position: absolute;
z-index: 9999;
pointer-events: auto;
//
transform: translate(-50%, calc(-100% - 20px));
min-width: 200px;
width: 300px;
// padding +
// padding
padding: calc(var(--tooltip-top-height)) var(--tooltip-side-padding) calc(var(--tooltip-bottom-height));
//
background-image:
url('@/views/cockpit/assets/emergencyForceTooltip/top.png'),
url('@/views/cockpit/assets/emergencyForceTooltip/bottom.png'),
url('@/views/cockpit/assets/emergencyForceTooltip/middle.png');
// // background-size
background-repeat: no-repeat, no-repeat, no-repeat;
//
// 1px +2px 1px
background-position:
center top,
center bottom,
center calc(var(--tooltip-top-height) - 1px);
// /
// 2px 1px
background-size:
100% var(--tooltip-top-height),
100% var(--tooltip-bottom-height),
100% calc(100% - var(--tooltip-top-height) - var(--tooltip-bottom-height) + 2px);
// border border-radius使
border: none;
//
// backdrop-filter: blur(12px);
// box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 14px;
line-height: 1.6;
//
// &::after {
// content: '';
// position: absolute;
// bottom: -10px;
// left: 50%;
// transform: translateX(-50%);
// width: 0;
// height: 0;
// border-left: 10px solid transparent;
// border-right: 10px solid transparent;
// border-top: 10px solid rgba(71, 186, 255, 0.4);
// }
}
.title {
white-space: nowrap;
position: absolute;
top: 12px;
left: 20px;
width: 24px;
height: 24px;
padding: 0;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 16px;
color: #FFFFFF;
line-height: 22px;
text-shadow: 0px 0px 20px #079DFF;
text-align: right;
font-style: normal;
}
// -
.close-button {
position: absolute;
top: 18px;
right: 2px;
width: 24px;
height: 24px;
padding: 0;
//
border: none;
background: transparent;
color: transparent;
font-size: 0;
line-height: 1;
box-shadow: none;
cursor: pointer;
//
&:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.8);
outline-offset: 2px;
}
}
//
.tooltip-content {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 5px;
.info-item:first-child {
grid-column: 1 / -1;
}
// padding-right: 1.5rem; //
}
.mb {
margin-bottom: 0.5rem
}
//
.info-item {
.label {
white-space: nowrap;
color: rgba(255, 255, 255, 0.7);
margin-right: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
.value {
font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: 500;
font-size: 14px;
color: #14FFF6;
line-height: 21px;
text-align: left;
font-style: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
//
.no-data {
color: rgba(255, 255, 255, 0.5);
text-align: center;
padding: 0.5rem 0;
}
//
.loading-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
// 使
background: rgba(0, 0, 0, 1);
backdrop-filter: blur(12px);
span {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
}
//
.loading-spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(71, 186, 255, 0.2);
border-top-color: rgba(71, 186, 255, 1);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
//
.error-message {
color: #ff6b6b;
font-size: 13px;
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255, 107, 107, 0.1);
border: 1px solid rgba(255, 107, 107, 0.3);
border-radius: 0.25rem;
}
//
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-enter-from {
opacity: 0;
transform: translate(-50%, calc(-100% - 10px));
}
.fade-leave-to {
opacity: 0;
transform: translate(-50%, calc(-100% - 30px));
}
</style>

View File

@ -3,6 +3,7 @@ import { getAllYHZList } from '@/views/cockpit/api/commonHttp'
import ImageMarkData from './ImageMarkData'
import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
import serviceFacilityMarkerIcon from '../assets/legendTool/服务设施icon定位.png'
@ -134,6 +135,28 @@ export const useMapImageMark = (mapStore) => {
imageMarkData.tooltip.close()
}
/**
* 聚焦到图片实例的中心点
*/
const focusOnMarkers = (dataList) => {
if (!dataList || dataList.length === 0) return
const positions = dataList.map((item) => {
return [item.mapData.position[0], item.mapData.position[1]]
})
const features = turf.points(positions)
const center = turf.center(features)
const position = center.geometry.coordinates
mapStore.viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(position[0], position[1], 160000),
orientation: {
heading: Cesium.Math.toRadians(0), // 方向角
pitch: Cesium.Math.toRadians(-90), // 俯仰角(-90 为垂直向下看)
roll: 0 // 翻滚角
}
})
}
/**
* 根据图标名称请求后台服务动态加载图标列表
*/
@ -165,7 +188,7 @@ export const useMapImageMark = (mapStore) => {
/**
* 过滤养护站图标
*/
const filterYHZMark = async (key, filterFn) => {
const filterYHZMark = async (key, filterFn, focusFn) => {
const imageMarkData = imageMarkMap[key]
if (!imageMarkData) return
// 取消上一次请求
@ -175,7 +198,15 @@ export const useMapImageMark = (mapStore) => {
return filterFn(item)
})
clearLayerEntity(key)
// 绘制过滤出来的点
drawImageEntities(dataList)
// 聚焦聚焦函数过滤出来的点
const focusDataList = dataList.filter((item)=>{
return focusFn(item)
})
if(focusDataList) {
focusOnMarkers(focusDataList)
}
}
/**