Compare commits

...

2 Commits

  1. 291
      web/src/components/VirtualCollapsePanel/index.vue
  2. 24
      web/src/views/HandDevice/Home/components/OpenLayerMap.vue
  3. 44
      web/src/views/HandDevice/Home/components/composables/useMapServices.ts
  4. 15
      web/src/views/HandDevice/Home/components/composables/useMapWatchers.ts
  5. 6
      web/src/views/HandDevice/Home/components/services/map.service.ts
  6. 149
      web/src/views/HandDevice/Home/components/services/marker.service.ts
  7. 2
      web/src/views/HandDevice/Home/components/utils/map.utils.ts
  8. 50
      web/src/views/HandDevice/Home/index.vue

291
web/src/components/VirtualCollapsePanel/index.vue

@ -1,229 +1,240 @@
<!-- 虚拟折叠面板 --> <!-- 虚拟折叠面板 -->
<template> <template>
<DynamicScroller
ref="scrollbarRef"
class="scroller"
:items="list"
:min-item-size="props.minItemSize"
:key-field="props.keyField"
v-slot="{ item, active }"
style="height: 100%"
@scroll="onScroll"
>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.__expanded]">
<div class="collapse-header" @click="toggleExpand(item)">
<div class="collapse-header-left">
<slot name="header" :item="item">{{ item[props.nameField] }}</slot>
</div>
<DynamicScroller
ref="scrollbarRef"
class="scroller"
:items="list"
:min-item-size="props.minItemSize"
:key-field="props.keyField"
v-slot="{ item, active }"
style="height: 100%"
@scroll="onScroll"
>
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item.__expanded]">
<div class="collapse-header" @click="toggleExpand(item)">
<div class="collapse-header-left">
<slot name="header" :item="item">{{ item[props.nameField] }}</slot>
</div>
<div class="collapse-header-right" v-if="props.showArrowRight">
<el-icon
class="arrow-right"
:class="{ 'rotate-icon': item.__expanded }"
:size="12"
:color="'#c1c1c1'"
>
<ArrowRight />
</el-icon>
</div>
</div>
<div class="collapse-header-right" v-if="props.showArrowRight">
<el-icon
class="arrow-right"
:class="{ 'rotate-icon': item.__expanded }"
:size="12"
:color="'#c1c1c1'"
>
<ArrowRight />
</el-icon>
</div>
</div>
<Transition name="fade" mode="out-in">
<div class="collapse-content" v-show="item.__expanded">
<slot name="content" :item="item"></slot>
</div>
</Transition>
</DynamicScrollerItem>
</DynamicScroller>
<Transition name="fade" mode="out-in">
<div class="collapse-content" v-show="item.__expanded">
<slot name="content" :item="item"></slot>
</div>
</Transition>
</DynamicScrollerItem>
</DynamicScroller>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onBeforeUnmount, onDeactivated, useTemplateRef } from 'vue'
import { ref, computed, onBeforeUnmount, onDeactivated, useTemplateRef, nextTick } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { ArrowRight } from '@element-plus/icons-vue' import { ArrowRight } from '@element-plus/icons-vue'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller' import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
const emit = defineEmits(['scroll']) const emit = defineEmits(['scroll'])
const props = defineProps({ const props = defineProps({
data: {
type: Array as PropType<Record<string, any>[]>,
default: () => []
},
keyField: {
type: String,
default: 'id'
},
nameField: {
type: String,
default: 'name'
},
/** 每个项的最小高度 */
minItemSize: {
type: Number,
default: 48
},
showArrowRight: {
type: Boolean,
default: true
}
data: {
type: Array as PropType<Record<string, any>[]>,
default: () => []
},
keyField: {
type: String,
default: 'id'
},
nameField: {
type: String,
default: 'name'
},
/** 每个项的最小高度 */
minItemSize: {
type: Number,
default: 48
},
showArrowRight: {
type: Boolean,
default: true
}
}) })
const itemHeight = computed(() => props.minItemSize + 'px') const itemHeight = computed(() => props.minItemSize + 'px')
const activeItem = ref<Record<string, any> | null>(null) const activeItem = ref<Record<string, any> | null>(null)
const list = computed(() => { const list = computed(() => {
console.time('list')
const activeItemId = activeItem.value ? activeItem.value[props.keyField] : null
const newList = props.data.map((listItem: Record<string, any>) => {
return {
...listItem,
__expanded: activeItemId && listItem[props.keyField] === activeItemId ? true : false
}
})
console.timeEnd('list')
return newList
console.time('list')
const activeItemId = activeItem.value ? activeItem.value[props.keyField] : null
const newList = props.data.map((listItem: Record<string, any>) => {
return {
...listItem,
__expanded: activeItemId && listItem[props.keyField] === activeItemId ? true : false
}
})
console.timeEnd('list')
return newList
}) })
const scrollbarRef = useTemplateRef('scrollbarRef') const scrollbarRef = useTemplateRef('scrollbarRef')
const scrollbarScrollTop = ref(0) const scrollbarScrollTop = ref(0)
function onScroll(e) { function onScroll(e) {
scrollbarScrollTop.value = e.target.scrollTop
emit('scroll', e)
// console.log('scroll-end',e);
scrollbarScrollTop.value = e.target.scrollTop
// emit('scroll', e)
} }
/** /**
* 切换折叠面板展开状态 * 切换折叠面板展开状态
* @param item 要切换展开状态的项 * @param item 要切换展开状态的项
*/ */
function toggleExpand(item) { function toggleExpand(item) {
activeItem.value = item.__expanded ? null : item
activeItem.value = item.__expanded ? null : item
} }
/** /**
* 滚动到指定项 * 滚动到指定项
* @param item 要滚动到的项 * @param item 要滚动到的项
*/ */
function scrollToItem(item) { function scrollToItem(item) {
if (!item) return
activeItem.value = item
const findIndex = list.value.findIndex((i) => i[props.keyField] === item[props.keyField])
if (findIndex === -1) {
return
}
const top = props.minItemSize * findIndex
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
if (!item) return
activeItem.value = item
const findIndex = list.value.findIndex((i) => i[props.keyField] === item[props.keyField])
if (findIndex === -1) {
return
}
const top = props.minItemSize * findIndex
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
} }
/** /**
* 滚动到指定索引项 * 滚动到指定索引项
* @param index 要滚动到的索引项 * @param index 要滚动到的索引项
*/ */
function scrollToIndex(index) { function scrollToIndex(index) {
if (index < 0 || index >= list.value.length) return
if (index < 0 || index >= list.value.length) return
activeItem.value = list.value[index]
activeItem.value = list.value[index]
const top = props.minItemSize * index
// const top = props.minItemSize * index
// console.log('top', top)
const top = props.minItemSize * index
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
// //
// cancelAnimationFrame(AnimationId.value as number)
// scrollTo(scrollbarScrollTop.value, top, scrollbarScrollTop.value)
// nextTick(() => {
// // scrollbarRef.value?.scrollToItem(index)
// scrollbarRef.value?.$refs?.scroller?.scrollToPosition(top)
// })
setTimeout(() => {
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(top)
}, 200)
} }
/** /**
* 滚动到指定位置 * 滚动到指定位置
* @param position 要滚动到的位置 * @param position 要滚动到的位置
*/ */
function scrollToPosition(position: number) { function scrollToPosition(position: number) {
if (position < 0) return
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, position, scrollbarScrollTop.value)
if (position < 0) return
//
cancelAnimationFrame(AnimationId.value as number)
scrollTo(scrollbarScrollTop.value, position, scrollbarScrollTop.value)
} }
// //
const AnimationId = ref<number>() const AnimationId = ref<number>()
function scrollTo(from: number, to: number, current: number) { function scrollTo(from: number, to: number, current: number) {
if (scrollbarScrollTop.value === to) {
return
}
const speed = (to - from) / 30
if (scrollbarScrollTop.value === to) {
return
}
const speed = (to - from) / props.minItemSize
if (speed < 0) {
if (current <= to) {
return
}
} else if (speed > 0) {
if (current >= to) {
return
}
if (speed < 0) {
if (current <= to) {
return
}
} else if (speed > 0) {
if (current >= to) {
return
} }
current = current + speed
}
current = current + speed
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
// DynamicScrollerRecycleScrollerRecycleScroller
scrollbarRef.value?.$refs?.scroller?.scrollToPosition(current)
AnimationId.value = requestAnimationFrame(() => {
scrollTo(from, to, current)
})
} }
onDeactivated(() => { onDeactivated(() => {
//
cancelAnimationFrame(AnimationId.value as number)
//
cancelAnimationFrame(AnimationId.value as number)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
//
cancelAnimationFrame(AnimationId.value as number)
//
cancelAnimationFrame(AnimationId.value as number)
}) })
defineExpose({ defineExpose({
toggleExpand,
scrollToItem,
scrollToIndex,
scrollToPosition
toggleExpand,
scrollToItem,
scrollToIndex,
scrollToPosition
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.collapse-header { .collapse-header {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
overflow: hidden;
height: v-bind(itemHeight);
padding: 0 4px;
cursor: pointer;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box;
.collapse-header-left {
font-size: 12px;
color: #303133;
flex: 1;
}
height: v-bind(itemHeight);
padding: 0 4px;
cursor: pointer;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
box-sizing: border-box;
.collapse-header-left {
font-size: 12px;
color: #303133;
flex: 1;
}
} }
.collapse-content { .collapse-content {
transform-origin: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
transform-origin: top;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }
// collapse-content // collapse-content
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: all 0.2s ease;
transition: all 0.1s ease;
} }
.fade-enter-from, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0;
transform: scaleY(0);
opacity: 0;
transform: scaleY(0);
} }
.arrow-right { .arrow-right {
transition: all 0.2s ease;
transition: all 0.2s ease;
} }
.rotate-icon { .rotate-icon {
transform: rotate(90deg);
transform: rotate(90deg);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1) !important;
border-radius: 4px;
background: rgba(0, 0, 0, 0.1) !important;
border-radius: 4px;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px;
background-color: #f5f5f5;
width: 8px;
background-color: #f5f5f5;
} }
</style> </style>

