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.
 
 
 
 
 
 

388 lines
9.5 KiB

<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="showTrajectoriesStatus"
:is-draw-fences-active="showDrawFences"
@toggle-markers="toggleMarkers"
@toggle-fences="toggleFences"
@toggle-trajectories="toggleTrajectories"
@toggle-draw-fences="toggleDrawFences"
/>
<TrajectoryControls
:show-controls="showTrajectoriesStatus"
:play-state="trajectoryPlayState"
@play="playTrajectory"
@pause="pauseTrajectory"
@stop="stopTrajectory"
@speed-change="setTrajectorySpeed"
@time-change="setTrajectoryTime"
@time-range-change="setTrajectoryTimeRange"
/>
<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 { 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,
forceSingleMark: 13,
enableCluster: MAP_DEFAULTS.enableCluster,
clusterDistance: MAP_DEFAULTS.clusterDistance,
showTrajectories: true,
showMarkers: true,
showFences: true,
showDrawFences: true
})
const emit = defineEmits<{
(e: 'fence-draw-complete', coordinates: [number, number][]): void
(e: 'refresh-fences'): void
}>()
// 响应式状态
const showMarkers = ref(props.showMarkers)
const showTrajectoriesStatus = ref(false)
const showFences = ref(props.showFences)
const showDrawFences = ref(false)
const mapContainerRef = ref<HTMLElement | null>(null)
// 左侧信息面板状态
const panelVisible = ref(false)
const selectedMarker = ref<MarkerData | null>(null)
/**
gasStatus 气体报警状态
batteryStatus 电池报警状态
fenceStatus 电子围栏报警状态
onlineStatus 1在线 0离线
enableStatus 1启用 0未启用
*/
// 使用组合式函数
const {
services,
initMap,
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 (showTrajectoriesStatus.value && trajectoryPlayState.value.isPlaying) {
cleanupTrajectory()
}
showTrajectoriesStatus.value = !showTrajectoriesStatus.value
// console.log(showTrajectoriesStatus.value, props.markers);
if (showTrajectoriesStatus.value) {
setTrajectoriesVisible(showTrajectoriesStatus.value, props.markers)
}
}
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)
}
}
let isMapInitialized = false
/**
* 初始化地图
*/
const init = () => {
if (!mapContainerRef.value) return
try {
// 初始化地图和图层
initMap(mapContainerRef.value, props)
if (!services.mapService?.map) {
throw new Error('Map instance is null')
}
// 设置初始显示状态
setMarkersVisible(showMarkers.value)
setTrajectoriesVisible(showTrajectoriesStatus.value, props.markers)
setFencesVisible(showFences.value)
// 设置事件监听器,打开弹窗,点击设备 marker 时触发
setupMapEventListeners(
services.mapService.map,
services.mapService.popupOverlay,
services.trajectoryService,
services.popupService,
{
isDrawing: () => !!services.fenceDrawService?.isCurrentlyDrawing?.(),
onMarkerClick: (marker: MarkerData) => {
console.log('marker clicked', marker)
selectedMarker.value = marker
panelVisible.value = true
},
onZoomEnd: (zoom: number) => {
// console.log('onZoomEnd', zoom)
services.markerService?.createMarkerLayer(props)
// updateMarkers(props.markers || [],props)
}
// refreshMarkerStyles
}
)
// 设置轨迹监听器
setupTrajectoryWatcher(services.trajectoryService as any, showTrajectoriesStatus)
// 设置状态监听器
const { setupAllWatchers } = useMapWatchers({
showMarkers,
showTrajectoriesStatus:showTrajectoriesStatus,
showFences,
showDrawFences,
setMarkersVisible,
setTrajectoriesVisible,
setFencesVisible,
toggleFenceDrawing,
markers: props.markers || []
})
setupAllWatchers()
// 标记地图已初始化
isMapInitialized = true
console.log('地图初始化成功')
} catch (error) {
console.error('地图初始化失败:', error)
}
}
/**
* 处理围栏绘制完成
*/
const handleFenceDrawComplete = (coordinates: [number, number][]) => {
if (coordinates.length < 3) {
// 围栏至少需要3个点
ElMessage.warning('围栏至少需要3个点才能形成有效区域')
return
}
console.log('围栏绘制完成:', coordinates)
emit('fence-draw-complete', coordinates)
clearFenceDrawLayer()
// 重置绘制状态
showDrawFences.value = false
}
// 缩放到合适范围
const fitToMarkers = () => {
if (isMapInitialized && props.markers && props.markers.length > 0) {
const markerCoords = props.markers.map((marker) => [
marker.longitude || 0,
marker.latitude || 0
])
services.mapService?.fitToMarkers(markerCoords)
}
}
const refreshFences = () => {
if (isMapInitialized) {
services.fenceService?.setFenceData(props.fences || [])
}
}
// 监听 markers props 变化
watch(
() => props.markers,
(newMarkers, oldMarkers) => {
updateMarkers(newMarkers, props)
if (newMarkers.length !== oldMarkers.length) {
fitToMarkers()
}
},
{ deep: true, immediate: false }
)
watch(
() => props.fences,
() => {
refreshFences()
},
{ deep: true, immediate: false }
)
onMounted(() => {
setTimeout(() => {
init()
}, 100)
})
defineExpose({ refreshFences, fitToMarkers })
</script>
<style scoped lang="scss">
.map-container {
width: 100%;
height: 100%;
: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: 313px;
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;
&: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;
}
}
}
</style>