You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
511 lines
12 KiB
511 lines
12 KiB
|
2 weeks ago
|
<template>
|
||
|
|
<div class="map-container" ref="mapContainerRef">
|
||
|
|
<MapControls
|
||
|
|
:show-markers="props.showMarkers"
|
||
|
|
:show-fences="props.showFences"
|
||
|
|
:show-trajectories="props.showTrajectories"
|
||
|
|
:show-draw-fences="props.showDrawFences"
|
||
|
|
:is-markers-active="showMarkers"
|
||
|
|
:is-fences-active="showFences"
|
||
|
|
:is-trajectories-active="showTrajectories"
|
||
|
|
:is-draw-fences-active="showDrawFences"
|
||
|
|
@toggle-markers="toggleMarkers"
|
||
|
|
@toggle-fences="toggleFences"
|
||
|
|
@toggle-trajectories="toggleTrajectories"
|
||
|
|
@toggle-draw-fences="toggleDrawFences"
|
||
|
|
/>
|
||
|
|
<TrajectoryControls
|
||
|
|
:show-controls="showTrajectories"
|
||
|
|
:play-state="trajectoryPlayState"
|
||
|
|
@play="playTrajectory"
|
||
|
|
@pause="pauseTrajectory"
|
||
|
|
@stop="stopTrajectory"
|
||
|
|
@speed-change="setTrajectorySpeed"
|
||
|
|
@time-change="setTrajectoryTime"
|
||
|
|
@time-range-change="setTrajectoryTimeRange"
|
||
|
|
/>
|
||
|
|
<div class="top-panel" v-show="!appStore.mobile">
|
||
|
|
<div class="top-panel__left">
|
||
|
|
<div class="search-group">
|
||
|
|
<el-input v-model="search" class="search-input" placeholder="请输入关键词" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="top-panel__center">
|
||
|
|
<div class="data_item">
|
||
|
|
<div class="data_item__title">手持设备</div>
|
||
|
|
<div class="data_item__value">2000<span class="data_item__unit">台</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="data_item">
|
||
|
|
<div class="data_item__title">在线数量</div>
|
||
|
|
<div class="data_item__value">200<span class="data_item__unit">台</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="data_item">
|
||
|
|
<div class="data_item__title">用户数量</div>
|
||
|
|
<div class="data_item__value">200<span class="data_item__unit">人</span></div>
|
||
|
|
</div>
|
||
|
|
<div class="data_item">
|
||
|
|
<div class="data_item__title">企业数量</div>
|
||
|
|
<div class="data_item__value">20<span class="data_item__unit">家</span></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="top-panel__right">
|
||
|
|
<span class="legend-title">报警图例:</span>
|
||
|
|
<div class="normal-legend">正常状态</div>
|
||
|
|
<div class="alarm1-legend">围栏报警</div>
|
||
|
|
<div class="alarm2-legend">气体报警</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div v-if="panelVisible" class="info-panel">
|
||
|
|
<div class="info-panel__header">
|
||
|
|
<span class="info-panel__title">设备详情</span>
|
||
|
|
<button class="info-panel__close" @click="panelVisible = false">×</button>
|
||
|
|
</div>
|
||
|
|
<div class="info-panel__body">
|
||
|
|
<div v-if="selectedMarker">
|
||
|
|
<div class="info-panel__name">{{ selectedMarker.name }}</div>
|
||
|
|
<div class="info-panel__coord"
|
||
|
|
>坐标:{{ selectedMarker.coordinates[0].toFixed(6) }},
|
||
|
|
{{ selectedMarker.coordinates[1].toFixed(6) }}</div
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
<div v-else class="info-panel__empty">未选择设备</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
<script lang="ts" setup>
|
||
|
|
import { useAppStore } from '@/store/modules/app'
|
||
|
|
import { useHandDetectorStore } from '@/store/modules/handDetector'
|
||
|
|
import { ref, onMounted, watch } from 'vue'
|
||
|
|
import { ElMessage } from 'element-plus'
|
||
|
|
// 导入类型定义
|
||
|
|
import type { MapProps, MarkerData } from './types/map.types'
|
||
|
|
|
||
|
|
// 导入常量配置
|
||
|
|
import { MAP_DEFAULTS, DEFAULT_MARKERS, DEFAULT_FENCES } from './constants/map.constants'
|
||
|
|
|
||
|
|
// 导入组合式函数
|
||
|
|
import { useMapServices } from './composables/useMapServices'
|
||
|
|
import { useMapEvents } from './composables/useMapEvents'
|
||
|
|
import { useTrajectoryControls } from './composables/useTrajectoryControls'
|
||
|
|
import { useMapWatchers } from './composables/useMapWatchers'
|
||
|
|
|
||
|
|
// 导入组件
|
||
|
|
import TrajectoryControls from './TrajectoryControls.vue'
|
||
|
|
import MapControls from './MapControls.vue'
|
||
|
|
const props = withDefaults(defineProps<MapProps>(), {
|
||
|
|
tileUrl: MAP_DEFAULTS.tileUrl,
|
||
|
|
center: () => MAP_DEFAULTS.center,
|
||
|
|
zoom: MAP_DEFAULTS.zoom,
|
||
|
|
maxZoom: MAP_DEFAULTS.maxZoom,
|
||
|
|
minZoom: MAP_DEFAULTS.minZoom,
|
||
|
|
markers: () => DEFAULT_MARKERS,
|
||
|
|
fences: () => DEFAULT_FENCES,
|
||
|
|
enableCluster: MAP_DEFAULTS.enableCluster,
|
||
|
|
clusterDistance: MAP_DEFAULTS.clusterDistance,
|
||
|
|
showTrajectories: true,
|
||
|
|
showMarkers: true,
|
||
|
|
showFences: true,
|
||
|
|
showDrawFences: true
|
||
|
|
})
|
||
|
|
// 响应式状态
|
||
|
|
const showMarkers = ref(props.showMarkers)
|
||
|
|
const showTrajectories = ref(false)
|
||
|
|
const showFences = ref(false)
|
||
|
|
const showDrawFences = ref(false)
|
||
|
|
const mapContainerRef = ref<HTMLElement | null>(null)
|
||
|
|
const handDetectorStore = useHandDetectorStore()
|
||
|
|
// 左侧信息面板状态
|
||
|
|
const appStore = useAppStore()
|
||
|
|
const panelVisible = ref(false)
|
||
|
|
const selectedMarker = ref<MarkerData | null>(null)
|
||
|
|
const search = ref('')
|
||
|
|
|
||
|
|
// 使用组合式函数
|
||
|
|
const {
|
||
|
|
services,
|
||
|
|
layerRefs,
|
||
|
|
initializeServices,
|
||
|
|
initializeMapAndLayers,
|
||
|
|
setMarkersVisible,
|
||
|
|
setTrajectoriesVisible,
|
||
|
|
setFencesVisible,
|
||
|
|
toggleFenceDrawing,
|
||
|
|
clearFenceDrawLayer,
|
||
|
|
updateMarkers,
|
||
|
|
refreshMarkerStyles
|
||
|
|
} = useMapServices()
|
||
|
|
|
||
|
|
const {
|
||
|
|
trajectoryPlayState,
|
||
|
|
playTrajectory,
|
||
|
|
pauseTrajectory,
|
||
|
|
stopTrajectory,
|
||
|
|
setTrajectorySpeed,
|
||
|
|
setTrajectoryTime,
|
||
|
|
setTrajectoryTimeRange,
|
||
|
|
setupTrajectoryWatcher,
|
||
|
|
cleanup: cleanupTrajectory
|
||
|
|
} = useTrajectoryControls()
|
||
|
|
|
||
|
|
const { setupMapEventListeners } = useMapEvents()
|
||
|
|
|
||
|
|
// 控制函数
|
||
|
|
const toggleTrajectories = () => {
|
||
|
|
if (showTrajectories.value && trajectoryPlayState.value.isPlaying) {
|
||
|
|
cleanupTrajectory()
|
||
|
|
}
|
||
|
|
showTrajectories.value = !showTrajectories.value
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleMarkers = () => {
|
||
|
|
showMarkers.value = !showMarkers.value
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleFences = () => {
|
||
|
|
showFences.value = !showFences.value
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleDrawFences = () => {
|
||
|
|
showDrawFences.value = !showDrawFences.value
|
||
|
|
|
||
|
|
if (showDrawFences.value) {
|
||
|
|
// 开始绘制围栏
|
||
|
|
toggleFenceDrawing(true, handleFenceDrawComplete)
|
||
|
|
} else {
|
||
|
|
// 停止绘制围栏
|
||
|
|
toggleFenceDrawing(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
import { MapService } from './services/map.service'
|
||
|
|
let mapService: MapService | null = null
|
||
|
|
let isMapInitialized = false
|
||
|
|
/**
|
||
|
|
* 初始化地图
|
||
|
|
*/
|
||
|
|
const initMap = () => {
|
||
|
|
if (!mapContainerRef.value) return
|
||
|
|
|
||
|
|
// 初始化服务
|
||
|
|
mapService = new MapService()
|
||
|
|
|
||
|
|
// 初始化地图
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 初始化服务
|
||
|
|
initializeServices()
|
||
|
|
const mapInstance = mapService.initMap(mapContainerRef.value, props)
|
||
|
|
// 初始化地图和图层
|
||
|
|
const { map, popupOverlay } = initializeMapAndLayers(mapInstance, props)
|
||
|
|
|
||
|
|
// 设置初始显示状态
|
||
|
|
setMarkersVisible(showMarkers.value)
|
||
|
|
setTrajectoriesVisible(showTrajectories.value, props.markers)
|
||
|
|
setFencesVisible(showFences.value)
|
||
|
|
|
||
|
|
// 设置事件监听器
|
||
|
|
setupMapEventListeners(
|
||
|
|
map,
|
||
|
|
popupOverlay,
|
||
|
|
services.trajectoryService as any,
|
||
|
|
services.popupService,
|
||
|
|
{
|
||
|
|
isDrawing: () => !!services.fenceDrawService?.isCurrentlyDrawing?.(),
|
||
|
|
onMarkerClick: (marker: MarkerData) => {
|
||
|
|
selectedMarker.value = marker
|
||
|
|
panelVisible.value = true
|
||
|
|
},
|
||
|
|
markerLayer: layerRefs.value?.markerLayer,
|
||
|
|
refreshMarkerStyles
|
||
|
|
}
|
||
|
|
)
|
||
|
|
// 设置轨迹监听器
|
||
|
|
setupTrajectoryWatcher(services.trajectoryService as any, showTrajectories)
|
||
|
|
|
||
|
|
// 设置状态监听器
|
||
|
|
const { setupAllWatchers } = useMapWatchers({
|
||
|
|
showMarkers,
|
||
|
|
showTrajectories,
|
||
|
|
showFences,
|
||
|
|
showDrawFences,
|
||
|
|
setMarkersVisible,
|
||
|
|
setTrajectoriesVisible,
|
||
|
|
setFencesVisible,
|
||
|
|
toggleFenceDrawing,
|
||
|
|
updateMarkers,
|
||
|
|
markers: props.markers || []
|
||
|
|
})
|
||
|
|
|
||
|
|
setupAllWatchers()
|
||
|
|
|
||
|
|
// 标记地图已初始化
|
||
|
|
isMapInitialized = true
|
||
|
|
|
||
|
|
console.log('地图初始化成功', { map, services: services })
|
||
|
|
} catch (error) {
|
||
|
|
console.error('地图初始化失败:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 处理围栏绘制完成
|
||
|
|
*/
|
||
|
|
const handleFenceDrawComplete = (coordinates: [number, number][]) => {
|
||
|
|
if (coordinates.length < 3) {
|
||
|
|
// 围栏至少需要3个点
|
||
|
|
ElMessage.warning('围栏至少需要3个点才能形成有效区域')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
console.log('围栏绘制完成:', coordinates)
|
||
|
|
clearFenceDrawLayer()
|
||
|
|
// 重置绘制状态
|
||
|
|
showDrawFences.value = false
|
||
|
|
}
|
||
|
|
// 监听 markers props 变化
|
||
|
|
watch(
|
||
|
|
() => props.markers,
|
||
|
|
(newMarkers) => {
|
||
|
|
if (newMarkers && newMarkers.length > 0 && isMapInitialized) {
|
||
|
|
updateMarkers(newMarkers, props)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{ deep: true, immediate: false }
|
||
|
|
)
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
setTimeout(() => {
|
||
|
|
initMap()
|
||
|
|
}, 100)
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
<style scoped>
|
||
|
|
.map-container {
|
||
|
|
width: 100%;
|
||
|
|
height: calc(100vh - 120px);
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ol-viewport) {
|
||
|
|
width: 100% !important;
|
||
|
|
height: 100% !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ol-map) {
|
||
|
|
width: 100% !important;
|
||
|
|
height: 100% !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel {
|
||
|
|
position: absolute;
|
||
|
|
top: 100px;
|
||
|
|
right: 20px;
|
||
|
|
background-color: rgba(255, 255, 255, 0.9);
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
|
|
z-index: 1000;
|
||
|
|
width: 300px;
|
||
|
|
max-height: 80vh;
|
||
|
|
overflow-y: auto;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 10px 15px;
|
||
|
|
border-bottom: 1px solid #eee;
|
||
|
|
background-color: #f5f5f5;
|
||
|
|
border-radius: 8px 8px 0 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__title {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: bold;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__close {
|
||
|
|
background-color: #ff4d4f;
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 50%;
|
||
|
|
width: 24px;
|
||
|
|
height: 24px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: bold;
|
||
|
|
transition: background-color 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__close:hover {
|
||
|
|
background-color: #d9363e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__body {
|
||
|
|
padding: 15px;
|
||
|
|
flex-grow: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__empty {
|
||
|
|
text-align: center;
|
||
|
|
color: #888;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__name {
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: bold;
|
||
|
|
margin-bottom: 5px;
|
||
|
|
color: #555;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel__coord {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 顶部面板样式 */
|
||
|
|
.top-panel {
|
||
|
|
position: absolute;
|
||
|
|
top: 12px;
|
||
|
|
left: 12px;
|
||
|
|
right: 12px;
|
||
|
|
z-index: 1000;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 12px;
|
||
|
|
padding: 10px 12px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
height: 100px;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-panel__left {
|
||
|
|
flex: 0 0 260px;
|
||
|
|
background: rgba(255, 255, 255, 0.7);
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||
|
|
border-radius: 10px;
|
||
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
||
|
|
height: 100%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-group {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-type {
|
||
|
|
flex: 0 0 120px;
|
||
|
|
}
|
||
|
|
.search-input {
|
||
|
|
width: 220px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-panel__center {
|
||
|
|
flex: 1 1 auto;
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
|
|
gap: 12px;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data_item {
|
||
|
|
background: rgba(255, 255, 255, 0.85);
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 8px 12px;
|
||
|
|
min-height: 56px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data_item__title {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #909399;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data_item__value {
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #303133;
|
||
|
|
margin-top: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.data_item__unit {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #909399;
|
||
|
|
margin-left: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-panel__right {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
flex: 0 0 auto;
|
||
|
|
background: rgba(255, 255, 255, 0.7);
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||
|
|
padding: 8px 12px;
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
||
|
|
white-space: nowrap;
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-title {
|
||
|
|
color: #606266;
|
||
|
|
}
|
||
|
|
|
||
|
|
.normal-legend,
|
||
|
|
.alarm1-legend,
|
||
|
|
.alarm2-legend {
|
||
|
|
padding: 4px 8px;
|
||
|
|
border-radius: 999px;
|
||
|
|
color: #fff;
|
||
|
|
font-size: 12px;
|
||
|
|
line-height: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.normal-legend {
|
||
|
|
background: #67c23a;
|
||
|
|
}
|
||
|
|
.alarm1-legend {
|
||
|
|
background: #e6a23c;
|
||
|
|
}
|
||
|
|
.alarm2-legend {
|
||
|
|
background: #f56c6c;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 992px) {
|
||
|
|
.top-panel__left {
|
||
|
|
flex: 1 1 100%;
|
||
|
|
}
|
||
|
|
.top-panel__center {
|
||
|
|
flex: 1 1 100%;
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
.top-panel__right {
|
||
|
|
flex: 1 1 100%;
|
||
|
|
justify-content: flex-start;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 600px) {
|
||
|
|
.top-panel__center {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|