Browse Source

feat(bd): 新增实时定位和围栏闯禁功能

- 新增实时定位页面,用于展示设备实时位置信息
- 新增围栏闯禁页面,用于展示闯禁事件列表和地图标记- 重构围栏页面,优化围栏绘制逻辑
- 更新地图组件,调整默认缩放级别- 新增 UUID 工具类,用于生成唯一标识符
chen.cheng 9 months ago
parent
commit
d3278223eb

+ 6 - 8
bd-location/src/main/java/com/ruoyi/bd/service/engine/EvtFusionEngine.java

@@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.LinkedBlockingQueue;
 
@@ -20,12 +19,8 @@ public abstract class EvtFusionEngine implements IFusionEngine {
     @Resource
     private RedisCache redisCache;
 
-
     private String engineName;
 
-    private static final List<LinkedBlockingQueue<JSONObject>> messageQueueList = new ArrayList<>(5);
-
-
     @Value("${evt-fusion.thread-pool-size:5}")
     private int threadPoolSize;
 
@@ -33,8 +28,9 @@ public abstract class EvtFusionEngine implements IFusionEngine {
     private ThreadPoolTaskExecutor threadPoolTaskExecutor;
 
     public void init() {
+        List<LinkedBlockingQueue<JSONObject>> queue = getQueue();
         for (int i = 0; i < threadPoolSize; i++) {
-            messageQueueList.add(new LinkedBlockingQueue<>());
+            queue.add(new LinkedBlockingQueue<>());
         }
     }
 
@@ -49,17 +45,19 @@ public abstract class EvtFusionEngine implements IFusionEngine {
             return;
         }
         int bucketIndex = computeHashModulo(key, threadPoolSize);
-        LinkedBlockingQueue<JSONObject> messageQueue = messageQueueList.get(bucketIndex);
+        List<LinkedBlockingQueue<JSONObject>> queue = getQueue();
+        LinkedBlockingQueue<JSONObject> messageQueue = queue.get(bucketIndex);
         messageQueue.offer(msg);
     }
 
     public void start() {
+        List<LinkedBlockingQueue<JSONObject>> queue = getQueue();
         for (int i = 0; i < threadPoolSize; i++) {
             int finalI = i;
             threadPoolTaskExecutor.execute(() -> {
                 while (true) {
                     try {
-                        JSONObject msg = messageQueueList.get(finalI).take();
+                        JSONObject msg = queue.get(finalI).take();
                         LocationInfo locationInfoNew = new LocationInfo(msg.getDouble("latitude"), msg.getDouble("longitude"), msg.getLong("srcTimestamp"));
                         locationInfoNew.setMsg(msg);
                         String key = getKey(locationInfoNew);

+ 10 - 0
bd-location/src/main/java/com/ruoyi/bd/service/engine/IFusionEngine.java

@@ -1,5 +1,11 @@
 package com.ruoyi.bd.service.engine;
 
+import com.alibaba.fastjson2.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
 public interface IFusionEngine {
     default Boolean check(LocationInfo older, LocationInfo newer) {
         return false;
@@ -17,4 +23,8 @@ public interface IFusionEngine {
 
     default void newEvtCallback(LocationInfo msg) {
     }
+
+    default List<LinkedBlockingQueue<JSONObject>> getQueue() {
+       return new ArrayList<>();
+    }
 }

+ 19 - 1
bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/FenceBreakInEngine.java

@@ -19,6 +19,8 @@ import com.ruoyi.web.core.config.MqttCfg;
 import net.dreamlu.iot.mqtt.spring.client.MqttClientTemplate;
 import org.apache.commons.lang3.ObjectUtils;
 import org.locationtech.jts.geom.Polygon;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.stereotype.Service;
@@ -26,11 +28,14 @@ import org.springframework.util.CollectionUtils;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 
 @Service
 @ConditionalOnBean(MqttCfg.class)
 public class FenceBreakInEngine extends EvtFusionEngine {
+    private static final Logger logger = LoggerFactory.getLogger(FenceBreakInEngine.class);
 
     @Autowired
     private IBdFenceInfoService fenceInfoService;
@@ -55,6 +60,8 @@ public class FenceBreakInEngine extends EvtFusionEngine {
     @Autowired
     private FenceVioEvtSocketServer fenceVioEvtSocketServer;
 
+    private final List<LinkedBlockingQueue<JSONObject>> messageQueueList = new ArrayList<>(5);
+
     @PostConstruct
     public void init() {
         super.init();
@@ -75,7 +82,15 @@ public class FenceBreakInEngine extends EvtFusionEngine {
         long gap = newer.getSrcTimestamp() - older.getSrcTimestamp();
         // 将gap 转换为秒
         gap = gap / 1000;
-        return gap <= MAX_EVT_TIME_GAP;
+
+        String oldFenceId = older.getMsg().getString("fenceId");
+        String newerFenceId = newer.getMsg().getString("fenceId");
+        return gap <= MAX_EVT_TIME_GAP && oldFenceId.equals(newerFenceId);
+    }
+
+    @Override
+    public List<LinkedBlockingQueue<JSONObject>> getQueue() {
+        return messageQueueList;
     }
 
     @Override
@@ -83,6 +98,7 @@ public class FenceBreakInEngine extends EvtFusionEngine {
         return BDConst.REDIS_KEY.FENCE_BREAK_IN_KEY + msg.getMsg().getString("deviceId");
     }
 
+
     @Override
     public void getBizId(LocationInfo msg) {
         BdFenceVioEvt bdFenceVioEvt = new BdFenceVioEvt();
@@ -118,9 +134,11 @@ public class FenceBreakInEngine extends EvtFusionEngine {
             return;
         }
         Polygon polygon;
+        logger.info("fence info size: {}", msg);
         for (BdFenceInfo fenceInfo : cacheList) {
             polygon = GeoUtils.getPolygon(fenceInfo.getPoly());
             if (GeoUtils.isPointInGeoFence(polygon, msg.getString("longitude"), msg.getString("latitude"))) {
+                logger.info("?>>>>>fence info size: {}", msg);
                 msg.put("fenceId", fenceInfo.getId());
                 msg.put("fenceName", fenceInfo.getDefenceName());
                 client.publish(BDConst.MQTT_TOPIC.EVT_LOCATION_TOPIC, JSON.toJSONBytes(msg));

+ 9 - 1
bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/PointFusionEngine.java

@@ -1,5 +1,6 @@
 package com.ruoyi.bd.service.engine.impl;
 
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.bd.domain.BdDevcTrailUwb;
 import com.ruoyi.bd.service.IBdDevcTrailUwbService;
 import com.ruoyi.bd.service.engine.EvtFusionEngine;
@@ -13,6 +14,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 
 /**
  * The type Point fusion engine.
@@ -45,7 +49,7 @@ public class PointFusionEngine extends EvtFusionEngine {
     @Autowired
     private PointWebSocketServer webSocketServer;
 
-
+    private final List<LinkedBlockingQueue<JSONObject>> messageQueueList = new ArrayList<>(5);
     @PostConstruct
     public void init() {
         super.init();
@@ -66,6 +70,10 @@ public class PointFusionEngine extends EvtFusionEngine {
     }
 
     @Override
+    public List<LinkedBlockingQueue<JSONObject>> getQueue() {
+        return messageQueueList;
+    }
+    @Override
     public void getBizId(LocationInfo msg) {
         long evtTimestamp = msg.getEvtTimestamp();
         BdDevcTrailUwb bdDevcTrailUwb = new BdDevcTrailUwb();

+ 1 - 1
bd-location/src/main/resources/application-druid.yml

@@ -101,4 +101,4 @@ mqtt:
       enabled: false
 bd:
   mqtt:
-    enabled: false
+    enabled: true

+ 1 - 0
bd-location/src/main/resources/mapper/bd/BdFenceVioEvtMapper.xml

@@ -44,6 +44,7 @@
             <if test="evtDesc != null  and evtDesc != ''">and evt_desc = #{evtDesc}</if>
             <if test="evtStatus !=null and evtStatus!=''">and evt_status = #{evtStatus}</if>
         </where>
+        order by id desc
     </select>
 
     <select id="selectBdFenceVioEvtById" parameterType="Long" resultMap="BdFenceVioEvtResult">

+ 2 - 2
ruoyi-ui/.env.development

@@ -7,9 +7,9 @@ ENV = 'development'
 # 若依管理系统/开发环境
 VUE_APP_BASE_API = '/dev-api'
 
-VUE_APP_BASE_URL = 'http://127.0.0.1:18080/tfc'
+VUE_APP_BASE_URL = 'http://127.0.0.1:28080/bd-api'
 
-VUE_APP_WS_URL = 'ws://127.0.0.1:18080/tfc'
+VUE_APP_WS_URL = 'ws://127.0.0.1:28080/bd-api'
 
 VUE_APP_DOMAIN_BASE_URL = 'http://127.0.0.1'
 

+ 1 - 0
ruoyi-ui/package.json

@@ -56,6 +56,7 @@
     "quill": "1.3.7",
     "screenfull": "5.0.2",
     "sortablejs": "1.10.2",
+    "uuidjs": "^5.1.0",
     "vue": "2.6.12",
     "vue-count-to": "1.0.13",
     "vue-cropper": "0.5.5",

+ 1 - 1
ruoyi-ui/src/components/WebsocketMessage/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div></div>
+  <div class="dds"></div>
 </template>
 
 <script>

+ 4 - 2
ruoyi-ui/src/utils/index.js

@@ -1,5 +1,5 @@
 import { parseTime } from './ruoyi'
-
+import { UUID } from 'uuidjs';
 /**
  * 表格时间格式化
  */
@@ -14,7 +14,9 @@ export function formatDate(cellValue) {
   var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
   return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
 }
-
+export const uuid = () => {
+  return UUID.generate();
+};
 /**
  * @param {number} time
  * @param {string} option

+ 14 - 42
ruoyi-ui/src/views/bd/fence/index.vue

@@ -77,6 +77,7 @@
 <script>
 
 import { addFenceInfo, listFenceInfo, updateFenceInfo } from '@/api/bd/fenceInfo';
+import maphandle from '@/views/bd/map/maphandle';
 import Pannel from '@/views/bd/pannel/index.vue';
 // this.drawtool = new BDLayers.Lib.Tools.CBDrawTool('mytool', this.mapView, 'Rectangle', true); // 绘制矩形,参数1:id,参数2:地图,参数3:绘制类型,参数4:是否可拖拽编辑
 // this.drawtool.enable(); // 开始绘制
@@ -88,12 +89,12 @@ import Pannel from '@/views/bd/pannel/index.vue';
 export default {
   name: 'fence',
   components: { Pannel },
+  mixins: [maphandle],
   data() {
     return {
       playItem: {},
       editState: false,
       editPolyInfo: {},
-      layer: null,
       dialogVisible: false,
       editingDrawGeom: null,
       drawState: false,
@@ -115,7 +116,6 @@ export default {
   },
   // 组件卸载前清空图层信息
   beforeDestroy() {
-    window.map.removeLayersById('vl');
     window.map.removeLayersById('drawLayer');
     window.map.removeLayersById('distanceLayer');
     this.drawtool?.disable();
@@ -153,10 +153,6 @@ export default {
   },
   methods: {
     async getFenceList() {
-      if (!this.layer) {
-        this.layer = new BDLayers.Lib.Layer.CBVectorLayer('vl');
-        window.map.addCustomLayers(this.layer);
-      }
       const { rows } = await listFenceInfo({
         pageNum: 1,
         pageSize: 10,
@@ -171,7 +167,7 @@ export default {
           defenceName,
           poly,
         } = item;
-        const polygon = this.drawPoly({
+        const polygon = this.custDrawPoly({
           name: defenceName,
           coordinates: this.polygonToCoordinates(poly),
           bizAttr: {
@@ -179,7 +175,6 @@ export default {
             name: defenceName,
           },
         });
-        this.layer.addGeometry(polygon);
         result.push({
           id,
           name: defenceName,
@@ -195,19 +190,6 @@ export default {
       //   duration: 5000,
       // });
     },
-    polygonToCoordinates(polygon) {
-      // 正则表达式匹配坐标点
-      const regex = /(-?\d+(\.\d+)?\s-?\d+(\.\d+)?)/g;
-      const matches = polygon.match(regex);
-      if (matches && matches.length > 0) {
-        return matches
-            .map(point => {
-              const [x, y] = point.trim().split(" ").map(Number);
-              return [x, y];
-            });;
-      }
-      return [];
-    },
     cancelEdit() {
       this.drawtool.clear();
       this.dialogVisible = false;
@@ -251,7 +233,7 @@ export default {
             },
             bizAttr: this.form,
           });
-          this.layer.addGeometry(polygon);
+          this.polyLayer.addGeometry(polygon);
           this.editingDrawGeom = null;
           this.editState = false;
           this.$refs.form.resetFields();
@@ -288,30 +270,20 @@ export default {
      * @param labelSymbol
      * @returns {BDLayers.Lib.Overlays.Polygon}
      */
-    drawPoly({
+    custDrawPoly({
       name = '多边形',
       coordinates,
       symbol = {},
       bizAttr = {},
       labelSymbol = {},
     }) {
-      const polygon = new BDLayers.Lib.Overlays.Polygon('p1', {
-        coordinates: coordinates,
-        symbol: Object.assign({
-          lineColor: '#34495e',
-          lineWidth: 2,
-          polygonFill: '#1bbc9b',
-          polygonOpacity: 0.4,
-        }, symbol),
-        labelSymbol: Object.assign({
-          labelText: name,
-          labelColor: '#fefefe',
-          labelTextSize: 20,
-        }, labelSymbol),
-        bizAttr: bizAttr,
-      });
-      //多边形的点击事件
-      polygon.on('click', (data) => {
+      return this.drawPoly({
+        name,
+        coordinates,
+        symbol,
+        bizAttr,
+        labelSymbol,
+        polyOnClick: (data) => {
         console.log(data.target.options);
         if (this.editState) {
           return;
@@ -342,8 +314,8 @@ export default {
           this.editingDrawGeom.endEdit();
           this.dialogVisible = true;
         });
-      });
-      return polygon;
+        },
+      })
     },
     delFence(fence) {
       // polygon.geom.startEdit();

+ 1 - 0
ruoyi-ui/src/views/bd/fenceEvt/icon/evt_vio.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1729590083923" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5847" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M421.606029 699.673971a326.492754 326.492754 0 1 1 180.595014 0C574.835014 735.543652 515.606261 822.168116 512 905.275362c-4.526377-83.107246-63.324754-169.820754-90.393971-205.601391z" fill="#FF1744" p-id="5848"></path><path d="M512 949.797101m-44.521739 0a44.521739 44.521739 0 1 0 89.043478 0 44.521739 44.521739 0 1 0-89.043478 0Z" fill="#FFFFFF" p-id="5849"></path><path d="M512 949.797101m-29.681159 0a29.681159 29.681159 0 1 0 59.362318 0 29.681159 29.681159 0 1 0-59.362318 0Z" fill="#FF1744" p-id="5850"></path><path d="M467.478261 504.57971a44.521739 44.521739 0 1 1 44.521739 44.521739 44.521739 44.521739 0 0 1-44.521739-44.521739z m0-74.202898V192.927536h89.043478v237.449276z" fill="#FFFFFF" p-id="5851"></path></svg>

+ 58 - 2
ruoyi-ui/src/views/bd/fenceEvt/index.vue

@@ -28,14 +28,18 @@
 
 <script>
 
+import { listFenceInfo } from '@/api/bd/fenceInfo';
 import { listFenceVioEvt } from '@/api/bd/fenceVioEvt';
 import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import maphandle from '@/views/bd/map/maphandle';
 import Pannel from '@/views/bd/pannel/index.vue';
 import FingerprintJS from '@fingerprintjs/fingerprintjs';
 import dayjs from 'dayjs';
+import evt_vio from './icon/evt_vio.svg';
 
 export default {
   name: 'fenceVioEvt',
+  mixins: [maphandle],
   components: {
     SocketMessage,
     Pannel,
@@ -45,17 +49,21 @@ export default {
       evtList: [],
       fp: null,
       ws: null,
+      markLayer: null,
     };
   },
   // 组件卸载前清空图层信息
   beforeDestroy() {
+    this.markLayer && window.map.removeLayersById('real_time_markerLayer');
   },
   created() {
-
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('real_time_markerLayer', true);
+    window.map.addCustomLayers(this.markLayer,5);
   },
   mounted() {
     this.getFenceVioEvtList();
     this.getFingerprint();
+    this.getFenceList();
   },
   methods: {
     dayjs,
@@ -65,7 +73,54 @@ export default {
         pageSize: 10,
       });
       this.evtList = rows;
-      debugger
+      this.addEvtToMap();
+    },
+    addEvtToMap() {
+      this.evtList.forEach((item, i) => {
+        const {
+          lng,
+          lat,
+        } = item;
+        var marker = new BDLayers.Lib.Overlays.MarkerImg(
+            `marker0${i}`,
+            [lng, lat],
+            {
+              imgurl: evt_vio,
+              iconSize: [45, 45],
+            },
+        );
+        this.markLayer.addMarker(marker);
+      });
+    },
+    async getFenceList() {
+      const { rows } = await listFenceInfo({
+        pageNum: 1,
+        pageSize: 10,
+      });
+      if (!rows || rows.length < 1) {
+        return;
+      }
+      const result = [];
+      rows.forEach(item => {
+        const {
+          id,
+          defenceName,
+          poly,
+        } = item;
+        const polygon = this.drawPoly({
+          name: defenceName,
+          coordinates: this.polygonToCoordinates(poly),
+          bizAttr: {
+            id,
+            name: defenceName,
+          },
+        });
+        result.push({
+          id,
+          name: defenceName,
+          polygon,
+        });
+      });
     },
     async getFingerprint() {
       // 初始化FingerprintJS
@@ -101,6 +156,7 @@ export default {
     },
     onMessage(a) {
       const data = JSON.parse(a.data);
+      console.log('>>>>>>>>>>>>>>>>>>>>>>>>', data);
       this.$notify({
         title: '警告',
         message: `${data.msg.fenceName}发生闯禁事件。`,

+ 35 - 6
ruoyi-ui/src/views/bd/index.vue

@@ -6,7 +6,7 @@
       <template v-for="item in Object.keys(menus)">
         <div
             :key="item"
-            :class="{'menu-item':true,'active': activeMenu === menus[item].name} "
+            :class="{'menu-item':true,'active': activeMenu[menus[item].name]} "
             :data-type="item"
             @click="menuClick"
         >
@@ -16,9 +16,11 @@
       </template>
     </div>
     <div class="right-container" v-if="mapLoaded">
-      <location v-if="activeMenu === menus.location_tail.name" />
-      <fence v-if="activeMenu=== menus.fence.name" />
-      <fence-vio-evt v-if="activeMenu=== menus.evt.name" />
+      <location v-if="activeMenu[menus.location_tail.name]" />
+      <fence v-if="activeMenu[menus.fence.name]" />
+      <fence-vio-evt v-if="activeMenu[menus.evt.name]" />
+      <realtime-location v-if="activeMenu[menus.realtime_location.name]"></realtime-location>
+      <room-location></room-location>
     </div>
   </div>
 </template>
@@ -29,6 +31,8 @@ import Fence from '@/views/bd/fence/index.vue';
 import FenceVioEvt from '@/views/bd/fenceEvt/index.vue';
 import Location from '@/views/bd/location/index.vue';
 import BdMap from '@/views/bd/map/index.vue';
+import RealtimeLocation from '@/views/bd/realtimeLocation/index.vue';
+import RoomLocation from '@/views/bd/roomlocation/index.vue';
 import menu from '@/views/system/menu/index.vue';
 
 const menus = {
@@ -61,6 +65,8 @@ export default {
     },
   },
   components: {
+    RoomLocation,
+    RealtimeLocation,
     FenceVioEvt,
     Fence,
     Location,
@@ -70,7 +76,9 @@ export default {
     return {
       menus,
       mapLoaded: false,
-      activeMenu: 'evt',
+      activeMenu: {
+        [menus.evt.name]: true,
+      },
     };
   },
   created() {
@@ -81,7 +89,28 @@ export default {
     },
     menuClick(e) {
       const type = e.currentTarget.dataset.type;
-      this.activeMenu = type;
+      this.activeMenu[type] = !this.activeMenu[type];
+      const result = {
+        ...this.activeMenu,
+      };
+      const selectedTogether = [this.menus.evt.name, this.menus.realtime_location.name];
+      // 选中以下的菜单,其他菜单均取消选中
+      if (selectedTogether.includes(type) && this.activeMenu[type]) {
+        Object.keys(result).forEach((key) => {
+          if (!selectedTogether.includes(key)) {
+            result[key] = false;
+          }
+        });
+      }
+      if (!selectedTogether.includes(type) && this.activeMenu[type]) {
+        // 如果当前选中的菜单是实时定位,则自动选中围栏闯禁
+        Object.keys(result).forEach((key) => {
+          if (key !== type) {
+            result[key] = false;
+          }
+        });
+      }
+      this.activeMenu = result;
     },
   },
 };

+ 1 - 1
ruoyi-ui/src/views/bd/map/index.vue

@@ -21,7 +21,7 @@ export default {
         mapType: BDLayers.Lib.Constant.BaseLayerType.Blank,
         mapModel: BDLayers.Lib.Constant.BaseLayerModel.Satellite,
         center: [118.8738802982145,32.010241883966096],
-        defaultZoom: 14,
+        defaultZoom: 15,
         showCenter: false,
         baseControl: true,
         pitch: 45,

+ 65 - 0
ruoyi-ui/src/views/bd/map/maphandle.js

@@ -0,0 +1,65 @@
+import { uuid } from '@/utils';
+
+export default {
+  data() {
+    return {
+      polyLayer: null,
+      layerId: 'vl',
+    };
+  },
+  mounted() {
+    this.layerId = uuid();
+    if (!this.polyLayer) {
+      this.polyLayer = new BDLayers.Lib.Layer.CBVectorLayer(
+        `poly_layer${this.layerId}`, { enableAltitude: true });
+      window.map.addCustomLayers(this.polyLayer, 1);
+    }
+  },
+  methods: {
+    drawPoly({
+      name = '多边形',
+      coordinates,
+      symbol = {},
+      bizAttr = {},
+      labelSymbol = {},
+      polyOnClick = () => {},
+    }) {
+      const polygon = new BDLayers.Lib.Overlays.Polygon('p1', {
+        coordinates: coordinates,
+        altitude: 0,
+        symbol: Object.assign({
+          lineColor: '#34495e',
+          lineWidth: 2,
+          polygonFill: '#1bbc9b',
+          polygonOpacity: 0.4,
+        }, symbol),
+        labelSymbol: Object.assign({
+          labelText: name,
+          labelColor: '#fefefe',
+          labelTextSize: 20,
+        }, labelSymbol),
+        bizAttr: bizAttr,
+      });
+      //多边形的点击事件
+      polygon.on('click', polyOnClick);
+      this.polyLayer.addGeometry(polygon);
+      return polygon;
+    },
+    polygonToCoordinates(polygon) {
+      // 正则表达式匹配坐标点
+      const regex = /(-?\d+(\.\d+)?\s-?\d+(\.\d+)?)/g;
+      const matches = polygon.match(regex);
+      if (matches && matches.length > 0) {
+        return matches
+          .map(point => {
+            const [x, y] = point.trim().split(' ').map(Number);
+            return [x, y];
+          });
+      }
+      return [];
+    },
+  },
+  beforeDestroy() {
+    this.polyLayer && window.map.removeLayersById(`poly_layer${this.layerId}`);
+  },
+};

+ 1 - 0
ruoyi-ui/src/views/bd/realtimeLocation/icon/position.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1729589950554" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6654" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M623.5 957.6h-226c-27.7 0-50.3-22.6-50.3-50.3 0-27.7 22.6-50.3 50.3-50.3h226c27.7 0 50.3 22.6 50.3 50.3 0 27.7-22.7 50.3-50.3 50.3z" fill="#00EFEF" p-id="6655"></path><path d="M510.4 812.3c-82.5 0-161.9-61.6-223.7-173.6-47.9-86.9-78.9-194.1-78.9-273.1C207.8 198.8 343.5 63 510.4 63S813 198.8 813 365.6c0 79-31 186.2-78.9 273.1-61.8 112-141.2 173.6-223.7 173.6z m0-634c-103.3 0-187.3 84-187.3 187.3 0 125.9 104.6 331.4 187.3 331.4s187.3-205.5 187.3-331.4c0-103.2-84-187.3-187.3-187.3z" fill="#52A9FF" p-id="6656"></path><path d="M510.4 359.6m-79.2 0a79.2 79.2 0 1 0 158.4 0 79.2 79.2 0 1 0-158.4 0Z" fill="#00EFEF" p-id="6657"></path></svg>

+ 2 - 0
ruoyi-ui/src/views/bd/realtimeLocation/index.scss

@@ -0,0 +1,2 @@
+.fence-location-container {
+}

+ 143 - 0
ruoyi-ui/src/views/bd/realtimeLocation/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:title>
+      实时坐标
+    </template>
+    <template v-slot:action>
+
+    </template>
+    <template v-slot:content>
+      <div class="location-list">
+        <template v-for="device in deviceList">
+          <div :key="`evt_${device.id}`" class="list-item">
+            <span class="over-flow-hidden" style="width: 40%">
+              {{ device.desc }}
+            </span>
+            <span class="over-flow-hidden" style="width: 40%">
+             {{ device.uwbKey }}
+            </span>
+            <span class="over-flow-hidden" style="width: 20%">
+              <i
+                  class="el-icon-location"
+                  title="查看设备定位"
+                  @click="()=>showLocation(device)"
+              />
+            </span>
+          </div>
+        </template>
+        <socket-message
+            v-for="item in selectedDevice"
+            :key="`device_${item.id}`" :onMessage="onMessage"
+            :ws="`/ws/point/${item.deviceId}`"></socket-message>
+      </div>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import item from '@/layout/components/Sidebar/Item.vue';
+import Pannel from '@/views/bd/pannel/index.vue';
+import dayjs from 'dayjs';
+import position from './icon/position.svg';
+
+export default {
+  name: 'realtime-location',
+  computed: {
+    item() {
+      return item;
+    },
+  },
+  components: {
+    SocketMessage,
+    Pannel,
+  },
+  data() {
+    return {
+      deviceList: [
+        {
+          id: 1,
+          desc: 'uwb001',
+          deviceId: 'uwb001',
+        },
+      ],
+      fp: null,
+      ws: null,
+      markLayer: null,
+      locationMarkers: {},
+      selectedDevice: [],
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && window.map.removeLayersById('markerLayer');
+  },
+  created() {
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('markerLayer', true);
+    window.map.addCustomLayers(this.markLayer);
+  },
+  mounted() {
+    this.getFenceVioEvtList();
+  },
+  methods: {
+    dayjs,
+    async getFenceVioEvtList() {
+      // const { rows } = await listFenceVioEvt({
+      //   pageNum: 1,
+      //   pageSize: 10,
+      // });
+      // this.deviceList = rows;
+    },
+    addMarker(data) {
+      const {
+        latitude,
+        longitude,
+        deviceId,
+      } = data;
+      const marker = new BDLayers.Lib.Overlays.MarkerImg(
+          `marker${deviceId}`,
+          [longitude, latitude],
+          {
+            imgurl: position,
+            iconSize: [45, 60],
+            symbol: {
+              'textName': 'm4',
+              'textSize': 14,
+              'markerFile': position,
+              'markerHorizontalAlignment': 'middle', // left, middle(默认), right
+              'markerVerticalAlignment': 'middle', // top, middle, bottom(默认)
+            },
+          },
+      );
+      this.markLayer.addMarker(marker);
+      return marker;
+    },
+    showLocation(device) {
+      const exist = this.selectedDevice.find(item => item.deviceId === device.deviceId);
+      if (exist) {
+        this.selectedDevice = this.selectedDevice.filter(item => item.deviceId !== device.deviceId);
+        return;
+      }
+      this.selectedDevice.push(device);
+      this.selectedDevice = [...this.selectedDevice];
+    },
+    onMessage(a) {
+      debugger
+      const data = JSON.parse(a.data);
+      const {
+        latitude,
+        longitude,
+        deviceId,
+      } = data;
+      console.log('>>>>>>>>>>>>>>>>>>>>>>>>', data);
+      if (!this.locationMarkers[deviceId]) {
+        this.locationMarkers[deviceId] = this.addMarker(data);
+        return;
+      }
+      this.locationMarkers[deviceId].moveMarker([longitude, latitude], 2000, false);
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss" />

+ 1 - 0
ruoyi-ui/src/views/bd/roomlocation/icon/evt_vio.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1729590083923" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5847" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M421.606029 699.673971a326.492754 326.492754 0 1 1 180.595014 0C574.835014 735.543652 515.606261 822.168116 512 905.275362c-4.526377-83.107246-63.324754-169.820754-90.393971-205.601391z" fill="#FF1744" p-id="5848"></path><path d="M512 949.797101m-44.521739 0a44.521739 44.521739 0 1 0 89.043478 0 44.521739 44.521739 0 1 0-89.043478 0Z" fill="#FFFFFF" p-id="5849"></path><path d="M512 949.797101m-29.681159 0a29.681159 29.681159 0 1 0 59.362318 0 29.681159 29.681159 0 1 0-59.362318 0Z" fill="#FF1744" p-id="5850"></path><path d="M467.478261 504.57971a44.521739 44.521739 0 1 1 44.521739 44.521739 44.521739 44.521739 0 0 1-44.521739-44.521739z m0-74.202898V192.927536h89.043478v237.449276z" fill="#FFFFFF" p-id="5851"></path></svg>

+ 2 - 0
ruoyi-ui/src/views/bd/roomlocation/index.scss

@@ -0,0 +1,2 @@
+.fence-location-container {
+}

+ 170 - 0
ruoyi-ui/src/views/bd/roomlocation/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:title>
+      围栏闯禁事件
+    </template>
+    <template v-slot:action>
+
+    </template>
+    <template v-slot:content>
+      <div class="location-list">
+        <template v-for="evt in evtList">
+          <div :key="`evt_${evt.id}`" class="list-item">
+          <span class="over-flow-hidden" style="width: 40%">
+            {{ evt.evtDesc }}
+          </span>
+            <span class="over-flow-hidden" style="width: 40%">
+           {{ dayjs(evt.evtTime).format('YYYY-MM-DD HH:mm') }}
+          </span>
+            <span class="over-flow-hidden" style="width: 20%">
+          </span>
+          </div>
+        </template>
+        <socket-message :onMessage="onMessage" :ws="ws"></socket-message>
+      </div>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import { listFenceInfo } from '@/api/bd/fenceInfo';
+import { listFenceVioEvt } from '@/api/bd/fenceVioEvt';
+import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import maphandle from '@/views/bd/map/maphandle';
+import Pannel from '@/views/bd/pannel/index.vue';
+import FingerprintJS from '@fingerprintjs/fingerprintjs';
+import dayjs from 'dayjs';
+import evt_vio from './icon/evt_vio.svg';
+
+export default {
+  name: 'room-location',
+  mixins: [maphandle],
+  components: {
+    SocketMessage,
+    Pannel,
+  },
+  data() {
+    return {
+      evtList: [],
+      fp: null,
+      ws: null,
+      markLayer: null,
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && window.map.removeLayersById('real_time_markerLayer');
+  },
+  created() {
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('real_time_markerLayer', true);
+    window.map.addCustomLayers(this.markLayer,5);
+  },
+  mounted() {
+    this.getFenceVioEvtList();
+    this.getFingerprint();
+    this.getFenceList();
+  },
+  methods: {
+    dayjs,
+    async getFenceVioEvtList() {
+      const { rows } = await listFenceVioEvt({
+        pageNum: 1,
+        pageSize: 10,
+      });
+      this.evtList = rows;
+      this.addEvtToMap();
+    },
+    addEvtToMap() {
+      this.evtList.forEach((item, i) => {
+        const {
+          lng,
+          lat,
+        } = item;
+        var marker = new BDLayers.Lib.Overlays.MarkerImg(
+            `marker0${i}`,
+            [lng, lat],
+            {
+              imgurl: evt_vio,
+              iconSize: [45, 45],
+            },
+        );
+        this.markLayer.addMarker(marker);
+      });
+    },
+    async getFenceList() {
+      const { rows } = await listFenceInfo({
+        pageNum: 1,
+        pageSize: 10,
+      });
+      if (!rows || rows.length < 1) {
+        return;
+      }
+      const result = [];
+      rows.forEach(item => {
+        const {
+          id,
+          defenceName,
+          poly,
+        } = item;
+        const polygon = this.drawPoly({
+          name: defenceName,
+          coordinates: this.polygonToCoordinates(poly),
+          bizAttr: {
+            id,
+            name: defenceName,
+          },
+        });
+        result.push({
+          id,
+          name: defenceName,
+          polygon,
+        });
+      });
+    },
+    async getFingerprint() {
+      // 初始化FingerprintJS
+      const fp = await FingerprintJS.load();
+      // 获取访问者的指纹
+      const result = await fp.get();
+      // 配置
+      const {
+        osCpu,
+        webGlBasics,
+        languages,
+        audioBaseLatency,
+        reducedTransparency,
+        vendor,
+        vendorFlavors,
+        fonts,
+        fontPreferences,
+        plugins,
+        forcedColors,
+        domBlockers,
+        pdfViewerEnabled,
+        audio,
+        canvas,
+        webGlExtensions,
+        math,
+        ...components
+      } = result.components;
+      const extendedComponents = {
+        ...components,
+      };
+      const fingerprintId = FingerprintJS.hashComponents(extendedComponents);
+      this.ws = `/ws/evt/${fingerprintId}`;
+    },
+    onMessage(a) {
+      const data = JSON.parse(a.data);
+      console.log('>>>>>>>>>>>>>>>>>>>>>>>>', data);
+      this.$notify({
+        title: '警告',
+        message: `${data.msg.fenceName}发生闯禁事件。`,
+        type: 'warning',
+      });
+      this.getFenceVioEvtList();
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss" />