feat: 冰雪阻断事件大屏
This commit is contained in:
parent
0d542d7471
commit
2059579bd0
12
packages/screen/src/views/cockpit/api/commonHttp.js
Normal file
12
packages/screen/src/views/cockpit/api/commonHttp.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { request } from '@shared/utils/request'
|
||||
|
||||
// 获取业务基础地图
|
||||
export function getBusinessBaseMapLayer() {
|
||||
return request({
|
||||
url: '/snow-ops-platform/dataDirectory/queryCatalog',
|
||||
method: 'GET',
|
||||
params: {
|
||||
pcatalog: 'DDT'
|
||||
}
|
||||
})
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 图例工具栏 -->
|
||||
<LegendToolbar class="legend-toolbar" @marker-toggle="handleLegendMarkerToggle" />
|
||||
<LegendToolbar class="legend-toolbar" :legendKeys="legendKeys" @marker-toggle="handleLegendMarkerToggle" />
|
||||
|
||||
<!-- 应急力量详情提示框 -->
|
||||
<EmergencyForceTooltip
|
||||
@ -42,11 +42,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { ref, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import useMapStore from '@/map/stores/mapStore'
|
||||
import { fetchEmergencyForceList } from '@/views/cockpit/api/emergencyForce'
|
||||
import { useEmergencyForceInteraction } from '../composables/useEmergencyForceInteraction'
|
||||
import { useMapBase } from '../composables/useMapBase'
|
||||
import { useMapImageMark } from '../composables/useMapImageMark'
|
||||
|
||||
import PageHeader from './PageHeader.vue'
|
||||
import WeatherWarning from './WeatherWarning.vue'
|
||||
@ -58,6 +60,11 @@ import LegendToolbar from './LegendToolbar.vue'
|
||||
import EmergencyForceTooltip from './EmergencyForceTooltip.vue'
|
||||
import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定位.png'
|
||||
|
||||
onMounted(()=>{
|
||||
// 加载地图业务底图 并 聚焦中心点
|
||||
mapBase.loadBaseData()
|
||||
})
|
||||
|
||||
// ==================== 常量定义 ====================
|
||||
|
||||
/**
|
||||
@ -86,6 +93,16 @@ const emergencyForceInteraction = useEmergencyForceInteraction(mapStore, {
|
||||
flyDistance: 500
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载地图的业务底图与聚焦中心点
|
||||
*/
|
||||
const mapBase = useMapBase(mapStore)
|
||||
|
||||
/**
|
||||
* 标记图hook,搭配lengendToolbar使用, 作用是点击某个图例项时,在地图上显示所有该图例的图
|
||||
*/
|
||||
const mapImageMark = useMapImageMark(mapStore)
|
||||
|
||||
/**
|
||||
* 应急力量数据加载状态
|
||||
*/
|
||||
@ -111,6 +128,12 @@ const emergencyForceCache = ref({
|
||||
*/
|
||||
const emergencyForceAbortController = ref(null)
|
||||
|
||||
/**
|
||||
* 工具图标列表通过key关联
|
||||
* key具体有哪些,请根据LegendToolbar.vue中定义的defaultLegendItems
|
||||
*/
|
||||
const legendKeys = ref(['serviceFacility','riskRoad','blockEvent','weatherAlert','emergencyForce'])
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
@ -401,6 +424,9 @@ const renderEmergencyForcePoints = async (entityService, points, markerIcon) =>
|
||||
const handleLegendMarkerToggle = async ({ key, active, markerIcon }) => {
|
||||
// 只处理应急力量图例项
|
||||
if (key !== 'emergencyForce') {
|
||||
|
||||
mapImageMark.toggleMark({ key, active, markerIcon })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible && position"
|
||||
class="common-tooltip"
|
||||
:style="{
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y - 20}px`
|
||||
}"
|
||||
>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
class="close-button"
|
||||
type="button"
|
||||
aria-label="关闭"
|
||||
@click="handleClose"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="tooltip-content">
|
||||
<component v-if="hasData" :is="contentMap[data.mapData.layer]" />
|
||||
|
||||
<!-- 如果没有数据 -->
|
||||
<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 } from 'vue'
|
||||
import serviceFacility from './serviceFacility.vue'
|
||||
import riskRoad from './riskRoad.vue'
|
||||
import weatherAlert from './weatherAlert.vue'
|
||||
import blockEvent from './blockEvent.vue'
|
||||
|
||||
/**
|
||||
* 应急力量详情提示框组件
|
||||
* 使用 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: () => ({})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载状态
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== State ====================
|
||||
const contentMap = ref({
|
||||
// 养护站内容
|
||||
serviceFacility,
|
||||
// 高海拔道路
|
||||
riskRoad,
|
||||
// 阻断事件
|
||||
blockEvent,
|
||||
// 气象预警
|
||||
weatherAlert,
|
||||
})
|
||||
|
||||
// ==================== Emits ====================
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// ==================== Computed ====================
|
||||
|
||||
/**
|
||||
* 是否有有效数据
|
||||
*/
|
||||
const hasData = computed(() => {
|
||||
return !!props.data
|
||||
})
|
||||
|
||||
// ==================== Methods ====================
|
||||
|
||||
/**
|
||||
* 处理关闭按钮点击
|
||||
*/
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
|
||||
.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;
|
||||
max-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);
|
||||
// }
|
||||
}
|
||||
|
||||
// 关闭按钮 - 仅作为透明点击热区,视觉由背景图中的关闭图标承载
|
||||
.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 {
|
||||
padding-right: 1.5rem; // 为关闭按钮留出空间
|
||||
}
|
||||
|
||||
// 信息项
|
||||
.info-item {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 无数据提示
|
||||
.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>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="">
|
||||
test
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
</style>
|
||||
@ -0,0 +1,78 @@
|
||||
import { createVNode, render } from 'vue'
|
||||
import ImageMarkTooltip from './ImageMarkTooltip.vue'
|
||||
|
||||
class ImageMarkTooltipUI {
|
||||
constructor() {
|
||||
this.instance = null
|
||||
this.container = null
|
||||
this.entity = null
|
||||
this.root = null
|
||||
}
|
||||
|
||||
// 显示 tooltip
|
||||
show(options = {}) {
|
||||
// 销毁之前的实例
|
||||
this.close()
|
||||
|
||||
// 创建容器
|
||||
this.container = document.createElement('div')
|
||||
this.container.className = 'tooltip-service-container'
|
||||
this.root = document.querySelector('.cockpit-main')
|
||||
this.root.appendChild(this.container)
|
||||
|
||||
this.entity = options.entity
|
||||
|
||||
// 创建 VNode
|
||||
const vnode = createVNode(ImageMarkTooltip, {
|
||||
visible: true,
|
||||
position: options.position || { x: 0, y: 0 },
|
||||
data: options.data || {},
|
||||
loading: options.loading || false,
|
||||
error: options.error || '',
|
||||
onClose: () => {
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染到容器
|
||||
render(vnode, this.container)
|
||||
this.instance = vnode.component
|
||||
return this.instance
|
||||
}
|
||||
|
||||
updatePosition(position) {
|
||||
if (this.instance) {
|
||||
this.instance.props.position = position
|
||||
this.instance.update()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 tooltip 内容
|
||||
update(options) {
|
||||
if (this.instance) {
|
||||
this.instance.props = {
|
||||
...this.instance.props,
|
||||
...options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 tooltip
|
||||
close() {
|
||||
if (this.container) {
|
||||
render(null, this.container)
|
||||
this.root.removeChild(this.container)
|
||||
this.container = null
|
||||
this.instance = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const instance = new ImageMarkTooltipUI()
|
||||
|
||||
export const CommonTooltip = instance
|
||||
|
||||
export const newImageMarkTooltip = () => {
|
||||
return new ImageMarkTooltipUI()
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="tool-tip-content">
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
</style>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div v-if="data.qxmc" class="info-item">
|
||||
<span class="label">区县:</span>
|
||||
<span class="value">{{ data.qxmc }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="data.yjllpz" class="info-item">
|
||||
<span class="label">应急力量配置:</span>
|
||||
<span class="value">{{ data.yjllpz }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="data.wzQtwz" class="info-item">
|
||||
<span class="label">物资:</span>
|
||||
<span class="value">{{ data.wzQtwz }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
</style>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="">
|
||||
test
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/mixins.scss' as *;
|
||||
</style>
|
||||
@ -76,6 +76,7 @@ import blockEventMarkerIcon from '../assets/legendTool/阻断事件icon定位.pn
|
||||
import emergencyForceIcon from '../assets/legendTool/应急力量icon.png'
|
||||
import emergencyForceMarkerIcon from '../assets/legendTool/应急力量icon定位.png'
|
||||
import weatherAlertIcon from '../assets/legendTool/气象预警icon.png'
|
||||
import weatherAlertMarkerIcon from '../assets/legendTool/气象预警icon定位.png'
|
||||
|
||||
// 默认图例项配置(包含普通图标和地图定位图标)
|
||||
const defaultLegendItems = [
|
||||
@ -84,12 +85,12 @@ const defaultLegendItems = [
|
||||
{ key: 'trafficMonitor', label: '交调', icon: trafficMonitorIcon, markerIcon: trafficMonitorMarkerIcon },
|
||||
{ key: 'bridge', label: '桥梁', icon: bridgeIcon, markerIcon: bridgeMarkerIcon },
|
||||
{ key: 'tunnel', label: '隧洞', icon: tunnelIcon, markerIcon: tunnelMarkerIcon },
|
||||
{ key: 'serviceFacility', label: '服务设施', icon: serviceFacilityIcon, markerIcon: serviceFacilityMarkerIcon },
|
||||
{ key: 'riskRoad', label: '风险路段', icon: riskRoadIcon, markerIcon: riskRoadMarkerIcon },
|
||||
{ key: 'serviceFacility', label: '养护站', icon: serviceFacilityIcon, markerIcon: serviceFacilityMarkerIcon },
|
||||
{ key: 'riskRoad', label: '高海拔路段', icon: riskRoadIcon, markerIcon: riskRoadMarkerIcon },
|
||||
{ key: 'hazardPoint', label: '涉灾隐患点', icon: hazardPointIcon, markerIcon: hazardPointMarkerIcon },
|
||||
{ key: 'blockEvent', label: '阻断事件', icon: blockEventIcon, markerIcon: blockEventMarkerIcon },
|
||||
{ key: 'emergencyForce', label: '应急力量', icon: emergencyForceIcon, markerIcon: emergencyForceMarkerIcon },
|
||||
{ key: 'weatherAlert', label: '气象预警', icon: weatherAlertIcon, markerIcon: weatherAlertIcon }
|
||||
{ key: 'weatherAlert', label: '气象预警', icon: weatherAlertIcon, markerIcon: weatherAlertMarkerIcon }
|
||||
]
|
||||
|
||||
// 定义 props
|
||||
@ -103,6 +104,14 @@ const props = defineProps({
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据key显示对应图例
|
||||
*/
|
||||
legendKeys: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Marker 数据配置(可选)
|
||||
* 如果提供,将在事件中传递完整的 marker 数据
|
||||
@ -135,10 +144,10 @@ const props = defineProps({
|
||||
* 兼容旧版:标记点切换回调函数
|
||||
* @deprecated 推荐使用 @marker-toggle 事件
|
||||
*/
|
||||
onMarkerToggle: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
// onMarkerToggle: {
|
||||
// type: Function,
|
||||
// default: null
|
||||
// }
|
||||
})
|
||||
|
||||
// 定义 emits
|
||||
@ -146,6 +155,13 @@ const emit = defineEmits(['marker-toggle', 'clear', 'update:modelValue', 'collap
|
||||
|
||||
// 计算图例项配置(支持自定义)
|
||||
const computedLegendItems = computed(() => {
|
||||
if(props.legendKeys && props.legendKeys.length > 0) {
|
||||
return props.legendKeys.map(key => {
|
||||
const item = defaultLegendItems.find(item => item.key === key)
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
return props.legendItems && props.legendItems.length > 0
|
||||
? props.legendItems
|
||||
: defaultLegendItems
|
||||
@ -260,7 +276,6 @@ const handleItemClick = (key) => {
|
||||
// 构建并发送事件
|
||||
const payload = buildPayload(key, newIsActive)
|
||||
emit('marker-toggle', payload)
|
||||
|
||||
// 兼容旧版 props 回调
|
||||
if (props.onMarkerToggle) {
|
||||
props.onMarkerToggle(
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import { newImageMarkTooltip } from '../components/ImageMarkTooltip'
|
||||
|
||||
// 专门用于绘制图片的api数据类
|
||||
export default class ImageMarkData {
|
||||
key = null
|
||||
markerIcon = null
|
||||
api = null
|
||||
// 提示框实例,当鼠标移入或者点击图例时,需要获取到该实例
|
||||
tooltip = null
|
||||
onResponse = null
|
||||
cacheData = null
|
||||
expiresAt = null
|
||||
abortController = null
|
||||
/**
|
||||
* 60秒内重复点击使用缓存数据,减少服务器压力
|
||||
*/
|
||||
cacheTime = 60 * 1000
|
||||
|
||||
constructor({ api, tooltip, onResponse }) {
|
||||
this.api = api
|
||||
this.response = onResponse
|
||||
// 初始化提示框
|
||||
this.tooltip = tooltip || newImageMarkTooltip()
|
||||
}
|
||||
|
||||
getCache = () => {
|
||||
// 检查缓存是否有效
|
||||
if (
|
||||
this.cacheData?.length &&
|
||||
this.expiresAt > Date.now()
|
||||
) {
|
||||
return this.cacheData
|
||||
}
|
||||
}
|
||||
|
||||
request = async (params) => {
|
||||
// 首先查看缓存是否可用
|
||||
const cacheData = this.getCache()
|
||||
if (cacheData) return cacheData
|
||||
|
||||
const controller = this.createAbortController()
|
||||
const config = {}
|
||||
if (controller) config.signal = controller.signal
|
||||
let res = null
|
||||
try {
|
||||
res = await this.api(params, config)
|
||||
} catch (error) {
|
||||
console.error('请求失败: ', error)
|
||||
} finally {
|
||||
let data = null
|
||||
if (this.response) data = this.response(res)
|
||||
else data = this.commonOnResponse(res)
|
||||
|
||||
this.cacheData = data
|
||||
this.expiresAt = Date.now() + this.cacheTime
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
createAbortController = () => {
|
||||
if (typeof AbortController === 'undefined') {
|
||||
console.warn('当前环境不支持 AbortController')
|
||||
return null
|
||||
}
|
||||
return new AbortController()
|
||||
}
|
||||
|
||||
cancelRequest = () => {
|
||||
if (this.abortController) {
|
||||
try {
|
||||
this.abortController.abort()
|
||||
} catch (error) {
|
||||
this.console.warn('取消请求失败', error)
|
||||
} finally {
|
||||
this.abortController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的响应处理
|
||||
commonOnResponse = (res) => {
|
||||
if (res?.success) {
|
||||
res.data = res.data.slice(0, 2)
|
||||
const dataList = res.data.map((item) => {
|
||||
item.mapData = {
|
||||
id: this.key + '-' + item.rid,
|
||||
layerId: this.key,
|
||||
position: [item.jd, item.wd, 0],
|
||||
image: this.markerIcon
|
||||
}
|
||||
return item
|
||||
})
|
||||
this.data = dataList
|
||||
return dataList
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
37
packages/screen/src/views/cockpit/composables/useMapBase.js
Normal file
37
packages/screen/src/views/cockpit/composables/useMapBase.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { getBusinessBaseMapLayer } from '@/views/cockpit/api/commonHttp.js'
|
||||
|
||||
// 当前页面的最基础地图服务
|
||||
// 主要是加载地图底图
|
||||
export const useMapBase = (mapStore) => {
|
||||
|
||||
const loadBusinessBaseMapLayer = async () => {
|
||||
const layerService = mapStore.services().layer
|
||||
const res = await getBusinessBaseMapLayer()
|
||||
const data = [...res]
|
||||
mapStore.baseMapGroups = data
|
||||
for (const item of data) {
|
||||
const layers = mapStore.getBaseMapLayersForGroup(item.Attribute?.rid || item.Rid)
|
||||
for (const layerConfig of layers) {
|
||||
const layer = {
|
||||
id: layerConfig.id,
|
||||
type: layerConfig.type,
|
||||
url: layerConfig.url,
|
||||
meta: layerConfig.meta,
|
||||
}
|
||||
await layerService.addLayer(layer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const loadBaseData = () => {
|
||||
setTimeout(() => {
|
||||
loadBusinessBaseMapLayer()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
loadBaseData
|
||||
}
|
||||
}
|
||||
177
packages/screen/src/views/cockpit/composables/useMapImageMark.js
Normal file
177
packages/screen/src/views/cockpit/composables/useMapImageMark.js
Normal file
@ -0,0 +1,177 @@
|
||||
import { fetchEmergencyForceList } from '@/views/cockpit/api/emergencyForce'
|
||||
|
||||
import ImageMarkData from './ImageMarkData'
|
||||
import * as Cesium from 'cesium'
|
||||
|
||||
/**
|
||||
* 当前业务下的地图服务
|
||||
* 主要是涉及需要调用后端服务获得地图数据
|
||||
* */
|
||||
|
||||
export const useMapImageMark = (mapStore) => {
|
||||
|
||||
// mapStore准备就绪进行初始化
|
||||
mapStore.onReady(() => init())
|
||||
|
||||
// 接口服务映射
|
||||
const imageMarkMap = {
|
||||
serviceFacility: new ImageMarkData({
|
||||
api: fetchEmergencyForceList,
|
||||
}),
|
||||
riskRoad: new ImageMarkData({
|
||||
api: fetchEmergencyForceList,
|
||||
}),
|
||||
blockEvent: new ImageMarkData({
|
||||
api: fetchEmergencyForceList,
|
||||
}),
|
||||
weatherAlert: new ImageMarkData({
|
||||
api: fetchEmergencyForceList,
|
||||
})
|
||||
}
|
||||
|
||||
let entityService
|
||||
let viewer
|
||||
let handler
|
||||
|
||||
const init = () => {
|
||||
entityService = mapStore.services().entity
|
||||
viewer = mapStore.getViewer()
|
||||
|
||||
// 事件注册
|
||||
handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
|
||||
|
||||
// 点击事件
|
||||
handler.setInputAction(function (event) {
|
||||
const pickedFeature = viewer.scene.pick(event.position);
|
||||
if (pickedFeature?.id) {
|
||||
const entity = pickedFeature.id
|
||||
const data = entity.properties.originalData.getValue()
|
||||
const imageMarkData = imageMarkMap[data.mapData.layerId]
|
||||
imageMarkData.tooltip.show({
|
||||
data,
|
||||
entity,
|
||||
position: getScreenPosition(entity)
|
||||
})
|
||||
}
|
||||
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
|
||||
// 帧渲染函数,也就是显示器的每一帧都会执行
|
||||
// 地图拖动与放大的监控,需要更新各个tooltips的位置, 通过消息来通知
|
||||
viewer.scene.postRender.addEventListener(() => {
|
||||
for(const key in imageMarkMap) {
|
||||
const imageMarkData = imageMarkMap[key]
|
||||
const entity = imageMarkData.tooltip.entity
|
||||
if(!entity) continue
|
||||
imageMarkData.tooltip.updatePosition(getScreenPosition(entity))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 绘制各个地图标,例如点击LengendItem的某一项,就会显示对应的图标列表
|
||||
*/
|
||||
const drawImageEntities = async (dataList) => {
|
||||
for (const item of dataList) {
|
||||
const mapData = item.mapData
|
||||
entityService.addBillboard({
|
||||
id: mapData.id,
|
||||
layerId: mapData.layerId,
|
||||
position: mapData.position,
|
||||
image: mapData.image,
|
||||
width: mapData.width || 44,
|
||||
height: mapData.height || 44,
|
||||
clampToGround: true,
|
||||
properties: { originalData: item }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 清除某个图层里的实体
|
||||
*/
|
||||
const clearLayerEntity = (layerId) => {
|
||||
entityService.clearLayerEntities(layerId)
|
||||
const imageMarkData = imageMarkMap[layerId]
|
||||
if (!imageMarkData) return
|
||||
imageMarkData.tooltip.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图标名称请求后台服务,动态加载图标列表
|
||||
*/
|
||||
const loadDynamicMark = async ({ key, markerIcon, params }) => {
|
||||
const imageMarkData = imageMarkMap[key]
|
||||
if (!imageMarkData) return
|
||||
imageMarkData.markerIcon = markerIcon
|
||||
imageMarkData.key = key
|
||||
const dataList = await imageMarkData.request(params)
|
||||
|
||||
drawImageEntities(dataList)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示/隐藏地图标
|
||||
*/
|
||||
const toggleMark = async ({ key, active, markerIcon, params }) => {
|
||||
const imageMarkData = imageMarkMap[key]
|
||||
|
||||
if (!active) {
|
||||
clearLayerEntity(key)
|
||||
if (imageMarkData) imageMarkData.cancelRequest()
|
||||
return
|
||||
}
|
||||
|
||||
loadDynamicMark({ key, markerIcon, params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得该实体位于屏幕的位置,用于html元素进行定位使用
|
||||
*/
|
||||
const getScreenPosition = (entity) => {
|
||||
|
||||
try {
|
||||
const position = entity.position?.getValue(Cesium.JulianDate.now())
|
||||
if (!position) return
|
||||
|
||||
// 处理 clampToGround 实体
|
||||
// 对于使用 CLAMP_TO_GROUND 的 billboard,需要获取实际的地形高度
|
||||
let clampedPosition = position
|
||||
|
||||
// 转换为地理坐标
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position)
|
||||
|
||||
// 尝试获取地形高度
|
||||
if (viewer.scene.globe) {
|
||||
const height = viewer.scene.globe.getHeight(cartographic)
|
||||
|
||||
// 如果成功获取地形高度,使用地形高度重建位置
|
||||
if (Cesium.defined(height)) {
|
||||
cartographic.height = height
|
||||
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
|
||||
} else {
|
||||
// 备用方案:尝试使用 clampToHeight
|
||||
const clampedCartesian = viewer.scene.clampToHeight(position)
|
||||
if (clampedCartesian) {
|
||||
clampedPosition = clampedCartesian
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用修正后的位置进行屏幕坐标转换
|
||||
const screenPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
|
||||
if (screenPosition) {
|
||||
return screenPosition
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新 Tooltip 位置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toggleMark
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user