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

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

@ -24,7 +24,7 @@
@time-change="setTrajectoryTime" @time-change="setTrajectoryTime"
@time-range-change="setTrajectoryTimeRange" @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="top-panel__left">
<div class="search-group"> <div class="search-group">
<el-input v-model="search" class="search-input" placeholder="请输入关键词" /> <el-input v-model="search" class="search-input" placeholder="请输入关键词" />
@ -106,12 +106,18 @@ const props = withDefaults(defineProps<MapProps>(), {
showTrajectories: true, showTrajectories: true,
showMarkers: true, showMarkers: true,
showFences: 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 showMarkers = ref(props.showMarkers)
const showTrajectories = ref(false) const showTrajectories = ref(false)
const showFences = ref(false)
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 handDetectorStore = useHandDetectorStore()
@ -258,10 +264,17 @@ const handleFenceDrawComplete = (coordinates: [number, number][]) => {
return return
} }
console.log('围栏绘制完成:', coordinates) console.log('围栏绘制完成:', coordinates)
emit('fence-draw-complete', coordinates)
clearFenceDrawLayer() clearFenceDrawLayer()
// //
showDrawFences.value = false showDrawFences.value = false
} }
const refreshFences = () => {
if (isMapInitialized) {
services.fenceService?.setFenceData(props.fences || [])
}
}
// markers props // markers props
watch( watch(
() => props.markers, () => props.markers,
@ -278,11 +291,13 @@ onMounted(() => {
initMap() initMap()
}, 100) }, 100)
}) })
defineExpose({ refreshFences })
</script> </script>
<style scoped> <style scoped>
.map-container { .map-container {
width: 100%; width: 100%;
height: calc(100vh - 120px);
height: 100%;
} }
:deep(.ol-viewport) { :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 { fromLonLat } from 'ol/proj'
import { TrajectoryService } from '../services/trajectory.service' import { TrajectoryService } from '../services/trajectory.service'
import { PopupService } from '../services/popup.service' import { PopupService } from '../services/popup.service'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
interface PopupContentGenerator { interface PopupContentGenerator {
handleTrajectoryPoint: (feature: any) => string handleTrajectoryPoint: (feature: any) => string
handleTrajectoryLine: (feature: any) => string handleTrajectoryLine: (feature: any) => string
@ -54,8 +54,8 @@ export const useMapEvents = () => {
handleFence: (feature: any): string => { handleFence: (feature: any): string => {
const fenceData = feature.get('fenceData') const fenceData = feature.get('fenceData')
const statusText = 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 ` return `
<div style="font-size: 12px; color: #333;"> <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 { Vector as VectorSource } from 'ol/source'
import { Feature } from 'ol' import { Feature } from 'ol'
import { Polygon, Point } from 'ol/geom' 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 { fromLonLat } from 'ol/proj'
import type { FenceData, MarkerData } from '../types/map.types' import type { FenceData, MarkerData } from '../types/map.types'
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
@ -25,7 +26,7 @@ export class FenceService {
fences.forEach((fence) => { fences.forEach((fence) => {
// 创建围栏多边形特征 // 创建围栏多边形特征
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord))
const coordinates = fence.fenceRange?.map((coord) => fromLonLat(coord)) || []
// 确保围栏是闭合的 // 确保围栏是闭合的
if (coordinates.length > 0) { if (coordinates.length > 0) {
@ -71,17 +72,18 @@ export class FenceService {
let strokeWidth = 2 let strokeWidth = 2
// 根据围栏状态设置样式 // 根据围栏状态设置样式
// 状态:0-禁用(绿色),1-启用(橙色),2-告警(红色)
switch (fence.status) { switch (fence.status) {
case 0:
case FENCE_STATUS.DISABLED:
strokeColor = '#67c23a' strokeColor = '#67c23a'
fillColor = 'rgba(103, 194, 58, 0.1)' fillColor = 'rgba(103, 194, 58, 0.1)'
break break
case 1:
case FENCE_STATUS.ENABLED:
strokeColor = '#e6a23c' strokeColor = '#e6a23c'
fillColor = 'rgba(230, 162, 60, 0.15)' fillColor = 'rgba(230, 162, 60, 0.15)'
strokeWidth = 3 strokeWidth = 3
break break
case 2:
case FENCE_STATUS.ALARM: // 告警状态
strokeColor = '#f56c6c' strokeColor = '#f56c6c'
fillColor = 'rgba(245, 108, 108, 0.2)' fillColor = 'rgba(245, 108, 108, 0.2)'
strokeWidth = 4 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({ return new Style({
stroke: new Stroke({ stroke: new Stroke({
@ -200,7 +203,7 @@ export class FenceService {
source.clear() source.clear()
fences.forEach((fence) => { fences.forEach((fence) => {
const coordinates = fence.fenceRange.map((coord) => fromLonLat(coord))
const coordinates = fence.fenceRange?.map((coord) => fromLonLat(coord)) || []
if (coordinates.length > 0) { if (coordinates.length > 0) {
const lastCoord = coordinates[coordinates.length - 1] 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 const fences = fenceId ? this.fenceData.filter((fence) => fence.id === fenceId) : this.fenceData
for (const fence of fences) { for (const fence of fences) {
if (this.pointInPolygon(point, fence.fenceRange)) {
if (this.pointInPolygon(point, fence.fenceRange || [])) {
return true return true
} }
} }
@ -276,8 +279,8 @@ export class FenceService {
const fenceIds: string[] = [] const fenceIds: string[] = []
for (const fence of this.fenceData) { 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) const fence = this.fenceData.find((f) => f.id === fenceId)
if (fence) { if (fence) {
fence.status = status fence.status = status
@ -301,7 +304,7 @@ export class FenceService {
if (source) { if (source) {
const features = source.getFeatures() const features = source.getFeatures()
const fenceFeature = features.find( 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) { if (fenceFeature) {
fenceFeature.setStyle(this.createFenceStyle(fence)) 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' 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 { export interface StatusDictItem {
value: string value: string
@ -60,24 +79,26 @@ export interface MapProps {
showFences?: boolean showFences?: boolean
/** 是否显示绘制围栏 */ /** 是否显示绘制围栏 */
showDrawFences?: boolean showDrawFences?: boolean
/** 是否隐藏顶部面板 */
hideTopPanel?: boolean
} }
// 围栏数据接口 // 围栏数据接口
export interface FenceData { export interface FenceData {
/** 围栏ID */ /** 围栏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> <template>
<OpenLayerMap v-if="inited" :markers="markers" />
<OpenLayerMap class="w-full map-container" v-if="inited" :markers="markers" :fences="fences" />
</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 { HandDetector } from '@/api/gas/handdetector' import { HandDetector } from '@/api/gas/handdetector'
import { MarkerData } from './components/types/map.types' 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 getDataTimer = ref<NodeJS.Timeout | null>(null)
const markers = ref<MarkerData[]>([]) const markers = ref<MarkerData[]>([])
const fences = ref<Fence[]>([])
const inited = ref(false) const inited = ref(false)
const getMarkers = async () => { const getMarkers = async () => {
console.log('getMarkers') console.log('getMarkers')
@ -24,14 +27,38 @@ const getMarkers = async () => {
inited.value = true 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(() => { onMounted(() => {
getMarkers() getMarkers()
getFences()
getDataTimer.value = setInterval(() => { getDataTimer.value = setInterval(() => {
getMarkers() getMarkers()
getFences()
}, 5000) }, 5000)
}) })
onUnmounted(() => { onUnmounted(() => {
clearInterval(getDataTimer.value as NodeJS.Timeout) clearInterval(getDataTimer.value as NodeJS.Timeout)
}) })
</script> </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> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="80%">
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
@ -8,13 +8,24 @@
v-loading="formLoading" v-loading="formLoading"
> >
<el-form-item label="围栏名称" prop="name"> <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>
<el-form-item label="围栏范围" prop="fenceRange"> <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>
<el-form-item label="状态" prop="status"> <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 <el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_STATUS)"
:key="dict.value" :key="dict.value"
@ -25,7 +36,7 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="围栏类型" prop="type"> <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 <el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE)" v-for="dict in getIntDictOptions(DICT_TYPE.HAND_DETECTOR_FENCE_TYPE)"
:key="dict.value" :key="dict.value"
@ -48,6 +59,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { FenceApi, Fence } from '@/api/gas/fence' import { FenceApi, Fence } from '@/api/gas/fence'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import OpenLayerMap from '@/views/HandDevice/Home/components/OpenLayerMap.vue'
/** GAS电子围栏 表单 */ /** GAS电子围栏 表单 */
defineOptions({ name: 'FenceForm' }) defineOptions({ name: 'FenceForm' })
@ -62,7 +74,7 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
name: undefined, name: undefined,
fenceRange: undefined,
fenceRange: undefined as unknown as [number, number][],
status: 1, status: 1,
type: 1, type: 1,
remark: undefined remark: undefined
@ -74,7 +86,7 @@ const formRules = reactive({
type: [{ required: true, message: '围栏类型不能为空', trigger: 'blur' }] type: [{ required: true, message: '围栏类型不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // Ref const formRef = ref() // Ref
const mapRef = ref() // Ref
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
@ -85,7 +97,18 @@ const open = async (type: string, id?: number) => {
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { 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 { } finally {
formLoading.value = false formLoading.value = false
} }
@ -103,10 +126,10 @@ const submitForm = async () => {
try { try {
const data = formData.value as unknown as Fence const data = formData.value as unknown as Fence
if (formType.value === 'create') { if (formType.value === 'create') {
await FenceApi.createFence(data)
await FenceApi.createFence({ ...data, fenceRange: JSON.stringify(data.fenceRange) })
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))
} else { } else {
await FenceApi.updateFence(data)
await FenceApi.updateFence({ ...data, fenceRange: JSON.stringify(data.fenceRange) })
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
} }
dialogVisible.value = false dialogVisible.value = false
@ -122,11 +145,19 @@ const resetForm = () => {
formData.value = { formData.value = {
id: undefined, id: undefined,
name: undefined, name: undefined,
fenceRange: undefined,
fenceRange: undefined as unknown as [number, number][],
status: 1, status: 1,
type: 1, type: 1,
remark: undefined remark: undefined
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
const handleFenceDrawComplete = (coordinates: [number, number][]) => {
formData.value.fenceRange = coordinates
mapRef.value.refreshFences()
}
const refreshFences = () => {
mapRef.value.refreshFences()
}
</script> </script>

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

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

Loading…
Cancel
Save