Browse Source

优化地图类型、统计面板调整

master
xh 1 week ago
parent
commit
e533bc9250
  1. 208
      web/src/views/HandDevice/Home/components/OpenLayerMap.vue
  2. 20
      web/src/views/HandDevice/Home/components/composables/useMapEvents.ts
  3. 5
      web/src/views/HandDevice/Home/components/composables/useMapServices.ts
  4. 2
      web/src/views/HandDevice/Home/components/constants/map.constants.ts
  5. 6
      web/src/views/HandDevice/Home/components/services/animation.service.ts
  6. 32
      web/src/views/HandDevice/Home/components/services/fence-draw.service.ts
  7. 6
      web/src/views/HandDevice/Home/components/services/fence.service.ts
  8. 10
      web/src/views/HandDevice/Home/components/services/map.service.ts
  9. 21
      web/src/views/HandDevice/Home/components/services/marker.service.ts
  10. 8
      web/src/views/HandDevice/Home/components/services/popup.service.ts
  11. 5
      web/src/views/HandDevice/Home/components/services/trajectory.service.ts
  12. 13
      web/src/views/HandDevice/Home/components/types/map.types.ts
  13. 16
      web/src/views/HandDevice/Home/components/utils/map.utils.ts
  14. 222
      web/src/views/HandDevice/Home/index.vue
  15. 2
      web/src/views/gas/fence/FenceForm.vue
  16. 37
      web/src/views/gas/handalarm/index.vue

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

@ -24,37 +24,7 @@
@time-change="setTrajectoryTime" @time-change="setTrajectoryTime"
@time-range-change="setTrajectoryTimeRange" @time-range-change="setTrajectoryTimeRange"
/> />
<div class="top-panel" v-show="!appStore.mobile && !props.hideTopPanel">
<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"
>{{ handDetectorCount }}<span class="data_item__unit"></span></div
>
</div>
<div class="data_item">
<div class="data_item__title">在线数量</div>
<div class="data_item__value"
>{{ onlineCount }}<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>
<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 v-if="panelVisible" class="info-panel">
<div class="info-panel__header"> <div class="info-panel__header">
<span class="info-panel__title">设备详情</span> <span class="info-panel__title">设备详情</span>
@ -74,8 +44,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStore } from '@/store/modules/app'
import { useHandDetectorStore } from '@/store/modules/handDetector'
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
// //
@ -101,13 +70,13 @@ const props = withDefaults(defineProps<MapProps>(), {
minZoom: MAP_DEFAULTS.minZoom, minZoom: MAP_DEFAULTS.minZoom,
markers: () => DEFAULT_MARKERS, markers: () => DEFAULT_MARKERS,
fences: () => DEFAULT_FENCES, fences: () => DEFAULT_FENCES,
forceSingleMark: 13,
enableCluster: MAP_DEFAULTS.enableCluster, enableCluster: MAP_DEFAULTS.enableCluster,
clusterDistance: MAP_DEFAULTS.clusterDistance, clusterDistance: MAP_DEFAULTS.clusterDistance,
showTrajectories: true, showTrajectories: true,
showMarkers: true, showMarkers: true,
showFences: true, showFences: true,
showDrawFences: true,
hideTopPanel: false
showDrawFences: true
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@ -120,24 +89,20 @@ const showTrajectories = ref(false)
const showFences = ref(props.showFences) const showFences = ref(props.showFences)
const showDrawFences = ref(false) const showDrawFences = ref(false)
const mapContainerRef = ref<HTMLElement | null>(null) const mapContainerRef = ref<HTMLElement | null>(null)
const handDetectorStore = useHandDetectorStore()
// //
const appStore = useAppStore()
const panelVisible = ref(false) const panelVisible = ref(false)
const selectedMarker = ref<MarkerData | null>(null) const selectedMarker = ref<MarkerData | null>(null)
const search = ref('')
/** /**
* gasStatus 气体报警状态
gasStatus 气体报警状态
batteryStatus 电池报警状态 batteryStatus 电池报警状态
fenceStatus 电子围栏报警状态 fenceStatus 电子围栏报警状态
onlineStatus 1在线 0离线 onlineStatus 1在线 0离线
enableStatus 1启用 0未启用 enableStatus 1启用 0未启用
*/ */
//
const handDetectorCount = computed(() => props.markers.length)
const onlineCount = computed(() => props.markers.filter((item) => item.onlineStatus === 1).length)
// 使 // 使
const { const {
services, services,
@ -196,6 +161,7 @@ const toggleDrawFences = () => {
} }
import { MapService } from './services/map.service' import { MapService } from './services/map.service'
let mapService: MapService | null = null let mapService: MapService | null = null
let isMapInitialized = false let isMapInitialized = false
/** /**
@ -233,6 +199,11 @@ const initMap = () => {
selectedMarker.value = marker selectedMarker.value = marker
panelVisible.value = true panelVisible.value = true
}, },
onZoomEnd: (zoom: number) => {
// console.log('onZoomEnd', zoom)
// services.markerService?.updateMarkers(props.markers)
},
markerLayer: layerRefs.value?.markerLayer, markerLayer: layerRefs.value?.markerLayer,
refreshMarkerStyles refreshMarkerStyles
} }
@ -322,157 +293,6 @@ defineExpose({ refreshFences })
} }
} }
/* 顶部面板样式 */
.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 {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #67c23a;
}
.alarm1-legend {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #e6a23c;
}
.alarm2-legend {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #f56c6c;
}
}
}
@media (max-width: 992px) {
.top-panel {
.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 {
.top-panel__center {
grid-template-columns: 1fr;
}
}
}
.info-panel { .info-panel {
position: absolute; position: absolute;
top: 100px; top: 100px;
@ -548,6 +368,4 @@ defineExpose({ refreshFences })
} }
} }
} }
</style> </style>

