瀏覽代碼

+ 视频跟随

chen.cheng 8 月之前
父節點
當前提交
3036675c1e
共有 88 個文件被更改,包括 7251 次插入54 次删除
  1. 11 1
      bd-location/src/main/java/com/ruoyi/bd/domain/LocationInfo.java
  2. 157 0
      bd-location/src/main/java/com/ruoyi/bd/domain/VideoFence.java
  3. 3 0
      bd-location/src/main/java/com/ruoyi/bd/mapper/BdFenceInfoMapper.java
  4. 3 0
      bd-location/src/main/java/com/ruoyi/bd/service/IBdFenceInfoService.java
  5. 96 0
      bd-location/src/main/java/com/ruoyi/bd/service/engine/DataSplitEngine.java
  6. 1 0
      bd-location/src/main/java/com/ruoyi/bd/service/engine/EvtFusionEngine.java
  7. 15 0
      bd-location/src/main/java/com/ruoyi/bd/service/engine/IDataSplitEngine.java
  8. 1 0
      bd-location/src/main/java/com/ruoyi/bd/service/engine/IFusionEngine.java
  9. 1 1
      bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/FenceBreakInEngine.java
  10. 1 1
      bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/PointFusionEngine.java
  11. 1 1
      bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/RoomBreakInEngine.java
  12. 107 0
      bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/VideoTraceEngine.java
  13. 6 0
      bd-location/src/main/java/com/ruoyi/bd/service/impl/BdFenceInfoServiceImpl.java
  14. 104 0
      bd-location/src/main/java/com/ruoyi/bd/socket/UWBVideoTraceSocketServer.java
  15. 4 0
      bd-location/src/main/java/com/ruoyi/common/BDConst.java
  16. 3 1
      bd-location/src/main/java/com/ruoyi/common/enums/FenceType.java
  17. 39 0
      bd-location/src/main/java/com/ruoyi/mqtt/UWBVideoTraceSubscribeListener.java
  18. 2 0
      bd-location/src/main/java/com/ruoyi/web/core/config/forest/UWBService.java
  19. 2 2
      bd-location/src/main/resources/application-druid.yml
  20. 110 0
      bd-location/src/main/resources/application-local.yml
  21. 13 0
      bd-location/src/main/resources/mapper/bd/BdFenceInfoMapper.xml
  22. 8 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/DateTimeUtil.java
  23. 11 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/geo/GeoUtils.java
  24. 22 0
      ruoyi-ui/.env.bd
  25. 20 0
      ruoyi-ui/.env.bdproduction
  26. 15 3
      ruoyi-ui/babel.config.js
  27. 104 0
      ruoyi-ui/deploy.sh
  28. 41 0
      ruoyi-ui/docker/conf/nginx.conf
  29. 14 0
      ruoyi-ui/jsconfig.json
  30. 15 6
      ruoyi-ui/package.json
  31. 2756 0
      ruoyi-ui/public/bdjs/BDLayers.js
  32. 1 0
      ruoyi-ui/public/bdjs/jquery-2.2.4.min.js
  33. 2 0
      ruoyi-ui/public/index.html
  34. 44 0
      ruoyi-ui/src/api/bd/devcTrail.js
  35. 52 0
      ruoyi-ui/src/api/bd/fenceInfo.js
  36. 52 0
      ruoyi-ui/src/api/bd/fenceVioEvt.js
  37. 11 0
      ruoyi-ui/src/assets/icons/svg/bd_evt.svg
  38. 7 0
      ruoyi-ui/src/assets/icons/svg/bd_fence.svg
  39. 4 0
      ruoyi-ui/src/assets/icons/svg/bd_location.svg
  40. 9 0
      ruoyi-ui/src/assets/icons/svg/bd_real_time.svg
  41. 1 0
      ruoyi-ui/src/assets/icons/svg/bd_room_location.svg
  42. 5 0
      ruoyi-ui/src/assets/icons/svg/bd_signal.svg
  43. 5 0
      ruoyi-ui/src/assets/icons/svg/bd_signal_off.svg
  44. 293 0
      ruoyi-ui/src/components/Audio/RecorderHandle.js
  45. 69 0
      ruoyi-ui/src/components/WebsocketMessage/index.vue
  46. 17 0
      ruoyi-ui/src/components/video/index.scss
  47. 121 0
      ruoyi-ui/src/components/video/index.vue
  48. 二進制
      ruoyi-ui/src/components/video/output.mp4
  49. 二進制
      ruoyi-ui/src/components/video/roomlocation.mp4
  50. 54 0
      ruoyi-ui/src/main.js
  51. 1 1
      ruoyi-ui/src/permission.js
  52. 5 0
      ruoyi-ui/src/router/index.js
  53. 22 0
      ruoyi-ui/src/utils/BDConst.js
  54. 56 0
      ruoyi-ui/src/utils/VideoTraceDataManager.js
  55. 4 1
      ruoyi-ui/src/utils/index.js
  56. 76 0
      ruoyi-ui/src/views/bd/VideoWall/icon/icon-recording.vue
  57. 69 0
      ruoyi-ui/src/views/bd/VideoWall/icon/icon-unrecord.vue
  58. 125 0
      ruoyi-ui/src/views/bd/VideoWall/index.scss
  59. 82 0
      ruoyi-ui/src/views/bd/VideoWall/index.vue
  60. 14 0
      ruoyi-ui/src/views/bd/fence/index.scss
  61. 381 0
      ruoyi-ui/src/views/bd/fence/index.vue
  62. 1 0
      ruoyi-ui/src/views/bd/fenceEvt/icon/evt_vio.svg
  63. 2 0
      ruoyi-ui/src/views/bd/fenceEvt/index.scss
  64. 234 0
      ruoyi-ui/src/views/bd/fenceEvt/index.vue
  65. 92 0
      ruoyi-ui/src/views/bd/index.scss
  66. 149 0
      ruoyi-ui/src/views/bd/index.vue
  67. 36 0
      ruoyi-ui/src/views/bd/location/index.scss
  68. 300 0
      ruoyi-ui/src/views/bd/location/index.vue
  69. 106 0
      ruoyi-ui/src/views/bd/map/index.vue
  70. 99 0
      ruoyi-ui/src/views/bd/map/maphandle.js
  71. 2 0
      ruoyi-ui/src/views/bd/mobile/index.scss
  72. 102 0
      ruoyi-ui/src/views/bd/mobile/index.vue
  73. 二進制
      ruoyi-ui/src/views/bd/pannel/img/bg_evt_stat.png
  74. 37 0
      ruoyi-ui/src/views/bd/pannel/img/bg_main_title_bg.svg
  75. 77 0
      ruoyi-ui/src/views/bd/pannel/index.scss
  76. 26 0
      ruoyi-ui/src/views/bd/pannel/index.vue
  77. 二進制
      ruoyi-ui/src/views/bd/realtimeLocation/icon/position.png
  78. 2 0
      ruoyi-ui/src/views/bd/realtimeLocation/index.scss
  79. 218 0
      ruoyi-ui/src/views/bd/realtimeLocation/index.vue
  80. 1 0
      ruoyi-ui/src/views/bd/roomlocation/icon/evt_vio.svg
  81. 二進制
      ruoyi-ui/src/views/bd/roomlocation/icon/room_location.jpg
  82. 2 0
      ruoyi-ui/src/views/bd/roomlocation/index.scss
  83. 135 0
      ruoyi-ui/src/views/bd/roomlocation/index.vue
  84. 228 0
      ruoyi-ui/src/views/bd/roomlocation/roommap/index.vue
  85. 15 0
      ruoyi-ui/src/views/bd/rooms/index.scss
  86. 76 0
      ruoyi-ui/src/views/bd/rooms/index.vue
  87. 1 0
      ruoyi-ui/version
  88. 42 36
      ruoyi-ui/vue.config.js

+ 11 - 1
bd-location/src/main/java/com/ruoyi/bd/service/engine/LocationInfo.java → bd-location/src/main/java/com/ruoyi/bd/domain/LocationInfo.java

@@ -1,4 +1,4 @@
-package com.ruoyi.bd.service.engine;
+package com.ruoyi.bd.domain;
 
 import com.alibaba.fastjson2.JSONObject;
 
@@ -13,6 +13,8 @@ public class LocationInfo {
 
     String bizId;
 
+    String key;
+
     JSONObject msg;
 
     public LocationInfo(double latitude, double longitude, long srcTimestamp) {
@@ -76,4 +78,12 @@ public class LocationInfo {
     public void setBizId(String bizId) {
         this.bizId = bizId;
     }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
 }

+ 157 - 0
bd-location/src/main/java/com/ruoyi/bd/domain/VideoFence.java

@@ -0,0 +1,157 @@
+package com.ruoyi.bd.domain;
+
+import com.ruoyi.common.annotation.Excel;
+import org.locationtech.jts.geom.Polygon;
+
+public class VideoFence {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     *
+     */
+    private Long id;
+
+    /**
+     * 围栏名称
+     */
+    @Excel(name = "围栏名称")
+    private String defenceName;
+
+    /**
+     * 围栏图形坐标
+     */
+    private String poly;
+
+    /**
+     * 中心点
+     */
+    @Excel(name = "中心点")
+    private Double centerLng;
+
+    /**
+     * 中心点
+     */
+    @Excel(name = "中心点")
+    private Double centerLat;
+
+    private Polygon polygon;
+
+    private String fenceType;
+
+    private String locationId;
+
+    private Integer altitude;
+
+
+    private String cameraName;
+
+    private String cameraStream;
+
+    private String liveStream;
+
+    private String channelCode;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getDefenceName() {
+        return defenceName;
+    }
+
+    public void setDefenceName(String defenceName) {
+        this.defenceName = defenceName;
+    }
+
+    public String getPoly() {
+        return poly;
+    }
+
+    public void setPoly(String poly) {
+        this.poly = poly;
+    }
+
+    public Double getCenterLng() {
+        return centerLng;
+    }
+
+    public void setCenterLng(Double centerLng) {
+        this.centerLng = centerLng;
+    }
+
+    public Double getCenterLat() {
+        return centerLat;
+    }
+
+    public void setCenterLat(Double centerLat) {
+        this.centerLat = centerLat;
+    }
+
+    public Polygon getPolygon() {
+        return polygon;
+    }
+
+    public void setPolygon(Polygon polygon) {
+        this.polygon = polygon;
+    }
+
+    public String getFenceType() {
+        return fenceType;
+    }
+
+    public void setFenceType(String fenceType) {
+        this.fenceType = fenceType;
+    }
+
+    public String getLocationId() {
+        return locationId;
+    }
+
+    public void setLocationId(String locationId) {
+        this.locationId = locationId;
+    }
+
+    public Integer getAltitude() {
+        return altitude;
+    }
+
+    public void setAltitude(Integer altitude) {
+        this.altitude = altitude;
+    }
+
+    public String getCameraName() {
+        return cameraName;
+    }
+
+    public void setCameraName(String cameraName) {
+        this.cameraName = cameraName;
+    }
+
+    public String getCameraStream() {
+        return cameraStream;
+    }
+
+    public void setCameraStream(String cameraStream) {
+        this.cameraStream = cameraStream;
+    }
+
+    public String getLiveStream() {
+        return liveStream;
+    }
+
+    public void setLiveStream(String liveStream) {
+        this.liveStream = liveStream;
+    }
+
+    public String getChannelCode() {
+        return channelCode;
+    }
+
+    public void setChannelCode(String channelCode) {
+        this.channelCode = channelCode;
+    }
+}

+ 3 - 0
bd-location/src/main/java/com/ruoyi/bd/mapper/BdFenceInfoMapper.java

@@ -1,6 +1,7 @@
 package com.ruoyi.bd.mapper;
 
 import com.ruoyi.bd.domain.BdFenceInfo;
+import com.ruoyi.bd.domain.VideoFence;
 
 import java.util.List;
 
@@ -59,4 +60,6 @@ public interface BdFenceInfoMapper
      * @return 结果
      */
     public int deleteBdFenceInfoByIds(Long[] ids);
+
+    List<VideoFence> qryVideoFence();
 }

+ 3 - 0
bd-location/src/main/java/com/ruoyi/bd/service/IBdFenceInfoService.java

@@ -1,6 +1,7 @@
 package com.ruoyi.bd.service;
 
 import com.ruoyi.bd.domain.BdFenceInfo;
+import com.ruoyi.bd.domain.VideoFence;
 
 import java.util.List;
 
@@ -59,4 +60,6 @@ public interface IBdFenceInfoService
      * @return 结果
      */
     public int deleteBdFenceInfoById(Long id);
+
+    List<VideoFence> qryVideoFence();
 }

+ 96 - 0
bd-location/src/main/java/com/ruoyi/bd/service/engine/DataSplitEngine.java

