Browse Source

电子围栏

master
whyzxhnd 2 days ago
parent
commit
6acceed209
  1. 4
      web/src/views/HandDevice/Home/components/MapControls.vue
  2. 23
      web/src/views/HandDevice/Home/components/OpenLayerMap.vue
  3. 6
      web/src/views/HandDevice/Home/components/composables/useMapEvents.ts
  4. 27
      web/src/views/HandDevice/Home/components/services/fence.service.ts
  5. 35
      web/src/views/HandDevice/Home/components/types/map.types.ts
  6. 31
      web/src/views/HandDevice/Home/index.vue
  7. 53
      web/src/views/gas/fence/FenceForm.vue
  8. 3
      web/src/views/gas/fence/index.vue

4
web/src/views/HandDevice/Home/components/MapControls.vue

@ -58,6 +58,8 @@ interface Props {
isTrajectoriesActive?: boolean
/** 绘制围栏按钮是否激活 */
isDrawFencesActive?: boolean
/** 是否隐藏顶部面板 */
hideTopPanel?: boolean
}
interface Emits {
@ -75,7 +77,7 @@ withDefaults(defineProps<Props>(), {
isMarkersActive: false,
isFencesActive: false,
isTrajectoriesActive: false,
isDrawFencesActive: false
isDrawFencesActive: false,
})
defineEmits<Emits>()

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

@ -24,7 +24,7 @@
@time-change="setTrajectoryTime"
@time-range-change="setTrajectoryTimeRange"
/>
<div class="top-panel" v-show="!appStore.mobile">
<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="请输入关键词" />
@ -106,12 +106,18 @@ const props = withDefaults(defineProps<MapProps>(), {
showTrajectories: true,
showMarkers: true,
showFences: true,
showDrawFences: true
showDrawFences: true,
hideTopPanel: false
})
const emit = defineEmits<{
(e: 'fence-draw-complete', coordinates: [number, number][]): void
(e: 'refresh-fences'): void
}>()
//
const showMarkers = ref(props.showMarkers)
const showTrajectories = ref(false)
const showFences = ref(false)
const showFences = ref(props.showFences)
const showDrawFences = ref(false)
const mapContainerRef = ref<HTMLElement | null>(null)
const handDetectorStore = useHandDetectorStore()
@ -258,10 +264,17 @@ const handleFenceDrawComplete = (coordinates: [number, number][]) => {
return
}
console.log('围栏绘制完成:', coordinates)
emit('fence-draw-complete', coordinates)
clearFenceDrawLayer()
//
showDrawFences.value = false
}
const refreshFences = () => {
if (isMapInitialized) {
services.fenceService?.setFenceData(props.fences || [])
}
}
// markers props
watch(
() => props.markers,
@ -278,11 +291,13 @@ onMounted(() => {
initMap()
}, 100)
})
defineExpose({ refreshFences })
</script>
<style scoped>
.map-container {
width: 100%;
height: calc(100vh - 120px);
height: 100%;
}
:deep(.ol-viewport) {

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

@ -5,7 +5,7 @@ import dayjs from 'dayjs'
import { fromLonLat } from 'ol/proj'
import { TrajectoryService } from '../services/trajectory.service'
import { PopupService } from '../services/popup.service'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
interface PopupContentGenerator {
handleTrajectoryPoint: (feature: any) => string
handleTrajectoryLine: (feature: any) => string
@ -54,8 +54,8 @@ export const useMapEvents = () => {
handleFence: (feature: any): string => {
const fenceData = feature.get('fenceData')
const statusText =
fenceData.status === 0 ? '正常' : fenceData.status === 1 ? '一级报警' : '二级报警'
const typeText = fenceData.type === 0 ? '包含' : '排斥'
getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS, fenceData.status)
const typeText = getDictLabel(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE, fenceData.type)
return `
<div style="font-size: 12px; color: #333;">

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

@ -5,9 +5,10 @@ import { Vector as VectorLayer } from 'ol/layer'
import { Vector as VectorSource } from 'ol/source'
import { Feature } from 'ol'
import { Polygon, Point } from 'ol/geom'
import { Style, Stroke, Fill, Circle, Text } from 'ol/style'
import { Style, Stroke, Fill, Text } from 'ol/style'
import { fromLonLat } from 'ol/proj'
import type { FenceData, MarkerData } from '../types/map.types'
import { FENCE_STATUS, FENCE_TYPE } from '../types/map.types'
export class FenceService {
private fenceLayer: VectorLayer<VectorSource> | null = null
@ -25,7 +26,7 @@ export class FenceService {
fences.forEach((fence) => {
// 创建围栏多边形特征
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord))
const coordinates = fence.fenceRange?.map((coord) => fromLonLat(coord)) || []
// 确保围栏是闭合的
if (coordinates.length > 0) {
@ -71,17 +72,18 @@ export class FenceService {
let strokeWidth = 2
// 根据围栏状态设置样式
// 状态:0-禁用(绿色),1-启用(橙色),2-告警(红色)
switch (fence.status) {
case 0:
case FENCE_STATUS.DISABLED:
strokeColor = '#67c23a'
fillColor = 'rgba(103, 194, 58, 0.1)'
break
case 1:
case FENCE_STATUS.ENABLED:
strokeColor = '#e6a23c'
fillColor = 'rgba(230, 162, 60, 0.15)'
strokeWidth = 3
break
case 2:
case FENCE_STATUS.ALARM: // 告警状态
strokeColor = '#f56c6c'
fillColor = 'rgba(245, 108, 108, 0.2)'
strokeWidth = 4
@ -89,7 +91,8 @@ export class FenceService {
}
// 根据围栏类型调整样式
const lineDash = fence.type === 1 ? [10, 5] : undefined
// 类型:1-超出(虚线),2-进入(实线)
const lineDash = fence.type === FENCE_TYPE.EXCEED ? [10, 5] : undefined
return new Style({
stroke: new Stroke({
@ -200,7 +203,7 @@ export class FenceService {
source.clear()
fences.forEach((fence) => {
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord))
const coordinates = fence.fenceRange?.map((coord) => fromLonLat(coord)) || []
if (coordinates.length > 0) {
const lastCoord = coordinates[coordinates.length - 1]
@ -243,7 +246,7 @@ export class FenceService {
const fences = fenceId ? this.fenceData.filter((fence) => fence.id === fenceId) : this.fenceData
for (const fence of fences) {
if (this.pointInPolygon(point, fence.fenceRange)) {
if (this.pointInPolygon(point, fence.fenceRange || [])) {
return true
}
}
@ -276,8 +279,8 @@ export class FenceService {
const fenceIds: string[] = []
for (const fence of this.fenceData) {
if (this.pointInPolygon(marker.coordinates, fence.fenceRange)) {
fenceIds.push(fence.id)
if (this.pointInPolygon(marker.coordinates, fence.fenceRange || [])) {
fenceIds.push(fence.id || '')
}
}
@ -290,7 +293,7 @@ export class FenceService {
/**
*
*/
updateFenceStatus(fenceId: string, status: number): void {
updateFenceStatus(fenceId: string | undefined, status: number): void {
const fence = this.fenceData.find((f) => f.id === fenceId)
if (fence) {
fence.status = status
@ -301,7 +304,7 @@ export class FenceService {
if (source) {
const features = source.getFeatures()
const fenceFeature = features.find(
(feature) => feature.get('type') === 'fence' && feature.get('fenceId') === fenceId
(feature) => feature.get('type') === 'fence' && feature.get('fenceId') === fenceId && fenceId
)
if (fenceFeature) {
fenceFeature.setStyle(this.createFenceStyle(fence))

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

@ -2,6 +2,25 @@
*
*/
import { HandDetector } from '@/api/gas/handdetector'
// 围栏状态枚举
export const FENCE_STATUS = {
/** 禁用 */
DISABLED: 1,
/** 启用 */
ENABLED: 2,
/** 告警 */
ALARM: 3
} as const
// 围栏类型枚举
export const FENCE_TYPE = {
/** 超出 */
EXCEED: 1,
/** 进入 */
ENTER: 2
} as const
// 状态字典配置
export interface StatusDictItem {
value: string
@ -60,24 +79,26 @@ export interface MapProps {
showFences?: boolean
/** 是否显示绘制围栏 */
showDrawFences?: boolean
/** 是否隐藏顶部面板 */
hideTopPanel?: boolean
}
// 围栏数据接口
export interface FenceData {
/** 围栏ID */
id: string
id?: string
/** 围栏名称 */
name: string
name?: string
/** 围栏范围 */
fenceRange: [number, number][]
fenceRange?: [number, number][]
/** 围栏状态 */
status: number
status?: number
/** 围栏类型 */
type: number
type?: number
/** 围栏备注 */
remark: string
remark?: string
/** 围栏数据 */
data: any
data?: any
}
// 探测器信息接口

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

@ -1,13 +1,16 @@
<template>
<OpenLayerMap v-if="inited" :markers="markers" />
<OpenLayerMap class="w-full map-container" v-if="inited" :markers="markers" :fences="fences" />
</template>
<script lang="ts" setup>
import OpenLayerMap from './components/OpenLayerMap.vue'
import { getLastestDetectorData } from '@/api/gas'
import { HandDetector } from '@/api/gas/handdetector'
import { MarkerData } from './components/types/map.types'
import { Fence } from '@/api/gas/fence'
import { FenceApi } from '@/api/gas/fence'
const getDataTimer = ref<NodeJS.Timeout | null>(null)
const markers = ref<MarkerData[]>([])
const fences = ref<Fence[]>([])
const inited = ref(false)
const getMarkers = async () => {
console.log('getMarkers')
@ -24,14 +27,38 @@ const getMarkers = async () => {
inited.value = true
})
}
const getFences = async () => {
console.log('getFences')
return await FenceApi.getFencePage({
pageNo: 1,
pageSize: 100
}).then((res) => {
console.log('getFences', res)
let fencesData = res.list as Fence[]
fencesData = fencesData.map((i) => {
return {
...i,
fenceRange: JSON.parse(i.fenceRange)
}
})
fences.value = fencesData as unknown as Fence[]
})
}
onMounted(() => {
getMarkers()
getFences()
getDataTimer.value = setInterval(() => {
getMarkers()
getFences()
}, 5000)
})
onUnmounted(() => {
clearInterval(getDataTimer.value as NodeJS.Timeout)
})
</script>
<style scoped></style>
<style scoped>
.map-container {
width: 100%;
height: calc(100vh - 140px);
}
</style>

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

@ -1,5 +1,5 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="80%">
<el-form
ref="formRef"
:model="formData"
@ -8,13 +8,24 @@
v-loading="formLoading"
>
<el-form-item label="围栏名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入围栏名称" />
<el-input @input="refreshFences" v-model="formData.name" placeholder="请输入围栏名称" />
</el-form-item>
<el-form-item label="围栏范围" prop="fenceRange">
<el-input v-model="formData.fenceRange" placeholder="请输入围栏范围" />
<div class="w-full h-[400px]">
<OpenLayerMap
ref="mapRef"
:show-markers="false"
:show-trajectories="false"
hide-top-panel
:show-fences="true"
:show-draw-fences="true"
@fence-draw-complete="handleFenceDrawComplete"
:fences="[formData]"
/>
</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio-group @change="refreshFences" v-model="formData.status">
<el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS)"
:key="dict.value"
@ -25,7 +36,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="围栏类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-group @change="refreshFences" v-model="formData.type">
<el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE)"
:key="dict.value"
@ -48,6 +59,7 @@
<script setup lang="ts">
import { FenceApi, Fence } from '@/api/gas/fence'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import OpenLayerMap from '@/views/HandDevice/Home/components/OpenLayerMap.vue'
/** GAS电子围栏 表单 */
defineOptions({ name: 'FenceForm' })
@ -62,7 +74,7 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
name: undefined,
fenceRange: undefined,
fenceRange: undefined as unknown as [number, number][],
status: 1,
type: 1,
remark: undefined
@ -74,7 +86,7 @@ const formRules = reactive({
type: [{ required: true, message: '围栏类型不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const mapRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -85,7 +97,18 @@ const open = async (type: string, id?: number) => {
if (id) {
formLoading.value = true
try {
formData.value = await FenceApi.getFence(id)
let data = await FenceApi.getFence(id)
let fenceData
try {
fenceData = JSON.parse(data.fenceRange) as [number, number][]
} catch (error) {
console.error('围栏范围解析失败:', error)
fenceData = undefined as unknown as [number, number][]
}
formData.value = { ...data, fenceRange: fenceData }
nextTick(() => {
mapRef.value.refreshFences()
})
} finally {
formLoading.value = false
}
@ -103,10 +126,10 @@ const submitForm = async () => {
try {
const data = formData.value as unknown as Fence
if (formType.value === 'create') {
await FenceApi.createFence(data)
await FenceApi.createFence({ ...data, fenceRange: JSON.stringify(data.fenceRange) })
message.success(t('common.createSuccess'))
} else {
await FenceApi.updateFence(data)
await FenceApi.updateFence({ ...data, fenceRange: JSON.stringify(data.fenceRange) })
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
@ -122,11 +145,19 @@ const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
fenceRange: undefined,
fenceRange: undefined as unknown as [number, number][],
status: 1,
type: 1,
remark: undefined
}
formRef.value?.resetFields()
}
const handleFenceDrawComplete = (coordinates: [number, number][]) => {
formData.value.fenceRange = coordinates
mapRef.value.refreshFences()
}
const refreshFences = () => {
mapRef.value.refreshFences()
}
</script>

3
web/src/views/gas/fence/index.vue

@ -20,7 +20,7 @@
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -139,7 +139,6 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { FenceApi, Fence } from '@/api/gas/fence'
import FenceForm from './FenceForm.vue'
/** GAS电子围栏 列表 */
defineOptions({ name: 'Fence' })

Loading…
Cancel
Save