24
web/src/views/HandDevice/Home/components/OpenLayerMap.vue

@ -93,8 +93,7 @@ const {
setTrajectoriesVisible, setTrajectoriesVisible,
setFencesVisible, setFencesVisible,
toggleFenceDrawing, toggleFenceDrawing,
clearFenceDrawLayer,
updateMarkers
clearFenceDrawLayer
} = useMapServices() } = useMapServices()
const { const {
@ -175,22 +174,21 @@ const init = () => {
{ {
isDrawing: () => !!services.fenceDrawService?.isCurrentlyDrawing?.(), isDrawing: () => !!services.fenceDrawService?.isCurrentlyDrawing?.(),
onMarkerClick: (marker: MarkerData) => { onMarkerClick: (marker: MarkerData) => {
console.log('marker clicked', marker)
// console.log('marker clicked', marker)
emit('on-click-marker', marker) emit('on-click-marker', marker)
}, },
onClusterClick: (features: FeatureLike[]) => { onClusterClick: (features: FeatureLike[]) => {
console.log('onClusterClick', features)
// console.log('onClusterClick', features)
const markerCoords = features.map((feature) => feature.get('markerData').coordinates) const markerCoords = features.map((feature) => feature.get('markerData').coordinates)
services.mapService?.fitToMarkers(markerCoords) services.mapService?.fitToMarkers(markerCoords)
}, },
onZoomEnd: (zoom: number) => { onZoomEnd: (zoom: number) => {
console.log('onZoomEnd', zoom) console.log('onZoomEnd', zoom)
// //
// services.markerService?.createMarkerLayerFromProps(props)
services.markerService?.setClusterDistance() services.markerService?.setClusterDistance()
services.markerService?.getSinglePointsInView() services.markerService?.getSinglePointsInView()
// console.log('marker', marker)
} }
} }
) )
@ -252,6 +250,7 @@ const setCenter = (coords: [number, number]) => {
if (isMapInitialized) { if (isMapInitialized) {
services.mapService?.setCenter(coords) services.mapService?.setCenter(coords)
services.mapService?.setZoom(17)
} }
} }
/** /**
@ -290,25 +289,24 @@ const refreshFences = () => {
watch( watch(
() => props.markers, () => props.markers,
(newMarkers, oldMarkers) => { (newMarkers, oldMarkers) => {
updateMarkers(newMarkers, props)
if (newMarkers.length !== oldMarkers.length) {
console.log('markers changed', newMarkers, oldMarkers)
services.markerService?.updateData(newMarkers)
if (oldMarkers == undefined || newMarkers.length !== oldMarkers.length) {
fitToMarkers() fitToMarkers()
} }
}, },
{ deep: true, immediate: false }
{ deep: false, immediate: true }
) )
watch( watch(
() => props.fences, () => props.fences,
() => { () => {
refreshFences() refreshFences()
}, },
{ deep: true, immediate: false }
{ deep: false, immediate: false }
) )
onMounted(() => { onMounted(() => {
setTimeout(() => {
init()
}, 100)
init()
}) })
defineExpose({ refreshFences, fitToMarkers, setCenter, showTrajectory }) defineExpose({ refreshFences, fitToMarkers, setCenter, showTrajectory })

44
web/src/views/HandDevice/Home/components/composables/useMapServices.ts

@ -16,7 +16,7 @@ import { FenceDrawService } from '../services/fence-draw.service'
interface ServiceInstances { interface ServiceInstances {
mapService: MapService | null mapService: MapService | null
markerService: MarkerService | null markerService: MarkerService | null
popupService: PopupService | null popupService: PopupService | null
trajectoryService: TrajectoryService | null trajectoryService: TrajectoryService | null
fenceService: FenceService | null fenceService: FenceService | null
@ -28,7 +28,7 @@ export const useMapServices = () => {
const services: ServiceInstances = { const services: ServiceInstances = {
mapService: null, mapService: null,
markerService: null, markerService: null,
popupService: null, popupService: null,
trajectoryService: null, trajectoryService: null,
fenceService: null, fenceService: null,
@ -48,14 +48,12 @@ export const useMapServices = () => {
// 重新初始化服务,确保markerService有地图实例 // 重新初始化服务,确保markerService有地图实例
services.mapService = mapService services.mapService = mapService
services.markerService = new MarkerService(mapService.map) services.markerService = new MarkerService(mapService.map)
services.popupService = new PopupService() services.popupService = new PopupService()
services.trajectoryService = new TrajectoryService(mapService.map) services.trajectoryService = new TrajectoryService(mapService.map)
services.fenceService = new FenceService(mapService.map) services.fenceService = new FenceService(mapService.map)
services.fenceDrawService = new FenceDrawService(mapService.map) services.fenceDrawService = new FenceDrawService(mapService.map)
// 创建marker图层
services.markerService.createMarkerLayer(props)
// 创建轨迹图层 // 创建轨迹图层
services.trajectoryService.createTrajectoryLayer() services.trajectoryService.createTrajectoryLayer()
// 创建围栏图层 // 创建围栏图层
@ -70,10 +68,8 @@ export const useMapServices = () => {
const setMarkersVisible = (visible: boolean) => { const setMarkersVisible = (visible: boolean) => {
if (visible) { if (visible) {
services.markerService?.show() services.markerService?.show()
} else { } else {
services.markerService?.hide() services.markerService?.hide()
} }
} }
@ -129,35 +125,7 @@ export const useMapServices = () => {
} }
} }
/**
*
*/
const updateMarkers = (markers: any[], currentProps?: MapProps) => {
if (services.markerService) {
const map = services.mapService?.getMap()
if (map) {
// console.log('updateMarkers', markers)
// 更新marker service(这可能会创建新的layer)
console.time('createMarkerLayer');
services.markerService.createMarkerLayer({
...currentProps,
markers
})
console.timeEnd('createMarkerLayer');
}
}
}
/**
*
*/
const refreshMarkerStyles = () => {
if (services.markerService) {
services.markerService.refreshStyles()
}
}
/** /**
* *
*/ */
@ -204,8 +172,8 @@ export const useMapServices = () => {
setFencesVisible, setFencesVisible,
toggleFenceDrawing, toggleFenceDrawing,
clearFenceDrawLayer, clearFenceDrawLayer,
updateMarkers,
refreshMarkerStyles,
destroyServices destroyServices
} }
} }