@@ -0,0 +1,96 @@
+package com.ruoyi.bd.service.engine;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.bd.domain.LocationInfo;
+import com.ruoyi.common.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public abstract class DataSplitEngine implements IDataSplitEngine {
+    private final static Logger logger = LoggerFactory.getLogger(DataSplitEngine.class);
+
+    private String engineName;
+
+    @Value("${evt-fusion.thread-pool-size:2}")
+    private int threadPoolSize;
+
+    @Resource(name = "threadPoolTaskExecutor")
+    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
+
+    public void init() {
+        List<LinkedBlockingQueue<LocationInfo>> queue = getQueue();
+        for (int i = 0; i < threadPoolSize; i++) {
+            queue.add(new LinkedBlockingQueue<>());
+        }
+    }
+
+
+    public void setEngineName(String name) {
+        this.engineName = name;
+    }
+
+    public void push(JSONObject msg) {
+        LocationInfo locationInfoNew = serialized(msg);
+        locationInfoNew.setMsg(msg);
+        String key = locationInfoNew.getKey();
+        if (StringUtils.isEmpty(key)) {
+            return;
+        }
+        int bucketIndex = computeHashModulo(key, threadPoolSize);
+        List<LinkedBlockingQueue<LocationInfo>> queue = getQueue();
+        LinkedBlockingQueue<LocationInfo> messageQueue = queue.get(bucketIndex);
+        messageQueue.offer(locationInfoNew);
+    }
+
+    public void start() {
+        List<LinkedBlockingQueue<LocationInfo>> queue = getQueue();
+        for (int i = 0; i < threadPoolSize; i++) {
+            int finalI = i;
+            threadPoolTaskExecutor.execute(() -> {
+                while (true) {
+                    try {
+                        LocationInfo msg = queue.get(finalI).take();
+                        afterProcess(msg);
+                    } catch (InterruptedException e) {
+                        logger.error("{} error", this.engineName, e);
+                        // 重置中断状态
+                        Thread.currentThread().interrupt();
+                        // 根据业务逻辑决定是否继续执行
+                        if (Thread.currentThread().isInterrupted()) {
+                            logger.error("任务已中断,不再继续执行");
+                            break;
+                        }
+                    } catch (Exception e) {
+                        logger.error("{} error", this.engineName, e);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * 计算字符串的哈希值并对某个数取余数。
+     *
+     * @param str    字符串
+     * @param modulo 取余数的基数
+     * @return 字符串哈希值对 modulo 取余的结果
+     */
+    private int computeHashModulo(String str, int modulo) {
+        long hash = 0;
+        long base = 31;
+        long baseMod = base % modulo;  // 预计算乘法因子
+
+        for (char c : str.toCharArray()) {
+            hash = (hash * baseMod + c) % modulo;  // 避免整数溢出
+        }
+
+        return (int) (hash % modulo);  // 最终结果转换为 int
+    }
+
+}

+ 1 - 0
bd-location/src/main/java/com/ruoyi/bd/service/engine/EvtFusionEngine.java

@@ -1,6 +1,7 @@
 package com.ruoyi.bd.service.engine;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.bd.domain.LocationInfo;
 import com.ruoyi.common.core.redis.RedisCache;
 import com.ruoyi.common.utils.DateTimeUtil;
 import com.ruoyi.common.utils.StringUtils;

+ 15 - 0
bd-location/src/main/java/com/ruoyi/bd/service/engine/IDataSplitEngine.java

@@ -0,0 +1,15 @@
+package com.ruoyi.bd.service.engine;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.bd.domain.LocationInfo;
+
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public interface IDataSplitEngine {
+    List<LinkedBlockingQueue<LocationInfo>> getQueue();
+
+    LocationInfo serialized(JSONObject jsonObject);
+
+    void afterProcess(LocationInfo locationInfo);
+}

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

@@ -1,6 +1,7 @@
 package com.ruoyi.bd.service.engine;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.bd.domain.LocationInfo;
 
 import java.util.ArrayList;
 import java.util.List;

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

@@ -7,7 +7,7 @@ import com.ruoyi.bd.domain.BdFenceVioEvt;
 import com.ruoyi.bd.service.IBdFenceInfoService;
 import com.ruoyi.bd.service.IBdFenceVioEvtService;
 import com.ruoyi.bd.service.engine.EvtFusionEngine;
-import com.ruoyi.bd.service.engine.LocationInfo;
+import com.ruoyi.bd.domain.LocationInfo;
 import com.ruoyi.bd.socket.FenceVioEvtSocketServer;
 import com.ruoyi.common.BDConst;
 import com.ruoyi.common.core.redis.RedisCache;

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

@@ -4,7 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.bd.domain.BdDevcTrailUwb;
 import com.ruoyi.bd.service.IBdDevcTrailUwbService;
 import com.ruoyi.bd.service.engine.EvtFusionEngine;
-import com.ruoyi.bd.service.engine.LocationInfo;
+import com.ruoyi.bd.domain.LocationInfo;
 import com.ruoyi.bd.socket.PointWebSocketServer;
 import com.ruoyi.common.BDConst;
 import com.ruoyi.common.utils.DateTimeUtil;

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

@@ -7,7 +7,7 @@ import com.ruoyi.bd.domain.BdFenceVioEvt;
 import com.ruoyi.bd.service.IBdFenceInfoService;
 import com.ruoyi.bd.service.IBdFenceVioEvtService;
 import com.ruoyi.bd.service.engine.EvtFusionEngine;
-import com.ruoyi.bd.service.engine.LocationInfo;
+import com.ruoyi.bd.domain.LocationInfo;
 import com.ruoyi.bd.socket.FenceVioEvtSocketServer;
 import com.ruoyi.common.BDConst;
 import com.ruoyi.common.core.redis.RedisCache;

+ 107 - 0
bd-location/src/main/java/com/ruoyi/bd/service/engine/impl/VideoTraceEngine.java

@@ -0,0 +1,107 @@
+package com.ruoyi.bd.service.engine.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.bd.domain.BdDevcTrailUwb;
+import com.ruoyi.bd.domain.BdFenceInfo;
+import com.ruoyi.bd.domain.LocationInfo;
+import com.ruoyi.bd.domain.VideoFence;
+import com.ruoyi.bd.service.IBdDevcTrailUwbService;
+import com.ruoyi.bd.service.IBdFenceInfoService;
+import com.ruoyi.bd.service.engine.DataSplitEngine;
+import com.ruoyi.bd.service.engine.EvtFusionEngine;
+import com.ruoyi.bd.socket.PointWebSocketServer;
+import com.ruoyi.bd.socket.UWBVideoTraceSocketServer;
+import com.ruoyi.common.BDConst;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.enums.FenceType;
+import com.ruoyi.common.utils.DateTimeUtil;
+import com.ruoyi.common.utils.geo.GeoUtils;
+import com.ruoyi.web.core.config.MqttCfg;
+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;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * The type Point fusion engine.
+ *
+ * @author chen.cheng
+ */
+@Service
+@ConditionalOnBean(MqttCfg.class)
+public class VideoTraceEngine extends DataSplitEngine {
+
+    private static final Logger log = LoggerFactory.getLogger(VideoTraceEngine.class);
+
+    @Autowired
+    private UWBVideoTraceSocketServer webSocketServer;
+
+
+    @Autowired
+    private IBdFenceInfoService fenceInfoService;
+
+    @Resource
+    private RedisCache redisCache;
+
+    private final List<LinkedBlockingQueue<LocationInfo>> messageQueueList = new ArrayList<>(2);
+
+    @PostConstruct
+    public void init() {
+        super.init();
+        this.setEngineName("视频跟随");
+        if (!redisCache.hasKey(BDConst.REDIS_KEY.FENCE_VIDEO)) {
+            List<VideoFence> videoFences = fenceInfoService.qryVideoFence();
+            if (CollectionUtils.isEmpty(videoFences)) {
+                return;
+            }
+            videoFences.removeIf(item -> ObjectUtils.isEmpty(item.getPoly()) || ObjectUtils.isEmpty(GeoUtils.getPolygon(item.getPoly())));
+            redisCache.setCacheList(BDConst.REDIS_KEY.FENCE_VIDEO, videoFences);
+        }
+    }
+
+    @Override
+    public List<LinkedBlockingQueue<LocationInfo>> getQueue() {
+        return messageQueueList;
+    }
+
+
+    @Override
+    public LocationInfo serialized(JSONObject jsonObject) {
+        Long displaydatetime = DateTimeUtil.parseDateStringToMills(jsonObject.getString("displaydatetime"), DateTimeUtil.DateFormatter.yyyyMMddHHmmssSSS);
+        LocationInfo locationInfo = new LocationInfo(jsonObject.getDouble("pos_x"), jsonObject.getDouble("pos_y"), displaydatetime);
+        locationInfo.setKey(jsonObject.getString("tag_id"));
+        return locationInfo;
+    }
+
+    @Override
+    public void afterProcess(LocationInfo locationInfo) {
+        List<VideoFence> cacheList = redisCache.getCacheList(BDConst.REDIS_KEY.FENCE_VIDEO);
+        if (CollectionUtils.isEmpty(cacheList)) {
+            return;
+        }
+        Polygon polygon;
+        for (VideoFence fenceInfo : cacheList) {
+            polygon = GeoUtils.getPolygon(fenceInfo.getPoly());
+            if (GeoUtils.isPointInGeoFence(polygon, locationInfo.getLatitude(), locationInfo.getLongitude())) {
+                webSocketServer.broadcast(locationInfo.getKey(), JSON.toJSONString(fenceInfo));
+                break;
+            }
+        }
+    }
+
+}

+ 6 - 0
bd-location/src/main/java/com/ruoyi/bd/service/impl/BdFenceInfoServiceImpl.java

@@ -1,6 +1,7 @@
 package com.ruoyi.bd.service.impl;
 
 import com.ruoyi.bd.domain.BdFenceInfo;
+import com.ruoyi.bd.domain.VideoFence;
 import com.ruoyi.bd.mapper.BdFenceInfoMapper;
 import com.ruoyi.bd.service.IBdFenceInfoService;
 import com.ruoyi.common.utils.DateUtils;
@@ -95,4 +96,9 @@ public class BdFenceInfoServiceImpl implements IBdFenceInfoService {
     public int deleteBdFenceInfoById(Long id) {
         return bdFenceInfoMapper.deleteBdFenceInfoById(id);
     }
+
+    @Override
+    public List<VideoFence> qryVideoFence() {
+        return bdFenceInfoMapper.qryVideoFence();
+    }
 }

+ 104 - 0
bd-location/src/main/java/com/ruoyi/bd/socket/UWBVideoTraceSocketServer.java

@@ -0,0 +1,104 @@
+package com.ruoyi.bd.socket;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.CrossOrigin;
+
+import javax.annotation.PostConstruct;
+import javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@ServerEndpoint("/ws/video/trace/{client}/{device}")
+@CrossOrigin(origins = "*")
+@Component
+public class UWBVideoTraceSocketServer {
+    private static final Logger logger = LoggerFactory.getLogger(UWBVideoTraceSocketServer.class);
+
+    // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
+    private static AtomicInteger onlineNum = new AtomicInteger();
+
+    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
+    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
+
+    public static final String SESSION_KEY_TEMPLATE = "%s-%s";
+
+    @PostConstruct
+    public void init() {
+        logger.info("WebSocketServer init");
+    }
+
+    // 发送消息
+    public void sendMessage(Session session, String message) throws IOException {
+        if (session != null) {
+            synchronized (session) {
+                logger.info("发送数据:{}", message);
+                session.getBasicRemote().sendText(message);
+            }
+        }
+    }
+
+    // 群发消息
+    public void broadcast(String device, String message) {
+        sessionPools.forEach((key, session) -> {
+            if (key.contains(device)) {
+                try {
+                    sendMessage(session, message);
+                } catch (Exception e) {
+                    logger.error("/ws/video/trace/{},{}", device, e.getMessage());
+                }
+            }
+        });
+    }
+
+    // 建立连接成功调用
+    @OnOpen
+    public void onOpen(Session session, @PathParam(value = "client") String client,
+                       @PathParam(value = "device") String device) {
+        sessionPools.put(String.format(SESSION_KEY_TEMPLATE, client, device), session);
+        addOnlineCount();
+    }
+
+    // 关闭连接时调用
+    @OnClose
+    public void onClose(@PathParam(value = "client") String client,
+                        @PathParam(value = "device") String device) {
+        sessionPools.remove(String.format(SESSION_KEY_TEMPLATE, client, device));
+        subOnlineCount();
+        logger.info("{}断开webSocket连接!当前人数为:{}", device, onlineNum);
+    }
+
+    // 收到客户端信息后,根据接收人的username把消息推下去或者群发
+    // to=-1群发消息
+    @OnMessage
+    public void onMessage(String message) throws IOException {
+        logger.info("server get {}", message);
+    }
+
+    // 错误时调用
+    @OnError
+    public void onError(Session session, Throwable throwable) {
+        logger.info("server get {}", throwable.getMessage());
+    }
+
+    public static void addOnlineCount() {
+        onlineNum.incrementAndGet();
+    }
+
+    public static void subOnlineCount() {
+        onlineNum.decrementAndGet();
+    }
+
+    public static AtomicInteger getOnlineNumber() {
+        return onlineNum;
+    }
+
+    public static ConcurrentHashMap<String, Session> getSessionPools() {
+        return sessionPools;
+    }
+
+}

+ 4 - 0
bd-location/src/main/java/com/ruoyi/common/BDConst.java

@@ -8,6 +8,8 @@ public interface BDConst {
 
         String FENCE_ROOM = "fence:room";
 
+        String FENCE_VIDEO = "fence:video";
+
         String FENCE_ROOM_BREAK_IN_KEY = "fence:room:break:in:";
     }
 
@@ -21,5 +23,7 @@ public interface BDConst {
 
         String UWB_VIDEO_TRACE_POINT = "/uwb/video-trace/point/%s";
 
+        String UWB_VIDEO_TRACE_TOPIC = "/uwb/video-trace/point/%s";
+
     }
 }

+ 3 - 1
bd-location/src/main/java/com/ruoyi/common/enums/FenceType.java

@@ -2,7 +2,9 @@ package com.ruoyi.common.enums;
 
 public enum FenceType {
     OUT_SIDE_FENCE_IN("1", "室外围栏闯禁"),
-    INSIDE_FENCE_IN("2", "室外围栏闯禁");
+    INSIDE_FENCE_IN("2", "室外围栏闯禁"),
+
+    VIDEO("3", "视频监控区域");
 
     private final String code;
     private final String info;

+ 39 - 0
bd-location/src/main/java/com/ruoyi/mqtt/UWBVideoTraceSubscribeListener.java

@@ -0,0 +1,39 @@
+package com.ruoyi.mqtt;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.JSONPath;
+import com.ruoyi.bd.service.engine.impl.FenceBreakInEngine;
+import com.ruoyi.bd.service.engine.impl.PointFusionEngine;
+import com.ruoyi.bd.service.engine.impl.RoomBreakInEngine;
+import com.ruoyi.bd.service.engine.impl.VideoTraceEngine;
+import com.ruoyi.common.BDConst;
+import com.ruoyi.web.core.config.MqttCfg;
+import net.dreamlu.iot.mqtt.codec.MqttQoS;
+import net.dreamlu.iot.mqtt.spring.client.MqttClientSubscribe;
+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;
+
+import javax.annotation.PostConstruct;
+import java.nio.charset.StandardCharsets;
+
+@Service
+@ConditionalOnBean(MqttCfg.class)
+public class UWBVideoTraceSubscribeListener {
+    private static final Logger logger = LoggerFactory.getLogger(UWBVideoTraceSubscribeListener.class);
+
+    @Autowired
+    private VideoTraceEngine videoTraceEngine;
+
+    @MqttClientSubscribe(value = BDConst.MQTT_TOPIC.UWB_VIDEO_TRACE_TOPIC, qos = MqttQoS.QOS0)
+    public void subQos0(String topic, byte[] payload) {
+        logger.debug("topic:{} payload:{}", topic, new String(payload, StandardCharsets.UTF_8));
+        JSONObject jsonObject = JSON.parseObject(payload);
+        JSONObject eval =(JSONObject) JSONPath.eval(jsonObject, "$.data.features[0].properties");
+        videoTraceEngine.push(eval);
+    }
+
+}

+ 2 - 0
bd-location/src/main/java/com/ruoyi/web/core/config/forest/UWBService.java

@@ -1,6 +1,7 @@
 package com.ruoyi.web.core.config.forest;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.stereotype.Service;
 
 import com.dtflys.forest.annotation.BindingVar;
@@ -8,6 +9,7 @@ import com.dtflys.forest.reflection.ForestMethod;
 import com.ruoyi.web.core.config.UWBCfg;
 
 @Service("uwbService")
+@ConditionalOnBean(UWBCfg.class)
 public class UWBService {
     @Autowired
     private UWBCfg uwbCfg;

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

@@ -101,9 +101,9 @@ mqtt:
       enabled: false
 bd:
   mqtt:
-    enabled: true
+    enabled: false
   uwb:
-    enabled: true
+    enabled: false
     uwb-socket: ws://172.192.13.77:2223/socket/websocket/pollingArea
     uwb-usr: admin
     uwb-pwd: admin123

+ 110 - 0
bd-location/src/main/resources/application-local.yml

@@ -0,0 +1,110 @@
+# 数据源配置
+spring:
+  # redis 配置
+  redis:
+    # 地址
+    host: 127.0.0.1
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 2
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 8
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: 3s
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    driverClassName: com.mysql.cj.jdbc.Driver
+    druid:
+      # 主库数据源
+      master:
+        url: jdbc:mysql://127.0.0.1:3306/hs_bds?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+        username: root
+        password: 123456
+      # 从库数据源
+      slave:
+        # 从数据源开关/默认关闭
+        enabled: false
+        url:
+        username:
+        password:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置连接超时时间
+      connectTimeout: 30000
+      # 配置网络超时时间
+      socketTimeout: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 60000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 300000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      # 配置检测连接是否有效
+      validationQuery: SELECT 1 FROM DUAL
+      testWhileIdle: true
+      testOnBorrow: false
+      testOnReturn: false
+      webStatFilter:
+        enabled: true
+      statViewServlet:
+        enabled: true
+        # 设置白名单,不填则允许所有访问
+        allow:
+        url-pattern: /druid/*
+        # 控制台管理用户名和密码
+        login-username: ruoyi
+        login-password: 123456
+      filter:
+        stat:
+          enabled: true
+          # 慢SQL记录
+          log-slow-sql: true
+          slow-sql-millis: 1000
+          merge-sql: true
+        wall:
+          config:
+            multi-statement-allow: true
+mqtt:
+  client:
+    enabled: true
+    ip: 200.200.19.121
+    port: 31005
+    name: uwb-location-client
+    client-id: uwb-000001
+    global-subscribe:
+    timeout: 5
+    reconnect: true
+    re-interval: 5000
+    version: mqtt_3_1_1
+    read-buffer-size: 8KB
+    max-bytes-in-message: 10MB
+    keep-alive-secs: 60
+    clean-session: true
+    ssl:
+      enabled: false
+bd:
+  mqtt:
+    enabled: true
+  uwb:
+    enabled: false
+    uwb-socket: ws://172.192.13.77:2223/socket/websocket/pollingArea
+    uwb-usr: admin
+    uwb-pwd: admin123
+    uwb-host: http://172.192.13.77:2223

+ 13 - 0
bd-location/src/main/resources/mapper/bd/BdFenceInfoMapper.xml

@@ -111,4 +111,17 @@
             #{id}
         </foreach>
     </delete>
+
+    <select id="qryVideoFence" resultType="VideoFence">
+        SELECT fence.id,
+               fence.defence_name,
+               fence.poly,
+               camera.camera_name,
+               camera.camera_stream,
+               camera.live_stream,
+               camera.channel_code
+        FROM bd_fence_info fence
+                 JOIN bd_camera_info camera ON fence.location_id = camera.id
+            AND fence.fence_type = 3
+    </select>
 </mapper>

+ 8 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/DateTimeUtil.java

@@ -153,6 +153,12 @@ public class DateTimeUtil {
         return instant.toEpochMilli();
     }
 
+    public static Long parseDateStringToMills(String dateString, String formatRegex) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatRegex);
+        LocalDateTime dateTime = LocalDateTime.parse(dateString, formatter);
+        return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
     /**
      * Parse local date local date.
      *
@@ -620,6 +626,8 @@ public class DateTimeUtil {
          */
         String yyyyMMddHHmmss = "yyyyMMddHHmmss";
 
+        String  yyyyMMddHHmmssSSS= "yyyy-MM-dd HH:mm:ss.SSS";
+
         /**
          * The constant yyyy_MM_dd.
          *

+ 11 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/geo/GeoUtils.java

@@ -133,6 +133,17 @@ public class GeoUtils {
         return geoFencePolygon.contains(point);
     }
 
+    public static boolean isPointInGeoFence(Polygon geoFencePolygon, Double lng, Double lat) {
+        // 将指定的GPS点转换为坐标
+        Coordinate testPoint = new Coordinate(lng, lat);
+
+        // 创建指定的GPS点
+        Point point = geometryFactory.createPoint(testPoint);
+
+        // 检查指定的GPS点是否在电子围栏内
+        return geoFencePolygon.contains(point);
+    }
+
     /**
      * 判断指定的GPS点是否在电子围栏内
      *

+ 22 - 0
ruoyi-ui/.env.bd

@@ -0,0 +1,22 @@
+# 页面标题
+VUE_APP_TITLE = 交通强国工作台
+
+# 开发环境配置
+ENV = 'development'
+
+# 若依管理系统/开发环境
+VUE_APP_BASE_API = '/dev-api'
+
+VUE_APP_BASE_URL = 'http://127.0.0.1:28080/bd-api'
+
+VUE_APP_WS_URL = 'ws://127.0.0.1:28080/bd-api'
+
+VUE_APP_BD_BASE_API = '/dev-db-api'
+
+VUE_APP_BD_BASE_URL = 'http://200.200.19.253:31838/bdgis'
+
+# 3D图层开关
+VUE_APP_3D_SWITCH = false
+
+# 路由懒加载
+VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 20 - 0
ruoyi-ui/.env.bdproduction

@@ -0,0 +1,20 @@
+# 页面标题
+VUE_APP_TITLE = 北斗能力中心方案
+
+BABEL_ENV = production
+
+NODE_ENV = production
+
+# 若依管理系统/生产环境
+VUE_APP_BASE_API = '/prod-api'
+
+VUE_APP_BD_BASE_API = '/prod-bd-api'
+
+VUE_APP_WS_URL = '/prod-api'
+
+VUE_APP_BASE_URL = 'http://172.192.13.80:8080/tfc'
+
+VUE_APP_DOMAIN_BASE_URL = 'http://200.200.19.254:31080'
+
+# 3D图层开关
+VUE_APP_3D_SWITCH = true

+ 15 - 3
ruoyi-ui/babel.config.js

@@ -7,7 +7,19 @@ module.exports = {
     'development': {
       // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
       // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
-      'plugins': ['dynamic-import-node']
-    }
+      'plugins': ['dynamic-import-node',["transform-es2015-arrow-functions", { spec: true }]]
+    },
+    // ���Ի���
+    'staging': {
+      'plugins': ['@babel/plugin-syntax-dynamic-import']
+    },
+    // ��������
+    'production': {
+      'plugins': ['@babel/plugin-syntax-dynamic-import']
+    },
+    // test
+    'test': {
+      'plugins': ['@babel/plugin-syntax-dynamic-import']
+    },
   }
-}
+}

+ 104 - 0
ruoyi-ui/deploy.sh

@@ -0,0 +1,104 @@
+#!/bin/bash
+
+# 设置变量
+PROJECT_NAME="bd-app"
+DOCKER_IMAGE_NAME="bd-app"
+REMOTE_REGISTRY=${REMOTE_REGISTRY:-"docker.xt.wenhq.top:8083/bd"}
+VERSION_FILE="version"
+LOG_FILE="deploy.log"
+
+# 日志函数
+log() {
+  echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE
+}
+
+# 读取当前版本号
+CURRENT_VERSION=$(cat $VERSION_FILE)
+if [ -z "$CURRENT_VERSION" ]; then
+  CURRENT_VERSION="0.0"
+fi
+
+# 分离大版本号和小版本号
+CURRENT_MAJOR=$(echo $CURRENT_VERSION | cut -d'.' -f1)
+CURRENT_MINOR=$(echo $CURRENT_VERSION | cut -d'.' -f2)
+
+# 用户选择是否修改大版本号
+read -p "是否修改大版本号?(a/i/ne): " modify_major_version
+
+if [ "$modify_major_version" == "a" ]; then
+  CURRENT_MAJOR=$((CURRENT_MAJOR + 1))
+  CURRENT_MINOR=0  # 重置小版本号
+elif [ "$modify_major_version" == "i" ]; then
+  CURRENT_MINOR=$((CURRENT_MINOR + 1))  # 增加小版本号
+else
+  echo "跳过版本号修改"
+fi
+
+# 更新版本号
+NEXT_VERSION="${CURRENT_MAJOR}.${CURRENT_MINOR}"
+echo $NEXT_VERSION > $VERSION_FILE
+echo  -e "\e[1;34;42m版本号:" $NEXT_VERSION " 日期:$(date +"%Y-%m-%d %H:%M:%S")" "\e[0m"
+# 用户选择执行的步骤
+echo "请选择要执行的步骤(输入数字):"
+echo "1. 编译bd-app项目"
+echo "2. 构建Docker镜像"
+echo "3. 推送Docker镜像到远程仓库"
+echo "4. 执行所有步骤"
+read -p "请输入选项(1/2/3/4): " choice
+
+# 编译项目
+compile_project() {
+  log "开始编译bd-app项目..."
+  yarn run build:prod
+  if [ $? -ne 0 ]; then
+    log "bd-app项目编译失败。退出脚本。"
+    exit 1
+  fi
+  log "bd-app项目编译完成"
+}
+
+# 构建Docker镜像
+build_docker_image() {
+  log "开始构建Docker镜像..."
+  docker build -t ${REMOTE_REGISTRY}/${DOCKER_IMAGE_NAME}:v${NEXT_VERSION} .
+  if [ $? -ne 0 ]; then
+    log "Docker镜像构建失败。退出脚本。"
+    exit 1
+  fi
+  log "Docker镜像构建完成"
+}
+
+# 推送Docker镜像到远程仓库
+push_docker_image() {
+  log "开始推送Docker镜像到远程仓库..."
+  docker push ${REMOTE_REGISTRY}/${DOCKER_IMAGE_NAME}:v${NEXT_VERSION}
+  if [ $? -ne 0 ]; then
+    log "Docker镜像推送失败。退出脚本。"
+    exit 1
+  fi
+  log "Docker镜像推送完成"
+}
+
+# 根据用户选择执行相应的步骤
+case $choice in
+  1)
+    compile_project
+    ;;
+  2)
+    build_docker_image
+    ;;
+  3)
+    push_docker_image
+    ;;
+  4)
+    compile_project
+    build_docker_image
+    push_docker_image
+    ;;
+  *)
+    log "无效的选项。退出脚本。"
+    exit 1
+    ;;
+esac
+
+log "所有任务完成"

+ 41 - 0
ruoyi-ui/docker/conf/nginx.conf

@@ -0,0 +1,41 @@
+worker_processes  1;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+    sendfile        on;
+    keepalive_timeout  65;
+
+    server {
+        listen       80;
+        server_name  localhost;
+
+		location / {
+            root   /home/ruoyi/projects/ruoyi-ui;
+            try_files $uri $uri/ /index.html;
+            index  index.html index.htm;
+        }
+
+        location /prod-api/ws {
+            proxy_pass http://200.200.19.254:31280;
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "Upgrade";
+        }
+
+		location /prod-api {
+			proxy_pass http://200.200.19.254:31280;
+		}
+	    location ~ ^/prod-bd-api/(.*)$ {
+			proxy_pass http://200.200.19.253:31838/bdgis/$1;
+		}
+        error_page   500 502 503 504  /50x.html;
+        location = /50x.html {
+            root   html;
+        }
+    }
+}

+ 14 - 0
ruoyi-ui/jsconfig.json

@@ -0,0 +1,14 @@
+{
+    "compilerOptions": {
+        "baseUrl": "./",
+        "paths": {
+            "@/*": [
+                "src/*"
+            ]
+        }
+    },
+    "exclude": [
+        "node_modules",
+        "dist"
+    ]
+}

+ 15 - 6
ruoyi-ui/package.json

@@ -1,13 +1,17 @@
 {
   "name": "ruoyi",
   "version": "3.8.8",
-  "description": "若依管理系统",
-  "author": "若依",
+  "description": "比斗研发平台",
+  "author": "华设",
   "license": "MIT",
   "scripts": {
     "dev": "vue-cli-service serve",
+    "bd": "vue-cli-service serve --mode bd",
+    "bdprod": "vue-cli-service serve --mode bdproduction",
+    "test": "vue-cli-service serve --mode test",
     "build:prod": "vue-cli-service build",
     "build:stage": "vue-cli-service build --mode staging",
+    "build:test": "vue-cli-service build --mode test",
     "preview": "node build/index.js --preview",
     "lint": "eslint --ext .js,.vue src"
   },
@@ -36,16 +40,17 @@
     "url": "https://gitee.com/y_project/RuoYi-Vue.git"
   },
   "dependencies": {
-    "@logicflow/core": "^1.2.15",
-    "@logicflow/extension": "^1.2.16",
+    "@fingerprintjs/fingerprintjs": "^4.5.1",
     "@riophae/vue-treeselect": "0.4.0",
     "axios": "0.28.1",
     "clipboard": "2.0.8",
     "core-js": "3.37.1",
+    "dayjs": "^1.11.13",
     "echarts": "5.4.0",
     "element-ui": "2.15.14",
     "file-saver": "2.0.5",
     "fuse.js": "6.4.3",
+    "g711": "^1.0.1",
     "highlight.js": "9.18.5",
     "js-beautify": "1.13.0",
     "js-cookie": "3.0.1",
@@ -54,12 +59,15 @@
     "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",
+    "vue-json-editor": "^1.4.3",
     "vue-meta": "2.4.0",
     "vue-router": "3.4.9",
-    "vuedraggable": "2.24.3",
+    "vue-video-player": "^5.0.1",
+    "vuedraggable": "^2.24.3",
     "vuex": "3.6.0"
   },
   "devDependencies": {
@@ -67,7 +75,8 @@
     "@vue/cli-plugin-eslint": "4.4.6",
     "@vue/cli-service": "4.4.6",
     "babel-eslint": "10.1.0",
-    "babel-plugin-dynamic-import-node": "2.3.3",
+    "babel-plugin-dynamic-import-node": "^2.3.3",
+    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
     "chalk": "4.1.0",
     "compression-webpack-plugin": "6.1.2",
     "connect": "3.6.6",

文件差異過大導致無法顯示
+ 2756 - 0
ruoyi-ui/public/bdjs/BDLayers.js


文件差異過大導致無法顯示
+ 1 - 0
ruoyi-ui/public/bdjs/jquery-2.2.4.min.js


+ 2 - 0
ruoyi-ui/public/index.html

@@ -7,6 +7,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <title><%= webpackConfig.name %></title>
+    <script src="<%= BASE_URL %>bdjs/jquery-2.2.4.min.js" crossorigin="anonymous"></script>
+    <script type="text/javascript" src="<%= BASE_URL %>bdjs/BDLayers.js"></script>
     <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
 	  <style>
     html,

+ 44 - 0
ruoyi-ui/src/api/bd/devcTrail.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询设备轨迹列表
+export function listDevcTrail(query) {
+  return request({
+    url: '/bd/devcTrail/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询设备轨迹详细
+export function getDevcTrail(id) {
+  return request({
+    url: '/bd/devcTrail/' + id,
+    method: 'get'
+  })
+}
+
+// 新增设备轨迹
+export function addDevcTrail(data) {
+  return request({
+    url: '/bd/devcTrail',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改设备轨迹
+export function updateDevcTrail(data) {
+  return request({
+    url: '/bd/devcTrail',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除设备轨迹
+export function delDevcTrail(id) {
+  return request({
+    url: '/bd/devcTrail/' + id,
+    method: 'delete'
+  })
+}

+ 52 - 0
ruoyi-ui/src/api/bd/fenceInfo.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request';
+
+// 查询围栏基础信息列表
+export function listFenceInfo(query) {
+  return request({
+    url: '/bd/fenceInfo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询围栏基础信息详细
+export function getFenceInfo(id) {
+  return request({
+    url: '/bd/fenceInfo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增围栏基础信息
+export function addFenceInfo(data) {
+  return request({
+    url: '/bd/fenceInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改围栏基础信息
+export function updateFenceInfo(data) {
+  return request({
+    url: '/bd/fenceInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除围栏基础信息
+export function delFenceInfo(id) {
+  return request({
+    url: '/bd/fenceInfo/' + id,
+    method: 'delete'
+  })
+}
+
+export const pushDevcLocation = (params, config = {}) => {
+  return request({
+    url: 'https://www.lj-info.com:8090/bd-api/bd/devcTrailUwb/point',
+    method: 'post',
+    data: params,
+  });
+};

+ 52 - 0
ruoyi-ui/src/api/bd/fenceVioEvt.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询围栏闯禁事件列表
+export function listFenceVioEvt(query) {
+  return request({
+    url: '/bd/fenceVioEvt/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listAllFenceVioEvt(query) {
+  return request({
+    url: '/bd/fenceVioEvt/listAll',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询围栏闯禁事件详细
+export function getFenceVioEvt(id) {
+  return request({
+    url: '/bd/fenceVioEvt/' + id,
+    method: 'get'
+  })
+}
+
+// 新增围栏闯禁事件
+export function addFenceVioEvt(data) {
+  return request({
+    url: '/bd/fenceVioEvt',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改围栏闯禁事件
+export function updateFenceVioEvt(data) {
+  return request({
+    url: '/bd/fenceVioEvt',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除围栏闯禁事件
+export function delFenceVioEvt(id) {
+  return request({
+    url: '/bd/fenceVioEvt/' + id,
+    method: 'delete'
+  })
+}

+ 11 - 0
ruoyi-ui/src/assets/icons/svg/bd_evt.svg

@@ -0,0 +1,11 @@
+<?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="1729307496337" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6498"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48">
+    <path d="M243.2 889.6H166.4c-57.6 0-102.4-44.8-102.4-102.4V236.8C64 179.2 108.8 128 166.4 128h518.4c57.6 0 102.4 44.8 102.4 102.4v76.8c0 19.2-12.8 32-32 32s-32-12.8-32-32V236.8c0-25.6-19.2-44.8-38.4-44.8H166.4c-19.2 0-38.4 19.2-38.4 44.8v550.4c0 19.2 19.2 38.4 38.4 38.4h76.8c19.2 0 32 12.8 32 32s-12.8 32-32 32z"
+          fill="#ffffff" p-id="6499"></path>
+    <path d="M524.8 377.6H236.8c-19.2 0-32-12.8-32-32s12.8-32 32-32h281.6c19.2 0 32 12.8 32 32s-12.8 32-25.6 32zM448 550.4H236.8c-19.2 0-32-12.8-32-32s12.8-32 32-32H448c19.2 0 32 12.8 32 32s-12.8 32-32 32zM345.6 704H230.4c-19.2 0-32-12.8-32-32s12.8-32 32-32h121.6c19.2 0 32 12.8 32 32s-19.2 32-38.4 32zM928 896H364.8c-12.8 0-19.2-6.4-25.6-19.2s-6.4-19.2 0-32l281.6-480c12.8-19.2 44.8-19.2 57.6 0l281.6 480c6.4 12.8 6.4 19.2 0 32-12.8 12.8-19.2 19.2-32 19.2z m-512-64h454.4l-224-384-230.4 384z"
+          fill="#ffffff" p-id="6500"></path>
+    <path d="M646.4 723.2c-19.2 0-32-12.8-32-32V576c0-19.2 12.8-32 32-32s32 12.8 32 32v115.2c0 19.2-12.8 32-32 32zM646.4 742.4c-19.2 0-38.4 19.2-38.4 38.4s19.2 38.4 38.4 38.4 38.4-19.2 38.4-38.4c-6.4-19.2-19.2-38.4-38.4-38.4z"
+          fill="#ffffff" p-id="6501"></path>
+</svg>

+ 7 - 0
ruoyi-ui/src/assets/icons/svg/bd_fence.svg

@@ -0,0 +1,7 @@
+<?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="1729307445723" class="icon" viewBox="0 0 1070 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5383"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="50.15625" height="48">
+    <path d="M93.090909 93.090909h884.363636v93.090909H93.090909V93.090909z m0 744.727273h884.363636v93.090909H93.090909v-93.090909zM93.090909 93.090909h93.090909v837.818182H93.090909V93.090909z m791.272727 0h93.090909v837.818182h-93.090909V93.090909zM0 0h279.272727v279.272727H0V0z m791.272727 0h279.272728v279.272727h-279.272728V0zM0 744.727273h279.272727v279.272727H0v-279.272727z m791.272727 0h279.272728v279.272727h-279.272728v-279.272727zM372.363636 186.181818h93.090909L279.272727 837.818182H186.181818z m232.727273 0h93.090909l-186.181818 651.636364H418.909091z m186.181818 0h93.090909l-186.181818 651.636364h-93.090909z"
+          fill="#ffffff" p-id="5384"></path>
+</svg>

文件差異過大導致無法顯示
+ 4 - 0
ruoyi-ui/src/assets/icons/svg/bd_location.svg


+ 9 - 0
ruoyi-ui/src/assets/icons/svg/bd_real_time.svg

@@ -0,0 +1,9 @@
+<?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="1729320983633" class="icon" viewBox="0 0 1260 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5173"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="59.0625" height="48">
+    <path d="M1260.798133 851.908044V184.439256L1009.28633 8.071432 741.660949 187.367932 474.037032 97.722611 206.411651 247.130503v717.17575l53.528883-29.881286 6.056503-3.924426 7.321691-3.545163 200.718304-112.062875 237.850992 75.321165 20.541736-48.526705-258.392728-86.557032-215.361686 113.525749V271.006538l215.361686-113.521356 267.623917 89.645321L1009.28633 67.835468l200.716839 134.473107v592.569353l-126.523215-0.437837 0.070288 58.185479"
+          p-id="5174" fill="#ffffff"></path>
+    <path d="M474.037032 334.160517v298.826035l33.449878 0.594521 24.67117-0.598914V334.160517M693.687764 331.23184v260.590701l33.451342 0.518375 24.671171-0.522769V331.23184M982.162393 200.905739v343.892508l33.451342 0.685311 24.67117-0.689704V200.905739M898.872299 800.862678c-45.772284 0-82.880079-37.503166-82.880078-83.675214s37.107795-83.675214 82.880078-83.675214 82.880079 37.503166 82.880079 83.675214-37.107795 83.675214-82.880079 83.675214z m0-223.134393c-76.302271 0-138.114916 62.407166-138.114916 139.459179s138.114916 306.811072 138.114916 306.811072 138.114916-229.759059 138.114917-306.811072-61.812645-139.459179-138.114917-139.459179z m0 0M90.557604 146.30496c-30.011612 0-54.341591-24.589167-54.341591-54.862896s24.329979-54.862896 54.341591-54.862895 54.341591 24.589167 54.341591 54.862895S120.567751 146.30496 90.557604 146.30496z m0-146.302031C40.528489 0.002929 0 40.920931 0 91.442064c0 50.519668 90.557604 201.164927 90.557604 201.164927s90.557604-150.645258 90.557604-201.164927C181.115208 40.920931 140.586719 0.002929 90.557604 0.002929z m0 0"
+          p-id="5175" fill="#ffffff"></path>
+</svg>

+ 1 - 0
ruoyi-ui/src/assets/icons/svg/bd_room_location.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="1729736779904" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4735" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M679.6 520L556 597.6l77.2 88L368 842.8l10.4 21.2 290.8-173.2-77.2-88 98-58.8-10.4-24z m-324 320S179.2 601.6 179.2 500c0-200 176.4-202.4 176.4-202.4s178.4 2 178.4 201.2C534 604 355.6 840 355.6 840z m0.8-440.4c-39.2 0-70.8 30.4-70.8 68s31.6 68 70.8 68c39.2 0 70.8-30.4 70.8-68 0.4-37.6-31.6-68-70.8-68zM684.8 516S552.4 355.2 552.4 286.4c0-135.2 132.4-136.4 132.4-136.4s134 1.2 134 136c0 70.8-134 230-134 230z m0.4-297.2c-29.6 0-53.2 20.4-53.2 46 0 25.2 24 46 53.2 46 29.6 0 53.2-20.4 53.2-46 0.4-25.6-23.6-46-53.2-46z m0 0" p-id="4736" fill="#ffffff"></path><path d="M925.2 854.4c0 40.8-35.2 74-78.4 74H175.2c-43.6 0-78.8-33.2-78.8-74V550.8h-80v303.6c0 82.4 71.2 149.6 158.8 149.6h671.6c87.6 0 158.8-67.2 158.8-149.6V550.8h-80v303.6zM846.8 14.8H175.2C87.6 14.8 16.4 82 16.4 164.4V468h80V164.4c0-40.8 35.2-74 78.8-74h671.6c43.6 0 78.4 33.2 78.4 74V468h80.4V164.4c0-82.4-71.6-149.6-158.8-149.6z m0 0" p-id="4737" fill="#ffffff"></path></svg>

+ 5 - 0
ruoyi-ui/src/assets/icons/svg/bd_signal.svg

@@ -0,0 +1,5 @@
+<svg t="1730359242827" class="icon" viewBox="0 0 1346 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4326"
+     width="48" height="48">
+    <path d="M614.762225 324.416035a578.94361 578.94361 0 0 0-397.653943 238.117618 89.918541 89.918541 0 1 0 154.434563 92.115133 407.432318 407.432318 0 0 1 262.421842-151.458536c118.438803-12.683545 237.054751 43.400399 352.482099 166.834678a89.95397 89.95397 0 0 0 131.370351-122.867416c-194.575504-207.825911-380.966932-235.81474-503.054912-222.741477z m60.866846 354.288972a172.645016 172.645016 0 1 0 172.574158 172.574159 172.645016 172.645016 0 0 0-172.645016-172.715874zM630.846944 0.985632C291.615253 16.609776 64.232591 258.695431 22.107632 307.055875a89.918541 89.918541 0 0 0 135.621819 118.049086c20.655047-23.701932 209.668214-232.023848 481.407855-244.459391 185.222275-8.502935 372.003421 75.746982 555.135391 250.553161a89.918541 89.918541 0 0 0 124.001141-130.130339C1098.366672 90.939602 867.015973-9.997326 630.846944 0.985632z"
+          p-id="4327" fill="#1296db"></path>
+</svg>

+ 5 - 0
ruoyi-ui/src/assets/icons/svg/bd_signal_off.svg

@@ -0,0 +1,5 @@
+<svg t="1730359709803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5421"
+     width="48" height="48">
+    <path d="M512 768c30.464 0 58.453333 10.666667 80.469333 28.416L512 896l-80.426667-99.584A127.445333 127.445333 0 0 1 512 768zM119.808 59.434667l754.218667 754.261333-60.330667 60.330667-157.098667-157.013334-10.538666 13.056A212.48 212.48 0 0 0 512 682.666667c-50.773333 0-97.408 17.749333-134.016 47.36l-80.426667-99.584a339.754667 339.754667 0 0 1 197.205334-75.349334l-75.648-75.648a425.088 425.088 0 0 0-175.189334 84.608L163.413333 464.512A553.941333 553.941333 0 0 1 316.416 376.746667L251.093333 311.466667a640 640 0 0 0-141.226666 86.656L29.397333 298.538667c39.04-31.573333 81.194667-59.349333 125.952-82.858667L59.434667 119.808l60.373333-60.373333zM686.250667 506.453333l-165.034667-164.992L512 341.333333c132.053333 0 253.312 46.122667 348.586667 123.178667l-80.469334 99.584a426.709333 426.709333 0 0 0-93.866666-57.685333zM512 128c182.826667 0 350.72 63.914667 482.56 170.581333L914.176 398.08A637.312 637.312 0 0 0 512 256c-24.405333 0-48.469333 1.365333-72.106667 4.010667L329.557333 149.802667C388.053333 135.552 449.152 128 512 128z"
+          fill="#1296db" p-id="5422"></path>
+</svg>

+ 293 - 0
ruoyi-ui/src/components/Audio/RecorderHandle.js

@@ -0,0 +1,293 @@
+import { alawFromPCM } from 'g711';
+
+export const Recorder = {
+  params: new Params(),
+  getUserMedia: async function (constraints) {
+    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+      // 最新标准的方法,支持现代浏览器
+      this.params.stream = await navigator.mediaDevices.getUserMedia(
+        constraints
+      );
+      return this.params.stream;
+    } else {
+      // 旧版浏览器兼容处理
+      let getUserMedia =
+        navigator.getUserMedia ||
+        navigator.webkitGetUserMedia ||
+        navigator.mozGetUserMedia ||
+        navigator.msGetUserMedia;
+      if (getUserMedia) {
+        return new Promise(function (resolve, reject) {
+          getUserMedia.call(navigator, constraints, resolve, reject);
+        });
+      } else {
+        return Promise.reject(
+          new Error('获取浏览器麦克风权限失败,请开启浏览器麦克风权限!')
+        );
+      }
+    }
+  },
+  start: function (
+    streamHandle = () => {
+      console.log('start streamHandle');
+    },
+    errorFunCell = () => {
+      console.log('start errorFunCell');
+    }
+  ) {
+    //开启录音
+    const _this = this;
+    return new Promise((resolve, reject) => {
+      this.stop();
+      this.getUserMedia({
+        audio: true,
+      })
+        .then(function (stream) {
+          _this.init(stream, streamHandle);
+          resolve();
+        })
+        .catch(function (error) {
+          console.log('获取麦克风权限失败-' + error);
+          let title = '获取麦克风权限异常';
+          if (error) {
+            title = error.message;
+            if (error.name.indexOf('NotAllowedError') > -1) {
+              if (error.message.indexOf('system') > -1) {
+                title = '获取系统麦克风权限失败,请开启系统麦克风权限!';
+              } else {
+                title = '获取浏览器麦克风权限失败,请开启浏览器麦克风权限!';
+              }
+            }
+          }
+          errorFunCell(title);
+          reject(new Error(title));
+        });
+    });
+  },
+  init: function (stream, streamHandle) {
+    //初始化组件
+    const AudioContext = window.AudioContext || window.webkitAudioContext;
+    this.params.context = new AudioContext();
+    this.params.littleEdian = (function () {
+      let buffer = new ArrayBuffer(2);
+      new DataView(buffer).setInt16(0, 256, true);
+      return new Int16Array(buffer)[0] === 256;
+    })();
+    this.params.input = this.params.context.createMediaStreamSource(stream);
+    this.params.recorder = this.params.context.createScriptProcessor(
+      4096,
+      1,
+      1
+    );
+    this.params.input.connect(this.params.recorder);
+    this.params.recorder.connect(this.params.context.destination);
+    this.params.recorder.onaudioprocess = (e) => {
+      this.params.timeStamp = e.timeStamp;
+      const input = e.inputBuffer.getChannelData(0); //解析输入的音频
+      const volume = Math.round(Math.max.apply(Math, input) * 100);
+      if (volume > 5) {
+        this.params.sendTime = e.timeStamp;
+        let pcm = this.transformIntoPCM(input, null);
+        this.toG711a_new(pcm, streamHandle);
+      } else {
+        if (
+          this.params.timeStamp - this.params.sendTime >
+          this.params.keepAlive
+        ) {
+          this.params.sendTime = e.timeStamp;
+          this.toRtp(new Uint8Array('AudioKeepAlive'.split('')), streamHandle);
+        }
+      }
+    };
+  },
+  transformIntoPCM: function (lData, rData) {
+    let lBuffer = new Float32Array(lData),
+      rBuffer = new Float32Array(rData);
+    const inputSampleRate = this.params.context.sampleRate; //48000
+    const outputSampleRate = this.params.sampleRate; //8000
+    let data = this.compress(
+      {
+        left: lBuffer,
+        right: rBuffer,
+      },
+      inputSampleRate,
+      outputSampleRate
+    );
+    return this.encodePCM(
+      data,
+      this.params.sampleBits,
+      this.params.littleEdian
+    );
+  },
+  encodePCM: function (bytes, sampleBits, littleEdian) {
+    let offset = 0,
+      dataLength = bytes.length * (sampleBits / 8),
+      buffer = new ArrayBuffer(dataLength),
+      data = new DataView(buffer);
+
+    // 写入采样数据
+    if (sampleBits === 8) {
+      for (let i = 0; i < bytes.length; i++, offset++) {
+        // 范围[-1, 1]
+        let s = Math.max(-1, Math.min(1, bytes[i]));
+        // 8位采样位划分成2^8=256份,它的范围是0-255;
+        // 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。
+        let val = s < 0 ? s * 128 : s * 127;
+        val = +val + 128;
+        data.setInt8(offset, val);
+      }
+    } else {
+      for (let i = 0; i < bytes.length; i++, offset += 2) {
+        let s = Math.max(-1, Math.min(1, bytes[i]));
+        // 16位的划分的是2^16=65536份,范围是-32768到32767
+        // 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。
+        data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, littleEdian);
+      }
+    }
+    return data;
+  },
+  compress: function (data, inputSampleRate, outputSampleRate) {
+    // 压缩,根据采样率进行压缩
+    let rate = inputSampleRate / outputSampleRate,
+      compression = Math.max(rate, 1),
+      lData = data.left,
+      rData = data.right,
+      length = Math.floor((lData.length + rData.length) / rate),
+      result = new Float32Array(length),
+      index = 0,
+      j = 0;
+
+    // 循环间隔 compression 位取一位数据
+    while (index < length) {
+      let temp = Math.floor(j);
+
+      result[index] = lData[temp];
+      index++;
+
+      if (rData.length) {
+        /*
+         * 双声道处理
+         * e.inputBuffer.getChannelData(0)得到了左声道4096个样本数据,1是右声道的数据,
+         * 此处需要组和成LRLRLR这种格式,才能正常播放,所以要处理下
+         */
+        result[index] = rData[temp];
+        index++;
+      }
+
+      j += compression;
+    }
+    // 返回压缩后的一维数据
+    return result;
+  },
+  toG711a_new: function (pcmData, streamHandle) {
+    //Uint8转G711a
+    // 假设有一个名为 dataView 的 DataView 对象
+    const length = pcmData.byteLength / 2; // 一个 16 位整数占用 2 个字节
+    const int16Array = new Int16Array(length);
+    for (let i = 0; i < length; i++) {
+      const offset = i * 2; // 一个 16 位整数占用 2 个字节
+      const value = pcmData.getInt16(offset, true); // true 表示使用小字节序
+      int16Array[i] = value;
+    }
+    const g711a = alawFromPCM(int16Array);
+    this.params.tempPCM.push(g711a);
+    this.toRtp(g711a, streamHandle);
+  },
+  toRtp: function (g711, streamHandle) {
+    //G711打包rtp
+    console.log('%c █░░░░░░░░░░░░█ ,注释: g711 数据', 'color: #FAC800', g711);
+    const rtp = new Uint8Array(g711.length + 12); // 创建一个RTP数据包数组,长度是G711数组长度加上12个字节的RTP头部
+    this.params.rtpSequenceNumber++;
+    this.params.rtpTimeStamp += g711.length;
+    rtp[0] = 128; //(2 << 6) | (0 << 5) | (0 << 4) | (0 << 0) 设置RTP头部的第一个字节,包括版本号(2)、填充标志(0)、扩展标志(0)和CSRC计数(0)
+    rtp[1] = 136; // (0 << 7) | (8 << 0) 设置RTP头部的第二个字节,包括标记位(0)和有效载荷类型(8,表示G711 A-law)
+    rtp[2] = (this.params.rtpSequenceNumber >> 8) & 255; // 设置RTP头部的第三和第四个字节,表示序列号(每次递增1)
+    rtp[3] = (this.params.rtpSequenceNumber >> 0) & 255;
+    rtp[4] = (this.params.rtpTimeStamp >> 24) & 255; // 设置RTP头部的第五到第八个字节,表示时间戳(每次递增采样点数)
+    rtp[5] = (this.params.rtpTimeStamp >> 16) & 255;
+    rtp[6] = (this.params.rtpTimeStamp >> 8) & 255;
+    rtp[7] = (this.params.rtpTimeStamp >> 0) & 255;
+    // 设置RTP头部的第九到第十二个字节,表示同步源标识符(固定不变)
+    // 在具体的设备推流是计算index(8-11)的识别符
+    rtp[8] = 0;
+    rtp[9] = 0;
+    rtp[10] = 0;
+    rtp[11] = 0;
+    for (let i = 0; i < g711.length; i++) {
+      // 将G711数据复制到RTP数据包数组的后面
+      rtp[i + 12] = g711[i];
+    }
+    console.log('send->success--', this.params.rtpTimeStamp);
+    console.log('%c █░░░░░░░░░░░░█ ,注释: rtp 数据', 'color: #FAC800', rtp);
+    streamHandle(rtp);
+  },
+  stop: function () {
+    //停止录音
+    this.params.isStop = true;
+    if (this.params.input) {
+      this.params.input.disconnect();
+    }
+    if (this.params.recorder) {
+      this.params.recorder.disconnect();
+    }
+    if (this.params.context) {
+      this.params.context.close();
+    }
+    if (this.params.stream) {
+      this.params.stream.getTracks().forEach((track) => {
+        track.stop();
+      });
+    }
+    this.params = new Params();
+  },
+  downloadPCM: function (name) {
+    if (this.params.tempPCM.length) {
+      // 计算合并后的总长度
+      let totalLength = 0;
+      for (const array of this.params.tempPCM) {
+        totalLength += array.length;
+      }
+      // 创建一个新的Uint8Array来存储合并后的数据
+      const mergedArray = new Uint8Array(totalLength);
+
+      // 使用set()方法将每个Uint8Array的数据复制到合并后的Uint8Array中
+      let offset = 0;
+      for (const array of this.params.tempPCM) {
+        mergedArray.set(array, offset);
+        offset += array.length;
+      }
+      try {
+        console.log('tempPCM', mergedArray);
+        let oA = document.createElement('a');
+
+        oA.href = window.URL.createObjectURL(
+          new Blob([mergedArray], { type: 'audio/g711a' })
+        );
+        oA.download = name + '.g711a';
+        oA.click();
+      } catch (e) {}
+    }
+  },
+};
+
+function Params() {
+  return {
+    errorFunCell: null,
+    token: null, //token
+    sampleRate: 44100, //采样率 摄像机默认8000,只有喇叭使用这个采样率
+    sampleBits: 16,
+    context: null, //音频控制
+    stream: null, //媒体流
+    input: null, //输入音频
+    recorder: null, //音频
+    timeStamp: 0, //时间戳
+    sendTime: 0, //发送时间
+    keepAlive: 3000, //保活时间(毫秒)
+    rtpSequenceNumber: 0, //rtp序列号
+    rtpTimeStamp: 0, //rtp时间戳
+    socket: null, //socket链接
+    littleEdian: true,
+    tempPCM: [],
+    isStop: false,
+  };
+}

+ 69 - 0
ruoyi-ui/src/components/WebsocketMessage/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="dds"></div>
+</template>
+
+<script>
+const prefix = process.env.VUE_APP_WS_URL;
+export default {
+  name: 'SocketMessage',
+  props: {
+    onMessage: {
+      type: Function,
+      default: function () {
+      },
+    },
+    ws: {
+      type: String,
+      default: '',
+    },
+  },
+  watch: {
+    ws(val) {
+      this.init();
+    },
+  },
+  data() {
+    return {
+      socket: null,
+      url: prefix,
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.socket.close();
+  },
+  created() {
+
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      if (!this.ws) {
+        return;
+      }
+      if (this.ws && this.socket) {
+        this.socket.close();
+        return;
+      }
+      this.socket = new WebSocket(`${prefix}${this.ws}`);
+      this.socket.onopen = () => {
+        console.log('连接成功:', this.ws);
+      };
+      // 监听socket错误信息
+      this.socket.onerror = (e) => {
+        console.log('%c FXY', 'color:#f6b2b1;font-size:50px', e);
+        console.log('socket连接失败');
+      };
+      // 监听socket关闭监听
+      this.socket.onclose = () => {
+        console.log('socket连接关闭');
+      };
+      this.socket.onmessage = (a) => {
+        this.onMessage(a);
+      };
+    },
+  },
+};
+</script>

+ 17 - 0
ruoyi-ui/src/components/video/index.scss

@@ -0,0 +1,17 @@
+.drag-video-container {
+  width: 100%;
+
+  .video-title {
+    height: 30px;
+    width: 100%;
+    background: #fefefe;
+    padding: 0 15px;
+    line-height: 30px;
+    cursor: move;
+  }
+  .video-js{
+    .vjs-control-bar{
+      display: none;
+    }
+  }
+}

+ 121 - 0
ruoyi-ui/src/components/video/index.vue

@@ -0,0 +1,121 @@
+<template>
+  <div v-drag class="drag-video-container">
+    <video-player
+      style="width:100%"
+      ref="videoPlayer"
+      class="player-video"
+      :playsinline="false"
+      :options="playOptions"
+      @ready="onPlayerReady"
+      @play="onPlayerPlay($event)"
+      @pause="onPlayerPause($event)"
+      @ended="onPlayerEnd($event)"
+      @waiting="onPlayerWaiting($event)"
+      @playing="onPlayerPlaying($event)"
+      @loadeddata="onPlayerLoadeddata($event)"
+      @timeupdate="onPlayerTimeupdate($event)"
+      @statechanged="playerStateChanged($event)"
+    />
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: "video-p",
+  props: {
+    path: {  // 传入的地址
+      type: String,
+      default: "",
+    },
+    lastTime: {  // 传入的上次播放位置
+      type: Number,
+      default: 0,
+    },
+  },
+  data() {
+    return {
+      playedTime: this.lastTime,
+      currentTime: 0,
+      maxTime: 0,
+      playOptions: {
+        height: "100%",
+        width: "100%",
+        playbackRates: [0.5], // 可选的播放速度
+        autoplay: true, // 如果为true,浏览器准备好时开始回放
+        muted: false, // 默认情况下静音播放
+        loop: false, // 是否视频一结束就重新开始
+        preload: "auto", // 建议浏览器在<video>加载元素后是否应该开始下载视频数据,auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
+        language: "zh-CN",
+        aspectRatio: "16:9", // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值,值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
+        fluid: true, // 当true时,Video.js player将拥有流体大小,换句话说,它将按比例缩放以适应其容器
+        sources: [
+          {
+            type: "video/mp4", // 类型
+            src: require("./output.mp4"), // url地址,在使用本地的资源时,需要用require()引入,否则控制台会报错
+          },
+        ],
+        notSupportedMessage: "此视频暂无法播放,请稍后再试", // 允许覆盖Video.js无法播放媒体源时显示的默认信息
+        controlBar: {
+          currentTimeDisplay: false,
+          progressControl: false,  // 是否显示进度条
+          playbackRateMenuButton: false, // 是否显示调整播放倍速按钮
+          timeDivider: false, // 当前时间和持续时间的分隔符
+          durationDisplay: false, // 显示持续时间
+          remainingTimeDisplay: false, // 是否显示剩余时间功能
+          fullscreenToggle: false, // 是否显示全屏按钮
+        },
+      },
+    };
+  },
+  computed: {},
+  mounted() {
+  },
+  methods: {
+    update() {
+
+    },
+    // 准备好了
+    onPlayerReady() {
+      console.log("准备好了");
+    },
+    // 视频播放
+    onPlayerPlay(player) {
+
+    },
+    // 视频暂停的
+    onPlayerPause(player) {
+
+    },
+    // 视频播放完
+    onPlayerEnd(player) {
+
+    },
+    // DOM元素上的readyState更改导致播放停止
+    onPlayerWaiting(player) {
+
+    },
+    // 视频已开始播放
+    onPlayerPlaying(player) {
+
+    },
+    // 当播放器在当前播放位置下载数据时触发
+    onPlayerLoadeddata(player) {
+
+    },
+    // 当前播放位置发生变化时触发
+    onPlayerTimeupdate(player) {
+
+    },
+    //播放状态改变
+    playerStateChanged(playerCurrentState) {
+    },
+    // 手动暂停视频的播放
+    pause() {
+      // 视频播放器的方法调用,要使用this.$refs.videoPlayer.player这个对象去调用
+      this.$refs.videoPlayer.player.pause()
+    }
+  },
+};
+</script>
+<style lang="scss" src="./index.scss"/>

二進制
ruoyi-ui/src/components/video/output.mp4


二進制
ruoyi-ui/src/components/video/roomlocation.mp4


+ 54 - 0
ruoyi-ui/src/main.js

@@ -38,6 +38,13 @@ import VueMeta from 'vue-meta'
 // 字典数据组件
 import DictData from '@/components/DictData'
 
+import VideoPlayer from 'vue-video-player'
+
+import 'video.js/dist/video-js.css'
+
+import 'vue-video-player/src/custom-theme.css'
+
+Vue.use(VideoPlayer)
 // 全局方法挂载
 Vue.prototype.getDicts = getDicts
 Vue.prototype.getConfigKey = getConfigKey
@@ -48,7 +55,54 @@ Vue.prototype.selectDictLabel = selectDictLabel
 Vue.prototype.selectDictLabels = selectDictLabels
 Vue.prototype.download = download
 Vue.prototype.handleTree = handleTree
+Vue.directive('drag', (el) => {
+  const oDiv = el // 当前元素
+  const minTop = oDiv.getAttribute('drag-min-top')
+  const ifMoveSizeArea = 20
+  oDiv.onmousedown = e => {
+    let target = oDiv
+    while (window.getComputedStyle(target).position !== 'absolute' && target !== document.body) {
+      target = target.parentElement
+    }
 
+    document.onselectstart = () => {
+      return false
+    }
+    if (!target.getAttribute('init_x')) {
+      target.setAttribute('init_x', target.offsetLeft)
+      target.setAttribute('init_y', target.offsetTop)
+    }
+
+    const initX = parseInt(target.getAttribute('init_x'))
+    const initY = parseInt(target.getAttribute('init_y'))
+
+    // 鼠标按下,计算当前元素距离可视区的距离
+    const disX = e.clientX - target.offsetLeft
+    const disY = e.clientY - target.offsetTop
+    document.onmousemove = e => {
+      // 通过事件委托,计算移动的距离
+      // 因为浏览器里并不能直接取到并且使用clientX、clientY,所以使用事件委托在内部做完赋值
+      const l = e.clientX - disX
+      const t = e.clientY - disY
+      const { marginTop: mt, marginLeft: ml } = window.getComputedStyle(target)
+      // 计算移动当前元素的位置,并且给该元素样式中的left和top值赋值
+      target.style.left = l - parseInt(ml) + 'px'
+      target.style.top = (t < minTop ? minTop : t) - parseInt(mt) + 'px'
+      if (Math.abs(l - initX) > ifMoveSizeArea || Math.abs(t - initY) > ifMoveSizeArea) {
+        target.setAttribute('dragged', '')
+      } else {
+        target.removeAttribute('dragged')
+      }
+    }
+    document.onmouseup = e => {
+      document.onmousemove = null
+      document.onmouseup = null
+      document.onselectstart = null
+    }
+    // return false不加的话可能导致黏连,拖到一个地方时div粘在鼠标上不下来,相当于onmouseup失效
+    return false
+  }
+})
 // 全局组件挂载
 Vue.component('DictTag', DictTag)
 Vue.component('Pagination', Pagination)

+ 1 - 1
ruoyi-ui/src/permission.js

@@ -8,7 +8,7 @@ import { isRelogin } from '@/utils/request'
 
 NProgress.configure({ showSpinner: false })
 
-const whiteList = ['/login', '/register']
+const whiteList = ['/login', '/register','/bd/']
 
 router.beforeEach((to, from, next) => {
   NProgress.start()

+ 5 - 0
ruoyi-ui/src/router/index.js

@@ -47,6 +47,11 @@ export const constantRoutes = [
     hidden: true
   },
   {
+    path: '/bd',
+    component: () => import('@/views/bd/index'),
+    hidden: true,
+  },
+  {
     path: '/register',
     component: () => import('@/views/register'),
     hidden: true

+ 22 - 0
ruoyi-ui/src/utils/BDConst.js

@@ -0,0 +1,22 @@
+export const FenceType = {
+  OUTSIDE: {
+    value: "1",
+    label: '室外围栏'
+  },
+  INSIDE: {
+    value: "2",
+    label: '室内围栏'
+  },
+}
+
+export const EvtType = {
+  OUTSIDE: {
+    value: "1",
+    label: '围栏闯禁'
+  },
+  INSIDE: {
+    value: "2",
+    label: '室内闯禁'
+  }
+
+}

+ 56 - 0
ruoyi-ui/src/utils/VideoTraceDataManager.js

@@ -0,0 +1,56 @@
+class WebSocketDataManager {
+  constructor(maxAgeMs = 5 * 60 * 1000, cleanupIntervalMs = 1 * 60 * 1000) {
+    this.dataList = []; // 存储数据的数组
+    this.maxAgeMs = maxAgeMs; // 数据最大存活时间(毫秒)
+    this.cleanupIntervalMs = cleanupIntervalMs; // 清理间隔(毫秒)
+    this.cleanupIntervalId = null; // 定时器的ID
+    // 启动定时清理任务
+    this.startCleanup();
+  }
+
+  // 启动定时清理任务
+  startCleanup() {
+    this.stopCleanup(); // 如果已经有定时器在运行,先停止它
+    this.cleanupIntervalId = setInterval(() => {
+      this.removeExpiredData();
+    }, this.cleanupIntervalMs);
+  }
+
+  // 停止定时清理任务
+  stopCleanup() {
+    if (this.cleanupIntervalId !== null) {
+      clearInterval(this.cleanupIntervalId);
+      this.cleanupIntervalId = null;
+    }
+  }
+
+  // 移除过期的数据
+  removeExpiredData() {
+    const now = Date.now(); // 当前时间戳
+    this.dataList = this.dataList.filter(data => {
+      return now - data.timestamp < this.maxAgeMs;
+    });
+  }
+
+  // 处理WebSocket推送的新数据
+  handleNewData(newData) {
+    const {channelCode} = newData; // 新数据的channelCode
+
+    // 如果新数据的channelCode和历史数据重复,移除历史数据
+    this.dataList = this.dataList.filter(data => {
+      return data.channelCode !== channelCode;
+    });
+
+    // 将新数据追加到末尾,并添加时间戳
+    this.dataList.push({...newData, timestamp: Date.now()});
+  }
+
+  // 获取当前存储的数据
+  getData() {
+    return this.dataList;
+  }
+}
+
+// 使用示例:
+const wsDataManager = new WebSocketDataManager();
+export default wsDataManager;

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

@@ -1,5 +1,5 @@
 import { parseTime } from './ruoyi'
-
+import { UUID } from 'uuidjs';
 /**
  * 表格时间格式化
  */
@@ -388,3 +388,6 @@ export function isNumberStr(str) {
   return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
 }
 
+export const uuid = () => {
+    return UUID.generate();
+};

+ 76 - 0
ruoyi-ui/src/views/bd/VideoWall/icon/icon-recording.vue

@@ -0,0 +1,76 @@
+<!--
+ * @Description:
+ * @Autor: he.chao
+ * @Date: 2022-10-19 15:07:55
+ * @LastEditors: liu.yongli
+ * @LastEditTime: 2023-12-13 14:36:14
+-->
+<template>
+
+  <i class='icon'>
+    <svg width="82px" height="82px" viewBox="0 0 82 82" preserveAspectRatio="none meet" version="1.1" xmlns="http://www.w3.org/2000/svg">
+      <defs>
+        <linearGradient x1="50%" y1="0.330528846%" x2="50%" y2="98.7703142%" id="linearGradient-1">
+          <stop stop-color="#07C0ED" offset="0%"></stop>
+          <stop stop-color="#0E82E3" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
+          <stop stop-color="#0095FF" offset="0%"></stop>
+          <stop stop-color="#00FFF6" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
+          <stop stop-color="#FFFFFF" offset="0%"></stop>
+          <stop stop-color="#FFFFFF" offset="100%"></stop>
+        </linearGradient>
+      </defs>
+      <g id="综合监测-大屏" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="1-6综合监测-弹框" transform="translate(-575.000000, -358.000000)">
+          <g id="云广播喊话" transform="translate(428.000000, 116.000000)">
+            <g id="编组-40" transform="translate(110.000000, 243.000000)">
+              <g id="icon_录音中_80_n" transform="translate(38.000000, 0.000000)">
+                <circle id="椭圆形" fill="url(#linearGradient-1)" cx="40" cy="40" r="33.7777778"></circle>
+                <circle id="椭圆形" stroke="#4F9FFF" stroke-width="0.888888889" cx="40" cy="40" r="40"></circle>
+                <g id="编组-8" transform="translate(28.000000, 22.000000)">
+                  <rect id="矩形" fill="#FFFFFF" x="10" y="37.5" width="5" height="5" rx="1"></rect>
+                  <rect id="矩形" fill="#FFFFFF" x="19" y="37.5" width="5" height="5" rx="1"></rect>
+                  <rect id="矩形" fill="#FFFFFF" x="1" y="37.5" width="5" height="5" rx="1"></rect>
+                  <g id="语音" fill="url(#linearGradient-3)" fill-rule="nonzero">
+                    <path d="M12.4444444,22.3577533 L12.4444444,22.3577533 C16.2871833,22.3577533 19.4312423,19.2136942 19.4312423,15.3709554 L19.4312423,6.9867979 C19.4312423,3.14405906 16.2871833,0 12.4444444,0 L12.4444444,0 C8.6017056,0 5.45764654,3.14405906 5.45764654,6.9867979 L5.45764654,15.3709554 C5.45764654,19.2136942 8.6017056,22.3577533 12.4444444,22.3577533 Z M24.8722112,17.3097918 C24.9901134,16.5456108 24.4704703,15.8338308 23.7062893,15.7159285 C22.9421083,15.6023931 22.2303283,16.1220362 22.112426,16.8818504 C21.3788123,21.597939 17.2260343,25.1524725 12.4444444,25.1524725 C7.66285463,25.1524725 3.50570987,21.5935723 2.77646284,16.8774837 C2.65856063,16.1133027 1.94241384,15.5936596 1.18259957,15.7115618 C0.418418551,15.829464 -0.101224544,16.541244 0.016677671,17.3054251 C0.898760907,23.0040321 5.43144605,27.2528786 11.0470849,27.8642234 L11.0470849,32.1392704 L6.85500612,32.1392704 C6.0820916,32.1392704 5.45764654,32.7637154 5.45764654,33.5366299 C5.45764654,34.3095445 6.0820916,34.9339895 6.85500612,34.9339895 L18.0338828,34.9339895 C18.8067973,34.9339895 19.4312423,34.3095445 19.4312423,33.5366299 C19.4312423,32.7637154 18.8067973,32.1392704 18.0338828,32.1392704 L13.841804,32.1392704 L13.841804,27.8642234 C19.4530761,27.2528786 23.990128,23.0083989 24.8722112,17.3097918 Z" id="形状"></path>
+                  </g>
+                  <rect id="矩形" fill="#0AA4E8" x="9" y="19" width="7" height="1" rx="0.5"></rect>
+                  <rect id="矩形" fill="#0AA4E8" x="7" y="17" width="11" height="1" rx="0.5"></rect>
+                  <rect id="矩形" fill="#0AA4E8" x="7" y="15" width="11" height="1" rx="0.5"></rect>
+                  <rect id="矩形" fill="#0AA4E8" x="7" y="13" width="11" height="1" rx="0.5"></rect>
+                  <rect id="矩形" fill="#0AA4E8" x="7" y="11" width="11" height="1" rx="0.5"></rect>
+                </g>
+              </g>
+            </g>
+          </g>
+        </g>
+      </g>
+    </svg>
+  </i>
+</template>
+
+<script>
+
+/**
+ * 组件说明:
+ * 此组件是一个基本的Vue组件示例,目前尚未实现具体的业务逻辑。
+ * 它展示了如何定义一个基本的Vue组件,包括props、data和methods的声明。
+ *
+ * @export
+ * @returns {Object} 返回一个Vue组件定义对象
+ */
+export default {
+  // 定义组件接收的外部属性,目前未定义任何props
+  props: {
+  },
+  // 定义组件内部的状态,目前未定义任何状态
+  data() {
+    return {};
+  },
+  // 定义组件的方法,目前未定义任何方法
+  methods: {}
+};
+</script>

+ 69 - 0
ruoyi-ui/src/views/bd/VideoWall/icon/icon-unrecord.vue

@@ -0,0 +1,69 @@
+<!--
+ * @Description:
+ * @Autor: he.chao
+ * @Date: 2022-10-19 15:07:55
+ * @LastEditors: liu.yongli
+ * @LastEditTime: 2023-12-13 14:36:14
+-->
+<template>
+
+  <i class='icon'>
+    <svg width='82px' height='82px' viewBox='0 0 82 82' version='1.1' xmlns='http://www.w3.org/2000/svg'
+    >
+      <defs>
+        <linearGradient x1='50%' y1='0.330528846%' x2='50%' y2='98.7703142%' id='linearGradient-10'>
+          <stop stop-color='#07C0ED' offset='0%'></stop>
+          <stop stop-color='#0E82E3' offset='100%'></stop>
+        </linearGradient>
+        <linearGradient x1='50%' y1='0%' x2='50%' y2='100%' id='linearGradient-2'>
+          <stop stop-color='#0095FF' offset='0%'></stop>
+          <stop stop-color='#00FFF6' offset='100%'></stop>
+        </linearGradient>
+        <linearGradient x1='50%' y1='0%' x2='50%' y2='100%' id='linearGradient-3'>
+          <stop stop-color='#FFFFFF' offset='0%'></stop>
+          <stop stop-color='#FFFFFF' offset='100%'></stop>
+        </linearGradient>
+      </defs>
+      <g id='综合监测-大屏' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'>
+        <g id='1-7综合监测-云广播' transform='translate(-143.000000, -911.000000)'>
+          <g id='云广播喊话' transform='translate(-4.000000, 721.000000)'>
+            <g id='编组-40' transform='translate(116.000000, 191.000000)'>
+              <g id='icon_录音_80_n' transform='translate(32.000000, 0.000000)'>
+                <circle id='椭圆形' fill='url(#linearGradient-10)' cx='40' cy='40' r='33.7777778'></circle>
+                <circle id='椭圆形' stroke='#4F9FFF' stroke-width='0.888888889' cx='40' cy='40' r='40'></circle>
+                <g id='语音' transform='translate(27.555556, 22.222222)' fill='url(#linearGradient-3)'
+                   fill-rule='nonzero'>
+                  <path
+                    d='M12.4444444,22.3577533 L12.4444444,22.3577533 C16.2871833,22.3577533 19.4312423,19.2136942 19.4312423,15.3709554 L19.4312423,6.9867979 C19.4312423,3.14405906 16.2871833,0 12.4444444,0 L12.4444444,0 C8.6017056,0 5.45764654,3.14405906 5.45764654,6.9867979 L5.45764654,15.3709554 C5.45764654,19.2136942 8.6017056,22.3577533 12.4444444,22.3577533 Z M24.8722112,17.3097918 C24.9901134,16.5456108 24.4704703,15.8338308 23.7062893,15.7159285 C22.9421083,15.6023931 22.2303283,16.1220362 22.112426,16.8818504 C21.3788123,21.597939 17.2260343,25.1524725 12.4444444,25.1524725 C7.66285463,25.1524725 3.50570987,21.5935723 2.77646284,16.8774837 C2.65856063,16.1133027 1.94241384,15.5936596 1.18259957,15.7115618 C0.418418551,15.829464 -0.101224544,16.541244 0.016677671,17.3054251 C0.898760907,23.0040321 5.43144605,27.2528786 11.0470849,27.8642234 L11.0470849,32.1392704 L6.85500612,32.1392704 C6.0820916,32.1392704 5.45764654,32.7637154 5.45764654,33.5366299 C5.45764654,34.3095445 6.0820916,34.9339895 6.85500612,34.9339895 L18.0338828,34.9339895 C18.8067973,34.9339895 19.4312423,34.3095445 19.4312423,33.5366299 C19.4312423,32.7637154 18.8067973,32.1392704 18.0338828,32.1392704 L13.841804,32.1392704 L13.841804,27.8642234 C19.4530761,27.2528786 23.990128,23.0083989 24.8722112,17.3097918 Z'
+                    id='形状'></path>
+                </g>
+              </g>
+            </g>
+          </g>
+        </g>
+      </g>
+    </svg>
+  </i>
+</template>
+
+<script>
+
+/**
+ * 组件说明:
+ * 此组件是一个基本的Vue组件示例,目前尚未实现具体的业务逻辑。
+ * 它展示了如何定义一个基本的Vue组件,包括props、data和methods的声明。
+ *
+ * @export
+ * @returns {Object} 返回一个Vue组件定义对象
+ */
+export default {
+  // 定义组件接收的外部属性,目前未定义任何props
+  props: {},
+  // 定义组件内部的状态,目前未定义任何状态
+  data() {
+    return {};
+  },
+  // 定义组件的方法,目前未定义任何方法
+  methods: {}
+};
+</script>

+ 125 - 0
ruoyi-ui/src/views/bd/VideoWall/index.scss

@@ -0,0 +1,125 @@
+.video-wall-container {
+  position: absolute;
+  border-radius: 10px;
+  top: 20vh;
+  left: 40vw;
+  background: linear-gradient(1deg, #1f2d3dd1 50%, #1f2d3db3 80%, #1f2d3d);
+
+  .video-title {
+    height: 30px;
+    width: 100%;
+    background: transparent;
+    color: #fff;
+    padding: 0 15px;
+    line-height: 30px;
+    cursor: move;
+    letter-spacing: 3px;
+    box-shadow: 0 4px 4px 0 #00000096
+  }
+
+  $container-width: 40vw;
+  $container-height: 50vh;
+
+  .wall-main-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .main-video {
+      height: $container-height;
+      width: 30vw;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: space-around;
+      margin: 14px;
+
+      .video-ctrl {
+        height: 5vh;
+        width: 100%;
+      }
+
+      .record {
+        color: #00afff;
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+        justify-content: space-between;
+        position: relative;
+        cursor: pointer;
+      }
+      .pulse {
+        position: absolute;
+        top: 16px;
+        left: 79px;
+        width: 50px;
+        height: 50px;
+        border: 5px solid #00afff;
+        border-radius: 100%;
+        z-index: 1;
+        opacity: 1;
+        -webkit-animation: warn 1.5s ease-out;
+        -moz-animation: warn 1.5s ease-out;
+        animation: warn 1.5s ease-out;
+        -webkit-animation-iteration-count: infinite;
+        -moz-animation-iteration-count: infinite;
+        animation-iteration-count: infinite;
+      }
+    }
+
+    .video-list {
+      height: $container-height;
+      width: calc(#{$container-width} - 30vw);
+      margin-right: 14px;
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      justify-content: flex-start;
+      overflow: hidden;
+      overflow-y: auto;
+      padding-bottom: 20px;
+
+      > div:not(:first-child) {
+        margin-top: 14px;
+      }
+
+      & > div {
+        cursor: pointer;
+        box-sizing: border-box;
+
+        &:hover {
+          border: #00afff 1px solid;
+          box-shadow: #00afff;
+        }
+      }
+
+      &::-webkit-scrollbar {
+        width: 0;
+        height: 0;
+      }
+    }
+  }
+}
+
+@keyframes warn {
+  0% {
+    transform: scale(1);
+    opacity: 0.0;
+  }
+  25% {
+    transform: scale(1.2);
+    opacity: 0.1;
+  }
+  50% {
+    transform: scale(1.5);
+    opacity: 0.3;
+  }
+  75% {
+    transform: scale(1.8);
+    opacity: 0.5;
+  }
+  100% {
+    transform: scale(2);
+    opacity: 0.0;
+  }
+}

+ 82 - 0
ruoyi-ui/src/views/bd/VideoWall/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <div v-drag class="video-wall-container">
+    <div class="video-title" style="">
+      视频跟随
+    </div>
+    <div class="wall-main-container">
+      <div class="main-video">
+        <video-p key="main_video"></video-p>
+        <div v-if='recordStat=== recordStatEnum.recorded' class='record'
+             @click='()=>startSpeaking(recordStatEnum.recording)'>
+          <icon-unrecord @click='()=>startSpeaking(recordStatEnum.recording)'/>
+          点击按钮可进行实时喊话!
+        </div>
+        <div v-if='recordStat=== recordStatEnum.recording' class='record'
+             @click='()=>stopSpeaking(recordStatEnum.recorded)'>
+          <div class="pulse"></div>
+          <icon-recording @click='()=>stopSpeaking(recordStatEnum.recorded)'/>
+          喊话中!点击按钮完成喊话!
+        </div>
+      </div>
+      <div class="video-list">
+        <video-p v-for="item in videoList" :key="item.key"></video-p>
+      </div>
+    </div>
+    <socket-message :onMessage="onMessage" :ws="ws"></socket-message>
+  </div>
+</template>
+
+<script>
+import VideoTraceDataManager from "@/utils/VideoTraceDataManager";
+import VideoP from "@/components/video/index.vue";
+import IconRecording from "@/views/bd/VideoWall/icon/icon-recording.vue";
+import SocketMessage from "@/components/WebsocketMessage/index.vue";
+import IconUnrecord from "@/views/bd/VideoWall/icon/icon-unrecord.vue";
+import {Recorder} from "@/components/Audio/RecorderHandle";
+
+const recordStatEnum = {
+  recording: '录音中',
+  recorded: '录音完成'
+};
+export default {
+  name: "video-wall",
+  components: {IconUnrecord, SocketMessage, IconRecording, VideoP},
+  props: {},
+  data() {
+    return {
+      recordStatEnum,
+      selectedVideo: {},
+      recordStat: recordStatEnum.recording,
+      videoList: [{
+        key: "devc_5"
+      }, {
+        key: "devc_4"
+      }, {
+        key: "devc_3"
+      }, {
+        key: "devc_2"
+      },
+        {
+          key: "devc_1"
+        }]
+    }
+  },
+  computed: {},
+  mounted() {
+    VideoTraceDataManager.startCleanup();
+  },
+  methods: {
+    onMessage(data) {
+      VideoTraceDataManager.handleNewData(data);
+    },
+    stopSpeaking(stat) {
+      this.recordStat = stat;
+    },
+    startSpeaking(stat) {
+      this.recordStat = stat;
+      Recorder.start();
+    }
+  },
+};
+</script>
+<style lang="scss" src="./index.scss"/>

+ 14 - 0
ruoyi-ui/src/views/bd/fence/index.scss

@@ -0,0 +1,14 @@
+.fence-location-container {
+  .el-tabs__item {
+    color: #fefefe;
+  }
+
+  .el-tabs__item.is-active {
+    color: #1890ff;
+  }
+
+  .el-tab-pane {
+
+  }
+}
+

+ 381 - 0
ruoyi-ui/src/views/bd/fence/index.vue

@@ -0,0 +1,381 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:icon>
+      <svg-icon icon-class="bd_fence"/>
+    </template>
+    <template v-slot:title>
+      围栏闯禁
+    </template>
+    <template v-slot:action>
+      <el-button
+        size="mini"
+        type="text"
+        icon="el-icon-plus"
+        round
+        @click="startDraw"
+        style="margin-left: auto"
+        :disabled="drawState"
+      >
+        {{ drawState ? '绘制中...' : '绘制围栏' }}
+      </el-button>
+    </template>
+    <template v-slot:content>
+      <el-tabs v-model="activeName" @tab-click="handleClick">
+        <el-tab-pane label="室外围栏" :name="Tabs.outside.id">
+        </el-tab-pane>
+        <el-tab-pane label="室内围栏" :name="Tabs.inside.id">
+          <room-layer v-if="activeName === Tabs.inside.id"/>
+        </el-tab-pane>
+      </el-tabs>
+      <div class="location-list">
+        <template v-for="fence in fenceList">
+          <div :key="`fence_${fence.id}`" class="list-item">
+            <span class="over-flow-hidden" style="width: 80%;cursor: pointer;" @click="()=>fenceClick(fence)">
+              {{ fence.name }}
+            </span>
+            <span class="over-flow-hidden" style="width: 20%">
+              <el-popconfirm
+                confirm-button-text='好的'
+                cancel-button-text='不用了'
+                icon="el-icon-info"
+                icon-color="red"
+                :title="`是否删除围栏【${fence.name}】?`"
+                @confirm="()=>delFence(fence)"
+              >
+               <i
+                 slot="reference"
+                 class="el-icon-delete"
+                 title="删除围栏"
+               ></i>
+              </el-popconfirm>
+              <i
+                class="el-icon-edit"
+                title="编辑围栏"
+                @click="()=>editFence(fence)"
+              />
+            </span>
+          </div>
+        </template>
+      </div>
+      <el-dialog
+        width="20%"
+        title="提示"
+        :visible="dialogVisible"
+        append-to-body>
+        <div>
+          <div style="margin-bottom: 14px">
+            检测到未保存的内容,是否保存修改?
+          </div>
+          <el-form ref="form" :model="form" label-width="80px" :rules="rules">
+            <el-form-item label="围栏名称" prop="name">
+              <el-input v-model="form.name"></el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div slot="footer" class="dialog-footer">
+          <el-button @click="cancelEdit">放弃修改</el-button>
+          <el-button type="primary" @click="saveEdit">保存</el-button>
+        </div>
+      </el-dialog>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import {addFenceInfo, listFenceInfo, updateFenceInfo} from '@/api/bd/fenceInfo';
+import maphandle from '@/views/bd/map/maphandle';
+import Pannel from '@/views/bd/pannel/index.vue';
+import {FenceType} from "@/utils/BDConst";
+import RoomLayer from "@/views/bd/rooms/index.vue";
+
+const Tabs = {
+  outside: {
+    id: 'outside',
+    param: {
+      fenceType: FenceType.OUTSIDE.value,
+    },
+  },
+  inside: {
+    id: 'inside',
+    param: {
+      fenceType: FenceType.INSIDE.value,
+    },
+  },
+}
+
+export default {
+  name: 'fence',
+  components: {RoomLayer, Pannel},
+  mixins: [maphandle],
+  data() {
+    return {
+      Tabs,
+      activeName: Tabs.outside.id,
+      playItem: {},
+      editState: false,
+      editPolyInfo: {},
+      dialogVisible: false,
+      editingDrawGeom: null,
+      drawState: false,
+      drawtool: null,
+      form: {
+        name: '',
+      },
+      rules: {
+        name: [
+          {
+            required: true,
+            message: '请输入围栏名称',
+            trigger: 'blur',
+          },
+        ],
+      },
+      fenceList: [],
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    window.map.removeLayersById('drawLayer');
+    window.map.removeLayersById('distanceLayer');
+    this.drawtool?.disable();
+  },
+  created() {
+    // 地图绘制工具
+    this.drawtool = new BDLayers.Lib.Tools.CBDrawTool('myTool', window.map, 'Polygon', true);
+    // 监听图形编辑
+    this.drawtool.on('selectDraw', geom => {
+      this.editingDrawGeom = geom.target.geometry ? geom.target.geometry : geom.target.geom
+        ? geom.target.geom
+        : geom.target;
+      if (this.editingDrawGeom.isEditing && this.editingDrawGeom.isEditing()) {
+        // 点击地图 图形取消编辑状态
+        window.map.map.once('click', () => {
+          this.editingDrawGeom.endEdit();
+          this.dialogVisible = true;
+        });
+      } else {
+        window.map.map.once('click', () => {
+          this.editingDrawGeom = null;
+        });
+      }
+    });
+    this.drawtool.on('drawend', (geom) => {
+      this.editingDrawGeom = geom.target.geometry ? geom.target.geometry : geom.target.geom
+        ? geom.target.geom
+        : geom.target;
+      this.dialogVisible = true;
+      this.drawState = false;
+    });
+
+
+  },
+  mounted() {
+    this.getFenceList({});
+  },
+  methods: {
+    fenceClick(fence) {
+      const {polygon} = fence;
+      map.setPitch(0);
+      map.fitExtent(polygon.getExtent(), 0);
+    },
+    async getFenceList(params) {
+      const {rows} = await listFenceInfo({
+        pageNum: 1,
+        pageSize: 10,
+        fenceType: 1,
+        ...params
+      });
+      if (!rows || rows.length < 1) {
+        return;
+      }
+      const result = [];
+      this.clearLayer();
+      rows.forEach(item => {
+        const {
+          id,
+          defenceName,
+          poly,
+        } = item;
+        const polygon = this.custDrawPoly({
+          name: defenceName,
+          coordinates: this.polygonToCoordinates(poly),
+          symbol: {
+            lineColor: 'rgba(241,0,23,0.49)',
+            lineWidth: 2,
+            polygonFill: 'rgba(241,0,23,0.49)',
+            polygonOpacity: 0.4,
+          },
+          bizAttr: {
+            id,
+            name: defenceName
+          },
+        });
+        result.push({
+          id,
+          name: defenceName,
+          polygon,
+        });
+      });
+      this.editingDrawGeom = null;
+      this.fenceList = result;
+      // window.map.flyToPoint([118.86318437, 31.52265586], {
+      //   zoom: 13,
+      //   pitch: 0,
+      //   bearing: 20,
+      //   duration: 5000,
+      // });
+    },
+    cancelEdit() {
+      this.drawtool.clear();
+      this.dialogVisible = false;
+    },
+    saveEdit() {
+      this.$refs.form.validate(async (valid) => {
+        if (valid) {
+          const resultCoor = [];
+          const coordinates = [];
+          this.editingDrawGeom._coordinates.forEach(coor => {
+            const {
+              x,
+              y,
+            } = coor;
+            resultCoor.push(`${x} ${y}`);
+            coordinates.push([x, y]);
+          });
+          resultCoor.push(resultCoor[0]);
+          this.form.poly = `POLYGON((${resultCoor.join(',')}))`;
+          if (!this.form.id) {
+            await addFenceInfo(this.formatParams(this.form))
+            this.$message({
+              type: 'success',
+              message: '围栏保存成功',
+            });
+          } else {
+            await updateFenceInfo(this.formatParams(this.form))
+            this.$message({
+              type: 'success',
+              message: '围栏编辑成功',
+            });
+            this.polyLayer.removeGeometry(this.form.polygonId);
+          }
+          this.dialogVisible = false;
+          this.drawtool.clear();
+          this.drawPoly({
+            coordinates: coordinates,
+            symbol: {
+              lineColor: 'rgba(241,0,23,0.49)',
+              lineWidth: 2,
+              polygonFill: 'rgba(241,0,23,0.49)',
+              polygonOpacity: 0.4,
+            },
+            labelSymbol: {
+              labelText: this.form.name,
+            },
+            bizAttr: this.form,
+          });
+          this.editingDrawGeom = null;
+          this.editState = false;
+          this.$refs.form.resetFields();
+        } else {
+          return false;
+        }
+      });
+    },
+    formatParams(form) {
+      return {
+        ...form,
+        defenceName: this.form.name,
+        poly: this.form.poly,
+      };
+    },
+    startDraw() {
+      this.editingDrawGeom = null;
+      this.drawState = true;
+      this.drawtool.enable();
+    },
+    handleClick(tab) {
+      this.activeName = tab.name;
+      this.getFenceList(Tabs[tab.name].param);
+    },
+    /**
+     *
+     * @param coordinates
+     *  [
+     *           [
+     *             [118.86318437, 31.52265586],
+     *             [118.86620514, 31.52541921],
+     *             [118.86520697, 31.52406319],
+     *             [118.86318437, 31.52265586],
+     *           ],
+     *         ]
+     * @param symbol
+     * @param bizAttr
+     * @param labelSymbol
+     * @returns {BDLayers.Lib.Overlays.Polygon}
+     */
+    custDrawPoly({
+                   name = '多边形',
+                   coordinates,
+                   symbol = {},
+                   bizAttr = {},
+                   labelSymbol = {},
+                 }) {
+      return this.drawPoly({
+        name,
+        coordinates,
+        symbol,
+        bizAttr,
+        labelSymbol,
+        polyOnClick: (data) => {
+          console.log(data.target.options);
+          if (this.editState) {
+            return;
+          }
+          this.form.name = data.target.options.bizAttr.name;
+          this.editingDrawGeom = data.target.geometry ? data.target.geometry : data.target.geom
+            ? data.target.geom
+            : data.target;
+          this.$confirm('检测到选中了围栏,请选择操作类型?', '提示', {
+            confirmButtonText: '编辑围栏',
+            cancelButtonText: '删除围栏',
+            distinguishCancelAndClose: true,
+            type: 'warning',
+          }).then(() => {
+            // 开始编辑围栏
+            this.editState = true;
+            this.editingDrawGeom.startEdit();
+          }).catch(() => {
+            this.editingDrawGeom.remove();
+            this.editState = false;
+            this.editingDrawGeom = null;
+          });
+          // 点击地图 图形取消编辑状态
+          window.map.map.once('click', () => {
+            this.editingDrawGeom.endEdit();
+            this.dialogVisible = true;
+          });
+        },
+      })
+    },
+    delFence(fence) {
+      // polygon.geom.startEdit();
+      fence.polygon.geom.remove();
+    },
+    editFence(fence) {
+      this.editingDrawGeom = fence.polygon.geom;
+      this.form.name = fence.polygon.options.bizAttr.name;
+      this.form.id = fence.polygon.options.bizAttr.id;
+      this.form.polygonId = fence.polygon.options.bizAttr.polygonId;
+      this.editState = true;
+      this.editingDrawGeom.startEdit();
+      // 点击地图 图形取消编辑状态
+      window.map.map.once('click', () => {
+        this.editingDrawGeom.endEdit();
+        this.dialogVisible = true;
+      });
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss"/>

+ 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>

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

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

+ 234 - 0
ruoyi-ui/src/views/bd/fenceEvt/index.vue

@@ -0,0 +1,234 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:icon>
+      <svg-icon icon-class="bd_evt"/>
+    </template>
+    <template v-slot:title>
+      围栏闯禁事件
+    </template>
+    <template v-slot:action>
+
+    </template>
+    <template v-slot:content>
+      <el-tabs v-model="activeName" @tab-click="handleClick">
+        <el-tab-pane label="室外围栏" :name="Tabs.outside.id">
+        </el-tab-pane>
+        <el-tab-pane label="室内围栏" :name="Tabs.inside.id">
+          <room-layer v-if="activeName === Tabs.inside.id" :onclick="onRoomClick"/>
+        </el-tab-pane>
+      </el-tabs>
+      <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: 60%">
+            {{ 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';
+import RoomLayer from "@/views/bd/rooms/index.vue";
+import {EvtType, FenceType} from "@/utils/BDConst";
+
+const Tabs = {
+  outside: {
+    id: 'outside',
+    param: {
+      evtType: EvtType.OUTSIDE.value,
+      fenceType: FenceType.OUTSIDE.value
+    },
+  },
+  inside: {
+    id: 'inside',
+    param: {
+      evtType: EvtType.INSIDE.value,
+      fenceType: FenceType.INSIDE.value,
+    },
+  },
+}
+export default {
+  name: 'fenceVioEvt',
+  mixins: [maphandle],
+  components: {
+    RoomLayer,
+    SocketMessage,
+    Pannel,
+  },
+  data() {
+    return {
+      Tabs,
+      activeName: Tabs.outside.id,
+      evtList: [],
+      fp: null,
+      ws: null,
+      markLayer: null,
+      heatlayer: null,
+      listParam: Tabs.outside.param,
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && window.map.removeLayersById('real_time_markerLayer');
+  },
+  created() {
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('real_time_markerLayer', true);
+    this.heatlayer = new BDLayers.LibViz.Layer.HeatLayer('heatlayer', [], {
+      heatValueScale: 0.5,
+      heatRadius: 10,
+      heatOpacity: 0.8,
+    });
+    window.map.addCustomLayers(this.heatlayer, 4);
+    window.map.addCustomLayers(this.markLayer, 5);
+  },
+  mounted() {
+    this.getFenceVioEvtList();
+    this.getFingerprint();
+    this.getFenceList();
+  },
+  methods: {
+    dayjs,
+    handleClick(tab) {
+      this.listParam = Tabs[tab.name].param;
+      this.getFenceVioEvtList();
+      this.getFenceList();
+    },
+    async getFenceVioEvtList() {
+      this.markLayer.removeAll();
+      const {rows} = await listFenceVioEvt({
+        pageNum: 1,
+        pageSize: 10,
+        fenceType: 1,
+        ...this.listParam,
+      });
+      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);
+      });
+    },
+    onRoomClick(item) {
+      this.listParam = {
+        ...this.listParam,
+        locationId: item.bizId,
+      };
+      this.getFenceVioEvtList();
+      this.getFenceList();
+    },
+    async getFenceList() {
+      this.clearLayer()
+      const {rows} = await listFenceInfo({
+        pageNum: 1,
+        pageSize: 10,
+        fenceType: 1,
+        ...this.listParam
+      });
+      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),
+          symbol: {
+            lineColor: 'rgba(241,0,23,0.49)',
+            lineWidth: 2,
+            polygonFill: 'rgba(241,0,23,0.49)',
+            polygonOpacity: 0.4,
+          },
+          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"/>

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

@@ -0,0 +1,92 @@
+
+.bd-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  $header-height: 60px;
+
+  .header {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: $header-height;
+    width: 100%;
+    background: linear-gradient(to bottom, #060624ba 30%, #06062466 80%, #06062400 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fefefe;
+    font-size: 20px;
+    font-weight: bolder;
+  }
+
+  .left-container {
+    position: absolute;
+    top: $header-height;
+    left: 0;
+    height: calc(100% - #{$header-height});
+    width: 20%;
+    box-sizing: border-box;
+    background: linear-gradient(180deg, #060624ba 50%, #06062400 100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: flex-start;
+  }
+
+  .right-container {
+    position: absolute;
+    top: $header-height;
+    right: 14px;
+    width: 25%;
+    box-sizing: border-box;
+    background: #060624ba;
+    border-radius: 10px;
+    padding: 14px;
+  }
+
+  .menus {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+    left: 0;
+    width: 150px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: column;
+
+    :not(:first-child) {
+      margin-top: 20px;
+    }
+    .menu-item {
+      height: 80px;
+      width: 70px;
+      background: rgba(0, 150, 253, 0.6);
+      border-radius: 5px;
+      display: flex;
+      align-items: center;
+      justify-content: space-evenly;
+      flex-direction: column;
+      cursor: pointer;
+      color: #fefefe;
+      font-size: 14px;
+      font-weight: bolder;
+      box-shadow: #000000 4px 1px 12px 0px;
+
+      &:hover {
+        background: rgba(0, 150, 253, 1);
+      }
+
+      &.active {
+        background: rgba(0, 150, 253, 1);
+      }
+
+      .svg-icon {
+        width: 30px;
+        height: 30px;
+        flex-shrink: 0;
+      }
+    }
+  }
+}

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

@@ -0,0 +1,149 @@
+<template>
+  <div class="bd-container">
+    <bd-map :loaded="loaded" />
+    <div class="header">北斗综合管理</div>
+    <div class="menus">
+      <template v-for="item in Object.keys(menus)">
+        <div
+            :key="item"
+            :class="{'menu-item':true,'active': activeMenu[menus[item].name]} "
+            :data-type="item"
+            @click="menuClick"
+        >
+          <svg-icon :icon-class="menus[item].icon"></svg-icon>
+          {{ menus[item].title }}
+        </div>
+      </template>
+    </div>
+    <video-wall></video-wall>
+    <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]" />
+      <realtime-location v-if="activeMenu[menus.realtime_location.name]"></realtime-location>
+      <room-location v-if="activeMenu[menus.room_location.name]"></room-location>
+    </div>
+  </div>
+</template>
+
+<script>
+
+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';
+import VideoWall from "@/views/bd/VideoWall/index.vue";
+
+const menus = {
+  evt: {
+    name: 'evt',
+    title: '围栏闯禁',
+    icon: 'bd_evt',
+  },
+  fence: {
+    name: 'fence',
+    title: '围栏管理',
+    icon: 'bd_fence',
+  },
+  location_tail: {
+    name: 'location_tail',
+    title: '轨迹回放',
+    icon: 'bd_location',
+  },
+  realtime_location: {
+    name: 'realtime_location',
+    title: '实时定位',
+    icon: 'bd_real_time',
+  },
+  room_location: {
+    name: 'room_location',
+    title: '室内定位',
+    icon: 'bd_room_location',
+  },
+};
+export default {
+  name: 'bd',
+  computed: {
+    menu() {
+      return menu;
+    },
+  },
+  components: {
+    VideoWall,
+    RoomLocation,
+    RealtimeLocation,
+    FenceVioEvt,
+    Fence,
+    Location,
+    BdMap,
+  },
+  data() {
+    return {
+      menus,
+      mapLoaded: false,
+      activeMenu: {
+        [menus.evt.name]: true,
+      },
+    };
+  },
+  created() {
+  },
+  methods: {
+    loaded(map) {
+      window.map = map;
+      const protocolAndHost = window.location.protocol + '//' + window.location.host;
+      // map.setPitch(0)
+      const d3tiles = new BDLayers.Lib.Layer.CB3DtilesLayer('3dtiles', {
+        offet: {
+          from: 'GCJ02',
+          to: 'WGS84',
+        },
+        offset: [13.4, -3.5],
+        mapView: this.mapView,
+        maxCacheSize: 10000,
+        loadingLimit: 10,
+        services: [
+          {
+            url: `${protocolAndHost}${process.env.VUE_APP_BD_BASE_API}/folder/zy9/tileset.json`, //"http://resource.dvgis.cn/data/3dtiles/dayanta/tileset.json",
+            maximumScreenSpaceError: 16.0,
+            heightOffset: -48, //-420,
+          },
+        ],
+      });
+      if (process.env.VUE_APP_3D_SWITCH === 'true') {
+        map.addCustomLayers(d3tiles, -1, true);
+      }
+      this.mapLoaded = true;
+    },
+    menuClick(e) {
+      const type = e.currentTarget.dataset.type;
+      this.activeMenu[type] = !this.activeMenu[type];
+      const result = {
+        ...this.activeMenu,
+      };
+      const selectedTogether = [this.menus.evt.name, this.menus.realtime_location.name, this.menus.room_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;
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss" />

+ 36 - 0
ruoyi-ui/src/views/bd/location/index.scss

@@ -0,0 +1,36 @@
+.devc-location-container {
+  .header-ctrl {
+    margin-bottom: 10px;
+    .el-date-editor{
+      margin-left: 10px;
+      .el-input__inner{
+        background-color: transparent;
+        color: #fefefe;
+        border: 1px solid #dcdfe663;
+      }
+      &.el-date-editor--datetimerange.el-input__inner{
+        background-color: transparent;
+        border: 1px solid #dcdfe663;
+        width: 300px;
+        .el-range-input{
+          background-color: transparent;
+          color: #fefefe;
+        }
+      }
+      .el-range-separator{
+        color: #fefefe;
+      }
+    }
+    .el-radio-button {
+      cursor: pointer;
+
+      &:not(.is-active) {
+        .el-radio-button__inner {
+          background-color: transparent;
+          border-color: #dcdfe663;
+          color: #fefefe;
+        }
+      }
+    }
+  }
+}

+ 300 - 0
ruoyi-ui/src/views/bd/location/index.vue

@@ -0,0 +1,300 @@
+<template>
+  <pannel class="devc-location-container">
+    <template v-slot:icon>
+      <svg-icon icon-class="bd_location"/>
+    </template>
+    <template v-slot:title>
+      轨迹查询
+    </template>
+    <template v-slot:action>
+      <i
+        class="el-icon-delete-location"
+        title="清空所有轨迹"
+        @click="clearAll"
+      />
+    </template>
+    <template v-slot:content>
+      <div class="header-ctrl">
+        <el-radio-group v-model="dateType" size="mini" @change="onDateTypeChange">
+          <template v-for="item in Object.keys(DateTypeRadio)">
+            <el-radio-button :label="item">{{ DateTypeRadio[item].name }}</el-radio-button>
+          </template>
+        </el-radio-group>
+        <el-date-picker
+          v-if="dateType === DateTypeRadio.day.key"
+          key="day"
+          v-model="selectDate"
+          align="right"
+          type="date"
+          placeholder="选择日期"
+          :picker-options="pickerOptions"
+          size="mini"
+        >
+        </el-date-picker>
+        <el-date-picker
+          key="cust-day"
+          format="yyyy-MM-dd HH:mm"
+          v-if="dateType === DateTypeRadio.cust.key"
+          v-model="selectDate"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          size="mini"
+        >
+        </el-date-picker>
+      </div>
+      <div class="location-list">
+        <div class="list-item">
+          <span class="over-flow-hidden" style="width: 40%">
+            园区安保巡逻轨迹
+          </span>
+          <span class="over-flow-hidden" style="width: 40%">
+            2024-08-21 15:30
+          </span>
+          <span class="over-flow-hidden" style="width: 20%">
+            <i
+              :class="playItem[1]&&playItem[1].play ? 'el-icon-video-pause':'el-icon-video-play'"
+              :title="playItem[1]&&playItem[1].play ? '暂停': '开始播放' "
+              @click="()=>onLocationPlay({key:1})"
+            />
+            <i class="el-icon-refresh-left" title="重播" @click="()=>replayLocation({key:1})"/>
+            <i class="el-icon-circle-close" title="清空轨迹" @click="()=>clearLocation({key:1})"/>
+          </span>
+        </div>
+      </div>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import Pannel from '@/views/bd/pannel/index.vue';
+
+const DateTypeRadio = {
+  day: {
+    key: 'day',
+    name: '日',
+  },
+  cust: {
+    key: 'cust',
+    name: '自定义',
+  }
+}
+export default {
+  name: 'location',
+  components: {Pannel},
+  data() {
+    return {
+      DateTypeRadio,
+      playItem: {},
+      selectDate: new Date(),
+      dateType: DateTypeRadio.day.key,
+      pickerOptions: {
+        disabledDate(time) {
+          return time.getTime() > Date.now();
+        },
+        shortcuts: [{
+          text: '今天',
+          onClick(picker) {
+            picker.$emit('pick', new Date());
+          }
+        }, {
+          text: '昨天',
+          onClick(picker) {
+            const date = new Date();
+            date.setTime(date.getTime() - 3600 * 1000 * 24);
+            picker.$emit('pick', date);
+          }
+        }, {
+          text: '一周前',
+          onClick(picker) {
+            const date = new Date();
+            date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
+            picker.$emit('pick', date);
+          }
+        }]
+      },
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.clearAll();
+  },
+  created() {
+    this.init();
+  },
+  methods: {
+    init() {
+      debugger
+
+      this.socket = new WebSocket(`ws://172.192.13.77:2223/socket/websocket/pollingArea`);
+
+      this.socket.onopen = () => {
+        this.socket.send(JSON.stringify({"register":"1731296055163","token":"9358d080-03d4-423c-91d4-d4d2a391183d"}));
+        console.log('连接成功:', this.socket);
+
+      };
+      // 监听socket错误信息
+      this.socket.onerror = (e) => {
+        console.log('%c FXY', 'color:#f6b2b1;font-size:50px', e);
+        console.log('socket连接失败');
+      };
+      // 监听socket关闭监听
+      this.socket.onclose = () => {
+        console.log('socket连接关闭');
+      };
+      this.socket.onmessage = (a) => {
+        const data = JSON.parse(a.data);
+        console.log(">>>>>>>>>",data);
+        if (data && data.message === "handshake"){
+          // tagId要去掉前缀0
+          this.socket.send(JSON.stringify({"key": "1731296055163","tagId": "847F3", "token": "9358d080-03d4-423c-91d4-d4d2a391183d"}));
+        }
+      };
+    },
+    onDateTypeChange(e, eh) {
+      if (e === DateTypeRadio.day.key) {
+        this.selectDate = new Date();
+      } else if (e === DateTypeRadio.cust.key) {
+        const date = new Date();
+        date.setTime(date.getTime() - 3600 * 1000 * 24);
+        this.selectDate = [date, new Date()];
+      }
+    },
+    getDevcTrail() {
+
+    },
+    loaded(map) {
+      console.log(map);
+    },
+    onLocationPlay({key}) {
+      if (this.playItem[key]) {
+        if (this.playItem[key].play) {
+          bdmap.comp.Trajectory.trajectoryPause(this.playItem[key].line);
+        } else {
+          bdmap.comp.Trajectory.trajectoryPlay(this.playItem[key].line);
+        }
+        this.playItem = {
+          [key]: {
+            ...this.playItem[key],
+            play: !this.playItem[key].play,
+          },
+        };
+      } else {
+        this.clearAll();
+        this.playItem = {
+          [key]: {
+            play: true,
+          },
+        };
+
+        this.playItem[key].line = this.startPlay();
+      }
+    },
+    startPlay() {
+      const coordinates = [
+        [118.868474555524, 32.013913750075],
+        [118.868459917135, 32.0138560330253],
+        [118.868460650064, 32.0137600494265],
+        [118.868469791963, 32.0135491295906],
+        [118.868450597046, 32.0135969285238],
+        [118.868451821878, 32.0134372342673],
+        [118.868369085172, 32.0134271059761],
+        [118.868229813542, 32.0134261784667],
+        [118.868037844538, 32.0134249000077],
+        [118.867988911263, 32.013424574126],
+        [118.867990212059, 32.0132809893746],
+        [118.867986825628, 32.0132395072755],
+        [118.867987028075, 32.0132171885197],
+        [118.867987432888, 32.0131725599655],
+        [118.867984539806, 32.0130769425047],
+        [118.867977454441, 32.0130291167663],
+        [118.86797791828, 32.0129781711549],
+        [118.867978150146, 32.0129527041872],
+        [118.867974855578, 32.0129017568948],
+        [118.868046176274, 32.0129117791912],
+        [118.868121507934, 32.0128931867922],
+        [118.868117943319, 32.0128708893801],
+        [118.868126616248, 32.012737367597],
+        [118.868127634971, 32.0126197858525],
+        [118.868127662494, 32.0126166091195],
+        [118.868165320753, 32.0126041528561],
+        [118.868173101262, 32.0125724412675],
+        [118.868240681514, 32.0125728913369],
+        [118.868308235972, 32.0125765173023],
+        [118.868334874981, 32.0125322352762],
+        [118.868324305199, 32.012446455205],
+        [118.868361835438, 32.0124467051483],
+        [118.868364679658, 32.0125610134717],
+        [118.868492254263, 32.0125713907483],
+        [118.868541014618, 32.0125780675563],
+        [118.868732580782, 32.012566639437],
+        [118.868841456955, 32.0125673645282],
+        [118.868976574771, 32.0125746162152],
+        [118.869070453844, 32.0125720654824],
+        [118.869096734551, 32.0125722405062],
+        [118.869156822422, 32.0125694647922],
+        [118.869179313588, 32.0125759664106],
+        [118.869201684821, 32.0126047016424],
+        [118.869186178487, 32.0126935647542],
+        [118.869193065594, 32.0128080658432],
+        [118.869192510216, 32.0129098661745],
+        [118.86919198924, 32.0130053604978],
+        [118.869179854563, 32.0131613771548],
+        [118.869190250492, 32.0133240705021],
+        [118.869185964264, 32.0134197772962],
+        [118.86917071489, 32.0134547924615],
+        [118.869159014503, 32.0135281642937],
+        [118.869166174307, 32.0135953030259],
+        [118.869165521059, 32.0137135723353],
+        [118.869187426642, 32.0138416759025],
+        [118.869153188005, 32.0138990608231],
+        [118.868990969654, 32.0139203908971],
+        [118.868768561862, 32.0139189097324],
+        [118.868572541435, 32.0139176042992],
+        [118.868557462941, 32.0139175038812],
+        [118.868474555524, 32.013913750075],
+      ];
+      // 19FB03
+      let playLine = bdmap.comp.Trajectory.loadTrajectory({
+        mapView: window.map,
+        latlons: coordinates,
+        unitTime: 500,
+        speedValue: 1,
+        mapToCenter: false,
+      });
+      bdmap.comp.Trajectory.trajectoryPlay(playLine);
+      return playLine;
+    },
+    replayLocation({key}) {
+      if (this.playItem[key] && this.playItem[key].line) {
+        bdmap.comp.Trajectory.trajectoryCancel(this.playItem[key].line);
+        this.playItem[key].play = false;
+      }
+
+    },
+    removeLine({key}) {
+      if (this.playItem[key] && this.playItem[key].line) {
+        bdmap.comp.Trajectory.trajectoryRemove(this.playItem[key].line);
+        delete this.playItem[key];
+      }
+
+    },
+    clearAll() {
+      Object.keys(this.playItem).forEach(keyItem => {
+        if (this.playItem[keyItem] && this.playItem[keyItem].line) {
+          this.removeLine({key: keyItem});
+        }
+      });
+      this.playItem = {};
+    },
+    clearLocation({key}) {
+      this.removeLine({key});
+      this.playItem = {};
+    },
+
+  },
+};
+</script>
+<style lang="scss" src="./index.scss"/>

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

@@ -0,0 +1,106 @@
+<template>
+  <div :id="mapId" class="map" style="width: 100%;height: 100%">
+
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'bd-map',
+  props: {
+    mapId: {
+      type: String,
+      default: 'map',
+    },
+    loaded: {
+      type: Function,
+      default: () => {},
+    },
+  },
+  mounted() {
+    this.$nextTick(() => {
+      const options = {
+        mapType: BDLayers.Lib.Constant.BaseLayerType.Blank,
+        mapModel: BDLayers.Lib.Constant.BaseLayerModel.Satellite,
+        center: [118.8738802982145, 32.010241883966096],
+        defaultZoom: 15,
+        showCenter: true,
+        baseControl: false,
+        pitch: 45,
+        lights: {
+          directional: {
+            direction: [-1, -1, -1],
+            color: [1, 1, 1],
+          },
+          ambient: {
+            exposure: 1.426,
+            hsv: [0, 0, 0],
+            orientation: 302.553,
+          },
+        },
+      };
+      bdmap.mapWrapper.loadMainMap(this.mapId, options);
+      this.mapView = bdmap.mapWrapper.MainMap(this.mapId);
+      this.mapView.createGLScense({
+        sceneConfig: {
+          environment: {
+            enable: true, // 是否开启环境天空盒绘制
+            mode: 1, // 天空盒模式: 0: 氛围模式, 1: 实景模式
+            level: 0, // 实景模式下的模糊级别,0-3
+            brightness: 0.915, // 天空盒的明亮度,-1 - 1, 默认为0
+          },
+          postProcess: {
+            enable: true, // 是否开启后处理
+          },
+          ground: {
+            enable: true, // 是否开启地面绘制
+            renderPlugin: { // 地面的绘制插件,取值范围 lit(pbr) 或者 fill(phong)
+              type: 'lit', // 用pbr材质渲染三维面数据的渲染插件
+            },
+            symbol: {
+              polygonOpacity: 1, // 透明度 0-1
+              material: { // 如果绘制插件为lit,设置pbr材质
+                baseColorFactor: [0.48235, 0.48235, 0.48235, 1], // 基础色,四位归一化数组
+                hsv: [
+                  0, 0, -
+                      0.532,
+                ], // hsv颜色参数,三位分别是hue,saturation,value,即色相,饱和度和明度,每一位的取值范围都是0-1
+                roughnessFactor: 0.22, // 粗糙度,取值范围 0 - 1,0为最光滑,1为最粗糙
+                metallicFactor: 0.58, // 金属度,取值范围 0 - 1,0为非金属,1为金属
+              },
+            },
+          },
+        },
+      });
+      // http://200.200.19.253:31838/bdgis/folder/zy9?time=1728373968099
+      const satelliteTileLayer = new BDLayers.Lib.Layer.CBTileLayer('blayer_tdt_s', {
+        url: [
+          'http://t{s}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=' + BDLayers.Lib
+              .Constant.WebMapKeys.Tianditu,
+          'http://t{s}.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=' + BDLayers.Lib
+              .Constant.WebMapKeys.Tianditu,
+        ],
+        subdomains: '01234567',
+        maxZoom: [18, 18],
+        minZoom: [3, 3],
+      });
+      this.mapView.addCustomLayers(satelliteTileLayer, -1, true);
+      this.mapView.on('click', function (param) {
+        console.log(new Date().toLocaleTimeString(),
+            'click map on', param.coordinate.toFixed(5).toArray().join());
+      });
+      this.loaded(this.mapView);
+    });
+  },
+  data() {
+    return {
+      mapView: null,
+    };
+  },
+  created() {
+  },
+  methods: {},
+};
+</script>
+

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

@@ -0,0 +1,99 @@
+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,
+               altitude = 0,
+               symbol = {},
+               bizAttr = {},
+               labelSymbol = {},
+               polyOnClick = () => {
+               },
+             }, polyLayer) {
+      const polygonId =`auto_poly_id${uuid()}`;
+      let symbolParam = {
+        lineColor: '#34495e',
+        lineWidth: 2,
+        polygonFill: '#1bbc9b',
+        polygonOpacity: 0.4,
+      };
+      let labelSymbolParam = {
+        labelText: name,
+        labelTextColor: '#fefefe',
+        labelTextSize: 15,
+      };
+      Object.assign(labelSymbolParam, labelSymbol);
+      Object.assign(symbolParam, symbol);
+      const polygon = new BDLayers.Lib.Overlays.Polygon('p1', {
+        coordinates: coordinates,
+        altitude,
+        symbol: symbolParam,
+        labelSymbol: labelSymbolParam,
+        bizAttr: {...bizAttr,polygonId},
+      });
+      //多边形的点击事件
+      polygon.on('click', polyOnClick);
+      if (!polyLayer) {
+        this.polyLayer.addGeometry(polygon);
+      } else {
+        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 [];
+    },
+    createLayer(mapIns) {
+      const uid = uuid();
+      const layerId = `poly_layer${uid}`;
+      const polyLayer = new BDLayers.Lib.Layer.CBVectorLayer(
+        layerId, {enableAltitude: true});
+      polyLayer.cust = {
+        layerId: uid,
+        mapIns,
+        clearLayer: (layer) => {
+          mapIns.removeLayersById(layerId);
+        }
+      }
+      mapIns.addCustomLayers(polyLayer, 1);
+      return polyLayer;
+    },
+    clearLayer(layer) {
+      if (layer) {
+        layer.removeAll();
+        return;
+      }
+      this.polyLayer && this.polyLayer.removeAll();
+    },
+  },
+  beforeDestroy() {
+    this.polyLayer && window.map.removeLayersById(`poly_layer${this.layerId}`);
+  },
+};

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

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

+ 102 - 0
ruoyi-ui/src/views/bd/mobile/index.vue

@@ -0,0 +1,102 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:title>
+      围栏闯禁事件
+    </template>
+    <template v-slot:action>
+
+    </template>
+    <template v-slot:content>
+       <div>
+        <socket-message :onMessage="onMessage" :ws="ws"></socket-message>
+      </div>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import { listFenceVioEvt } from '@/api/bd/fenceVioEvt';
+import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import Pannel from '@/views/bd/pannel/index.vue';
+import FingerprintJS from '@fingerprintjs/fingerprintjs';
+import dayjs from 'dayjs';
+
+export default {
+  name: 'fenceVioEvt',
+  components: {
+    SocketMessage,
+    Pannel,
+  },
+  data() {
+    return {
+      evtList: [],
+      fp: null,
+      ws: null,
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+  },
+  created() {
+
+  },
+  mounted() {
+    this.getFenceVioEvtList();
+    this.getFingerprint();
+  },
+  methods: {
+    dayjs,
+    async getFenceVioEvtList() {
+      const { rows } = await listFenceVioEvt({
+        pageNum: 1,
+        pageSize: 10,
+      });
+      this.evtList = rows;
+      debugger
+    },
+    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);
+      this.$notify({
+        title: '警告',
+        message: `${data.msg.fenceName}发生闯禁事件。`,
+        type: 'warning',
+      });
+      this.getFenceVioEvtList();
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss" />

二進制
ruoyi-ui/src/views/bd/pannel/img/bg_evt_stat.png


+ 37 - 0
ruoyi-ui/src/views/bd/pannel/img/bg_main_title_bg.svg

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="342px" height="20px" viewBox="0 0 342 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>bg_标题_342_n</title>
+    <defs>
+        <linearGradient x1="87.5410483%" y1="50.0468908%" x2="1.31711231%" y2="50%" id="linearGradient-1">
+            <stop stop-color="#4F9FFF" stop-opacity="0" offset="0%"></stop>
+            <stop stop-color="#4F9FFF" stop-opacity="0.24728799" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="87.5410483%" y1="50.008513%" x2="1.31711231%" y2="50%" id="linearGradient-2">
+            <stop stop-color="#4F9FFF" stop-opacity="0" offset="0%"></stop>
+            <stop stop-color="#4F9FFF" stop-opacity="0.5" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="87.5410483%" y1="50.0004689%" x2="1.31711231%" y2="50%" id="linearGradient-3">
+            <stop stop-color="#4F9FFF" stop-opacity="0" offset="0%"></stop>
+            <stop stop-color="#4F9FFF" stop-opacity="0.5" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="综合监测-大屏" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="1-1综合监测-宏观" transform="translate(-1554.000000, -118.000000)">
+            <g id="重点关注事件" transform="translate(1548.000000, 92.000000)">
+                <g id="bg_标题_342_n" transform="translate(6.000000, 12.000000)">
+                    <g transform="translate(0.000000, 14.000000)" id="矩形">
+                        <rect fill="url(#linearGradient-1)" x="6" y="0" width="336" height="20"></rect>
+                        <rect fill="#4F9FFF" x="3" y="4" width="4" height="4"></rect>
+                        <rect fill="#4F9FFF" x="0" y="11" width="2" height="2"></rect>
+                        <rect fill="#4F9FFF" x="6" y="13" width="3" height="3"></rect>
+                        <rect fill="#4F9FFF" x="8" y="8" width="3" height="3"></rect>
+                        <rect fill="#0C1E33" x="13" y="5" width="3" height="3"></rect>
+                        <rect fill="#4F9FFF" x="5" y="10" width="2" height="2"></rect>
+                        <polygon fill="url(#linearGradient-2)" points="34 5 136.07803 5 141.200326 9 310 12 34 12"></polygon>
+                        <rect fill="url(#linearGradient-3)" x="6" y="18" width="336" height="2"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 77 - 0
ruoyi-ui/src/views/bd/pannel/index.scss

@@ -0,0 +1,77 @@
+.pannel {
+  width: 100%;
+
+  .pannel-header {
+    width: 100%;
+    height: 40px;
+    padding: 0 10px;
+    margin: 0;
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    background: url("./img/bg_main_title_bg.svg") 100% 100% no-repeat;
+    background-size: contain;
+    font-family: PingFangSC-Medium;
+    font-size: 16px;
+    color: #E8F3FF;
+    letter-spacing: 0;
+    text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.50);
+    font-weight: 500;
+    .svg-icon{
+      width: 20px;
+      height: 20px;
+      margin-right: 14px;
+    }
+    > i {
+      margin-left: auto;
+
+      &:hover {
+        color: #00afff;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .pannel-body {
+    width: 100%;
+    margin-top: 10px;
+  }
+}
+.location-list {
+  width: 100%;
+  padding: 0 10px;
+
+  .list-item {
+    padding: 0 10px;
+    height: 30px;
+    box-sizing: border-box;
+    background: url("./img/bg_evt_stat.png") 100% 100% no-repeat;
+    background-size: 100% 100%;
+    color: #fefefe;
+    display: flex;
+    align-items: flex-start;
+    justify-content: flex-start;
+
+    > span {
+      &:not(:first-child) {
+        margin-left: 10px;
+      }
+
+      i {
+        &:hover {
+          color: #00afff;
+          cursor: pointer;
+        }
+
+        &:not(:first-child) {
+          margin-left: 5px;
+        }
+      }
+    }
+  }
+}
+.over-flow-hidden {
+  white-space: nowrap; //强制文本在一行内输出
+  overflow: hidden; //隐藏溢出部分
+  text-overflow: ellipsis; //对溢出部分加上...
+}

+ 26 - 0
ruoyi-ui/src/views/bd/pannel/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="devc-location-container pannel">
+    <div class="pannel-header">
+      <slot name="icon" />
+      <slot name="title" />
+      <slot name="action" />
+    </div>
+    <div class="pannel-body">
+      <slot name="content" />
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'pannel',
+  data() {
+    return {};
+  },
+  created() {
+  },
+  methods: {},
+};
+</script>
+<style lang="scss" src="./index.scss" />

二進制
ruoyi-ui/src/views/bd/realtimeLocation/icon/position.png


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

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

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

@@ -0,0 +1,218 @@
+<template>
+  <pannel class="fence-location-container">
+    <template v-slot:icon>
+      <svg-icon icon-class="bd_real_time"/>
+    </template>
+    <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%">
+              <svg-icon :icon-class="`${selectedFlag[device.deviceId]?'bd_signal':'bd_signal_off'}`"
+                        title="查看设备定位"
+                        @click="()=>showLocation(device)"
+              />
+              <!--               <i-->
+              <!--                   class="el-icon-circle-close"-->
+              <!--                   title="查看设备定位"-->
+              <!--                   @click="()=>play(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 {pushDevcLocation} from '@/api/bd/fenceInfo';
+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.png';
+
+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: [],
+      playInterval: null,
+      selectedFlag: {},
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && window.map.removeLayersById('markerLayer');
+    this.playInterval && clearInterval(this.playInterval);
+  },
+  created() {
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('markerLayer', true);
+    window.map.addCustomLayers(this.markLayer, 5);
+  },
+  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: [50, 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);
+        this.selectedFlag = {
+          ...this.selectedFlag,
+          [device.deviceId]: false,
+        };
+        return;
+      }
+      this.selectedDevice.push(device);
+      this.selectedFlag = {
+        ...this.selectedFlag,
+        [device.deviceId]: true,
+      };
+      this.selectedDevice = [...this.selectedDevice];
+      this.play();
+    },
+    onMessage(a) {
+      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);
+    },
+    play() {
+      this.playInterval && clearInterval(this.playInterval);
+      const gps =
+          [
+            [118.868446580271, 32.0123378608057],
+            [118.868406952344, 32.0123370689339],
+            [118.868366815026, 32.0123546053505],
+            [118.868358676065, 32.0123880632266],
+            [118.868350452205, 32.0124245758027],
+            [118.86834900893, 32.0124765058939],
+            [118.868344642304, 32.012503926295],
+            [118.868332136717, 32.0125648044994],
+            [118.868320565014, 32.0125920808978],
+            [118.86830547567, 32.0126162305943],
+            [118.868297336709, 32.0126496883748],
+            [118.868296572622, 32.0126771807122],
+            [118.868295214246, 32.0127260559583],
+            [118.868293601174, 32.0127840952791],
+            [118.868289064751, 32.0128176249864],
+            [118.868281265384, 32.012838863901],
+            [118.868280671094, 32.0128602467866],
+            [118.868290375029, 32.0129001738105],
+            [118.86833686844, 32.0129133284373],
+            [118.868358313875, 32.0129198697561],
+            [118.868408240027, 32.0129392057582],
+            [118.868415360206, 32.0129424044289],
+            [118.868425828228, 32.012954839171],
+            [118.86843277861, 32.0129641472296],
+            [118.868435786859, 32.0129856020738],
+            [118.868442737241, 32.0129949101293],
+            [118.868445915288, 32.0130102555814],
+            [118.868445066302, 32.0130408025022],
+            [118.868447395363, 32.013086694852],
+            [118.868446716175, 32.0131111323691],
+            [118.868446036987, 32.0131355698797],
+            [118.868444933306, 32.0131752808204],
+            [118.868447517062, 32.0132120090459],
+            [118.868446668077, 32.0132425558994],
+            [118.868445479497, 32.0132853214773],
+            [118.868463492191, 32.0132856814153],
+            [118.868506722657, 32.0132865452666]
+          ];
+      let i = 0;
+      this.playInterval = setInterval(() => {
+        if (i === gps.length - 1) {
+          return;
+        }
+        pushDevcLocation({
+          devcKey: 'uwb001',
+          lat: gps[i][1],
+          lng: gps[i][0],
+        });
+        i++;
+      }, 2000);
+
+    },
+  },
+};
+</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>

二進制
ruoyi-ui/src/views/bd/roomlocation/icon/room_location.jpg


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

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

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

@@ -0,0 +1,135 @@
+<template>
+  <pannel class="rooms-container">
+    <template v-slot:icon>
+      <svg-icon icon-class="bd_room_location" />
+    </template>
+    <template v-slot:title>
+      室内定位
+    </template>
+    <template v-slot:action>
+
+    </template>
+    <template v-slot:content>
+      <div class="location-list">
+        <template v-for="room in roomList">
+          <div :key="`room_${room.id}`" class="list-item">
+          <span class="over-flow-hidden" style="width: 80%">
+            {{ room.room }}
+          </span>
+            <span class="over-flow-hidden" style="width: 20%">
+          </span>
+          </div>
+        </template>
+        <room-map ref="roomMap"></room-map>
+        <socket-message :onMessage="onMessage" :ws="ws"></socket-message>
+      </div>
+    </template>
+  </pannel>
+</template>
+
+<script>
+
+import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import { uuid } from '@/utils';
+import maphandle from '@/views/bd/map/maphandle';
+import Pannel from '@/views/bd/pannel/index.vue';
+import RoomMap from '@/views/bd/roomlocation/roommap/index.vue';
+import dayjs from 'dayjs';
+import room_location from './icon/room_location.jpg';
+
+export default {
+  name: 'room-location',
+  mixins: [maphandle],
+  components: {
+    RoomMap,
+    SocketMessage,
+    Pannel,
+  },
+  data() {
+    return {
+      roomList: [],
+      fp: null,
+      ws: null,
+      markLayer: null,
+      markLayerId: '',
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && window.map.removeLayersById(this.markLayerId);
+  },
+  created() {
+    if (this.markLayer) {
+      return;
+    }
+    this.markLayerId = `real_time_markerLayer${uuid()}`;
+    this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer(this.markLayerId, {
+      enableAltitude: true,
+    });
+    window.map.addCustomLayers(this.markLayer, 5);
+  },
+  mounted() {
+    this.getRoom();
+  },
+  methods: {
+    dayjs,
+    getRoom() {
+      this.roomList = [
+        {
+          id: 1,
+          room: '创研院C4',
+          poly: 'POLYGON((118.869042069359 32.0131156239015, 118.86839482667 32.0131180416999, 118.868399686907 32.0133391503578, 118.869046506393 32.0133281708171, 118.869042069359 32.0131156239015))',
+          altitude: 11.4,
+        },
+      ];
+      // this.roomList.forEach(item => {
+      //   const {
+      //     id,
+      //     room,
+      //     poly,
+      //     altitude,
+      //   } = item;
+      //   const polygon = this.drawPoly({
+      //     name: room,
+      //     altitude,
+      //     coordinates: this.polygonToCoordinates(poly),
+      //     bizAttr: {
+      //       id,
+      //       name: room,
+      //     },
+      //   });
+      // });
+      // var marker = new BDLayers.Lib.Overlays.UIMarker('marker00', [118.869042069359, 32.0131156239015],
+      //     {
+      //       'draggable': false,
+      //       'single': false,
+      //       'content': `<div class="text_marker" style="color:#fff"><img src="${room_location}" width="40px" height="40px"/></div>`,
+      //       altitude: 800,
+      //     });
+      var marker = new BDLayers.Lib.Overlays.MarkerImg(
+          'marker01',
+          [118.869046506393,32.0133281708171], {
+            imgurl: room_location,
+            iconSize: [90, 54],
+            altitude: 20,
+          },
+      );
+      marker.on('click', () => {
+        this.$refs.roomMap.show();
+      });
+      this.markLayer.addMarker(marker);
+      // marker.getMarker().addTo(window.map.map);
+    },
+    onMessage(a) {
+      const data = JSON.parse(a.data);
+      console.log('>>>>>>>>>>>>>>>>>>>>>>>>', data);
+      this.$notify({
+        title: '警告',
+        message: `${data.msg.fenceName}发生闯禁事件。`,
+        type: 'warning',
+      });
+    },
+  },
+};
+</script>
+<style lang="scss" src="./index.scss" />

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

@@ -0,0 +1,228 @@
+<template>
+  <el-dialog
+      title="室内定位融合"
+      :visible.sync="centerDialogVisible"
+      width="70vw"
+      @closed="onDialogClose"
+      @opened="play"
+      center>
+    <div style="width: 100%;height: 70vh;position: relative">
+      <bd-map :loaded="loaded" map-id="room-map"/>
+      <video-p/>
+      <socket-message
+          v-if="centerDialogVisible"
+          :key="`device_uwb001`" :onMessage="onMessage"
+          :ws="`/ws/point/uwb001`"></socket-message>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+
+import {listFenceInfo, pushDevcLocation} from '@/api/bd/fenceInfo';
+import SocketMessage from '@/components/WebsocketMessage/index.vue';
+import BdMap from '@/views/bd/map/index.vue';
+import maphandle from '@/views/bd/map/maphandle';
+import position from '@/views/bd/realtimeLocation/icon/position.png';
+import dayjs from 'dayjs';
+import VideoP from "@/components/video";
+
+export default {
+  name: 'room-map',
+  mixins: [maphandle],
+  components: {
+    VideoP,
+    BdMap,
+    SocketMessage,
+  },
+  data() {
+    return {
+      centerDialogVisible: false,
+      locationMarkers: {},
+      mapIns: null,
+      playInterval: null,
+      fencePolyLayer: null,
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    this.markLayer && this.mapIns.removeLayersById('markerLayer');
+    this.fencePolyLayer && this.fencePolyLayer.cust.clearLayer()
+    this.playInterval && clearInterval(this.playInterval);
+  },
+  created() {
+
+  },
+  mounted() {
+  },
+  methods: {
+    dayjs,
+    onDialogClose() {
+      this.playInterval && clearInterval(this.playInterval);
+    },
+    loaded(map) {
+      const vtLayer = new BDLayers.Lib.Layer.CBTileLayer('blayer_ng_s', {
+        url: 'http://bd.xt.wenhq.top:8083/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=bdlayers:bdl_cyy_pingmian&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&Format=image/jpeg&TILECOL={x}&TILEROW={y}',
+        maxZoom: 24,
+        minZoom: 15,
+      });
+      map.addCustomLayers(vtLayer);
+      this.markLayer = new BDLayers.Lib.Layer.CBVectorLayer('markerLayer', true);
+      map.addCustomLayers(this.markLayer);
+      this.mapIns = map;
+      map.setPitch(0);
+      map.setCenter([118.86895, 32.01326]);
+      this.fencePolyLayer = this.createLayer(map);
+      this.getFenceList();
+    },
+    show() {
+      this.centerDialogVisible = true;
+    },
+    addMarker(data) {
+      const {
+        latitude,
+        longitude,
+        deviceId,
+      } = data;
+      const marker = new BDLayers.Lib.Overlays.MarkerImg(
+          `marker${deviceId}`,
+          [longitude, latitude],
+          {
+            imgurl: position,
+            iconSize: [25, 30],
+            symbol: {
+              'textName': 'm4',
+              'textSize': 14,
+              'markerFile': position,
+              'markerHorizontalAlignment': 'middle', // left, middle(默认), right
+              'markerVerticalAlignment': 'middle', // top, middle, bottom(默认)
+            },
+          },
+      );
+      this.markLayer.addMarker(marker);
+      return marker;
+    },
+    async getFenceList() {
+      const {rows} = await listFenceInfo({
+        pageNum: 1,
+        pageSize: 10,
+        fenceType: 2,
+      });
+      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),
+          symbol: {
+            lineColor: 'rgba(241,0,23,0.49)',
+            lineWidth: 2,
+            polygonFill: 'rgba(241,0,23,0.49)',
+            polygonOpacity: 0.4,
+          },
+          bizAttr: {
+            id,
+            name: defenceName,
+          },
+        }, this.fencePolyLayer);
+        result.push({
+          id,
+          name: defenceName,
+          polygon,
+        });
+      });
+    },
+    onMessage(a) {
+      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);
+    },
+    play() {
+      if (!this.mapIns) {
+        return;
+      }
+      this.mapIns.setZoom(22)
+      const gps = [
+        [118.868928794571, 32.0132512031322],
+        [118.868928459294, 32.0132364200935],
+        [118.868929129847, 32.0132159512668],
+        [118.868929129847, 32.0132108340593],
+        [118.868929129847, 32.0132057168517],
+        [118.868930135675, 32.0132017368011],
+        [118.868963998564, 32.0132008839331],
+        [118.868967686601, 32.0132025896691],
+        [118.868967351325, 32.0132116869273],
+        [118.868968021877, 32.0132185098704],
+        [118.868967016049, 32.0132259013915],
+        [118.868966680773, 32.0132313028872],
+        [118.868967016049, 32.0132440959023],
+        [118.868967016049, 32.0132500659755],
+        [118.868967016049, 32.0132526245781],
+        [118.868974056848, 32.0132523402889],
+        [118.86897908599, 32.0132523402889],
+        [118.868979756542, 32.0132458016376],
+        [118.868979756542, 32.0132406844318],
+        [118.86898076237, 32.0132392629857],
+        [118.868983444579, 32.0132352829366],
+        [118.868986462064, 32.0132369886719],
+        [118.868990820654, 32.0132375572504],
+        [118.868992497035, 32.0132440959023],
+        [118.868998196729, 32.0132449487699],
+        [118.869004566975, 32.0132483602403],
+        [118.869004902251, 32.0132543303132],
+        [118.869005572804, 32.013257457494],
+        [118.869014625259, 32.0132580260723],
+        [118.8690266952, 32.0132577417831],
+        [118.869029712685, 32.0132577417831],
+        [118.869031724342, 32.0132577417831],
+        [118.869032394894, 32.0132625746988],
+        [118.868924100705, 32.0132523402889]
+      ]
+      let i = 0;
+      this.playInterval && clearInterval(this.playInterval);
+      this.playInterval = setInterval(() => {
+        pushDevcLocation({
+          devcKey: 'uwb001',
+          lat: gps[i][1],
+          lng: gps[i][0],
+        });
+        // this.onMessage({
+        //       data: JSON.stringify({
+        //         latitude: gps[i][1],
+        //         longitude: gps[i][0],
+        //         deviceId: 'uwb001',
+        //       })
+        //     }
+        // );
+        // var marker = new BDLayers.Lib.Overlays.MarkerIcon(`marker${uuid()}`, gps[i], {
+        //   iconSize: 45,
+        //   iconColor: 'red',
+        //   labelText: `${i}`
+        // });
+        // marker.getMarker().addTo(this.mapIns.map);
+        if (i === gps.length - 1) {
+          i = 0;
+          return;
+        }
+        i++;
+      }, 2000);
+    },
+  },
+};
+</script>

+ 15 - 0
ruoyi-ui/src/views/bd/rooms/index.scss

@@ -0,0 +1,15 @@
+.room-layers-container {
+  .el-tag {
+    margin-right: 10px;
+    background-color: transparent;
+    cursor: pointer;
+    border-color: #d1e9ff80;
+
+    &:hover, &.selected {
+      background-color: rgba(24, 144, 255, 0.85);
+      color: #fff;
+    }
+  }
+
+  margin-bottom: 14px;
+}

+ 76 - 0
ruoyi-ui/src/views/bd/rooms/index.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="room-layers-container">
+    <template v-for="roomKey in Object.keys(Layers)">
+      <el-tag
+        @click="()=>roomTagClick(Layers[roomKey])"
+        :class="Layers[roomKey].id === activeTag? 'selected':''"
+      >
+        {{ Layers[roomKey].label }}
+      </el-tag>
+    </template>
+  </div>
+</template>
+
+<script>
+const Layers = {
+  cyy: {
+    id: 'cyy',
+    bizId: 2,
+    label: '创研院',
+    layer: "http://bd.xt.wenhq.top:8083/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=bdlayers:bdl_cyy_pingmian&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&Format=image/png&TILECOL={x}&TILEROW={y}",
+  },
+  cyy1: {
+    id: 'cyy1',
+    bizId: 1,
+    label: '创研院1',
+    layer: "http://bd.xt.wenhq.top:8083/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=bdlayers:bdl_cyy_pingmian&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&Format=image/png&TILECOL={x}&TILEROW={y}",
+  }
+}
+export default {
+  name: 'room-layer',
+  components: {},
+  props: {
+    onclick: {
+      type: Function,
+      default: () => {
+      },
+      required: false,
+    },
+  },
+  data() {
+    return {
+      Layers,
+      layerId: 'blayer_ng_s',
+      activeTag: '',
+    };
+  },
+  // 组件卸载前清空图层信息
+  beforeDestroy() {
+    window.map.removeLayersById(this.layerId);
+  },
+  created() {
+
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    roomTagClick(layer) {
+      window.map.removeLayersById(this.layerId);
+      const vtLayer = new BDLayers.Lib.Layer.CBTileLayer(this.layerId, {
+        url: layer.layer,
+        minZoom: 15,
+        maxZoom: 24,
+      });
+      window.map.addCustomLayers(vtLayer);
+      window.map.setPitch(0);
+      this.activeTag = layer.id;
+      this.onclick(layer);
+    },
+    init() {
+      this.roomTagClick(Layers['cyy']);
+    }
+  },
+};
+</script>
+<style lang="scss" src="./index.scss"/>

+ 1 - 0
ruoyi-ui/version

@@ -0,0 +1 @@
+2.5

+ 42 - 36
ruoyi-ui/vue.config.js

@@ -7,10 +7,9 @@ function resolve(dir) {
 
 const CompressionPlugin = require('compression-webpack-plugin')
 
-const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
+const name = process.env.VUE_APP_TITLE || '交通强国工作台' // 网页标题
 
 const port = process.env.port || process.env.npm_config_port || 80 // 端口
-
 // vue.config.js 配置说明
 //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
 // 这里只列一部分,具体配置参考文档
@@ -31,16 +30,23 @@ module.exports = {
   devServer: {
     host: '0.0.0.0',
     port: port,
-    open: true,
+    open: false,
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
       [process.env.VUE_APP_BASE_API]: {
-        target: `http://localhost:8080`,
+        target: process.env.VUE_APP_BASE_URL,
         changeOrigin: true,
         pathRewrite: {
           ['^' + process.env.VUE_APP_BASE_API]: ''
         }
       },
+      [process.env.VUE_APP_BD_BASE_API]: {
+        target: process.env.VUE_APP_BD_BASE_URL,
+        changeOrigin: true,
+        pathRewrite: {
+          ['^' + process.env.VUE_APP_BD_BASE_API]: ''
+        }
+      }
     },
     disableHostCheck: true
   },
@@ -92,39 +98,39 @@ module.exports = {
       .end()
 
     config.when(process.env.NODE_ENV !== 'development', config => {
-          config
-            .plugin('ScriptExtHtmlWebpackPlugin')
-            .after('html')
-            .use('script-ext-html-webpack-plugin', [{
-            // `runtime` must same as runtimeChunk name. default is `runtime`
-              inline: /runtime\..*\.js$/
-            }])
-            .end()
+      config
+        .plugin('ScriptExtHtmlWebpackPlugin')
+        .after('html')
+        .use('script-ext-html-webpack-plugin', [{
+          // `runtime` must same as runtimeChunk name. default is `runtime`
+          inline: /runtime\..*\.js$/
+        }])
+        .end()
 
-          config.optimization.splitChunks({
-            chunks: 'all',
-            cacheGroups: {
-              libs: {
-                name: 'chunk-libs',
-                test: /[\\/]node_modules[\\/]/,
-                priority: 10,
-                chunks: 'initial' // only package third parties that are initially dependent
-              },
-              elementUI: {
-                name: 'chunk-elementUI', // split elementUI into a single package
-                test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
-                priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app
-              },
-              commons: {
-                name: 'chunk-commons',
-                test: resolve('src/components'), // can customize your rules
-                minChunks: 3, //  minimum common number
-                priority: 5,
-                reuseExistingChunk: true
-              }
-            }
-          })
-          config.optimization.runtimeChunk('single')
+      config.optimization.splitChunks({
+        chunks: 'all',
+        cacheGroups: {
+          libs: {
+            name: 'chunk-libs',
+            test: /[\\/]node_modules[\\/]/,
+            priority: 10,
+            chunks: 'initial' // only package third parties that are initially dependent
+          },
+          elementUI: {
+            name: 'chunk-elementUI', // split elementUI into a single package
+            test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
+            priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app
+          },
+          commons: {
+            name: 'chunk-commons',
+            test: resolve('src/components'), // can customize your rules
+            minChunks: 3, //  minimum common number
+            priority: 5,
+            reuseExistingChunk: true
+          }
+        }
+      })
+      config.optimization.runtimeChunk('single')
     })
   }
 }

部分文件因文件數量過多而無法顯示