334 lines
7.1 KiB
Vue
Raw Normal View History

2025-11-18 17:53:04 +08:00
<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>