15
web/src/views/HandDevice/Home/components/composables/useMapWatchers.ts

@ -86,21 +86,6 @@ export const useMapWatchers = (options: WatchOptions) => {
}) })
} }
/**
*
*/
// const setupMarkersDataWatcher = () => {
// return watch(
// markers,
// (newMarkers = []) => {
// // if (newMarkers && newMarkers.length > 0) {
// console.log('Markers data changed, updating markers:', newMarkers.length)
// updateMarkers(newMarkers)
// // }
// },
// { deep: true, immediate: false }
// )
// }
/** /**
* *

6
web/src/views/HandDevice/Home/components/services/map.service.ts

@ -108,7 +108,11 @@ export class MapService {
center = fromLonLat(center) center = fromLonLat(center)
this.map.getView().setCenter(center) this.map.getView().setCenter(center)
} }
// 设置缩放级别
setZoom(zoom: number): void {
if (!this.map) return
this.map.getView().setZoom(zoom)
}
/** /**
* *
*/ */

149
web/src/views/HandDevice/Home/components/services/marker.service.ts

@ -13,14 +13,13 @@ import { AnimationService } from './animation.service'
import type { MarkerData, MapProps } from '../types/map.types' import type { MarkerData, MapProps } from '../types/map.types'
import { createMarkerStyle, getClusterMarkerData, getStatusColor } from '../utils/map.utils' import { createMarkerStyle, getClusterMarkerData, getStatusColor } from '../utils/map.utils'
import { ANIMATION_CONFIG } from '../constants/map.constants'
// 防抖 // 防抖
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
export class MarkerService { export class MarkerService {
private map: Map | null = null private map: Map | null = null
markerLayer: VectorLayer<VectorSource | Cluster> | null = null
// 当前图层模式(single或cluster聚合):避免重复创建图层
private currentLayerMode: 'single' | 'cluster' | '' = ''
markerLayer: VectorLayer<Cluster> | null = null
private vectorSource: VectorSource private vectorSource: VectorSource
private animationService: AnimationService | null = null private animationService: AnimationService | null = null
@ -28,6 +27,7 @@ export class MarkerService {
this.map = map this.map = map
this.vectorSource = new VectorSource() this.vectorSource = new VectorSource()
this.animationService = new AnimationService(map) this.animationService = new AnimationService(map)
this.createMarkerLayer()
} }
show() { show() {
this.markerLayer?.setVisible(true) this.markerLayer?.setVisible(true)
@ -37,27 +37,14 @@ export class MarkerService {
this.markerLayer?.setVisible(false) this.markerLayer?.setVisible(false)
this.animationService?.hide() this.animationService?.hide()
} }
/**
*
*/
createMarkerLayer = debounce((props: MapProps) => {
console.time('updateData')
this.updateData(props)
console.timeEnd('updateData')
console.time('createMarkerLayerFromProps')
this.createMarkerLayerFromProps(props)
console.timeEnd('createMarkerLayerFromProps')
}, 1000)
/** /**
* *
*/ */
updateData(props: MapProps): void {
updateData(markers: MarkerData[]): void {
this.animationService?.clear() this.animationService?.clear()
this.vectorSource.clear() this.vectorSource.clear()
// 添加标记
const markers = props.markers || []
// debugger
const features: Feature<Point>[] = [] const features: Feature<Point>[] = []
markers.forEach((marker) => { markers.forEach((marker) => {
const feature = new Feature({ const feature = new Feature({
@ -72,7 +59,7 @@ export class MarkerService {
this.vectorSource.addFeatures(features) this.vectorSource.addFeatures(features)
this.getSinglePointsInView() this.getSinglePointsInView()
} }
setClusterDistance= debounce(()=> {
setClusterDistance = debounce(() => {
if (!this.map) return if (!this.map) return
const clusterLayer = this.markerLayer const clusterLayer = this.markerLayer
if (!clusterLayer) return if (!clusterLayer) return
@ -80,20 +67,20 @@ export class MarkerService {
if (!clusterSource) return if (!clusterSource) return
const zoom = this.map.getView().getZoom() || 0 const zoom = this.map.getView().getZoom() || 0
let distance = 2 let distance = 2
if (zoom <= 4) {
if (zoom <= 4) {
distance = 100 distance = 100
}else if (zoom <= 6) {
} else if (zoom <= 6) {
distance = 80 distance = 80
} else if (zoom <= 10) { } else if (zoom <= 10) {
distance = 30 distance = 30
} else if (zoom <= 16) { } else if (zoom <= 16) {
distance = 30 distance = 30
}else if (zoom <= 17) {
} else if (zoom <= 17) {
distance = 10 distance = 10
} }
console.log('zoom',zoom,'distance',distance)
console.log('zoom', zoom, 'distance', distance)
clusterSource?.setDistance(distance) clusterSource?.setDistance(distance)
},200)
}, 200)
/** /**
* *
* @returns {Array} * @returns {Array}
@ -107,6 +94,7 @@ export class MarkerService {
// 获取聚合图层的源 // 获取聚合图层的源
const clusterSource = clusterLayer.getSource() const clusterSource = clusterLayer.getSource()
if (!clusterSource) return
// 获取当前视图范围 // 获取当前视图范围
const view = map.getView() const view = map.getView()
@ -117,7 +105,7 @@ export class MarkerService {
// console.log('featuresInView',featuresInView) // console.log('featuresInView',featuresInView)
featuresInView.forEach((clusterFeature) => { featuresInView.forEach((clusterFeature) => {
// 关键:获取聚合要素中包含的所有原始要素 // 关键:获取聚合要素中包含的所有原始要素
const originalFeatures = clusterFeature.get('features')
const originalFeatures = clusterFeature.get('features')
// console.log('originalFeatures',originalFeatures); // console.log('originalFeatures',originalFeatures);
if (originalFeatures && originalFeatures.length === 1) { if (originalFeatures && originalFeatures.length === 1) {
@ -127,89 +115,51 @@ export class MarkerService {
} }
}) })
this.animationService?.addAll(singlePoints || []) this.animationService?.addAll(singlePoints || [])
return singlePoints
},300)
}, 300)
/** /**
* props创建markerLayer * props创建markerLayer
* *
*/ */
// : VectorLayer<VectorSource | Cluster> // : VectorLayer<VectorSource | Cluster>
createMarkerLayerFromProps(props: MapProps) {
// console.log('createMarkerLayerFromProps')
// this.updateData(props)
createMarkerLayer() {
let newLayer: VectorLayer<Cluster> | null = null
// 检查是否应该强制使用单个marker模式
const shouldForceSingleMark = () => {
if (!props.forceSingleMark || !this.map) return false
const currentZoom = this.map.getView().getZoom()
console.log('聚合图层')
return currentZoom && currentZoom >= ANIMATION_CONFIG.clusterThreshold
}
const clusterSource = new Cluster({
source: this.vectorSource,
distance: 20 // 单位是像素
})
let newLayer: VectorLayer<VectorSource | Cluster> | null = null
// 如果启用聚合且不强制使用单个marker模式
if (props.enableCluster && !shouldForceSingleMark()) {
if (this.currentLayerMode === 'cluster') return
this.currentLayerMode = 'cluster'
// this.animationService?.clear()
console.log('聚合图层')
const clusterSource = new Cluster({
source: this.vectorSource,
distance: 20 // 单位是像素
})
newLayer = new VectorLayer({
source: clusterSource,
zIndex: 1,
style: (feature) => {
// 视图变化新视口内所有要素的样式重计算​ (如平移、缩放),所以不能调用this.animationService?.add
const features = feature.get('features')
newLayer = new VectorLayer({
source: clusterSource,
zIndex: 1,
style: (feature) => {
// 视图变化新视口内所有要素的样式重计算​ (如平移、缩放),所以不能调用this.animationService?.add
const features = feature.get('features')
// 确保features存在且不为空
if (!features || features.length === 0) {
return new Style() // 返回空样式,隐藏无效的feature
}
// console.log('聚合元素', features)
if (features.length === 1) {
// 单个marker
const markerData: MarkerData = features[0].get('markerData')
return markerData ? createMarkerStyle(markerData.statusColor || '') : new Style()
} else {
// 聚合marker
const highestStatus = getClusterMarkerData(features)
const color = getStatusColor(highestStatus)
return createMarkerStyle(color, true, features.length)
}
// 确保features存在且不为空
if (!features || features.length === 0) {
return new Style() // 返回空样式,隐藏无效的feature
} }
})
} else {
if (this.currentLayerMode === 'single') return
this.currentLayerMode = 'single'
console.log('基础marker图层')
newLayer = new VectorLayer({
source: this.vectorSource,
zIndex: 1,
renderOrder: (a, b) => {
// 按xxx属性排列
return b.get('markerData').statusPriority - a.get('markerData').statusPriority
}
})
}
// console.log('聚合元素', features)
if (this.markerLayer) {
const isVisible = this.markerLayer?.getVisible() || false
newLayer.setVisible(isVisible) // 新图层保持当前可见状态
this.map?.removeLayer(this.markerLayer)
}
if (features.length === 1) {
// 单个marker
const markerData: MarkerData = features[0].get('markerData')
return markerData ? createMarkerStyle(markerData.statusColor || '') : new Style()
} else {
// 聚合marker
const highestStatus = getClusterMarkerData(features)
const color = getStatusColor(highestStatus)
return createMarkerStyle(color, true, features.length)
}
}
})
this.markerLayer = newLayer this.markerLayer = newLayer
this.map?.addLayer(this.markerLayer) this.map?.addLayer(this.markerLayer)
@ -230,8 +180,7 @@ export class MarkerService {
destroy(): void { destroy(): void {
this.markerLayer = null this.markerLayer = null
this.animationService?.destroy() this.animationService?.destroy()
// this.currentProps = null
this.createMarkerLayer.cancel()
this.map = null this.map = null
} }
} }

2
web/src/views/HandDevice/Home/components/utils/map.utils.ts

@ -158,7 +158,7 @@ export const createMarkerStyle = (
styleCache[key]= new Style({ styleCache[key]= new Style({
image: new Circle({ image: new Circle({
radius: Math.min(20 + clusterSize, 40),
radius: Math.min(20 + clusterSize/4, 40),
fill: new Fill({ fill: new Fill({
color: color + '80' // 添加透明度 color: color + '80' // 添加透明度
}), }),

50
web/src/views/HandDevice/Home/index.vue

@ -30,10 +30,7 @@
<template #header="{ item }"> <template #header="{ item }">
<div class="marker-item"> <div class="marker-item">
<div class="flex-1 text-13px"> {{ item.name }}</div> <div class="flex-1 text-13px"> {{ item.name }}</div>
<div
class="text-12px pr-1"
:style="{ color: item.statusColor }"
>
<div class="text-12px pr-1" :style="{ color: item.statusColor }">
{{ item.statusLabel }} {{ item.statusLabel }}
</div> </div>
</div> </div>
@ -113,7 +110,7 @@ import { MarkerData, FenceData } from './components/types/map.types'
import { useHandDetectorStore } from '@/store/modules/handDetector' import { useHandDetectorStore } from '@/store/modules/handDetector'
import { ElMessage, ElScrollbar } from 'element-plus' import { ElMessage, ElScrollbar } from 'element-plus'
import dayjs, { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { getDistance } from 'ol/sphere' import { getDistance } from 'ol/sphere'
import { shallowRef } from 'vue' import { shallowRef } from 'vue'
const componentsIsActive = ref(false) const componentsIsActive = ref(false)
@ -126,7 +123,7 @@ const fences = ref<FenceData[]>([])
const mapRef = ref<InstanceType<typeof OpenLayerMap>>() const mapRef = ref<InstanceType<typeof OpenLayerMap>>()
const search = ref('') const search = ref('')
const selectStatus = ref(['normal', 'offline', 'fenceStatus_1', 'alarm','batteryStatus_1'])
const selectStatus = ref(['normal', 'offline', 'fenceStatus_1', 'alarm', 'batteryStatus_1'])
watch( watch(
() => search.value, () => search.value,
(newSearch, oldSearch) => { (newSearch, oldSearch) => {
@ -152,33 +149,30 @@ const filterMarkers = computed(() => {
}) })
} }
if (selectStatus.value.length !== 0) { if (selectStatus.value.length !== 0) {
arr = arr.filter((item) => { arr = arr.filter((item) => {
// console.log('selectStatus', selectStatus.value,item.statusStr);
// console.log('selectStatus', selectStatus.value,item.statusStr);
if (!item.statusStr) { if (!item.statusStr) {
return true return true
} }
if (item.statusStr == 'gasStatus_2' || item.statusStr == 'gasStatus_1') { if (item.statusStr == 'gasStatus_2' || item.statusStr == 'gasStatus_1') {
return selectStatus.value.includes('alarm') return selectStatus.value.includes('alarm')
} }
// if (item.statusStr == 'fenceStatus_1') {
// return selectStatus.value.includes('fenceStatus_1')
// }
return selectStatus.value.includes(item.statusStr) return selectStatus.value.includes(item.statusStr)
}) })
} }
// console.log('markers.value', markers.value)
return arr return arr
}) })
const filterMarkers2 = function getFilterMarkers2() { const filterMarkers2 = function getFilterMarkers2() {
var arr: MarkerData[] = [] var arr: MarkerData[] = []
var nowTime=dayjs().format('YYYY-MM-DD HH:mm:ss')
for (let i = 0; i < 10000; i++) {
const lon= 80+Math.random()*20
const lat= 30+Math.random()*20
var nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
for (let i = 0; i < 50000; i++) {
const lon = 100 + Math.random() * 2
const lat = 30 + Math.random() * 2
arr.push({ arr.push({
id: i + 1, id: i + 1,
sn: '867989072728120', sn: '867989072728120',
@ -222,17 +216,14 @@ const filterMarkers2 = function getFilterMarkers2() {
expanded: false expanded: false
}) })
} }
markers.value=arr
markers.value = arr
} }
const getMarkers = async () => { const getMarkers = async () => {
console.log('getMarkers') console.log('getMarkers')
return await getLastDetectorData().then((res: HandDetectorData[]) => { return await getLastDetectorData().then((res: HandDetectorData[]) => {
console.time('getLastDetectorData');
res = res.filter((i) => i.enableStatus === 1) res = res.filter((i) => i.enableStatus === 1)
var res2 = res var res2 = res
.map((i) => { .map((i) => {
// console.log([i.longitude, i.latitude])
let statusStr = getHighestPriorityStatus({ let statusStr = getHighestPriorityStatus({
gasStatus: i.gasStatus, // gasStatus: i.gasStatus, //
batteryStatus: i.batteryStatus, // batteryStatus: i.batteryStatus, //
@ -252,13 +243,13 @@ const getMarkers = async () => {
} }
}) })
.sort((a, b) => a.statusPriority - b.statusPriority) .sort((a, b) => a.statusPriority - b.statusPriority)
console.timeEnd('getLastDetectorData');
markers.value = res2 markers.value = res2
}) })
} }
const getFences = async () => { const getFences = async () => {
return await handDetectorStore.getAllFences().then((res) => { return await handDetectorStore.getAllFences().then((res) => {
// console.log('getFences', res)
let fencesData = res let fencesData = res
.map((i) => { .map((i) => {
return { return {
@ -272,7 +263,6 @@ const getFences = async () => {
} }
// //
function setCenter(item: MarkerData) { function setCenter(item: MarkerData) {
console.log('setCenter', item)
if (item.longitude && item.latitude) { if (item.longitude && item.latitude) {
mapRef.value?.setCenter([item.longitude || 0, item.latitude || 0]) mapRef.value?.setCenter([item.longitude || 0, item.latitude || 0])
} }
@ -396,13 +386,11 @@ async function showTrajectory(item: MarkerData) {
const historicalCurveRef = ref<InstanceType<typeof HistoricalCurve>>() const historicalCurveRef = ref<InstanceType<typeof HistoricalCurve>>()
// 线 // 线
function onClickHistoricalCurve(item: MarkerData) { function onClickHistoricalCurve(item: MarkerData) {
// console.log('onClickHistoricalCurve', item)
historicalCurveRef.value?.openDrawer(toRaw(item)) historicalCurveRef.value?.openDrawer(toRaw(item))
} }
// //
function onClickTrajectory(item: MarkerData) { function onClickTrajectory(item: MarkerData) {
console.log('onClickTrajectory', item)
trajectoryTimeRange.value = [dayjs().subtract(1, 'hour').valueOf(), dayjs().valueOf()] trajectoryTimeRange.value = [dayjs().subtract(1, 'hour').valueOf(), dayjs().valueOf()]
showTrajectory(item) showTrajectory(item)
} }
@ -410,20 +398,18 @@ function onClickTrajectory(item: MarkerData) {
const scrollbarRef = useTemplateRef<InstanceType<typeof VirtualCollapsePanel>>('scrollbarRef') const scrollbarRef = useTemplateRef<InstanceType<typeof VirtualCollapsePanel>>('scrollbarRef')
// //
function onClickMarker(markerItem: MarkerData) { function onClickMarker(markerItem: MarkerData) {
console.log('onClickMarker', markerItem)
var findIndex = filterMarkers.value.findIndex((item) => item.id === markerItem.id) var findIndex = filterMarkers.value.findIndex((item) => item.id === markerItem.id)
if (findIndex === -1) { if (findIndex === -1) {
return return
} }
setTimeout(() => {
scrollbarRef.value?.scrollToIndex(findIndex)
}, 300)
// console.log('findIndex', findIndex, filterMarkers.value[findIndex])
scrollbarRef.value?.scrollToIndex(findIndex)
} }
onMounted(() => { onMounted(() => {
getMarkers() getMarkers()
// filterMarkers2()
// filterMarkers2()
getFences() getFences()
@ -466,7 +452,7 @@ onUnmounted(() => {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
padding: 5px 0 10px 10px; padding: 5px 0 10px 10px;
margin-left: 10px; margin-left: 10px;
.marker-item { .marker-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

Loading…
Cancel
Save