feat: 地图实时显示设备位置,功能代码提交。
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
VITE_APP_TITLE = 智聪物联网管理平台
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
VITE_APP_ENV = 'development'
|
VITE_APP_ENV = 'development'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
VITE_APP_TITLE = 智聪物联网管理平台
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
VITE_APP_ENV = 'production'
|
VITE_APP_ENV = 'production'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
VITE_APP_TITLE = 智聪物联网管理平台
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
VITE_APP_ENV = 'staging'
|
VITE_APP_ENV = 'staging'
|
||||||
|
|||||||
9
src/api/td/tdEngine.js
Normal file
9
src/api/td/tdEngine.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取最新设备传感器数据
|
||||||
|
export function getLastData() {
|
||||||
|
return request({
|
||||||
|
url: '/tdengine/td/getLastData',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { getAssetsFile } from '@/utils'
|
import { getAssetsFile } from '@/utils'
|
||||||
|
|
||||||
export const mapConfig = {
|
export const mapConfig = {
|
||||||
zoom: 13,
|
zoom: 10,
|
||||||
minZoom: 4,
|
minZoom: 4,
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
pitch: 0,
|
pitch: 0,
|
||||||
center: ['120.43325056066504', '36.183868828044005'],
|
center: ['121.00', '35.80'],
|
||||||
logoVisible: false
|
logoVisible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import usePermissionStore from '@/store/modules/permission'
|
|||||||
|
|
||||||
NProgress.configure({ showSpinner: false })
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
const whiteList = ['/login', '/register', '/redirectTo']
|
const whiteList = ['/login', '/register', '/redirectTo', '/bdsLocation']
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ export const constantRoutes = [
|
|||||||
meta: { title: '个人中心', icon: 'user' }
|
meta: { title: '个人中心', icon: 'user' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/bdsLocation',
|
||||||
|
component: () => import('@/views/mapTest/index.vue'),
|
||||||
|
hidden: true,
|
||||||
|
meta: { title: '北斗上位机实时显示位置' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
759
src/views/mapTest/index.vue
Normal file
759
src/views/mapTest/index.vue
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
<template>
|
||||||
|
<div class="map-test-container">
|
||||||
|
<div id="mapContainer" class="map-container" v-loading="loading" element-loading-text="加载设备数据中..."></div>
|
||||||
|
<div class="info-panel">
|
||||||
|
<h3>地图信息</h3>
|
||||||
|
<p>总点位数: {{ points.length }}</p>
|
||||||
|
<p>当前缩放级别: {{ currentZoom }}</p>
|
||||||
|
<div class="legend">
|
||||||
|
<h4>图例</h4>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span>设备点位</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 隐藏的SVG定义 -->
|
||||||
|
<svg width="0" height="0" style="position: absolute;">
|
||||||
|
<defs>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- 点位信息弹出框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="位置详情"
|
||||||
|
width="400px"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
>
|
||||||
|
<div class="point-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">编号:</span>
|
||||||
|
<span class="value">{{ selectedPoint.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">上报时间:</span>
|
||||||
|
<span class="value">{{ selectedPoint.reportTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">经度:</span>
|
||||||
|
<span class="value">{{ selectedPoint.longitude }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="label">纬度:</span>
|
||||||
|
<span class="value">{{ selectedPoint.latitude }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="MapTest">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { initMap, initLayer, addImage } from '@/utils/map'
|
||||||
|
import { mapConfig, layerUrl, imageArr } from '@/config/map'
|
||||||
|
import { PointLayer, Marker } from '@antv/l7'
|
||||||
|
import { getLastData } from '@/api/td/tdEngine'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const scene = ref(null)
|
||||||
|
const pointLayer = ref(null)
|
||||||
|
const currentZoom = ref(13)
|
||||||
|
const points = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const selectedPoint = ref({
|
||||||
|
id: '',
|
||||||
|
reportTime: '',
|
||||||
|
longitude: '',
|
||||||
|
latitude: ''
|
||||||
|
})
|
||||||
|
const loading = ref(false)
|
||||||
|
let refreshTimer = null // 定时刷新定时器
|
||||||
|
|
||||||
|
// 从接口获取设备数据
|
||||||
|
const fetchDeviceData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getLastData()
|
||||||
|
// 转换后端数据格式为前端需要的格式
|
||||||
|
const devicePoints = response.map((item, index) => {
|
||||||
|
return {
|
||||||
|
id: item.deviceId || `PT${String(index + 1).padStart(4, '0')}`,
|
||||||
|
name: `设备${item.deviceId || index + 1}`,
|
||||||
|
longitude: item.longitude,
|
||||||
|
latitude: item.latitude,
|
||||||
|
reportTime: item.createTime || item.time || new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}),
|
||||||
|
status: 'online'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
points.value = devicePoints
|
||||||
|
console.log('从接口获取到', points.value.length, '个设备点位')
|
||||||
|
|
||||||
|
// 如果已经有图层,更新图层数据;否则创建新图层
|
||||||
|
if (scene.value) {
|
||||||
|
updatePointLayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
return devicePoints
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备数据失败:', error)
|
||||||
|
ElMessage.error('获取设备数据失败,请稍后重试')
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
const initializeMap = async () => {
|
||||||
|
try {
|
||||||
|
// 从接口获取设备数据
|
||||||
|
await fetchDeviceData()
|
||||||
|
|
||||||
|
// 如果有数据,使用第一个设备的经纬度作为地图中心
|
||||||
|
let mapCenter = mapConfig.center
|
||||||
|
if (points.value.length > 0) {
|
||||||
|
const firstPoint = points.value[0]
|
||||||
|
mapCenter = [firstPoint.longitude, firstPoint.latitude]
|
||||||
|
console.log('地图中心设置为:', mapCenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建地图场景,使用动态中心点
|
||||||
|
const mapScene = await initMap('mapContainer', {
|
||||||
|
...mapConfig,
|
||||||
|
center: mapCenter
|
||||||
|
})
|
||||||
|
scene.value = mapScene
|
||||||
|
|
||||||
|
// 添加图层
|
||||||
|
initLayer(mapScene, layerUrl)
|
||||||
|
|
||||||
|
// 添加图片资源
|
||||||
|
addImage(mapScene, imageArr)
|
||||||
|
|
||||||
|
// 创建点图层
|
||||||
|
if (points.value.length > 0) {
|
||||||
|
createPointLayer()
|
||||||
|
// 启动定时刷新
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('暂无设备数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听缩放事件
|
||||||
|
mapScene.on('zoom', () => {
|
||||||
|
currentZoom.value = Math.round(mapScene.getZoom())
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('地图初始化成功,已添加', points.value.length, '个点位')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('地图初始化失败:', error)
|
||||||
|
ElMessage.error('地图初始化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建3D立体标记图标HTML
|
||||||
|
const create3DMarkerElement = (point) => {
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.className = 'marker-3d'
|
||||||
|
element.innerHTML = `
|
||||||
|
<div class="marker-container">
|
||||||
|
<div class="pulse-ring"></div>
|
||||||
|
<div class="outer-glow"></div>
|
||||||
|
<div class="marker-pin">
|
||||||
|
<div class="marker-glow"></div>
|
||||||
|
<div class="inner-shine"></div>
|
||||||
|
<svg class="marker-icon" viewBox="0 0 24 24" fill="white">
|
||||||
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="marker-shadow"></div>
|
||||||
|
<div class="status-ring"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
element.addEventListener('click', () => {
|
||||||
|
selectedPoint.value = {
|
||||||
|
id: point.id,
|
||||||
|
reportTime: point.reportTime,
|
||||||
|
longitude: point.longitude.toFixed(6),
|
||||||
|
latitude: point.latitude.toFixed(6)
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建点图层(使用Marker实现3D效果)
|
||||||
|
const createPointLayer = () => {
|
||||||
|
if (!scene.value || points.value.length === 0) return
|
||||||
|
|
||||||
|
const markers = []
|
||||||
|
|
||||||
|
// 为每个点创建3D Marker
|
||||||
|
points.value.forEach(point => {
|
||||||
|
const element = create3DMarkerElement(point)
|
||||||
|
const marker = new Marker({
|
||||||
|
element: element,
|
||||||
|
anchor: 'bottom'
|
||||||
|
}).setLnglat([point.longitude, point.latitude])
|
||||||
|
|
||||||
|
scene.value.addMarker(marker)
|
||||||
|
markers.push(marker)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 脉冲动画底层光圈
|
||||||
|
const pulseLayer = new PointLayer({
|
||||||
|
name: 'pulsePoints',
|
||||||
|
zIndex: 100
|
||||||
|
})
|
||||||
|
.source(points.value, {
|
||||||
|
parser: {
|
||||||
|
type: 'json',
|
||||||
|
x: 'longitude',
|
||||||
|
y: 'latitude'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shape('circle')
|
||||||
|
.size(40)
|
||||||
|
.color('#5B8FF9')
|
||||||
|
.style({
|
||||||
|
opacity: 0.15
|
||||||
|
})
|
||||||
|
.animate({
|
||||||
|
enable: true,
|
||||||
|
type: 'breathe',
|
||||||
|
duration: 2000,
|
||||||
|
interval: 0.5,
|
||||||
|
trailLength: 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
scene.value.addLayer(pulseLayer)
|
||||||
|
pointLayer.value = { markers, pulseLayer }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新点图层数据
|
||||||
|
const updatePointLayer = () => {
|
||||||
|
if (!scene.value || points.value.length === 0) return
|
||||||
|
|
||||||
|
// 只更新脉冲图层的数据源,不重建整个图层
|
||||||
|
if (pointLayer.value && pointLayer.value.pulseLayer) {
|
||||||
|
pointLayer.value.pulseLayer.setData(points.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新现有markers的位置和数据
|
||||||
|
if (pointLayer.value && pointLayer.value.markers) {
|
||||||
|
const oldMarkers = pointLayer.value.markers
|
||||||
|
const newPoints = points.value
|
||||||
|
|
||||||
|
// 如果点位数量相同,只更新位置
|
||||||
|
if (oldMarkers.length === newPoints.length) {
|
||||||
|
oldMarkers.forEach((marker, index) => {
|
||||||
|
const newPoint = newPoints[index]
|
||||||
|
if (newPoint) {
|
||||||
|
// 更新位置
|
||||||
|
marker.setLnglat([newPoint.longitude, newPoint.latitude])
|
||||||
|
// 更新点击事件数据
|
||||||
|
const element = marker.getElement()
|
||||||
|
element.onclick = () => {
|
||||||
|
selectedPoint.value = {
|
||||||
|
id: newPoint.id,
|
||||||
|
reportTime: newPoint.reportTime,
|
||||||
|
longitude: newPoint.longitude.toFixed(6),
|
||||||
|
latitude: newPoint.latitude.toFixed(6)
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('已无感更新', newPoints.length, '个点位位置')
|
||||||
|
} else {
|
||||||
|
// 点位数量变化时才完全重建
|
||||||
|
rebuildPointLayer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 首次创建或图层不存在时
|
||||||
|
createPointLayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完全重建点图层(仅在点位数量变化时使用)
|
||||||
|
const rebuildPointLayer = () => {
|
||||||
|
if (!scene.value) return
|
||||||
|
|
||||||
|
// 清除旧的markers和图层
|
||||||
|
if (pointLayer.value) {
|
||||||
|
if (pointLayer.value.markers) {
|
||||||
|
pointLayer.value.markers.forEach(marker => {
|
||||||
|
marker.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (pointLayer.value.pulseLayer) {
|
||||||
|
scene.value.removeLayer(pointLayer.value.pulseLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新创建图层
|
||||||
|
createPointLayer()
|
||||||
|
console.log('点位数量变化,已重建图层')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动定时刷新
|
||||||
|
const startAutoRefresh = () => {
|
||||||
|
// 清除已存在的定时器
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每10秒刷新一次数据
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
console.log('定时刷新设备数据...')
|
||||||
|
fetchDeviceData()
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
console.log('已启动自动刷新,每10秒更新一次')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止定时刷新
|
||||||
|
const stopAutoRefresh = () => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
refreshTimer = null
|
||||||
|
console.log('已停止自动刷新')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeMap()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 停止定时刷新
|
||||||
|
stopAutoRefresh()
|
||||||
|
|
||||||
|
if (pointLayer.value) {
|
||||||
|
// 清理markers
|
||||||
|
if (pointLayer.value.markers) {
|
||||||
|
pointLayer.value.markers.forEach(marker => {
|
||||||
|
marker.remove() // 使用marker.remove()方法移除标记
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 清理图层
|
||||||
|
if (pointLayer.value.pulseLayer) {
|
||||||
|
scene.value?.removeLayer(pointLayer.value.pulseLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scene.value) {
|
||||||
|
scene.value.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.map-test-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 220px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #00D9FF;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #00D9FF;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
background-color: #5B8FF9;
|
||||||
|
box-shadow: 0 0 6px rgba(91, 143, 249, 0.5);
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3D标记样式
|
||||||
|
:deep(.marker-3d) {
|
||||||
|
cursor: pointer;
|
||||||
|
transform-origin: center bottom;
|
||||||
|
|
||||||
|
.marker-container {
|
||||||
|
position: relative;
|
||||||
|
width: 40px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 2px solid #5B8FF9;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outer-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(91, 143, 249, 0.4) 0%, transparent 70%);
|
||||||
|
animation: glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-pin {
|
||||||
|
position: relative;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50% 50% 50% 0;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
background: linear-gradient(135deg, #5B8FF9 0%, #7BA3FF 100%);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow:
|
||||||
|
0 3px 6px rgba(0, 0, 0, 0.3),
|
||||||
|
0 6px 12px rgba(0, 0, 0, 0.2),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0 -2px 3px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 2px 3px rgba(255, 255, 255, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 20px rgba(91, 143, 249, 0.5), 0 0 35px rgba(91, 143, 249, 0.3);
|
||||||
|
animation: glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-shine {
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
left: 20%;
|
||||||
|
width: 40%;
|
||||||
|
height: 40%;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-shadow {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 8px;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.35) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: shadow-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: 2px solid #5B8FF9;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: status-ring-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.marker-pin {
|
||||||
|
transform: rotate(-45deg) scale(1.1);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.4),
|
||||||
|
0 8px 16px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 0 2px rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0 -2px 3px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 2px 3px rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ring {
|
||||||
|
animation: status-ring-hover 0.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-ring {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.75);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.6);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translate(-50%, -50%) scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shadow-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%) scale(0.88);
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes status-ring-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.12);
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes status-ring-hover {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.08);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点位信息弹出框样式
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 24px 30px;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
.el-dialog__title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
top: 24px;
|
||||||
|
right: 30px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 32px 30px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-info {
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
background: linear-gradient(90deg, rgba(102, 126, 234, 0.08) 0%, transparent 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||||
|
border-left-color: #764ba2;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
min-width: 95px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ':';
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #212529;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user