20
web/src/views/HandDevice/Home/components/composables/useMapEvents.ts

@ -1,6 +1,8 @@
/** /**
* composable * composable
*/ */
import type { Map } from 'ol'
import type Overlay from 'ol/Overlay'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { fromLonLat } from 'ol/proj' import { fromLonLat } from 'ol/proj'
import { TrajectoryService } from '../services/trajectory.service' import { TrajectoryService } from '../services/trajectory.service'
@ -53,8 +55,7 @@ export const useMapEvents = () => {
handleFence: (feature: any): string => { handleFence: (feature: any): string => {
const fenceData = feature.get('fenceData') const fenceData = feature.get('fenceData')
const statusText =
getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS, fenceData.status)
const statusText = getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS, fenceData.status)
const typeText = getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE, fenceData.type) const typeText = getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE, fenceData.type)
return ` return `
@ -78,13 +79,14 @@ export const useMapEvents = () => {
* *
*/ */
const setupMapEventListeners = ( const setupMapEventListeners = (
map: any,
popupOverlay: any,
map: Map,
popupOverlay: Overlay | null,
trajectoryService: TrajectoryService | null, trajectoryService: TrajectoryService | null,
popupService: PopupService | null, popupService: PopupService | null,
opts?: { opts?: {
isDrawing?: () => boolean isDrawing?: () => boolean
onMarkerClick?: (markerData: any) => void onMarkerClick?: (markerData: any) => void
onZoomEnd?: (zoom: number) => void
markerLayer?: any markerLayer?: any
refreshMarkerStyles?: () => void refreshMarkerStyles?: () => void
} }
@ -98,6 +100,7 @@ export const useMapEvents = () => {
hidePopup(popupOverlay) hidePopup(popupOverlay)
return return
} }
//
const feature = map.forEachFeatureAtPixel(event.pixel, (feature: any) => feature) const feature = map.forEachFeatureAtPixel(event.pixel, (feature: any) => feature)
if (feature) { if (feature) {
@ -123,16 +126,23 @@ export const useMapEvents = () => {
// 地图移动结束事件(包括放缩) // 地图移动结束事件(包括放缩)
const handleMoveEnd = () => { const handleMoveEnd = () => {
// console.log('handleMoveEnd');
// OpenLayers的Cluster会自动重新计算聚合,只需要刷新样式 // OpenLayers的Cluster会自动重新计算聚合,只需要刷新样式
if (opts?.markerLayer) { if (opts?.markerLayer) {
opts.markerLayer.changed() opts.markerLayer.changed()
} }
if (opts?.onZoomEnd) {
const zoom = map.getView().getZoom() || 0
opts.onZoomEnd(zoom)
}
} }
map.on('pointermove', handlePointerMove) map.on('pointermove', handlePointerMove)
map.on('click', handleClick) map.on('click', handleClick)
map.on('moveend', handleMoveEnd) map.on('moveend', handleMoveEnd)
return { return {
destroy: () => { destroy: () => {
map.un('pointermove', handlePointerMove) map.un('pointermove', handlePointerMove)
@ -148,7 +158,7 @@ export const useMapEvents = () => {
const showPopup = ( const showPopup = (
event: any, event: any,
feature: any, feature: any,
popupOverlay: any,
popupOverlay: Overlay | null,
popupGenerator: PopupContentGenerator popupGenerator: PopupContentGenerator
) => { ) => {
if (!popupOverlay) return if (!popupOverlay) return

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

@ -2,7 +2,7 @@
* composable * composable
*/ */
import { ref, onUnmounted, reactive } from 'vue' import { ref, onUnmounted, reactive } from 'vue'
import type { MapProps } from '../types/map.types'
import type { MapProps,MapInstance } from '../types/map.types'
import { MapService } from '../services/map.service' import { MapService } from '../services/map.service'
import { MarkerService } from '../services/marker.service' import { MarkerService } from '../services/marker.service'
import { AnimationService } from '../services/animation.service' import { AnimationService } from '../services/animation.service'
@ -66,7 +66,7 @@ export const useMapServices = () => {
* *
*/ */
const initializeMapAndLayers = ( const initializeMapAndLayers = (
mapInstance: { map: any; popupOverlay: any },
mapInstance: MapInstance,
props: MapProps props: MapProps
) => { ) => {
// 初始化地图 // 初始化地图
@ -205,6 +205,7 @@ export const useMapServices = () => {
// 从地图中移除旧的marker layer // 从地图中移除旧的marker layer
map.removeLayer(layerRefs.value.markerLayer) map.removeLayer(layerRefs.value.markerLayer)
map.removeLayer(layerRefs.value.rippleLayer) map.removeLayer(layerRefs.value.rippleLayer)
console.log('updateMarkers', markers);
// 更新marker service(这会创建新的layer) // 更新marker service(这会创建新的layer)
services.markerService.updateMarkers(markers) services.markerService.updateMarkers(markers)

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

@ -52,7 +52,7 @@ export const MAP_DEFAULTS = {
maxZoom: 18, maxZoom: 18,
minZoom: 0, minZoom: 0,
enableCluster: true, enableCluster: true,
clusterDistance: 0
clusterDistance: 1
} }
// 动画配置 // 动画配置

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

@ -1,6 +1,8 @@
/** /**
* *
*/ */
import type { Map } from 'ol'
import { Vector as VectorLayer } from 'ol/layer' import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource } from 'ol/source' import { Vector as VectorSource } from 'ol/source'
import { Feature } from 'ol' import { Feature } from 'ol'
@ -14,7 +16,7 @@ import { ANIMATION_CONFIG } from '../constants/map.constants'
export class AnimationService { export class AnimationService {
private rippleLayer: VectorLayer<VectorSource> | null = null private rippleLayer: VectorLayer<VectorSource> | null = null
private animationTimer: number | null = null private animationTimer: number | null = null
private map: any = null
private map: Map | null = null
private enableCluster: boolean = true private enableCluster: boolean = true
/** /**
@ -22,7 +24,7 @@ export class AnimationService {
*/ */
createRippleLayer( createRippleLayer(
markers: MarkerData[], markers: MarkerData[],
map: any,
map: Map,
enableCluster: boolean = true enableCluster: boolean = true
): VectorLayer<VectorSource> { ): VectorLayer<VectorSource> {
this.map = map this.map = map

32
web/src/views/HandDevice/Home/components/services/fence-draw.service.ts

@ -1,6 +1,7 @@
/** /**
* *
*/ */
import type { Map } from 'ol'
import { Vector as VectorLayer } from 'ol/layer' import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource } from 'ol/source' import { Vector as VectorSource } from 'ol/source'
import { Draw, Modify, Snap } from 'ol/interaction' import { Draw, Modify, Snap } from 'ol/interaction'
@ -11,7 +12,7 @@ import { toLonLat, fromLonLat } from 'ol/proj'
import type { FenceData } from '../types/map.types' import type { FenceData } from '../types/map.types'
export class FenceDrawService { export class FenceDrawService {
private map: any = null
private map: Map | null = null
private drawLayer: VectorLayer<VectorSource> | null = null private drawLayer: VectorLayer<VectorSource> | null = null
private drawInteraction: Draw | null = null private drawInteraction: Draw | null = null
private modifyInteraction: Modify | null = null private modifyInteraction: Modify | null = null
@ -24,7 +25,7 @@ export class FenceDrawService {
/** /**
* *
*/ */
init(map: any): void {
init(map: Map): void {
this.map = map this.map = map
this.createDrawLayer() this.createDrawLayer()
} }
@ -60,7 +61,7 @@ export class FenceDrawService {
zIndex: 10 // 确保绘制图层在最上层 zIndex: 10 // 确保绘制图层在最上层
}) })
this.map.addLayer(this.drawLayer)
this.map && this.map.addLayer(this.drawLayer)
} }
/** /**
@ -143,13 +144,14 @@ export class FenceDrawService {
source: this.drawLayer!.getSource()! source: this.drawLayer!.getSource()!
}) })
// 添加交互到地图
this.map.addInteraction(this.drawInteraction)
this.map.addInteraction(this.modifyInteraction)
this.map.addInteraction(this.snapInteraction)
// 改变鼠标样式 // 改变鼠标样式
this.map.getViewport().style.cursor = 'crosshair'
if (this.map) {
// 添加交互到地图
this.map.addInteraction(this.drawInteraction)
this.map.addInteraction(this.modifyInteraction)
this.map.addInteraction(this.snapInteraction)
this.map.getViewport().style.cursor = 'crosshair'
}
} }
/** /**
@ -162,20 +164,22 @@ export class FenceDrawService {
// 移除交互 // 移除交互
if (this.drawInteraction) { if (this.drawInteraction) {
this.map.removeInteraction(this.drawInteraction)
this.map && this.map.removeInteraction(this.drawInteraction)
this.drawInteraction = null this.drawInteraction = null
} }
if (this.modifyInteraction) { if (this.modifyInteraction) {
this.map.removeInteraction(this.modifyInteraction)
this.map && this.map.removeInteraction(this.modifyInteraction)
this.modifyInteraction = null this.modifyInteraction = null
} }
if (this.snapInteraction) { if (this.snapInteraction) {
this.map.removeInteraction(this.snapInteraction)
this.map && this.map.removeInteraction(this.snapInteraction)
this.snapInteraction = null this.snapInteraction = null
} }
// 恢复鼠标样式 // 恢复鼠标样式
this.map.getViewport().style.cursor = ''
if (this.map) {
this.map.getViewport().style.cursor = ''
}
// 清空绘制图层 // 清空绘制图层
if (this.drawLayer) { if (this.drawLayer) {
@ -220,7 +224,7 @@ export class FenceDrawService {
showEditFence(fence: FenceData): void { showEditFence(fence: FenceData): void {
this.clearDrawLayer() this.clearDrawLayer()
if (this.drawLayer && fence.fenceRange.length > 0) {
if (this.drawLayer && fence.fenceRange && fence.fenceRange.length > 0) {
// 将围栏坐标转换为地图坐标并创建多边形 // 将围栏坐标转换为地图坐标并创建多边形
const coordinates = fence.fenceRange.map((coord) => [coord[0], coord[1]]) const coordinates = fence.fenceRange.map((coord) => [coord[0], coord[1]])

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

@ -1,6 +1,8 @@
/** /**
* *
*/ */
import type { Map } from 'ol'
import { Vector as VectorLayer } from 'ol/layer' import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource } from 'ol/source' import { Vector as VectorSource } from 'ol/source'
import { Feature } from 'ol' import { Feature } from 'ol'
@ -13,13 +15,13 @@ import { FENCE_STATUS, FENCE_TYPE } from '../types/map.types'
export class FenceService { export class FenceService {
private fenceLayer: VectorLayer<VectorSource> | null = null private fenceLayer: VectorLayer<VectorSource> | null = null
private fenceData: FenceData[] = [] private fenceData: FenceData[] = []
private map: any = null
private map: Map | null = null
private isVisible: boolean = true private isVisible: boolean = true
/** /**
* *
*/ */
createFenceLayer(fences: FenceData[], map: any): VectorLayer<VectorSource> {
createFenceLayer(fences: FenceData[], map: Map): VectorLayer<VectorSource> {
this.map = map this.map = map
this.fenceData = fences this.fenceData = fences
const source = new VectorSource() const source = new VectorSource()

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

@ -48,8 +48,8 @@ export class MapService {
this.popupOverlay = new Overlay({ this.popupOverlay = new Overlay({
element: popupElement, element: popupElement,
positioning: 'bottom-center',
stopEvent: false,
positioning: 'bottom-center',// 弹窗位置相对于标记的位置
stopEvent: false,// 是否阻止事件冒泡
offset: [0, -10] offset: [0, -10]
}) })
@ -67,8 +67,10 @@ export class MapService {
this.map = new Map({ this.map = new Map({
target: container, target: container,
layers: [this.tileLayer],
overlays: [popup],
layers: [this.tileLayer],//图层数组
overlays: [popup],//弹窗数组
// 定义地图的显示范围、投影、缩放级别和旋转角度
view: new View({ view: new View({
center: center, center: center,
zoom: props.zoom, zoom: props.zoom,

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

@ -1,6 +1,7 @@
/** /**
* *
*/ */
import type { Map } from 'ol'
import { Vector as VectorLayer } from 'ol/layer' import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource, Cluster } from 'ol/source' import { Vector as VectorSource, Cluster } from 'ol/source'
import { Feature } from 'ol' import { Feature } from 'ol'
@ -13,19 +14,19 @@ import { createMarkerStyle, getClusterMarkerData } from '../utils/map.utils'
export class MarkerService { export class MarkerService {
private markerLayer: VectorLayer<VectorSource | Cluster> | null = null private markerLayer: VectorLayer<VectorSource | Cluster> | null = null
private currentProps: MapProps | null = null private currentProps: MapProps | null = null
private map: any = null
private map: Map | null = null
/** /**
* *
*/ */
setMap(map: any): void {
setMap(map: Map): void {
this.map = map this.map = map
} }
/** /**
* *
*/ */
createMarkerLayer(props: MapProps, map?: any): VectorLayer<VectorSource | Cluster> {
createMarkerLayer(props: MapProps, map?: Map): VectorLayer<VectorSource | Cluster> {
// 保存地图实例 // 保存地图实例
if (map) { if (map) {
this.map = map this.map = map
@ -50,8 +51,9 @@ export class MarkerService {
const shouldForceSingleMark = () => { const shouldForceSingleMark = () => {
if (!props.forceSingleMark || !this.map) return false if (!props.forceSingleMark || !this.map) return false
const currentZoom = this.map.getView().getZoom() const currentZoom = this.map.getView().getZoom()
return currentZoom >= props.forceSingleMark
return currentZoom && currentZoom >= props.forceSingleMark
} }
console.log('shouldForceSingleMark', shouldForceSingleMark())
// 如果启用聚合且不强制使用单个marker模式 // 如果启用聚合且不强制使用单个marker模式
if (props.enableCluster && !shouldForceSingleMark()) { if (props.enableCluster && !shouldForceSingleMark()) {
@ -86,7 +88,9 @@ export class MarkerService {
source: source source: source
}) })
} }
// this.markerLayer = new VectorLayer({
// source: source
// })
return this.markerLayer return this.markerLayer
} }
@ -102,10 +106,13 @@ export class MarkerService {
*/ */
updateMarkers(markers: MarkerData[]): void { updateMarkers(markers: MarkerData[]): void {
if (!this.currentProps) return if (!this.currentProps) return
console.log('updateMarkers', markers)
// 更新props中的markers // 更新props中的markers
this.currentProps.markers = markers this.currentProps.markers = markers
console.log('updateMarkers', markers)
// 完全重新创建markerLayer // 完全重新创建markerLayer
const newLayer = this.createMarkerLayerFromProps(this.currentProps) const newLayer = this.createMarkerLayerFromProps(this.currentProps)
@ -138,9 +145,9 @@ export class MarkerService {
const shouldForceSingleMark = () => { const shouldForceSingleMark = () => {
if (!props.forceSingleMark || !this.map) return false if (!props.forceSingleMark || !this.map) return false
const currentZoom = this.map.getView().getZoom() const currentZoom = this.map.getView().getZoom()
return currentZoom >= props.forceSingleMark
return currentZoom && currentZoom >= props.forceSingleMark
} }
console.log('createMarkerLayerFromProps shouldForceSingleMark', shouldForceSingleMark())
// 如果启用聚合且不强制使用单个marker模式 // 如果启用聚合且不强制使用单个marker模式
if (props.enableCluster && !shouldForceSingleMark()) { if (props.enableCluster && !shouldForceSingleMark()) {
const clusterSource = new Cluster({ const clusterSource = new Cluster({

8
web/src/views/HandDevice/Home/components/services/popup.service.ts

@ -44,6 +44,14 @@ export class PopupService {
<div style="color: ${getStatusColor(status)};"> <div style="color: ${getStatusColor(status)};">
状态: ${getStatusLabel(status)} 状态: ${getStatusLabel(status)}
</div> </div>
<div>gasStatus: ${markerData.gasStatus}</div>
<div>onlineStatus: ${markerData.onlineStatus}</div>
<div>enableStatus: ${markerData.enableStatus}</div>
<div>fenceStatus: ${markerData.fenceStatus}</div>
<div>batteryStatus: ${markerData.batteryStatus}</div>
<div>${markerData.gasChemical}</div> <div>${markerData.gasChemical}</div>
<div>${markerData.value} ${markerData.unit ? markerData.unit : ''}</div> <div>${markerData.value} ${markerData.unit ? markerData.unit : ''}</div>
<div>${markerData.time ? markerData.time : '-'} </div> <div>${markerData.time ? markerData.time : '-'} </div>

5
web/src/views/HandDevice/Home/components/services/trajectory.service.ts

@ -7,6 +7,7 @@ import { Feature } from 'ol'
import { LineString, Point } from 'ol/geom' import { LineString, Point } from 'ol/geom'
import { Style, Stroke, Circle, Fill, Text, Icon } from 'ol/style' import { Style, Stroke, Circle, Fill, Text, Icon } from 'ol/style'
import { fromLonLat } from 'ol/proj' import { fromLonLat } from 'ol/proj'
import type { Map } from 'ol'
import type { import type {
TrajectoryData, TrajectoryData,
TrajectoryPoint, TrajectoryPoint,
@ -19,7 +20,7 @@ import dayjs from 'dayjs'
export class TrajectoryService { export class TrajectoryService {
private trajectoryLayer: VectorLayer<VectorSource> | null = null private trajectoryLayer: VectorLayer<VectorSource> | null = null
private trajectoryData: TrajectoryData[] = [] private trajectoryData: TrajectoryData[] = []
private map: any = null
private map: Map | null = null
private animationTimer: number | null = null private animationTimer: number | null = null
// 当前移动的 marker 图层 // 当前移动的 marker 图层
@ -31,7 +32,7 @@ export class TrajectoryService {
/** /**
* *
*/ */
createTrajectoryLayer(map: any): VectorLayer<VectorSource> {
createTrajectoryLayer(map: Map): VectorLayer<VectorSource> {
this.map = map this.map = map
const source = new VectorSource() const source = new VectorSource()

13
web/src/views/HandDevice/Home/components/types/map.types.ts

@ -1,3 +1,7 @@
import type { Map } from 'ol'
import type { Tile as TileLayer } from 'ol/layer'
import type { OSM, XYZ } from 'ol/source'
import type Overlay from 'ol/Overlay'
/** /**
* *
*/ */
@ -84,8 +88,7 @@ export interface MapProps {
showFences?: boolean showFences?: boolean
/** 是否显示绘制围栏 */ /** 是否显示绘制围栏 */
showDrawFences?: boolean showDrawFences?: boolean
/** 是否隐藏顶部面板 */
hideTopPanel?: boolean
} }
// 围栏数据接口 // 围栏数据接口
@ -168,10 +171,10 @@ export interface TrajectoryPlayState {
// 地图实例接口 // 地图实例接口
export interface MapInstance { export interface MapInstance {
map: any
tileLayer: any
map: Map
tileLayer: TileLayer<XYZ | OSM>
markerLayer: any markerLayer: any
rippleLayer: any rippleLayer: any
popupOverlay: any
popupOverlay: Overlay|null
trajectoryLayer?: any trajectoryLayer?: any
} }

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

@ -37,13 +37,21 @@ export const getHighestPriorityStatus = (markerData: MarkerData): keyof typeof S
const onlineStatus = String(markerData.onlineStatus) === '0' ? 'offline' : null const onlineStatus = String(markerData.onlineStatus) === '0' ? 'offline' : null
// 收集非正常状态 // 收集非正常状态
if (gasStatus && markerData.gasStatus !== 0)
if (gasStatus && markerData.gasStatus !== 0) {
statuses.push(gasStatus as keyof typeof STATUS_PRIORITY) statuses.push(gasStatus as keyof typeof STATUS_PRIORITY)
if (batteryStatus && markerData.batteryStatus !== 0)
}
if (batteryStatus && markerData.batteryStatus !== 0) {
statuses.push(batteryStatus as keyof typeof STATUS_PRIORITY) statuses.push(batteryStatus as keyof typeof STATUS_PRIORITY)
if (fenceStatus && markerData.fenceStatus !== 0)
}
if (fenceStatus && markerData.fenceStatus !== 0) {
statuses.push(fenceStatus as keyof typeof STATUS_PRIORITY) statuses.push(fenceStatus as keyof typeof STATUS_PRIORITY)
if (onlineStatus) statuses.push(onlineStatus)
}
if (onlineStatus) {
statuses.push(onlineStatus)
}
// 如果没有报警状态,则为正常 // 如果没有报警状态,则为正常
if (statuses.length === 0) return 'normal' if (statuses.length === 0) return 'normal'

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

@ -1,18 +1,68 @@
<template> <template>
<OpenLayerMap class="w-full map-container" v-if="inited" :markers="markers" :fences="fences" />
<OpenLayerMap
class="w-full map-container"
v-if="inited"
:showDrawFences="false"
:showTrajectories="false"
:markers="markers"
:fences="fences"
/>
<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>
<el-checkbox-group v-model="openFences">
<el-checkbox label="显示电子围栏" value="1" />
</el-checkbox-group>
</div> -->
</div>
<div class="top-panel__center">
<div class="data_item">
<span class="data_item__title">手持设备</span>
<span class="data_item__value">{{ handDetectorCount }}</span>
<span class="data_item__unit"></span>
</div>
<div class="data_item">
<span class="data_item__title">在线数量</span>
<span class="data_item__value">{{ onlineCount }}</span>
<span class="data_item__unit"></span>
</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>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import OpenLayerMap from './components/OpenLayerMap.vue' import OpenLayerMap from './components/OpenLayerMap.vue'
import { getLastestDetectorData } from '@/api/gas' import { getLastestDetectorData } from '@/api/gas'
import { HandDetectorData } from '@/api/gas/handdetector' import { HandDetectorData } from '@/api/gas/handdetector'
import { MarkerData,FenceData } from './components/types/map.types'
import { Fence } from '@/api/gas/fence'
import { FenceApi } from '@/api/gas/fence'
import { MarkerData, FenceData } from './components/types/map.types'
import { useAppStore } from '@/store/modules/app'
import { useHandDetectorStore } from '@/store/modules/handDetector'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const appStore = useAppStore()
const handDetectorStore = useHandDetectorStore() // store
const getDataTimer = ref<NodeJS.Timeout | null>(null) const getDataTimer = ref<NodeJS.Timeout | null>(null)
const markers = ref<MarkerData[]>([]) const markers = ref<MarkerData[]>([])
const fences = ref<FenceData[]>([]) const fences = ref<FenceData[]>([])
const inited = ref(false) const inited = ref(false)
const search = ref('')
//
const handDetectorCount = computed(() => markers.value.length)
const onlineCount = computed(() => markers.value.filter((item) => item.onlineStatus === 1).length)
const getMarkers = async () => { const getMarkers = async () => {
console.log('getMarkers') console.log('getMarkers')
return await getLastestDetectorData().then((res: HandDetectorData[]) => { return await getLastestDetectorData().then((res: HandDetectorData[]) => {
@ -37,14 +87,9 @@ const getMarkers = async () => {
}) })
} }
const getFences = async () => { const getFences = async () => {
console.log('getFences')
return await FenceApi.getFencePage({
pageNo: 1,
pageSize: 100
}).then((res) => {
return await handDetectorStore.getAllFences().then((res) => {
console.log('getFences', res) console.log('getFences', res)
let fencesData = res.list as Fence[]
fencesData = fencesData.map((i) => {
let fencesData = res.map((i) => {
return { return {
...i, ...i,
fenceRange: JSON.parse(i.fenceRange) fenceRange: JSON.parse(i.fenceRange)
@ -67,9 +112,162 @@ onUnmounted(() => {
clearInterval(getDataTimer.value as NodeJS.Timeout) clearInterval(getDataTimer.value as NodeJS.Timeout)
}) })
</script> </script>
<style scoped>
<style scoped lang="scss">
.map-container { .map-container {
width: 100%; width: 100%;
height: calc(100vh - 140px); height: calc(100vh - 140px);
} }
/* 顶部面板样式 */
.top-panel {
position: absolute;
top: 12px;
left: 50px;
right: 10px;
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
flex-wrap: wrap;
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;
gap: 8px;
padding: 10px;
.search-group {
display: flex;
align-items: center;
gap: 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.9);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
padding: 12px 12px;
// min-height: 56px;
// display: flex;
// flex-direction: column;
// justify-content: center;
.data_item__title {
font-size: 14px;
color: #909399;
vertical-align: middle;
}
.data_item__value {
display: inline-block;
padding: 0 10px;
font-size: 18px;
font-weight: 600;
color: #3399ff;
vertical-align: middle;
}
.data_item__unit {
font-size: 14px;
color: #909399;
vertical-align: middle;
}
}
}
.top-panel__right {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.06);
padding: 12px 12px;
border-radius: 8px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
white-space: nowrap;
height: 100%;
.legend-title {
color: #606266;
font-size: 14px;
}
.normal-legend {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #67c23a;
}
.alarm1-legend {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #e6a23c;
}
.alarm2-legend {
padding: 4px 8px;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 1;
background: #f56c6c;
}
}
}
@media (max-width: 992px) {
.top-panel {
.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 {
.top-panel__center {
grid-template-columns: 1fr;
}
}
}
</style> </style>

2
web/src/views/gas/fence/FenceForm.vue

@ -16,7 +16,7 @@
ref="mapRef" ref="mapRef"
:show-markers="false" :show-markers="false"
:show-trajectories="false" :show-trajectories="false"
hide-top-panel
:show-fences="true" :show-fences="true"
:show-draw-fences="true" :show-draw-fences="true"
@fence-draw-complete="handleFenceDrawComplete" @fence-draw-complete="handleFenceDrawComplete"

37
web/src/views/gas/handalarm/index.vue

@ -8,9 +8,9 @@
:inline="true" :inline="true"
label-width="120px" label-width="120px"
> >
<el-form-item label="持有人" prop="name">
<el-form-item label="持有人" prop="detectorId">
<el-select <el-select
v-model="queryParams.name"
v-model="queryParams.detectorId"
placeholder="请选择持有人" placeholder="请选择持有人"
clearable clearable
filterable filterable
@ -25,6 +25,24 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="持有人name" prop="name">
<el-select
v-model="queryParams.name"
placeholder="请选择持有人"
clearable
filterable
@keyup.enter="handleQuery"
class="!w-240px"
>
<el-option
v-for="item in handDetectorStore.getHandDetectorList"
:key="item.id"
:label="item.name"
:value="String(item.name)"
/>
</el-select>
</el-form-item>
<el-form-item label="设备编号" prop="sn"> <el-form-item label="设备编号" prop="sn">
<el-input <el-input
v-model="queryParams.sn" v-model="queryParams.sn"
@ -268,6 +286,7 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
detectorId: undefined,
name: undefined, name: undefined,
sn: undefined, sn: undefined,
@ -318,19 +337,7 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id) formRef.value.open(type, id)
} }
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await HandAlarmApi.deleteHandAlarm(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除GAS手持探测器警报 */ /** 批量删除GAS手持探测器警报 */
const handleDeleteBatch = async () => { const handleDeleteBatch = async () => {
try { try {

Loading…
Cancel
Save