Quellcode durchsuchen

实时监控、录像回放、路口模块

chengqian vor 2 Jahren
Ursprung
Commit
44b174030d
62 geänderte Dateien mit 2584 neuen und 82 gelöschten Zeilen
  1. 2 0
      server/api/v1/devices/dev_camera.go
  2. 2 2
      server/api/v1/devices/dev_gateway.go
  3. 123 0
      server/api/v1/devices/dev_intersect.go
  4. 6 4
      server/api/v1/devices/enter.go
  5. 2 0
      server/api/v1/enter.go
  6. 15 0
      server/api/v1/monitor/enter.go
  7. 17 0
      server/api/v1/monitor/mon_common.go
  8. 33 0
      server/api/v1/monitor/mon_multiscreen.go
  9. 22 0
      server/api/v1/monitor/mon_playback.go
  10. 1 1
      server/api/v1/system/sys_user.go
  11. 3 0
      server/config.yaml
  12. 6 0
      server/config/Webrtc.go
  13. 2 0
      server/config/config.go
  14. 13 1
      server/dao/devices/dev_camera.go
  15. 21 19
      server/dao/devices/dev_gateway.go
  16. 80 0
      server/dao/devices/dev_intersect.go
  17. 1 0
      server/dao/devices/dev_radar.go
  18. 5 9
      server/dao/system/sys_user.go
  19. 1 0
      server/initialize/gorm.go
  20. 9 4
      server/initialize/router.go
  21. 1 0
      server/model/devices/dev_camera.go
  22. 8 8
      server/model/devices/dev_gateway.go
  23. 8 0
      server/model/devices/dev_intersect.go
  24. 27 0
      server/model/monitor/mon_multiscreen.go
  25. 28 0
      server/router/devices/dev_intersect.go
  26. 1 0
      server/router/devices/enter.go
  27. 2 0
      server/router/enter.go
  28. 6 0
      server/router/monitor/enter.go
  29. 25 0
      server/router/monitor/mon_multiscreen.go
  30. 21 0
      server/router/monitor/mon_playback.go
  31. 50 0
      server/service/devices/dev_intersect.go
  32. 1 0
      server/service/devices/enter.go
  33. 2 0
      server/service/enter.go
  34. 7 0
      server/service/monitor/enter.go
  35. 49 0
      server/service/monitor/mon_common.go
  36. 82 0
      server/service/monitor/mon_multiscreen.go
  37. 41 0
      server/service/monitor/mon_palyback.go
  38. 1 1
      server/task/devices.go
  39. 8 3
      server/utils/mqtt/mqtt_handle.go
  40. 2 0
      web/index.html
  41. 2 0
      web/package.json
  42. BIN
      web/public/img/map/jietu.png
  43. BIN
      web/public/img/map/lishi-icon.png
  44. 8 0
      web/public/static/avue.min.js
  45. 359 0
      web/public/static/jswebrtc.min.js
  46. 7 0
      web/src/api/gateway.js
  47. 33 0
      web/src/api/intersect.js
  48. 25 0
      web/src/api/multiScreen.js
  49. 18 0
      web/src/api/playback.js
  50. 15 0
      web/src/view/devicesAdmin/camera/camera.vue
  51. 34 16
      web/src/view/devicesAdmin/gateway/gateway.vue
  52. 348 0
      web/src/view/devicesAdmin/intersect/intersect.vue
  53. 15 14
      web/src/view/devicesAdmin/radar/radar.vue
  54. 23 0
      web/src/view/monitoring/index.vue
  55. 279 0
      web/src/view/monitoring/multiScreen/multiScreen.vue
  56. 50 0
      web/src/view/monitoring/multiScreen/videoplay.vue
  57. 49 0
      web/src/view/monitoring/multiScreen/videoplay2.vue
  58. 50 0
      web/src/view/monitoring/multiScreen/videoplay3.vue
  59. 51 0
      web/src/view/monitoring/multiScreen/videoplay4.vue
  60. 49 0
      web/src/view/monitoring/multiScreen/videoplay5.vue
  61. 51 0
      web/src/view/monitoring/multiScreen/videoplay6.vue
  62. 384 0
      web/src/view/monitoring/playback/playback.vue

+ 2 - 0
server/api/v1/devices/dev_camera.go

@@ -79,6 +79,7 @@ func (cameraApi *CameraApi) AddCamera(c *gin.Context) {
 		InstallTime:   ca.InstallTime,
 		Shunt:         ca.Shunt,
 		ShuntDescribe: ca.ShuntDescribe,
+		StreamId:      ca.StreamId,
 	}
 	err = CameraService.AddCamera(camera)
 	if err != nil {
@@ -114,6 +115,7 @@ func (cameraApi *CameraApi) UpdateCamera(c *gin.Context) {
 		InstallTime:   ca.InstallTime,
 		Shunt:         ca.Shunt,
 		ShuntDescribe: ca.ShuntDescribe,
+		StreamId:      ca.StreamId,
 	}
 	err = CameraService.UpdateCamera(camera)
 	if err != nil {

+ 2 - 2
server/api/v1/devices/dev_gateway.go

@@ -45,7 +45,7 @@ func (g *GateWayApi) AddGateway(c *gin.Context) {
 		response.FailWithMessage(err.Error(), c)
 		return
 	}
-	gateWay := devices.GateWay{GatewayName: w.GatewayName, GatewayAddress: w.GatewayAddress, GatewayCode: w.GatewayCode, GatewayBrand: w.GatewayBrand, GatewayModel: w.GatewayModel, IpAddress: w.IpAddress, InstallTime: w.InstallTime}
+	gateWay := devices.GateWay{GatewayName: w.GatewayName, IntersectId: w.IntersectId, GatewayCode: w.GatewayCode, GatewayBrand: w.GatewayBrand, GatewayModel: w.GatewayModel, IpAddress: w.IpAddress, InstallTime: w.InstallTime}
 	err = GateWayService.AddGateway(gateWay)
 	if err != nil {
 		global.GVA_LOG.Error(err.Error(), zap.Error(err))
@@ -68,7 +68,7 @@ func (g *GateWayApi) UpdateGateway(c *gin.Context) {
 		return
 	}
 
-	gateWay := devices.GateWay{ID: w.ID, GatewayName: w.GatewayName, GatewayAddress: w.GatewayAddress, GatewayCode: w.GatewayCode, GatewayBrand: w.GatewayBrand, GatewayModel: w.GatewayModel, IpAddress: w.IpAddress, InstallTime: w.InstallTime}
+	gateWay := devices.GateWay{ID: w.ID, GatewayName: w.GatewayName, IntersectId: w.IntersectId, GatewayCode: w.GatewayCode, GatewayBrand: w.GatewayBrand, GatewayModel: w.GatewayModel, IpAddress: w.IpAddress, InstallTime: w.InstallTime}
 	err = GateWayService.UpdateGateway(gateWay)
 	if err != nil {
 		global.GVA_LOG.Error("设置失败!", zap.Error(err))

+ 123 - 0
server/api/v1/devices/dev_intersect.go

@@ -0,0 +1,123 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"server/dao/devices"
+	"server/global"
+	"server/model/common/request"
+	"server/model/common/response"
+	model "server/model/devices"
+	"server/utils"
+	"time"
+)
+
+type IntersectApi struct{}
+
+func (i *IntersectApi) PublicIntersect(c *gin.Context) {
+	list, err := IntersectService.PublicIntersect()
+	if err != nil {
+		global.GVA_LOG.Error("获取失败!", zap.Error(err))
+		response.FailWithMessage("获取失败", c)
+		return
+	}
+	response.OkWithDetailed(gin.H{"pubIntersect": list}, "获取成功", c)
+}
+
+func (i *IntersectApi) AddIntersect(c *gin.Context) {
+	var reqi model.ReqIntersect
+	err := c.ShouldBindJSON(&reqi)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	intersect := devices.Intersect{
+		IntersectName: reqi.IntersectName,
+		Address:       reqi.Address,
+		IntersectType: reqi.IntersectType,
+		CreateTime:    time.Now(),
+	}
+	err = IntersectService.AddIntersect(intersect)
+	if err != nil {
+		global.GVA_LOG.Error(err.Error(), zap.Error(err))
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithMessage("添加成功", c)
+}
+
+func (i *IntersectApi) UpdateIntersect(c *gin.Context) {
+	var reqi model.ReqIntersect
+	err := c.ShouldBindJSON(&reqi)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	err = utils.Verify(reqi, utils.IdVerify)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+
+	intersect := devices.Intersect{
+		ID:            reqi.ID,
+		IntersectName: reqi.IntersectName,
+		Address:       reqi.Address,
+		IntersectType: reqi.IntersectType,
+	}
+	err = IntersectService.UpdateIntersect(intersect)
+	if err != nil {
+		global.GVA_LOG.Error("设置失败!", zap.Error(err))
+		response.FailWithMessage("设置失败", c)
+		return
+	}
+	response.OkWithMessage("设置成功", c)
+}
+
+func (i *IntersectApi) DelIntersect(c *gin.Context) {
+	var reqId request.GetById
+	err := c.ShouldBindJSON(&reqId)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	err = utils.Verify(reqId, utils.IdVerify)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+
+	err = IntersectService.DelIntersect(reqId.ID)
+	if err != nil {
+		global.GVA_LOG.Error("删除失败!", zap.Error(err))
+		response.FailWithMessage("删除失败", c)
+		return
+	}
+	response.OkWithMessage("删除成功", c)
+}
+
+func (i *IntersectApi) IntersectList(c *gin.Context) {
+	var pageInfo request.PageInfo
+	err := c.ShouldBindJSON(&pageInfo)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	err = utils.Verify(pageInfo, utils.PageInfoVerify)
+	if err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	list, total, err := IntersectService.GetIntersectInfoList(pageInfo)
+	if err != nil {
+		global.GVA_LOG.Error("获取失败!", zap.Error(err))
+		response.FailWithMessage("获取失败", c)
+		return
+	}
+	response.OkWithDetailed(response.PageResult{
+		List:     list,
+		Total:    total,
+		Page:     pageInfo.Page,
+		PageSize: pageInfo.PageSize,
+	}, "获取成功", c)
+}

+ 6 - 4
server/api/v1/devices/enter.go

@@ -7,11 +7,13 @@ type ApiGroup struct {
 	CameraApi
 	ScreensApi
 	RadarApi
+	IntersectApi
 }
 
 var (
-	GateWayService = service.ServiceGroupApp.DevicesServiceGroup.GateWayService
-	CameraService  = service.ServiceGroupApp.DevicesServiceGroup.CameraService
-	ScreensService = service.ServiceGroupApp.DevicesServiceGroup.ScreensService
-	RadarService   = service.ServiceGroupApp.DevicesServiceGroup.RadarService
+	GateWayService   = service.ServiceGroupApp.DevicesServiceGroup.GateWayService
+	CameraService    = service.ServiceGroupApp.DevicesServiceGroup.CameraService
+	ScreensService   = service.ServiceGroupApp.DevicesServiceGroup.ScreensService
+	RadarService     = service.ServiceGroupApp.DevicesServiceGroup.RadarService
+	IntersectService = service.ServiceGroupApp.DevicesServiceGroup.IntersectService
 )

+ 2 - 0
server/api/v1/enter.go

@@ -3,6 +3,7 @@ package v1
 import (
 	"server/api/v1/devices"
 	"server/api/v1/example"
+	"server/api/v1/monitor"
 	"server/api/v1/system"
 )
 
@@ -10,6 +11,7 @@ type ApiGroup struct {
 	SystemApiGroup  system.ApiGroup
 	ExampleApiGroup example.ApiGroup
 	DevicesApiGroup devices.ApiGroup
+	MonitorApiGroup monitor.ApiGroup
 }
 
 var ApiGroupApp = new(ApiGroup)

+ 15 - 0
server/api/v1/monitor/enter.go

@@ -0,0 +1,15 @@
+package monitor
+
+import "server/service"
+
+type ApiGroup struct {
+	MultiScreenApi
+	PlayBackApi
+	CommonApi
+}
+
+var (
+	MultiScreenService = service.ServiceGroupApp.MonitorServiceGroup.MultiScreenService
+	CommonService      = service.ServiceGroupApp.MonitorServiceGroup.CommonService
+	PlayBackService    = service.ServiceGroupApp.MonitorServiceGroup.PlayBackService
+)

+ 17 - 0
server/api/v1/monitor/mon_common.go

@@ -0,0 +1,17 @@
+package monitor
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/model/common/response"
+)
+
+type CommonApi struct{}
+
+func (m CommonApi) DevicesCameras(c *gin.Context) {
+	cameras, err := CommonService.DevicesCameras()
+	if err != nil {
+		response.FailWithMessage("获取失败", c)
+		return
+	}
+	response.OkWithDetailed(cameras, "获取成功", c)
+}

+ 33 - 0
server/api/v1/monitor/mon_multiscreen.go

@@ -0,0 +1,33 @@
+package monitor
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/model/common/response"
+	"server/utils"
+	"strconv"
+)
+
+type MultiScreenApi struct{}
+
+func (m MultiScreenApi) EditMultiScreen(c *gin.Context) {
+	userid := utils.GetUserID(c)
+	site, _ := strconv.Atoi(c.Query("site"))
+	id, _ := strconv.Atoi(c.Query("id"))
+	err := MultiScreenService.EditMultiScreen(userid, site, id)
+	if err != nil {
+		response.FailWithMessage("编辑失败", c)
+		return
+	}
+	response.OkWithMessage("编辑成功", c)
+
+}
+
+func (m MultiScreenApi) MultiScreenList(c *gin.Context) {
+	userid := utils.GetUserID(c)
+	list, err := MultiScreenService.MultiScreenList(userid)
+	if err != nil {
+		response.FailWithMessage("获取失败", c)
+		return
+	}
+	response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
+}

+ 22 - 0
server/api/v1/monitor/mon_playback.go

@@ -0,0 +1,22 @@
+package monitor
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/model/common/response"
+)
+
+type PlayBackApi struct{}
+
+func (p PlayBackApi) RewindText(c *gin.Context) {
+	streamId := c.Query("streamId")
+	date := c.Query("date")
+	operate, err := PlayBackService.GetRewindText(streamId, date)
+	if err != nil {
+		response.FailWithMessage("请求文件链接接口错误", c)
+	}
+	if operate == "" {
+		response.FailWithMessage("无数据", c)
+		return
+	}
+	response.OkWithDetailed(operate, "获取成功", c)
+}

+ 1 - 1
server/api/v1/system/sys_user.go

@@ -167,7 +167,7 @@ func (b *BaseApi) Register(c *gin.Context) {
 		return
 	}
 
-	user := &system.SysUser{Username: r.Username, NickName: r.NickName, Password: r.Password, HeaderImg: r.HeaderImg, AuthorityId: r.AuthorityId, Enable: r.Enable, Phone: r.Phone, Email: r.Email}
+	user := &system.SysUser{Username: r.Username, NickName: r.NickName, Password: r.Password, HeaderImg: r.HeaderImg, AuthorityId: r.AuthorityId, Enable: r.Enable, Phone: r.Phone, Email: r.Email, SixScreen: "-1,-1,-1,-1,-1,-1"}
 	userReturn, err := userService.Register(*user)
 	if err != nil {
 		global.GVA_LOG.Error("注册失败!", zap.Error(err))

+ 3 - 0
server/config.yaml

@@ -227,3 +227,6 @@ logger:
     path: "./log"
     level: "info"
     name: "info"
+webrtc:
+    path: "webrtc://106.52.134.22/live/" #这是实时预览的流 的地址前缀
+    rewind-url: "http://106.52.134.22:9099" #这是去mediasvr拿回放的路径,这个服务专门干这个用的

+ 6 - 0
server/config/Webrtc.go

@@ -0,0 +1,6 @@
+package config
+
+type Webrtc struct {
+	Path      string `mapstructure:"path" json:"path" yaml:"path"`
+	RewindUrl string `mapstructure:"rewind-url" json:"rewind-url" yaml:"rewind-url"`
+}

+ 2 - 0
server/config/config.go

@@ -31,4 +31,6 @@ type Server struct {
 
 	Mqtt   Mqtt   `mapstructure:"mqtt" json:"mqtt" yaml:"mqtt"`
 	Logrus Logrus `mapstructure:"logger" json:"logger" yaml:"logger"`
+
+	Webrtc Webrtc `mapstructure:"webrtc" json:"webrtc" yaml:"webrtc"`
 }

+ 13 - 1
server/dao/devices/dev_camera.go

@@ -18,6 +18,7 @@ type Camera struct {
 	InstallTime   time.Time      `gorm:"type:date" json:"installTime"`          //安装时间
 	Shunt         int            `gorm:"type:int" json:"shunt"`                 //所属分路 1主路,2支路
 	ShuntDescribe string         `gorm:"type:varchar(60)" json:"shuntDescribe"` //分路描述
+	StreamId      string         `gorm:"type:varchar(80)" json:"streamId"`      //流地址
 	IsDeleted     gorm.DeletedAt `gorm:"default:null" json:"isDeleted"`         //是否删除
 	Status        int            `gorm:"type:int;default:1" json:"status"`      //在线状态 0=在线,1=离线
 
@@ -45,7 +46,7 @@ func (c Camera) DelCamera(id int) error {
 
 func (c Camera) UpdateCamera() error {
 	err := global.GVA_DB.Model(&c).
-		Select("camera_name", "camera_code", "camera_type", "gateway_id", "camera_brand", "camera_model", "ip_address", "install_time", "shunt", "shunt_describe").
+		Select("camera_name", "camera_code", "camera_type", "gateway_id", "camera_brand", "camera_model", "ip_address", "install_time", "shunt", "shunt_describe", "stream_id").
 		Where("id=?", c.ID).
 		Updates(map[string]interface{}{
 			"camera_name":    c.CameraName,
@@ -58,10 +59,21 @@ func (c Camera) UpdateCamera() error {
 			"install_time":   c.InstallTime,
 			"shunt":          c.Shunt,
 			"shunt_describe": c.ShuntDescribe,
+			"stream_id":      c.StreamId,
 		}).Error
 	return err
 }
 
+func (c Camera) GetCamera(id int) (Camera, error) {
+	err := global.GVA_DB.Model(&c).Preload("GateWay").Preload("GateWay.Intersect").Where("id = ?", id).Find(&c).Error
+	return c, err
+}
+
+func (c Camera) GetCameraTree() (cameraList []Camera, err error) {
+	err = global.GVA_DB.Model(&c).Preload("GateWay").Preload("GateWay.Intersect").Find(&cameraList).Error
+	return cameraList, err
+}
+
 func QueryCameraInfoList(limit, offset int, keyword, shunt string) (cameraList []Camera, total int64, err error) {
 	db := global.GVA_DB.Model(&Camera{})
 	if keyword != "" {

+ 21 - 19
server/dao/devices/dev_gateway.go

@@ -7,17 +7,19 @@ import (
 )
 
 type GateWay struct {
-	ID             int            `gorm:"primary_key" json:"id"`                          //编号
-	GatewayName    string         `gorm:"type:varchar(64)"  json:"gatewayName"`           //网关名称
-	GatewayAddress string         `gorm:"type:varchar(64)"  json:"gatewayAddress"`        //网关地址
-	GatewayCode    string         `gorm:"type:varchar(60)" json:"gatewayCode"`            //设备编号
-	GatewayBrand   string         `gorm:"type:varchar(60)" json:"gatewayBrand"`           //网关品牌
-	GatewayModel   string         `gorm:"type:varchar(60)" json:"gatewayModel"`           //网关型号
-	IpAddress      string         `gorm:"type:varchar(50)" json:"ipAddress"`              //IP地址
-	InstallTime    time.Time      `gorm:"type:date" json:"installTime"`                   //安装时间
-	RecentOnline   time.Time      `gorm:"type:datetime;default:null" json:"recentOnline"` //最近在线时间
-	IsDeleted      gorm.DeletedAt `gorm:"default:null" json:"isDeleted"`                  //是否删除
-	Status         int            `gorm:"type:int;default:1" json:"status"`               //在线状态 0=在线,1=离线
+	ID           int            `gorm:"primary_key" json:"id"`                          //编号
+	GatewayName  string         `gorm:"type:varchar(64)"  json:"gatewayName"`           //网关名称
+	IntersectId  int            `gorm:"type:int" json:"intersectId"`                    //路口id
+	GatewayCode  string         `gorm:"type:varchar(60)" json:"gatewayCode"`            //设备编号
+	GatewayBrand string         `gorm:"type:varchar(60)" json:"gatewayBrand"`           //网关品牌
+	GatewayModel string         `gorm:"type:varchar(60)" json:"gatewayModel"`           //网关型号
+	IpAddress    string         `gorm:"type:varchar(50)" json:"ipAddress"`              //IP地址
+	InstallTime  time.Time      `gorm:"type:date" json:"installTime"`                   //安装时间
+	RecentOnline time.Time      `gorm:"type:datetime;default:null" json:"recentOnline"` //最近在线时间
+	IsDeleted    gorm.DeletedAt `gorm:"default:null" json:"isDeleted"`                  //是否删除
+	Status       int            `gorm:"type:int;default:1" json:"status"`               //在线状态 0=在线,1=离线
+
+	Intersect Intersect `gorm:"foreignkey:IntersectId"`
 }
 
 func (GateWay) TableName() string {
@@ -33,7 +35,7 @@ func QueryGateWayInfoList(limit, offset int, keyword string) (gatewayList []Gate
 	if err != nil {
 		return
 	}
-	err = db.Limit(limit).Offset(offset).Find(&gatewayList).Error
+	err = db.Limit(limit).Offset(offset).Preload("Intersect").Find(&gatewayList).Error
 	return
 }
 
@@ -49,15 +51,15 @@ func (g GateWay) AddGateway() error {
 
 func (g GateWay) UpdateGateway() error {
 	err := global.GVA_DB.Model(&g).
-		Select("gateway_name", "gateway_address", "gateway_brand", "gateway_model", "ip_address", "install_time").
+		Select("gateway_name", "intersect_id", "gateway_brand", "gateway_model", "ip_address", "install_time").
 		Where("id=?", g.ID).
 		Updates(map[string]interface{}{
-			"gateway_name":    g.GatewayName,
-			"gateway_address": g.GatewayAddress,
-			"gateway_brand":   g.GatewayBrand,
-			"gateway_model":   g.GatewayModel,
-			"ip_address":      g.IpAddress,
-			"install_time":    g.InstallTime,
+			"gateway_name":  g.GatewayName,
+			"intersect_id":  g.IntersectId,
+			"gateway_brand": g.GatewayBrand,
+			"gateway_model": g.GatewayModel,
+			"ip_address":    g.IpAddress,
+			"install_time":  g.InstallTime,
 		}).Error
 	return err
 }

+ 80 - 0
server/dao/devices/dev_intersect.go

@@ -0,0 +1,80 @@
+package devices
+
+import (
+	"gorm.io/gorm"
+	"server/global"
+	"time"
+)
+
+type Intersect struct {
+	ID            int            `gorm:"primary_key" json:"id"`                 //编号
+	IntersectName string         `gorm:"type:varchar(64)" json:"intersectName"` //路口名称
+	IntersectType string         `gorm:"type:varchar(64)" json:"intersectType"` //路口类型
+	Address       string         `gorm:"type:varchar(64)"  json:"address"`      //地址
+	CreateTime    time.Time      `gorm:"type:datetime" json:"createTime"`       //创建时间
+	IsDeleted     gorm.DeletedAt `gorm:"default:null" json:"isDeleted"`         //是否删除
+	Status        int            `gorm:"type:int;default:0" json:"status"`      //运行状态 0=正常,1=异常
+}
+
+func (Intersect) TableName() string {
+	return "dev_intersect"
+}
+
+func (i Intersect) PublicIntersect() (intersectList []Intersect, err error) {
+	err = global.GVA_DB.Model(&Intersect{}).Select("id", "intersect_name", "address").Find(&intersectList).Error
+	return
+}
+
+func (i Intersect) IsExistedByName() error {
+	err := global.GVA_DB.Model(&Intersect{}).Where("intersect_name = ?", i.IntersectName).First(&i).Error
+	return err
+}
+
+func (i Intersect) AddIntersect() error {
+	err := global.GVA_DB.Model(&Intersect{}).Create(&i).Error
+	return err
+}
+
+func (i Intersect) UpdateIntersect() error {
+	err := global.GVA_DB.Model(&i).
+		Select("intersect_name", "address", "intersect_type").
+		Where("id=?", i.ID).
+		Updates(map[string]interface{}{
+			"intersect_name": i.IntersectName,
+			"address":        i.Address,
+			"intersect_type": i.IntersectType,
+		}).Error
+	return err
+}
+
+func (i Intersect) DelIntersect(id int) error {
+	err := global.GVA_DB.Model(&i).Where("id = ?", id).Delete(&i).Error
+	return err
+}
+
+func UpdateIntersectStatus(id string, status int) error {
+	var intersect Intersect
+	err := global.GVA_DB.Model(&intersect).
+		Select("status").
+		Where("id = ?", id).
+		Updates(map[string]interface{}{
+			"status": status,
+		}).Error
+	return err
+}
+
+func QueryIntersectInfoList(limit, offset int, keyword, shunt string) (intersectList []Intersect, total int64, err error) {
+	db := global.GVA_DB.Model(&Intersect{})
+	if keyword != "" {
+		db.Where("intersect_name like ? ", "%"+keyword+"%")
+	}
+	if shunt != "" {
+		db.Where("intersect_type like ?", "%"+shunt+"%")
+	}
+	err = db.Count(&total).Error
+	if err != nil {
+		return
+	}
+	err = db.Limit(limit).Offset(offset).Find(&intersectList).Error
+	return
+}

+ 1 - 0
server/dao/devices/dev_radar.go

@@ -83,6 +83,7 @@ func QueryRadarToDetail(id int) (radarList []Radar, err error) {
 	err = db.Where("gateway_id = ?", id).Find(&radarList).Error
 	return
 }
+
 func UpdateRadarStatusByGatewayId(id, status int) error {
 	var radar Radar
 	err := global.GVA_DB.Model(&radar).

+ 5 - 9
server/dao/system/sys_user.go

@@ -22,19 +22,13 @@ type SysUser struct {
 	Phone       string       `json:"phone"  gorm:"comment:用户手机号"`                     // 用户手机号
 	Email       string       `json:"email"  gorm:"comment:用户邮箱"`                      // 用户邮箱
 	Enable      int          `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结
+	SixScreen   string       `json:"sixScreen" gorm:"comment:安防六屏"`                   //安防六屏幕
 }
 
 func (SysUser) TableName() string {
 	return "sys_users"
 }
 
-// TODO:查询
-// TODO:新增
-// TODO:修改
-// TODO:删除
-
-// TODO: 用户查询
-
 // QueryUserByUserName  按用户名查询用户
 func QueryUserByUserName(userName string) (SysUser, error) {
 	var user SysUser
@@ -85,6 +79,10 @@ func (u SysUser) CreateUser() error {
 	return global.GVA_DB.Create(&u).Error
 }
 
+func (u *SysUser) GetUser(id uint) error {
+	return global.GVA_DB.Model(&u).Where("id = ?", id).Find(&u).Error
+}
+
 // TODO: 用户修改
 
 // UpdateUser 修改用户
@@ -139,8 +137,6 @@ func (u SysUser) SetUserInfoById() error {
 		Updates(&u).Error
 }
 
-// TODO: 用户删除
-
 // DeleteUser 删除用户
 func DeleteUser(id int) (err error) {
 	return global.GVA_DB.Transaction(func(tx *gorm.DB) error {

+ 1 - 0
server/initialize/gorm.go

@@ -53,6 +53,7 @@ func RegisterTables() {
 		devices.Camera{},
 		devices.Screens{}, devices.Resolution{}, devices.ScreensSize{}, //屏幕以及一些屏参
 		devices.Radar{},
+		devices.Intersect{},
 	)
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 9 - 4
server/initialize/router.go

@@ -43,6 +43,7 @@ func Routers() *gin.Engine {
 	systemRouter := router.RouterGroupApp.System
 	exampleRouter := router.RouterGroupApp.Example
 	devicesRouter := router.RouterGroupApp.Devices
+	monitorRouter := router.RouterGroupApp.Monitor
 	// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
 	// VUE_APP_BASE_API = /
 	// VUE_APP_BASE_PATH = http://localhost
@@ -87,10 +88,14 @@ func Routers() *gin.Engine {
 
 		exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup)
 
-		devicesRouter.InitGateWayRouter(PrivateGroup, PublicGroup) //注册网关路由
-		devicesRouter.InitCameraRouter(PrivateGroup)               //注册摄像头路由
-		devicesRouter.InitScreensRouter(PrivateGroup, PublicGroup) //注册LED屏路由
-		devicesRouter.InitRadarRouter(PrivateGroup, PublicGroup)   //注册雷达路由
+		devicesRouter.InitGateWayRouter(PrivateGroup, PublicGroup)   //注册网关路由
+		devicesRouter.InitCameraRouter(PrivateGroup)                 //注册摄像头路由
+		devicesRouter.InitScreensRouter(PrivateGroup, PublicGroup)   //注册LED屏路由
+		devicesRouter.InitRadarRouter(PrivateGroup, PublicGroup)     //注册雷达路由
+		devicesRouter.InitIntersectRouter(PrivateGroup, PublicGroup) //注册路口路由
+
+		monitorRouter.InitMultiScreenRouter(PrivateGroup, PublicGroup) //注册多屏监控路由
+		monitorRouter.InitPlayBackRouter(PrivateGroup, PublicGroup)    // 注册监控回放路由
 
 	}
 

+ 1 - 0
server/model/devices/dev_camera.go

@@ -13,5 +13,6 @@ type ReqCamera struct {
 	GatewayID     int       `json:"gatewayId"`     //所属网关
 	Shunt         int       `json:"shunt"`         //所属分路 1主路,2支路
 	ShuntDescribe string    `json:"shuntDescribe"` //分路描述
+	StreamId      string    `json:"streamId"`      //流id
 	InstallTime   time.Time `json:"installTime"`   //安装时间
 }

+ 8 - 8
server/model/devices/dev_gateway.go

@@ -19,12 +19,12 @@ type RelationDev struct {
 }
 
 type ReqGateway struct {
-	ID             int       `json:"id"`             //ID
-	GatewayName    string    `json:"gatewayName"`    //网关名称
-	GatewayAddress string    `json:"gatewayAddress"` //网关地址
-	GatewayCode    string    `json:"gatewayCode"`    //设备编号
-	GatewayBrand   string    `json:"gatewayBrand"`   //网关品牌
-	GatewayModel   string    `json:"gatewayModel"`   //网关型号
-	IpAddress      string    `json:"ipAddress"`      //IP地址
-	InstallTime    time.Time `json:"installTime"`    //安装时间
+	ID           int       `json:"id"`           //ID
+	GatewayName  string    `json:"gatewayName"`  //网关名称
+	IntersectId  int       `json:"intersectId"`  //路口id
+	GatewayCode  string    `json:"gatewayCode"`  //设备编号
+	GatewayBrand string    `json:"gatewayBrand"` //网关品牌
+	GatewayModel string    `json:"gatewayModel"` //网关型号
+	IpAddress    string    `json:"ipAddress"`    //IP地址
+	InstallTime  time.Time `json:"installTime"`  //安装时间
 }

+ 8 - 0
server/model/devices/dev_intersect.go

@@ -0,0 +1,8 @@
+package devices
+
+type ReqIntersect struct {
+	ID            int    `json:"id"`            //ID
+	IntersectName string `json:"intersectName"` //路口名字
+	Address       string `json:"address"`       //路口地址
+	IntersectType string `json:"intersectType"` //路口类型
+}

+ 27 - 0
server/model/monitor/mon_multiscreen.go

@@ -0,0 +1,27 @@
+package monitor
+
+type Placement struct {
+	Slot          int    `json:"slot"`
+	ID            int    `json:"id"`
+	Name          string `json:"name"`
+	Code          string `json:"code"`
+	StreamId      string `json:"streamId"`
+	DeviceAddress string `json:"deviceAddress"`
+	Url           string `json:"url"`
+}
+
+type CameraTree struct {
+	PoleGroupName string      `json:"poleGroupName"`
+	CameraList    []CameraDev `json:"cameraList"`
+}
+
+type CameraDev struct {
+	Id            int    `json:"id"`
+	PoleGroupName string `json:"poleGroupName"`
+	DeviceName    string `json:"deviceName"`
+	DeviceAddress string `json:"deviceAddress"`
+	DeviceCode    string `json:"deviceCode"`
+	Status        int    `json:"status"`
+	StreamAddress string `json:"streamAddress"`
+	StreamId      string `json:"streamId"`
+}

+ 28 - 0
server/router/devices/dev_intersect.go

@@ -0,0 +1,28 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type IntersectRouter struct{}
+
+func (i *IntersectRouter) InitIntersectRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) {
+	intersectRouter := Router.Group("intersect").Use(middleware.OperationRecord())
+	intersectRouterWithoutRecord := Router.Group("intersect")
+
+	apiPublicRouterWithoutRecord := RouterPub.Group("intersect")
+	baseApi := v1.ApiGroupApp.DevicesApiGroup.IntersectApi
+	{
+		intersectRouter.POST("addIntersect", baseApi.AddIntersect)      // 增加路口
+		intersectRouter.PUT("updateIntersect", baseApi.UpdateIntersect) // 编辑路口
+		intersectRouter.DELETE("delIntersect", baseApi.DelIntersect)    // 删除路口
+	}
+	{
+		intersectRouterWithoutRecord.POST("getIntersectList", baseApi.IntersectList) // 分页获取路口列表
+	}
+	{
+		apiPublicRouterWithoutRecord.GET("getPublicIntersect", baseApi.PublicIntersect)
+	}
+}

+ 1 - 0
server/router/devices/enter.go

@@ -5,4 +5,5 @@ type RouterGroup struct {
 	CameraRouter
 	ScreensRouter
 	RadarRouter
+	IntersectRouter
 }

+ 2 - 0
server/router/enter.go

@@ -3,6 +3,7 @@ package router
 import (
 	"server/router/devices"
 	"server/router/example"
+	"server/router/monitor"
 	"server/router/system"
 )
 
@@ -10,6 +11,7 @@ type RouterGroup struct {
 	System  system.RouterGroup
 	Example example.RouterGroup
 	Devices devices.RouterGroup
+	Monitor monitor.RouterGroup
 }
 
 var RouterGroupApp = new(RouterGroup)

+ 6 - 0
server/router/monitor/enter.go

@@ -0,0 +1,6 @@
+package monitor
+
+type RouterGroup struct {
+	MultiScreenRouter
+	PlayBackRouter
+}

+ 25 - 0
server/router/monitor/mon_multiscreen.go

@@ -0,0 +1,25 @@
+package monitor
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type MultiScreenRouter struct{}
+
+func (m *MultiScreenRouter) InitMultiScreenRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) {
+	multiscreenRouter := Router.Group("multiscreen").Use(middleware.OperationRecord())
+	multiscreenRouterWithoutRecord := Router.Group("multiscreen")
+	//apiPublicRouterWithoutRecord := RouterPub.Group("multiscreen")
+	commonApi := v1.ApiGroupApp.MonitorApiGroup.CommonApi
+	baseApi := v1.ApiGroupApp.MonitorApiGroup.MultiScreenApi
+	{
+		multiscreenRouter.POST("/multiscreenSubmit", baseApi.EditMultiScreen) //编辑六宫格
+	}
+	{
+		multiscreenRouterWithoutRecord.POST("/multiscreenList", baseApi.MultiScreenList) //获取六宫格位置
+		multiscreenRouterWithoutRecord.POST("/devicesCameras", commonApi.DevicesCameras) //获取摄像头设备列表
+	}
+
+}

+ 21 - 0
server/router/monitor/mon_playback.go

@@ -0,0 +1,21 @@
+package monitor
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+)
+
+type PlayBackRouter struct{}
+
+func (p *PlayBackRouter) InitPlayBackRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) {
+	//playbackRouter := Router.Group("multiscreen").Use(middleware.OperationRecord())
+	multiscreenRouterWithoutRecord := Router.Group("playback")
+	//apiPublicRouterWithoutRecord := RouterPub.Group("multiscreen")
+	commonApi := v1.ApiGroupApp.MonitorApiGroup.CommonApi
+	baseApi := v1.ApiGroupApp.MonitorApiGroup.PlayBackApi
+	{
+		multiscreenRouterWithoutRecord.POST("/rewind", baseApi.RewindText)               //获取回放数据
+		multiscreenRouterWithoutRecord.POST("/devicesCameras", commonApi.DevicesCameras) //获取摄像头设备列表
+	}
+
+}

+ 50 - 0
server/service/devices/dev_intersect.go

@@ -0,0 +1,50 @@
+package devices
+
+import (
+	"errors"
+	"gorm.io/gorm"
+	"server/dao/devices"
+	"server/model/common/request"
+	"server/utils/logger"
+)
+
+type IntersectService struct{}
+
+func (intersectService *IntersectService) PublicIntersect() (interface{}, error) {
+	intersect := devices.Intersect{}
+	list, err := intersect.PublicIntersect()
+	return list, err
+}
+
+func (intersectService *IntersectService) AddIntersect(intersect devices.Intersect) error {
+	err := intersect.IsExistedByName()
+	if !errors.Is(err, gorm.ErrRecordNotFound) {
+		logger.Logger.Errorf("Create IsExistedByName \n")
+		return errors.New("路口名称已存在!")
+	}
+
+	err = intersect.AddIntersect()
+	return err
+}
+
+func (intersectService *IntersectService) UpdateIntersect(intersect devices.Intersect) error {
+	err := intersect.UpdateIntersect()
+	return err
+}
+
+func (intersectService *IntersectService) DelIntersect(id int) error {
+	intersect := devices.Intersect{}
+	err := intersect.DelIntersect(id)
+	return err
+}
+
+func (intersectService *IntersectService) GetIntersectInfoList(info request.PageInfo) (list interface{}, total int64, err error) {
+	limit := info.PageSize
+	offset := info.PageSize * (info.Page - 1)
+	keyword := info.Keyword
+	shunt := info.Shunt
+
+	userList, total, err := devices.QueryIntersectInfoList(limit, offset, keyword, shunt)
+
+	return userList, total, err
+}

+ 1 - 0
server/service/devices/enter.go

@@ -5,4 +5,5 @@ type ServiceGroup struct {
 	CameraService
 	ScreensService
 	RadarService
+	IntersectService
 }

+ 2 - 0
server/service/enter.go

@@ -3,6 +3,7 @@ package service
 import (
 	"server/service/devices"
 	"server/service/example"
+	"server/service/monitor"
 	"server/service/system"
 )
 
@@ -10,6 +11,7 @@ type ServiceGroup struct {
 	SystemServiceGroup  system.ServiceGroup
 	ExampleServiceGroup example.ServiceGroup
 	DevicesServiceGroup devices.ServiceGroup
+	MonitorServiceGroup monitor.ServiceGroup
 }
 
 var ServiceGroupApp = new(ServiceGroup)

+ 7 - 0
server/service/monitor/enter.go

@@ -0,0 +1,7 @@
+package monitor
+
+type ServiceGroup struct {
+	MultiScreenService
+	PlayBackService
+	CommonService
+}

+ 49 - 0
server/service/monitor/mon_common.go

@@ -0,0 +1,49 @@
+package monitor
+
+import (
+	"server/dao/devices"
+	"server/global"
+	"server/model/monitor"
+)
+
+type CommonService struct{}
+
+func (s CommonService) DevicesCameras() ([]monitor.CameraTree, error) {
+	intersect := devices.Intersect{}
+	intersectList, _ := intersect.PublicIntersect()
+
+	camera := devices.Camera{}
+	list, _ := camera.GetCameraTree()
+
+	var rsp []monitor.CameraTree
+	for _, luk := range intersectList {
+		var cameraList []monitor.CameraDev
+		for _, cam := range list {
+			if cam.GateWay.Intersect.ID == luk.ID {
+				poleGroupName := cam.CameraName + "(在线)"
+				if cam.Status == 1 {
+					poleGroupName = cam.CameraName + "(离线)"
+				}
+				devAddress := luk.Address + "--" + cam.ShuntDescribe + "(主路)"
+				if cam.Shunt == 2 {
+					devAddress = luk.Address + "--" + cam.ShuntDescribe + "(支路)"
+				}
+				cameraList = append(cameraList, monitor.CameraDev{
+					Id:            cam.ID,
+					PoleGroupName: poleGroupName,
+					DeviceName:    cam.CameraName,
+					DeviceAddress: devAddress,
+					DeviceCode:    cam.CameraCode,
+					Status:        cam.Status,
+					StreamAddress: global.GVA_CONFIG.Webrtc.Path + cam.StreamId,
+					StreamId:      cam.StreamId,
+				})
+			}
+		}
+		rsp = append(rsp, monitor.CameraTree{
+			PoleGroupName: luk.IntersectName,
+			CameraList:    cameraList,
+		})
+	}
+	return rsp, nil
+}

+ 82 - 0
server/service/monitor/mon_multiscreen.go

@@ -0,0 +1,82 @@
+package monitor
+
+import (
+	"server/dao/devices"
+	"server/dao/system"
+	"server/global"
+	"server/model/monitor"
+	"server/utils/logger"
+	"strconv"
+	"strings"
+)
+
+type MultiScreenService struct{}
+
+func (s MultiScreenService) MultiScreenList(id uint) ([]monitor.Placement, error) {
+	user := system.SysUser{}
+	err := user.GetUser(id)
+	if err != nil {
+		logger.Logger.Error("获取用户失败")
+		return nil, err
+	}
+	idsSplit := strings.Split(user.SixScreen, ",")
+
+	var list []monitor.Placement
+	for i, id := range idsSplit {
+		idInt, _ := strconv.Atoi(id)
+		placement := monitor.Placement{}
+		placement.ID = idInt
+		placement.Slot = i + 1
+		if idInt != -1 {
+			ca := devices.Camera{}
+			camera, err := ca.GetCamera(idInt)
+			if err != nil {
+				logger.Logger.Error("获取摄像头信息失败")
+				continue
+			}
+			placement.Code = camera.CameraCode
+			placement.Name = camera.CameraName
+			placement.StreamId = camera.StreamId
+			devAddress := camera.GateWay.Intersect.Address + "--" + camera.ShuntDescribe + "(主路)"
+			if camera.Shunt == 2 {
+				devAddress = camera.GateWay.Intersect.Address + "--" + camera.ShuntDescribe + "(支路)"
+			}
+			placement.DeviceAddress = devAddress
+			placement.Url = global.GVA_CONFIG.Webrtc.Path + camera.StreamId
+		}
+		list = append(list, placement)
+	}
+	return list, nil
+}
+
+func (s MultiScreenService) EditMultiScreen(userid uint, site int, cameraId int) error {
+	user := system.SysUser{}
+	err := user.GetUser(userid)
+	if err != nil {
+		logger.Logger.Error("获取用户失败")
+		return err
+	}
+	idsSplit := strings.Split(user.SixScreen, ",")
+	idStr := ""
+	for i := range idsSplit {
+		if i+1 == site {
+			idsSplit[site-1] = strconv.Itoa(cameraId)
+		}
+		idStr += idsSplit[i]
+		if i < (len(idsSplit) - 1) {
+			idStr += ","
+		}
+	}
+	sysUser := system.SysUser{
+		GVA_MODEL: global.GVA_MODEL{
+			ID: userid,
+		},
+		SixScreen: idStr,
+	}
+	err = sysUser.SetUserInfoById()
+	if err != nil {
+		logger.Logger.Error("EditMultiScreen()... 修改用户信息失败")
+		return err
+	}
+	return err
+}

+ 41 - 0
server/service/monitor/mon_palyback.go

@@ -0,0 +1,41 @@
+package monitor
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"server/global"
+)
+
+type PlayBackService struct{}
+
+// getRewindApi 公用请求接口
+func (f *PlayBackService) getRewindApi(url string) (string, error) {
+	method := "GET"
+
+	client := &http.Client{}
+	req, err := http.NewRequest(method, url, nil)
+	if err != nil {
+		return "", nil
+	}
+	res, err := client.Do(req)
+	if err != nil {
+		return "", nil
+	}
+	defer res.Body.Close()
+
+	body, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return "", nil
+	}
+
+	return string(body), err
+}
+
+// GetRewindText 请求回话加密数据
+func (f *PlayBackService) GetRewindText(streamId string, date string) (string, error) {
+	api := global.GVA_CONFIG.Webrtc.RewindUrl + "/list/%v/%v"
+	url2 := fmt.Sprintf(api, (streamId), url.QueryEscape(date))
+	return f.getRewindApi(url2)
+}

+ 1 - 1
server/task/devices.go

@@ -16,7 +16,7 @@ func GatewayRoutingInspection() {
 			status = 1
 			_ = devices.UpdateCameraStatusByGatewayId(gateway.ID, status)
 			_ = devices.UpdateScreensStatusByGatewayId(gateway.ID, status)
-			_ = devices.UpdateRadarStatusByGatewayId(gateway.ID, status)
+			//_ = devices.UpdateRadarStatusByGatewayId(gateway.ID, status)
 		}
 		_ = devices.UpdateGatewayStatus(gateway.GatewayCode, status)
 	}

+ 8 - 3
server/utils/mqtt/mqtt_handle.go

@@ -101,8 +101,13 @@ func (o *MqttHandler) Handler() interface{} {
 			if err != nil {
 				logger.Logger.Error("UpdateScreensStatus err", zap.Error(err))
 			}
-		case TopicDeviceRadar:
-			//TODO:修改雷达状态
+		case TopicIntersect:
+			//修改路口运行状态(根据路口id)
+			status, _ := strconv.Atoi(m.PayloadString())
+			err := Dev.UpdateIntersectStatus(deviceCode, status)
+			if err != nil {
+				logger.Logger.Error("UpdateIntersectStatus err", zap.Error(err))
+			}
 		}
 	}
 }
@@ -120,5 +125,5 @@ const (
 	TopicDeviceGateway = "device/gateway"
 	TopicDeviceCamera  = "device/camera"
 	TopicDeviceScreens = "device/screens"
-	TopicDeviceRadar   = "device/radar"
+	TopicIntersect     = "device/intersect"
 )

+ 2 - 0
web/index.html

@@ -13,6 +13,8 @@
 <body>
     <div id="app"></div>
     <script type="module" src="./src/main.js"></script>
+    <script src="./public/static/jswebrtc.min.js" type="text/javascript"></script>
+    <script src="./public/static/avue.min.js" type="text/javascript"></script>
 </body>
 
 </html>

+ 2 - 0
web/package.json

@@ -35,6 +35,8 @@
         "sortablejs": "^1.15.2",
         "spark-md5": "^3.0.2",
         "tailwindcss": "^3.3.3",
+        "video.js": "^8.16.1",
+        "videojs-contrib-hls": "^5.15.0",
         "vue": "^3.4.21",
         "vue-router": "^4.3.2",
         "vuedraggable": "^4.1.0"

BIN
web/public/img/map/jietu.png


BIN
web/public/img/map/lishi-icon.png


Datei-Diff unterdrückt, da er zu groß ist
+ 8 - 0
web/public/static/avue.min.js


+ 359 - 0
web/public/static/jswebrtc.min.js

@@ -0,0 +1,359 @@
+var JSWebrtc = {
+    Player: null,
+    VideoElement: null,
+    CreateVideoElements: function() {
+        var elements = document.querySelectorAll(".jswebrtc");
+        for (var i = 0; i < elements.length; i++) {
+            new JSWebrtc.VideoElement(elements[i])
+        }
+    },
+    FillQuery: function(query_string, obj) {
+        obj.user_query = {};
+        if (query_string.length == 0)
+            return;
+        if (query_string.indexOf("?") >= 0)
+            query_string = query_string.split("?")[1];
+        var queries = query_string.split("&");
+        for (var i = 0; i < queries.length; i++) {
+            var query = queries[i].split("=");
+            obj[query[0]] = query[1];
+            obj.user_query[query[0]] = query[1]
+        }
+        if (obj.domain)
+            obj.vhost = obj.domain
+    },
+    ParseUrl: function(rtmp_url) {
+        var a = document.createElement("a");
+        a.href = rtmp_url.replace("rtmp://", "http://").replace("webrtc://", "http://").replace("rtc://", "http://");
+        var vhost = a.hostname;
+        var app = a.pathname.substr(1, a.pathname.lastIndexOf("/") - 1);
+        var stream = a.pathname.substr(a.pathname.lastIndexOf("/") + 1);
+        app = app.replace("...vhost...", "?vhost=");
+        if (app.indexOf("?") >= 0) {
+            var params = app.substr(app.indexOf("?"));
+            app = app.substr(0, app.indexOf("?"));
+            if (params.indexOf("vhost=") > 0) {
+                vhost = params.substr(params.indexOf("vhost=") + "vhost=".length);
+                if (vhost.indexOf("&") > 0) {
+                    vhost = vhost.substr(0, vhost.indexOf("&"))
+                }
+            }
+        }
+        if (a.hostname == vhost) {
+            var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
+            if (re.test(a.hostname))
+                vhost = "__defaultVhost__"
+        }
+        var schema = "rtmp";
+        if (rtmp_url.indexOf("://") > 0)
+            schema = rtmp_url.substr(0, rtmp_url.indexOf("://"));
+        var port = a.port;
+        if (!port) {
+            if (schema === "http") {
+                port = 80
+            } else if (schema === "https") {
+                port = 443
+            } else if (schema === "rtmp") {
+                port = 1935
+            } else if (schema === "webrtc" || schema === "rtc") {
+                port = 1985
+            }
+        }
+        var ret = {
+            url: rtmp_url,
+            schema: schema,
+            server: a.hostname,
+            port: port,
+            vhost: vhost,
+            app: app,
+            stream: stream
+        };
+        JSWebrtc.FillQuery(a.search, ret);
+        return ret
+    },
+    HttpPost: function(url, data) {
+        return new Promise(function(resolve, reject) {
+                var xhr = new XMLHttpRequest;
+                xhr.onreadystatechange = function() {
+                    if (xhr.readyState === 4 && (xhr.status >= 200 && xhr.status < 300)) {
+                        var respone = JSON.parse(xhr.responseText);
+                        xhr.onreadystatechange = new Function;
+                        xhr = null;
+                        resolve(respone)
+                    }
+                }
+                ;
+                xhr.open("POST", url, true);
+                xhr.timeout = 5e3;
+                xhr.responseType = "text";
+                xhr.setRequestHeader("Content-Type", "application/json");
+                xhr.send(data)
+            }
+        )
+    }
+};
+if (document.readyState === "complete") {
+    JSWebrtc.CreateVideoElements()
+} else {
+    document.addEventListener("DOMContentLoaded", JSWebrtc.CreateVideoElements)
+}
+JSWebrtc.VideoElement = function() {
+    "use strict";
+    var VideoElement = function(element) {
+        var url = element.dataset.url;
+        if (!url) {
+            throw "VideoElement has no `data-url` attribute"
+        }
+        var addStyles = function(element, styles) {
+            for (var name in styles) {
+                element.style[name] = styles[name]
+            }
+        };
+        this.container = element;
+        addStyles(this.container, {
+            display: "inline-block",
+            position: "relative",
+            minWidth: "80px",
+            minHeight: "80px"
+        });
+        this.video = document.createElement("video");
+        this.video.width = 960;
+        this.video.height = 540;
+        addStyles(this.video, {
+            display: "block",
+            width: "100%"
+        });
+        this.container.appendChild(this.video);
+        this.playButton = document.createElement("div");
+        this.playButton.innerHTML = VideoElement.PLAY_BUTTON;
+        addStyles(this.playButton, {
+            zIndex: 2,
+            position: "absolute",
+            top: "0",
+            bottom: "0",
+            left: "0",
+            right: "0",
+            maxWidth: "75px",
+            maxHeight: "75px",
+            margin: "auto",
+            opacity: "0.7",
+            cursor: "pointer"
+        });
+        this.container.appendChild(this.playButton);
+        var options = {
+            video: this.video
+        };
+        for (var option in element.dataset) {
+            try {
+                options[option] = JSON.parse(element.dataset[option])
+            } catch (err) {
+                options[option] = element.dataset[option]
+            }
+        }
+        this.player = new JSWebrtc.Player(url,options);
+        element.playerInstance = this.player;
+        if (options.poster && !options.autoplay) {
+            options.decodeFirstFrame = false;
+            this.poster = new Image;
+            this.poster.src = options.poster;
+            this.poster.addEventListener("load", this.posterLoaded);
+            addStyles(this.poster, {
+                display: "block",
+                zIndex: 1,
+                position: "absolute",
+                top: 0,
+                left: 0,
+                bottom: 0,
+                right: 0
+            });
+            this.container.appendChild(this.poster)
+        }
+        if (!this.player.options.streaming) {
+            this.container.addEventListener("click", this.onClick.bind(this))
+        }
+        if (options.autoplay) {
+            this.playButton.style.display = "none"
+        }
+        if (this.player.audioOut && !this.player.audioOut.unlocked) {
+            var unlockAudioElement = this.container;
+            if (options.autoplay) {
+                this.unmuteButton = document.createElement("div");
+                this.unmuteButton.innerHTML = VideoElement.UNMUTE_BUTTON;
+                addStyles(this.unmuteButton, {
+                    zIndex: 2,
+                    position: "absolute",
+                    bottom: "10px",
+                    right: "20px",
+                    width: "75px",
+                    height: "75px",
+                    margin: "auto",
+                    opacity: "0.7",
+                    cursor: "pointer"
+                });
+                this.container.appendChild(this.unmuteButton);
+                unlockAudioElement = this.unmuteButton
+            }
+            this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement);
+            unlockAudioElement.addEventListener("touchstart", this.unlockAudioBound, false);
+            unlockAudioElement.addEventListener("click", this.unlockAudioBound, true)
+        }
+    };
+    VideoElement.prototype.onUnlockAudio = function(element, ev) {
+        if (this.unmuteButton) {
+            ev.preventDefault();
+            ev.stopPropagation()
+        }
+        this.player.audioOut.unlock(function() {
+            if (this.unmuteButton) {
+                this.unmuteButton.style.display = "none"
+            }
+            element.removeEventListener("touchstart", this.unlockAudioBound);
+            element.removeEventListener("click", this.unlockAudioBound)
+        }
+            .bind(this))
+    }
+    ;
+    VideoElement.prototype.onClick = function(ev) {
+        if (this.player.isPlaying) {
+            this.player.pause();
+            this.playButton.style.display = "block"
+        } else {
+            this.player.play();
+            this.playButton.style.display = "none";
+            if (this.poster) {
+                this.poster.style.display = "none"
+            }
+        }
+    }
+    ;
+    VideoElement.PLAY_BUTTON = '<svg style="max-width: 75px; max-height: 75px;" ' + 'viewBox="0 0 200 200" alt="Play video">' + '<circle cx="100" cy="100" r="90" fill="none" ' + 'stroke-width="15" stroke="#fff"/>' + '<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>' + "</svg>";
+    VideoElement.UNMUTE_BUTTON = '<svg style="max-width: 75px; max-height: 75px;" viewBox="0 0 75 75">' + '<polygon class="audio-speaker" stroke="none" fill="#fff" ' + 'points="39,13 22,28 6,28 6,47 21,47 39,62 39,13"/>' + '<g stroke="#fff" stroke-width="5">' + '<path d="M 49,50 69,26"/>' + '<path d="M 69,50 49,26"/>' + "</g>" + "</svg>";
+    return VideoElement
+}();
+JSWebrtc.Player = function() {
+    "use strict";
+    var Player = function(url, options) {
+        this.options = options || {};
+        if (!url.match(/^webrtc?:\/\//)) {
+            throw "JSWebrtc just work with webrtc"
+        }
+        if (!this.options.video) {
+            throw "VideoElement is null"
+        }
+        this.urlParams = JSWebrtc.ParseUrl(url);
+        this.pc = null;
+        this.autoplay = !!options.autoplay || false;
+        this.paused = true;
+        if (this.autoplay)
+            this.options.video.muted = true;
+        this.startLoading()
+    };
+    Player.prototype.startLoading = function() {
+        var _self = this;
+        if (_self.pc) {
+            _self.pc.close()
+        }
+        _self.pc = new RTCPeerConnection(null);
+        _self.pc.ontrack = function(event) {
+            _self.options.video["srcObject"] = event.streams[0]
+        }
+        ;
+        _self.pc.addTransceiver("audio", {
+            direction: "recvonly"
+        });
+        _self.pc.addTransceiver("video", {
+            direction: "recvonly"
+        });
+        _self.pc.createOffer().then(function(offer) {
+            return _self.pc.setLocalDescription(offer).then(function() {
+                return offer
+            })
+        }).then(function(offer) {
+            return new Promise(function(resolve, reject) {
+                    var port = _self.urlParams.port || 1985;
+                    var api = _self.urlParams.user_query.play || "/rtc/v1/play/";
+                    if (api.lastIndexOf("/") != api.length - 1) {
+                        api += "/"
+                    }
+                    var url = "http://" + _self.urlParams.server + ":" + port + api;
+                    for (var key in _self.urlParams.user_query) {
+                        if (key != "api" && key != "play") {
+                            url += "&" + key + "=" + _self.urlParams.user_query[key]
+                        }
+                    }
+                    var data = {
+                        api: url,
+                        streamurl: _self.urlParams.url,
+                        clientip: null,
+                        sdp: offer.sdp
+                    };
+                    console.log("offer: " + JSON.stringify(data));
+                    JSWebrtc.HttpPost(url, JSON.stringify(data)).then(function(res) {
+                        console.log("answer: " + JSON.stringify(res));
+                        resolve(res.sdp)
+                    }, function(rej) {
+                        reject(rej)
+                    })
+                }
+            )
+        }).then(function(answer) {
+            return _self.pc.setRemoteDescription(new RTCSessionDescription({
+                type: "answer",
+                sdp: answer
+            }))
+        }).catch(function(reason) {
+            throw reason
+        });
+        if (this.autoplay) {
+            this.play()
+        }
+    }
+    ;
+    Player.prototype.play = function(ev) {
+        if (this.animationId) {
+            return
+        }
+        this.animationId = requestAnimationFrame(this.update.bind(this));
+        this.paused = false
+    }
+    ;
+    Player.prototype.pause = function(ev) {
+        if (this.paused) {
+            return
+        }
+        cancelAnimationFrame(this.animationId);
+        this.animationId = null;
+        this.isPlaying = false;
+        this.paused = true;
+        this.options.video.pause();
+        if (this.options.onPause) {
+            this.options.onPause(this)
+        }
+    }
+    ;
+    Player.prototype.stop = function(ev) {
+        this.pause()
+    }
+    ;
+    Player.prototype.destroy = function() {
+        this.pause();
+        this.pc && this.pc.close() && this.pc.destroy();
+        this.audioOut && this.audioOut.destroy()
+    }
+    ;
+    Player.prototype.update = function() {
+        this.animationId = requestAnimationFrame(this.update.bind(this));
+        if (this.options.video.readyState < 4) {
+            return
+        }
+        if (!this.isPlaying) {
+            this.isPlaying = true;
+            this.options.video.play();
+            if (this.options.onPlay) {
+                this.options.onPlay(this)
+            }
+        }
+    }
+    ;
+    return Player
+}();

+ 7 - 0
web/src/api/gateway.js

@@ -32,4 +32,11 @@ export const deleteBaseGateway = (data) => {
   })
 }
 
+export const getPublicIntersect = () => {
+  return service({
+    url: '/intersect/getPublicIntersect',
+    method: 'get'
+  })
+}
+
 

+ 33 - 0
web/src/api/intersect.js

@@ -0,0 +1,33 @@
+import service from "@/utils/request";
+
+export const getIntersectList = (data) => {
+    return service({
+        url: '/intersect/getIntersectList',
+        method: 'post',
+        data: data
+    })
+}
+
+export const addIntersect = (data) => {
+    return service({
+        url: '/intersect/addIntersect',
+        method: 'post',
+        data: data
+    })
+}
+
+export const setIntersectInfo = (data) => {
+    return service({
+        url: '/intersect/updateIntersect',
+        method: 'put',
+        data: data
+    })
+}
+
+export const deleteBaseIntersect = (data) => {
+    return service({
+        url: '/intersect/delIntersect',
+        method: 'delete',
+        data: data
+    })
+}

+ 25 - 0
web/src/api/multiScreen.js

@@ -0,0 +1,25 @@
+import service from "@/utils/request";
+
+export const GetmultiscreenList = () => { // 列表
+    return service({
+        url: '/multiscreen/multiscreenList',
+        method: 'post'
+    })
+}
+
+export const GetDevicesCameras = () => { // 获取监控设备
+    return service({
+        url: '/multiscreen/devicesCameras',
+        method: 'post'
+    })
+}
+
+export const multiscreenSubmit = (site,id) => { //更换设备
+    return service({
+        url: '/multiscreen/multiscreenSubmit',
+        method: 'post',
+        params: {
+            site,id
+        }
+    })
+}

+ 18 - 0
web/src/api/playback.js

@@ -0,0 +1,18 @@
+import service from "@/utils/request";
+
+export const GetDevicesCameras = () => { // 获取监控设备
+    return service({
+        url: '/playback/devicesCameras',
+        method: 'post'
+    })
+}
+
+export const rewindText = (streamId, date) => {
+    return service({
+        url: '/playback/rewind',
+        method: 'post',
+        params: {
+            streamId, date
+        }
+    })
+}

+ 15 - 0
web/src/view/devicesAdmin/camera/camera.vue

@@ -184,6 +184,13 @@
                 </el-select>
               </el-form-item>
             </el-col>
+            <el-col :span="12">
+              <el-form-item label="流ID:" :inline="false" prop="streamId">
+                <el-input v-model="cameraInfo.streamId"></el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
             <el-col :span="12">
               <el-form-item label="安装时间:" :inline="false">
                 <el-date-picker
@@ -274,6 +281,11 @@
             </el-col>
           </el-row>
           <el-row>
+            <el-col :span="12">
+              <el-form-item label="流ID:" :inline="false">
+                <input style="border: none; outline: none; " v-model="cameraInfo.streamId"/>
+              </el-form-item>
+            </el-col>
             <el-col :span="12">
               <el-form-item label="安装时间:" :inline="false">
                 {{cameraInfo.installTime }}
@@ -319,6 +331,9 @@ const rules = ref({
   ],
   gatewayId: [
     { required: true,  message:'请选择网关',trigger: 'blur'},
+  ],
+  streamId: [
+    { required: true,  message:'请填写流ID',trigger: 'blur'},
   ]
 })
 

+ 34 - 16
web/src/view/devicesAdmin/gateway/gateway.vue

@@ -50,7 +50,7 @@
         </el-table-column>
         <el-table-column prop="gateway.gatewayBrand" label="品牌" />
         <el-table-column prop="gateway.gatewayModel" label="型号"/>
-        <el-table-column prop="gateway.gatewayAddress" label="所在地址"/>
+        <el-table-column prop="gateway.Intersect.intersectName" label="所属路口"/>
         <el-table-column prop="gateway.status" label="在线状态">
           <template #default="scope">
             <div class="onlinebox" :style="{'background': scope.row.gateway.status===1 ? 'red':'green' }"></div>
@@ -136,8 +136,10 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item label="所在地址:" :inline="false">
-                <el-input v-model="gatewayInfo.gateway.gatewayAddress"></el-input>
+              <el-form-item label="所属路口:" :inline="false" prop="gateway.intersectId">
+                <el-select v-model="gatewayInfo.gateway.intersectId" placeholder="请选择所属路口">
+                  <el-option v-for=" (item) in intersectList" :label="item.intersectName" :value="item.id"></el-option>
+                </el-select>
               </el-form-item>
             </el-col>
           </el-row>
@@ -201,8 +203,8 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item label="所在地址:" :inline="false">
-                {{gatewayInfo.gateway.gatewayAddress }}
+              <el-form-item label="所属路口:" :inline="false">
+                {{gatewayInfo.gateway.Intersect.intersectName }}
               </el-form-item>
             </el-col>
           </el-row>
@@ -213,9 +215,8 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item label="在线状态:" :inline="false">
-                <div class="onlinebox" :style="{'background': gatewayInfo.gateway.status===1 ? 'red':'green' }"></div>
-                <span :style="{ color: gatewayInfo.gateway.status === 1 ? 'red' : 'green' }">{{ gatewayInfo.gateway.status === 1 ? '离线' : '在线' }}</span>
+              <el-form-item label="所在地址:" :inline="false">
+                {{gatewayInfo.gateway.Intersect.address}}
               </el-form-item>
             </el-col>
           </el-row>
@@ -225,6 +226,12 @@
                 {{gatewayInfo.gateway.recentOnline === "0001-01-01 08:05:43"?'——':gatewayInfo.gateway.recentOnline}}
               </el-form-item>
             </el-col>
+            <el-col :span="12">
+              <el-form-item label="在线状态:" :inline="false">
+                <div class="onlinebox" :style="{'background': gatewayInfo.gateway.status===1 ? 'red':'green' }"></div>
+                <span :style="{ color: gatewayInfo.gateway.status === 1 ? 'red' : 'green' }">{{ gatewayInfo.gateway.status === 1 ? '离线' : '在线' }}</span>
+              </el-form-item>
+            </el-col>
           </el-row>
         </el-form>
         <div style="padding: 20px 0px;font-weight: bolder; border-top: solid 1px #e8eaec">关联设备:</div>
@@ -281,12 +288,12 @@
               <el-table-column prop="radarModel" label="型号" width="250px"/>
               <el-table-column prop="ipAddress" label="ip地址" width="150px"/>
               <el-table-column prop="shuntDescribe" label="分路描述" width="200px"/>
-              <el-table-column prop="status" fixed="right" label="状态" width="80px">
-                <template #default="scope">
-                  <div class="onlinebox" :style="{'background': scope.row.status===1 ? 'red':'green' }"></div>
-                  <span :style="{ color: scope.row.status === 1 ? 'red' : 'green' }">{{ scope.row.status === 1 ? '离线' : '在线' }}</span>
-                </template>
-              </el-table-column>
+<!--              <el-table-column prop="status" fixed="right" label="状态" width="80px">-->
+<!--                <template #default="scope">-->
+<!--                  <div class="onlinebox" :style="{'background': scope.row.status===1 ? 'red':'green' }"></div>-->
+<!--                  <span :style="{ color: scope.row.status === 1 ? 'red' : 'green' }">{{ scope.row.status === 1 ? '离线' : '在线' }}</span>-->
+<!--                </template>-->
+<!--              </el-table-column>-->
             </el-table>
           </el-tab-pane>
         </el-tabs>
@@ -295,7 +302,7 @@
   </div>
 </template>
 <script setup>
-import {addGateway, deleteBaseGateway, getGatewayList, setGatewayInfo} from "@/api/gateway";
+import {addGateway, deleteBaseGateway, getGatewayList, getPublicIntersect, setGatewayInfo} from "@/api/gateway";
 import {ref} from 'vue'
 import {format,parseISO} from "date-fns";
 import {ElMessage, ElMessageBox} from "element-plus";
@@ -308,6 +315,7 @@ const searchInfo = ref({})
 const gatewayInfo = ref({
   gateway:{}
 })
+const intersectList = ref([])
 
 const gatewayForm = ref(null)
 const dialogFlag = ref('add')
@@ -328,6 +336,9 @@ const rules = ref({
     "gateway.ipAddress": [
     { required: true,  message:'请输入ip地址',trigger: 'blur'},
     { pattern:/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/,message: '请输入正确的ip地址',trigger: 'blur'}
+  ],
+  "gateway.intersectId":[
+    {required: true, message: '请选择路口',trigger: 'blur'}
   ]
 })
 
@@ -361,6 +372,13 @@ const getTableData = async() => {
     pageSize.value = table.data.pageSize
   }
 }
+const getIntersect = async ()=>{
+  const table = await getPublicIntersect();
+  if (table.code === 0) {
+    intersectList.value = table.data.pubIntersect
+  }
+}
+getIntersect()
 getTableData()
 
 const handleSizeChange = (val) => {
@@ -379,7 +397,7 @@ const cameracount = ref(0)
 const screenscount = ref(0)
 const radarcount = ref(0)
 const add = () => {
-  gatewayInfo.value={gateway:{},relationDev:{}}
+  gatewayInfo.value={gateway:{Intersect:{}},relationDev:{}}
   dialogFlag.value = 'add'
   addGatewayDialog.value = true
   getNowDay()

+ 348 - 0
web/src/view/devicesAdmin/intersect/intersect.vue

@@ -0,0 +1,348 @@
+<template>
+  <div>
+    <div class="gva-search-box">
+      <el-form
+          ref="searchForm"
+          :inline="true"
+          :model="searchInfo"
+      >
+        <el-form-item label="名称">
+          <el-input
+              v-model="searchInfo.keyword"
+              placeholder="搜索名称"
+          />
+        </el-form-item>
+        <el-form-item label="路口类型">
+          <el-select v-model="searchInfo.shunt" placeholder="路口类型">
+            <el-option label="十字路口" :value="'十字路口'"></el-option>
+            <el-option label="T型路口" :value="'T型路口'"></el-option>
+            <el-option label="弯道" :value="'弯道'"></el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button
+              type="primary"
+              icon="search"
+              @click="onSubmit"
+          >查询</el-button>
+          <el-button
+              icon="refresh"
+              @click="onReset"
+          >重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="gva-table-box">
+      <div class="gva-btn-list">
+        <el-button
+            type="primary"
+            icon="plus"
+            @click="add()"
+        >新增</el-button>
+      </div>
+      <el-table
+          :data="tableData"
+          style="width: 100%; margin-bottom: 20px"
+          row-key="id"
+          border
+          :table-layout="'fixed'"
+          :row-class-name="tableRowClassName"
+      >
+        <el-table-column prop="id" label="#" :width="80"/>
+        <el-table-column prop="intersectName" label="名称" />
+        <el-table-column prop="intersectType" label="类型"/>
+        <el-table-column prop="address" label="所在地址"/>
+        <el-table-column prop="status" label="运行状态">
+          <template #default="scope">
+            <div class="onlinebox" :style="{'background': scope.row.status===1 ? 'red':'green' }"></div>
+            <span :style="{ color: scope.row.status === 1 ? 'red' : 'green' }">{{ scope.row.status === 1 ? '异常' : '正常' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+            align="left"
+            fixed="right"
+            label="操作"
+            :width="300"
+        >
+          <template #default="scope">
+            <el-button
+                type="primary"
+                link
+                icon="view"
+                @click="view(scope.row)"
+            >查看</el-button>
+            <el-button
+                type="primary"
+                link
+                icon="edit"
+                @click="editIntersect(scope.row)"
+            >编辑</el-button>
+            <el-button
+                type="primary"
+                link
+                icon="delete"
+                @click="deleteIntersect(scope.row)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="gva-pagination">
+        <el-pagination
+            :current-page="page"
+            :page-size="pageSize"
+            :page-sizes="[10, 30, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @current-change="handleCurrentChange"
+            @size-change="handleSizeChange"
+        />
+      </div>
+
+      <el-dialog v-model="addIntersectDialog" title="摄像头" width="900">
+        <el-form
+            ref="intersectForm"
+            :rules="rules"
+            :model="intersectInfo"
+            label-width="110px"
+            style="padding: 15px"
+        >
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="路口名称:" :inline="false" prop="intersectName">
+                <el-input v-model="intersectInfo.intersectName"></el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="路口类型:" :inline="false" prop="intersectType">
+                <el-select v-model="intersectInfo.intersectType" placeholder="路口类型">
+                  <el-option label="十字路口" :value="'十字路口'"></el-option>
+                  <el-option label="T型路口" :value="'T型路口'"></el-option>
+                  <el-option label="弯道" :value="'弯道'"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="路口地址:" :inline="false">
+                <el-input type="textarea" v-model="intersectInfo.address"></el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button @click="closeAddIntersectDialog">取消</el-button>
+            <el-button type="primary" @click="enterAddIntersectDialog">
+              确定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+
+      <el-dialog v-model="viewIntersectDialog" title="详情" style="padding: 35px">
+        <el-form
+            :model="intersectInfo"
+            label-width="100px"
+            style="padding: 20px"
+        >
+          <el-row>
+            <el-col :span="12">
+              <el-form-item label="路口名称:" :inline="false">
+                {{intersectInfo.intersectName }}
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="路口类型:" :inline="false">
+                {{intersectInfo.intersectType }}
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="12">
+              <el-form-item label="安装时间:" :inline="false">
+                {{intersectInfo.createTime }}
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="在线状态:" :inline="false">
+                <div class="onlinebox" :style="{'background': intersectInfo.status===1 ? 'red':'green' }"></div>
+                <span :style="{ color: intersectInfo.status === 1 ? 'red' : 'green' }">{{ intersectInfo.status === 1 ? '异常' : '正常' }}</span>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="所在地址:" :inline="false">
+                {{intersectInfo.address }}
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+<script setup>
+import {deleteBaseIntersect, getIntersectList,setIntersectInfo,addIntersect} from "@/api/intersect";
+import { ref} from 'vue'
+import {ElMessage, ElMessageBox} from "element-plus";
+const page = ref(1)
+const total = ref(0)
+const pageSize = ref(10)
+const tableData = ref([])
+const searchInfo = ref({})
+const intersectInfo = ref({})
+const intersectForm = ref(null)
+const dialogFlag = ref('add')
+const isFlag = ref('')
+const addIntersectDialog = ref(false)
+const viewIntersectDialog = ref(false)
+
+const rules = ref({
+  intersectName: [
+    { required: true, message: '请输入名称',trigger: 'blur'}
+  ],
+  intersectType: [
+    {required: true, message: '请选择类型',trigger: 'blur' }
+  ]
+})
+const tableRowClassName = ({row, rowIndex})=>{
+  if ((rowIndex+1) % 2  === 0) {
+    return 'success-row';
+  }
+  return '';
+}
+const getTableData = async() => {
+  const table = await getIntersectList({page: page.value, pageSize: pageSize.value,...searchInfo.value})
+  if (table.code === 0) {
+    tableData.value = table.data.list
+    total.value = table.data.total
+    page.value = table.data.page
+    pageSize.value = table.data.pageSize
+  }
+}
+getTableData()
+
+const view = (row)=>{
+  viewIntersectDialog.value = true
+  intersectInfo.value = JSON.parse(JSON.stringify(row))
+  intersectInfo.value.createTime = new Date(intersectInfo.value.createTime).toLocaleDateString()
+}
+
+
+
+const handleSizeChange = (val) => {
+  pageSize.value = val
+  getTableData()
+}
+
+const handleCurrentChange = (val) => {
+  page.value = val
+  getTableData()
+}
+
+const add = () => {
+  intersectInfo.value={}
+  dialogFlag.value = 'add'
+  addIntersectDialog.value = true
+}
+
+const closeAddIntersectDialog = () => {
+  intersectForm.value.resetFields()
+  addIntersectDialog.value = false
+  isFlag.value=''
+}
+
+// 弹窗相关
+const onSubmit = () => {
+  page.value = 1
+  pageSize.value = 10
+  getTableData()
+}
+const onReset = () => {
+  searchInfo.value = {}
+}
+
+const enterAddIntersectDialog = async() => {
+  intersectForm.value.validate(async valid => {
+    if (valid) {
+      const req = {
+        ...intersectInfo.value
+      }
+      if (dialogFlag.value === 'add') {
+        const res = await addIntersect(req)
+        if (res.code === 0) {
+          ElMessage({type: 'success', message: '创建成功'})
+          await getTableData()
+          closeAddIntersectDialog()
+        }
+      }
+      if (dialogFlag.value === 'edit') {
+        const res = await setIntersectInfo(req)
+        if (res.code === 0) {
+          ElMessage({type: 'success', message: '编辑成功'})
+          await getTableData()
+          closeAddIntersectDialog()
+        }
+      }
+    }
+  })
+}
+
+const editIntersect = (row) => {
+  dialogFlag.value = 'edit'
+  intersectInfo.value = JSON.parse(JSON.stringify(row))
+  addIntersectDialog.value = true
+}
+const deleteIntersect = (obj) => {
+  ElMessageBox.confirm('您确定要删除吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+      .then(async () => {
+        const res = await deleteBaseIntersect({ID: obj.id})
+        if (res.code === 0) {
+          ElMessage({
+            type: 'success',
+            message: '删除成功!',
+          })
+          getTableData()
+        }
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '已取消删除',
+        })
+      })
+}
+
+</script>
+<style>
+.el-table .success-row {
+  background: #f3fdef;
+}
+
+.onlinebox {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  display: inline-block;
+  margin-right: 4px;
+  position: relative;
+  top: 1px;
+}
+
+.el-dialog__header {
+  border-bottom: 1px solid #e8eaec;
+}
+
+.el-dialog__footer {
+  border-top: 1px solid #e8eaec;
+}
+</style>

+ 15 - 14
web/src/view/devicesAdmin/radar/radar.vue

@@ -63,12 +63,12 @@
         <el-table-column prop="radarBrand" label="品牌" />
         <el-table-column prop="radarModel" label="型号"/>
         <el-table-column prop="GateWay.gatewayName" label="所属网关"/>
-        <el-table-column prop="status" label="在线状态">
-          <template #default="scope">
-            <div class="onlinebox" :style="{'background': scope.row.status===1 ? 'red':'green' }"></div>
-            <span :style="{ color: scope.row.status === 1 ? 'red' : 'green' }">{{ scope.row.status === 1 ? '离线' : '在线' }}</span>
-          </template>
-        </el-table-column>
+<!--        <el-table-column prop="status" label="在线状态">-->
+<!--          <template #default="scope">-->
+<!--            <div class="onlinebox" :style="{'background': scope.row.status===1 ? 'red':'green' }"></div>-->
+<!--            <span :style="{ color: scope.row.status === 1 ? 'red' : 'green' }">{{ scope.row.status === 1 ? '离线' : '在线' }}</span>-->
+<!--          </template>-->
+<!--        </el-table-column>-->
         <el-table-column
             align="left"
             fixed="right"
@@ -263,20 +263,21 @@
                 {{radarInfo.installTime }}
               </el-form-item>
             </el-col>
-            <el-col :span="12">
-              <el-form-item label="在线状态:" :inline="false">
-                <div class="onlinebox" :style="{'background': radarInfo.status===1 ? 'red':'green' }"></div>
-                <span :style="{ color: radarInfo.status === 1 ? 'red' : 'green' }">{{ radarInfo.status === 1 ? '离线' : '在线' }}</span>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row>
+<!--            <el-col :span="12">-->
+<!--              <el-form-item label="在线状态:" :inline="false">-->
+<!--                <div class="onlinebox" :style="{'background': radarInfo.status===1 ? 'red':'green' }"></div>-->
+<!--                <span :style="{ color: radarInfo.status === 1 ? 'red' : 'green' }">{{ radarInfo.status === 1 ? '离线' : '在线' }}</span>-->
+<!--              </el-form-item>-->
+<!--            </el-col>-->
             <el-col :span="12">
               <el-form-item label="探测距离:" :inline="false">
                 {{radarInfo.detectionRange }}
               </el-form-item>
             </el-col>
           </el-row>
+<!--          <el-row>-->
+<!--            -->
+<!--          </el-row>-->
         </el-form>
       </el-dialog>
     </div>

+ 23 - 0
web/src/view/monitoring/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>
+    <router-view v-slot="{ Component }">
+      <transition
+        mode="out-in"
+        name="el-fade-in-linear"
+      >
+        <keep-alive :include="routerStore.keepAliveRouters">
+          <component :is="Component" />
+        </keep-alive>
+      </transition>
+    </router-view>
+  </div>
+</template>
+
+<script setup>
+import { useRouterStore } from '@/pinia/modules/router'
+const routerStore = useRouterStore()
+
+defineOptions({
+  name: 'Monitoring'
+})
+</script>

+ 279 - 0
web/src/view/monitoring/multiScreen/multiScreen.vue

@@ -0,0 +1,279 @@
+<template>
+  <div class="wrap" style="padding: 30px 0px">
+    <el-row :gutter="20" style="background:#fff;padding-top:10px;">
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName1}}  <p class="lishihuifang" v-show="url1 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName1+'('+code1+')',streamId:streamId1,deviceAddress:deviceAddress1}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(1)">更换</span> <span @click="delHand(1)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(1)"><Plus /></el-icon>
+          <video-play :url="url1" v-show="url1 !== ''" ref="child1"></video-play>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName2}}  <p class="lishihuifang" v-show="url2 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName2+'('+code2+')',streamId:streamId2,deviceAddress:deviceAddress2}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(2)">更换</span> <span @click="delHand(2)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(2)"><Plus /></el-icon>
+          <video-play2 :url="url2" v-show="url2 !== ''" ref="child2"></video-play2>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName3}}  <p class="lishihuifang" v-show="url3 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName3+'('+code3+')',streamId:streamId3,deviceAddress:deviceAddress3}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(3)">更换</span> <span @click="delHand(3)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(3)"><Plus /></el-icon>
+          <video-play3 :url="url3" v-show="url3 !== ''" ref="child3"></video-play3>
+        </div>
+      </el-col>
+    </el-row>
+    <el-row :gutter="20" style="margin-top:20px;">
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName4}}  <p class="lishihuifang" v-show="url4 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName4+'('+code4+')',streamId:streamId4,deviceAddress:deviceAddress4}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(4)">更换</span> <span @click="delHand(4)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(4)"><Plus /></el-icon>
+          <video-play4 :url="url4" v-show="url4 !== ''" ref="child4"></video-play4>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName5}}  <p class="lishihuifang" v-show="url5 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName5+'('+code5+')',streamId:streamId5,deviceAddress:deviceAddress5}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(5)">更换</span> <span @click="delHand(5)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(5)"><Plus /></el-icon>
+          <video-play5 :url="url5" v-show="url5 !== ''" ref="child5"></video-play5>
+        </div>
+      </el-col>
+      <el-col :span="8">
+        <div class="top-operate-box">{{cameraName6}}  <p class="lishihuifang" v-show="url6 !== ''"><router-link :to="{name:'playback',query:{cameraName:cameraName6+'('+code6+')',streamId:streamId6,deviceAddress:deviceAddress6}}" style="color: rgb(40, 198, 217);"><img src="/img/map/lishi-icon.png" />录像回放</router-link> <span @click="editHand(6)">更换</span> <span @click="delHand(6)">删除</span></p></div>
+        <div class="video-box">
+          <el-icon class="el-icon-plus" @click="editHand(6)"><Plus /></el-icon>
+          <video-play6 :url="url6" v-show="url6 !== ''" ref="child6"></video-play6>
+        </div>
+      </el-col>
+    </el-row>
+    <el-dialog
+        title="选择监控设备" left :close-on-click-modal="false"
+        v-model="dialogSelMonitor"
+        width="600px" :modal="false">
+      <el-tree highlight-current :data="cameraList" :props="defaultProps" @node-click="cameranodeClick"></el-tree>
+      <el-button type="primary" style="margin:10px auto 0;display:block;" @click="addChangeVideo">添加</el-button>
+    </el-dialog>
+  </div>
+</template>
+<script setup>
+import {GetDevicesCameras, GetmultiscreenList,multiscreenSubmit} from "@/api/multiScreen";
+import {ref,onMounted} from 'vue';
+import videoPlay from './videoplay.vue'
+import videoPlay2 from './videoplay2.vue'
+import videoPlay3 from './videoplay3.vue'
+import videoPlay4 from './videoplay4.vue'
+import videoPlay5 from './videoplay5.vue'
+import videoPlay6 from './videoplay6.vue'
+import {Plus} from "@element-plus/icons-vue";
+import {ElMessage, ElMessageBox} from "element-plus";
+const url1 = ref("")
+const url2 = ref("")
+const url3 = ref("")
+const url4 = ref("")
+const url5 = ref("")
+const url6 = ref("")
+const cameraName1 = ref("")
+const cameraName2 = ref("")
+const cameraName3 = ref("")
+const cameraName4 = ref("")
+const cameraName5 = ref("")
+const cameraName6 = ref("")
+const code1 =ref("")
+const code2 =ref("")
+const code3 =ref("")
+const code4 =ref("")
+const code5 =ref("")
+const code6 =ref("")
+const streamId1 = ref("")
+const streamId2 = ref("")
+const streamId3 = ref("")
+const streamId4 = ref("")
+const streamId5 = ref("")
+const streamId6 = ref("")
+const deviceAddress1 = ref("")
+const deviceAddress2 = ref("")
+const deviceAddress3 = ref("")
+const deviceAddress4 = ref("")
+const deviceAddress5 = ref("")
+const deviceAddress6 = ref("")
+
+const current = ref(0)
+const cameraId = ref(0)
+const dialogSelMonitor = ref(false)
+const cameraList = ref([])
+const defaultProps = ref({
+  children: 'cameraList',
+  label: 'poleGroupName'
+})
+
+onMounted(()=>{
+  multiscreenListFun()
+  getCameraLiveList()
+})
+
+const editHand = (val)=> {
+  current.value = val
+  dialogSelMonitor.value = true
+}
+
+const cameranodeClick = (val)=>{
+  if (!val.cameraList && val.cameraList !== null){
+    cameraId.value =  val.id
+  }
+}
+const addChangeVideo = async () => {
+  const res = await multiscreenSubmit(current.value, cameraId.value);
+  if (res.code === 0) {
+    dialogSelMonitor.value = false
+    ElMessage({ type: 'success', message: '操作成功' })
+    multiscreenListFun()
+  }
+}
+
+const child1 = ref()
+const child2 = ref()
+const child3 = ref()
+const child4 = ref()
+const child5 = ref()
+const child6 = ref()
+const delHand =(val)=> {
+  ElMessageBox.confirm('您确定要删除该摄像头吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+      .then(async() => {
+        if (val===1){
+          url1.value = ""
+          child1.value.endde()
+        }else if(val===2){
+          url2.value = ""
+          child2.value.endde()
+        }else if(val===3){
+          url3.value = ""
+          child3.value.endde()
+        }else if(val===4){
+          url4.value = ""
+          child4.value.endde()
+        }else if(val===5){
+          url5.value = ""
+          child5.value.endde()
+        }else{
+          url6.value = ""
+          child6.value.endde()
+        }
+        const res = await multiscreenSubmit(val,-1)
+        if (res.code === 0) {
+          ElMessage({
+            type: 'success',
+            message: '删除成功!',
+          })
+          multiscreenListFun()
+        }
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '已取消删除',
+        })
+      })
+}
+const props = ref(['url','cameraName','code','streamId','deviceAddress'])
+
+const multiscreenListFun = async () => {
+  const res = await GetmultiscreenList()
+  if (res.code === 0) {
+    url1.value = res.data.list[0].url;
+    url2.value = res.data.list[1].url;
+    url3.value = res.data.list[2].url;
+    url4.value = res.data.list[3].url;
+    url5.value = res.data.list[4].url;
+    url6.value = res.data.list[5].url;
+    cameraName1.value = res.data.list[0].name;
+    cameraName2.value = res.data.list[1].name;
+    cameraName3.value = res.data.list[2].name;
+    cameraName4.value = res.data.list[3].name;
+    cameraName5.value = res.data.list[4].name;
+    cameraName6.value = res.data.list[5].name;
+    code1.value = res.data.list[0].code;
+    code2.value = res.data.list[1].code;
+    code3.value = res.data.list[2].code;
+    code4.value = res.data.list[3].code;
+    code5.value = res.data.list[4].code;
+    code6.value = res.data.list[5].code;
+    streamId1.value = res.data.list[0].streamId;
+    streamId2.value = res.data.list[1].streamId;
+    streamId3.value = res.data.list[2].streamId;
+    streamId4.value = res.data.list[3].streamId;
+    streamId5.value = res.data.list[4].streamId;
+    streamId6.value = res.data.list[5].streamId;
+    deviceAddress1.value = res.data.list[0].deviceAddress;
+    deviceAddress2.value = res.data.list[1].deviceAddress;
+    deviceAddress3.value = res.data.list[2].deviceAddress;
+    deviceAddress4.value = res.data.list[3].deviceAddress;
+    deviceAddress5.value = res.data.list[4].deviceAddress;
+    deviceAddress6.value = res.data.list[5].deviceAddress;
+
+  }
+}
+
+const getCameraLiveList =async () => {
+  const res = await GetDevicesCameras()
+  cameraList.value = res.data
+}
+
+</script>
+<style scoped>
+.wrap{background:#fff;}
+.icon-tuichuquanping{
+  position:absolute;
+  top:5px;
+  right:6px;
+  cursor:pointer;
+}
+.video-box{
+  /* border:1px solid #ced7ea; */
+  border-radius:4px;
+  width:100%;
+  height:56.2%;
+  min-height:282px;
+  position:relative;
+  background: #fff;
+}
+.video-box .el-icon-plus{
+  font-size:100px;
+  color:#d8d8d8;
+  position:absolute;
+  left:50%;
+  top:50%;
+  transform:translate(-50%,-50%);
+  cursor:pointer;
+}
+
+.lishihuifang{
+  position:absolute;
+  top:0;
+  right:0;
+  color: rgb(40, 198, 217);
+  width:187px;
+  margin:0;
+  padding:0;
+  font-size:14px;
+
+}
+.lishihuifang img{
+  vertical-align: middle;
+  margin-right:4px;
+  position:relative;
+  top:-2px;
+}
+.lishihuifang span{
+  margin-left:20px;
+  cursor:pointer;
+}
+.top-operate-box{
+  position:relative;width:100%;margin-bottom:10px;
+  height:20px;
+}
+
+</style>

+ 50 - 0
web/src/view/monitoring/multiScreen/videoplay.vue

@@ -0,0 +1,50 @@
+<template>
+    <div style="position:relative;">
+       <video id="video-webrtc"  controls autoplay style="width:100%"></video>
+    </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+
+var player = ''
+const getScript = ()=>{
+   var video = '';
+   video = document.getElementById('video-webrtc');
+   player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+    .video-js .vjs-progress-control.vjs-control {
+        display: none;
+    }
+    .btn-zhuapai{
+    position:absolute;
+    top:15px;
+    right:10px;
+    background: rgba(255,255,255,0.5);
+    width:44px;
+    height:27px;
+    text-align:center;
+    line-height: 27px;
+    font-size:14px;
+    cursor:pointer;
+  }
+</style>

+ 49 - 0
web/src/view/monitoring/multiScreen/videoplay2.vue

@@ -0,0 +1,49 @@
+<template>
+  <div style="position:relative;">
+    <video id="video-webrtc2"  controls autoplay style="width:100%"></video>
+  </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+var player = ''
+const getScript = ()=>{
+  var video = '';
+  video = document.getElementById('video-webrtc2');
+  player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.btn-zhuapai{
+  position:absolute;
+  top:15px;
+  right:10px;
+  background: rgba(255,255,255,0.5);
+  width:44px;
+  height:27px;
+  text-align:center;
+  line-height: 27px;
+  font-size:14px;
+  cursor:pointer;
+}
+</style>

+ 50 - 0
web/src/view/monitoring/multiScreen/videoplay3.vue

@@ -0,0 +1,50 @@
+<template>
+  <div style="position:relative;">
+    <video id="video-webrtc3"  controls autoplay style="width:100%"></video>
+  </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+
+var player = ''
+const getScript = ()=>{
+  var video = '';
+  video = document.getElementById('video-webrtc3');
+  player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.btn-zhuapai{
+  position:absolute;
+  top:15px;
+  right:10px;
+  background: rgba(255,255,255,0.5);
+  width:44px;
+  height:27px;
+  text-align:center;
+  line-height: 27px;
+  font-size:14px;
+  cursor:pointer;
+}
+</style>

+ 51 - 0
web/src/view/monitoring/multiScreen/videoplay4.vue

@@ -0,0 +1,51 @@
+<template>
+  <div style="position:relative;">
+    <video id="video-webrtc4"  controls autoplay style="width:100%"></video>
+  </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+
+var player = ''
+const getScript = ()=>{
+  var video = '';
+  video = document.getElementById('video-webrtc4');
+  player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.btn-zhuapai{
+  position:absolute;
+  top:15px;
+  right:10px;
+  background: rgba(255,255,255,0.5);
+  width:44px;
+  height:27px;
+  text-align:center;
+  line-height: 27px;
+  font-size:14px;
+  cursor:pointer;
+}
+</style>

+ 49 - 0
web/src/view/monitoring/multiScreen/videoplay5.vue

@@ -0,0 +1,49 @@
+<template>
+  <div style="position:relative;">
+    <video id="video-webrtc5"  controls autoplay style="width:100%"></video>
+  </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+var player = ''
+const getScript = ()=>{
+  var video = '';
+  video = document.getElementById('video-webrtc5');
+  player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.btn-zhuapai{
+  position:absolute;
+  top:15px;
+  right:10px;
+  background: rgba(255,255,255,0.5);
+  width:44px;
+  height:27px;
+  text-align:center;
+  line-height: 27px;
+  font-size:14px;
+  cursor:pointer;
+}
+</style>

+ 51 - 0
web/src/view/monitoring/multiScreen/videoplay6.vue

@@ -0,0 +1,51 @@
+<template>
+  <div style="position:relative;">
+    <video id="video-webrtc6"  controls autoplay style="width:100%"></video>
+  </div>
+</template>
+<script setup>
+
+import { ref,toRef,watch,defineProps } from 'vue'
+const props = defineProps({
+  url: String
+});
+
+const refToWatch = toRef(props,'url')
+watch(refToWatch,(newVal,oldValue)=>{
+  if (newVal !== oldValue && newVal !== ""){
+    getScript()
+  }
+})
+
+var player = ''
+const getScript = ()=>{
+  var video = '';
+  video = document.getElementById('video-webrtc6');
+  player = new JSWebrtc.Player(props.url, { video: video, autoplay: true, onPlay: (obj) => {  } });
+}
+
+const endde =()=>{
+  player.destroy()
+}
+
+defineExpose({
+  endde
+})
+</script>
+<style scoped>
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.btn-zhuapai{
+  position:absolute;
+  top:15px;
+  right:10px;
+  background: rgba(255,255,255,0.5);
+  width:44px;
+  height:27px;
+  text-align:center;
+  line-height: 27px;
+  font-size:14px;
+  cursor:pointer;
+}
+</style>

+ 384 - 0
web/src/view/monitoring/playback/playback.vue

@@ -0,0 +1,384 @@
+<template>
+  <div class="wrap">
+    <el-row :gutter="20" style="background:#fff;">
+      <el-col :span="5">
+        <p style="font-size:17px;font-weight:bold;">摄像机列表</p>
+        <el-tree :data="cameraList" :props="defaultProps" @node-click="cameranodeClick"></el-tree>
+      </el-col>
+      <el-col :span="18" style="position:relative">
+        <p style="font-size:17px;font-weight:bold;">录像回放</p>
+        <el-row style="margin:15px 0px">
+          <el-col :span="10">
+            <div>摄像头名称及编码:{{cameraName}}</div>
+          </el-col>
+          <el-col :span="10">
+            <div >位置:{{lampPoleLocation}}</div>
+          </el-col>
+        </el-row>
+        <el-row style="margin:10px 0px">
+          <el-col :span="10">
+            <div>
+              回放日期:<el-date-picker v-model="backDate" @change="changeBackDate" type="date" placeholder="请选择" :disabled-date="disabledDate" :clearable="false" />
+            </div>
+          </el-col>
+          <el-col :span="10">
+            录像列表:<el-select v-model="backVideo" @change="changemp4" placeholder="请选择" style="width: 250px">
+                <el-option
+                    v-for="item in chooseTimePlayArr"
+                    :key="item"
+                    :label="item"
+                    :value="item">
+                </el-option>
+              </el-select>
+          </el-col>
+        </el-row>
+        <div style="position:relative;width:90%">
+          <video width="100%"  controls v-if="reFresh" autoplay="autoplay" id="videoElementID" crossorigin="anonymous" style="margin-top:0;vertical-align:top;padding-bottom:20px">
+            <source :src="playPath" :key="playPath"  type="video/mp4">
+            您的浏览器不支持Video标签。
+          </video>
+          <img src="/img/map/jietu.png" class="jietu" @click="savePic('jietu.png')" />
+          <div class="beisu-box">倍速
+            <el-select v-model="rateValue" id="rate" @change="beisuHand" style="width:75px">
+              <el-option v-for="item in rateOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+            </el-select>
+          </div>
+        </div>
+        <div><el-button @click="kuaitui">快退</el-button><el-button @click="kuijin">快进</el-button></div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script setup>
+import {GetDevicesCameras, rewindText} from '@/api/playback'
+import {ref,onMounted,nextTick} from 'vue'
+import {useRoute} from "vue-router";
+const route = useRoute()
+
+
+//定义一些变量
+const rateValue = ref("1.0")
+const cameraName = ref("")
+const lampPoleLocation = ref("")
+const reFresh = ref(true)
+const rateOptions = ref([{value:'0.5', label:'0.5'},{value:'1.0', label:'1.0'},{value:'1.25', label:'1.25'},{value:'1.5', label:'1.5'},{value:'2.0', label:'2.0'},{value:'3.0', label:'3.0'}])
+const cameraList = ref([])
+const streamId = ref("")
+const backDate = ref("")
+const playPath = ref("")
+const chooseTimePlayArr = ref([])
+const backVideo = ref("")
+
+const defaultProps = ref({
+  children: 'cameraList',
+  label: 'poleGroupName'
+})
+
+function getTodayDate() {
+  var today = new Date();
+  var year = today.getFullYear();
+  var month = (today.getMonth() + 1).toString().padStart(2, '0'); // 获取月份并补零
+  var day = today.getDate().toString().padStart(2, '0'); // 获取日期并补零
+  return `${year}-${month}-${day}`;
+}
+
+const disabledDate =  (time)=>{
+    return new Date(time).getTime() > Date.now();
+}
+const cameranodeClick = (val)=>{
+  if (!val.cameraList && val.cameraList !== null){
+    backDate.value  = getTodayDate()
+    streamId.value = val.streamId
+    cameraName.value = val.deviceName + "("+val.deviceCode+")"
+    lampPoleLocation.value = val.deviceAddress
+    getAddress()
+  }
+}
+
+onMounted(()=>{
+  backDate.value = getTodayDate()
+  getCameraLiveList()
+})
+
+const getCameraLiveList =async () => {
+  const res = await GetDevicesCameras()
+  if (res.code == 0){
+    cameraList.value = res.data
+    if (route.query.cameraName === "" || !route.query.cameraName){
+      streamId.value = cameraList.value[0].cameraList[0].streamId
+      cameraName.value = cameraList.value[0].cameraList[0].deviceName + "("+cameraList.value[0].cameraList[0].deviceCode+")"
+      lampPoleLocation.value = cameraList.value[0].cameraList[0].deviceAddress
+      getAddress()
+    }else {
+      cameraName.value =  route.query.cameraName
+      streamId.value =  route.query.streamId
+      lampPoleLocation.value =  route.query.deviceAddress
+      getAddress()
+    }
+  }
+
+}
+
+const getAddress = async () => {
+  // streamId.value = "34020000002000000001@34020000001320000015" backDate.value = "2024-08-01"
+  const date = new Date(backDate.value);
+  backDate.value = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
+  // console.log(backDate.value)
+  // console.log(streamId.value)
+  const res = await rewindText(streamId.value,backDate.value);
+  if (res.code == 0){
+    var encrypturls = atob(res.data.substring(2));
+    var urls = JSON.parse(encrypturls);
+
+    let backVideoArr = []
+
+    if (urls && urls.length>0){
+      for (let i = 0; i < urls.length; i++) {
+        backVideoArr.push(urls[i].slice(-9))
+      }
+    }else {
+      backVideoArr = []
+    }
+    chooseTimePlayArr.value = backVideoArr
+
+    reFresh.value = false
+    nextTick(()=>{
+      reFresh.value = true;
+    })
+    backVideo.value = chooseTimePlayArr.value[0]
+    playPath.value = urls[0]
+  }
+}
+
+const kuijin =()=>{
+  let video = document.querySelector('#videoElementID');
+  let time = video.currentTime;
+  video.currentTime = time + 30;
+}
+const kuaitui =()=>{
+  let video = document.querySelector('#videoElementID');
+  let time = video.currentTime;
+  video.currentTime = time - 30;
+}
+
+const beisuHand = (item)=>{
+  let video = document.querySelector('#videoElementID');
+  video.playbackRate = item;
+}
+
+const changeBackDate = (item)=>{
+  backDate.value = item
+  backVideo.value = ""
+  playPath.value = ""
+  getAddress()
+}
+
+const changemp4 = (item)=>{
+  reFresh.value = false
+  backVideo.value = item
+  playPath.value = 'http://106.52.134.22:9099/live/'+streamId.value+"/"+backDate.value+"/"+item
+  nextTick(()=>{
+    reFresh.value = true
+  })
+}
+
+const savePic = ()=>{
+    let scale = 1;
+    let video = document.querySelector('#videoElementID');
+    let canvas = document.createElement('canvas');
+    canvas.width = video.offsetWidth * scale;
+    canvas.height = video.offsetHeight * scale;
+
+    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
+
+    // 显示抓图
+    let img = document.createElement('img');
+
+    img.setAttribute("crossOrigin",'Anonymous');
+    img.src = canvas.toDataURL('image/png');
+    // 下载抓图
+    let a = document.createElement('a');
+    let event = new MouseEvent('click');
+    a.download = 'vedio'
+    a.href = canvas.toDataURL('image/png');
+    a.dispatchEvent(event)
+
+  console.log("截图")
+}
+
+</script>
+
+<style scoped lang="scss">
+.video-js .vjs-progress-control.vjs-control {
+  display: none;
+}
+.wrap >>> .el-icon-circle-close{
+  display: none;
+}
+.beisu-box >>> .el-input--suffix .el-input__inner{
+  background: rgba(255,255,255,0.2);
+  color:#fff;
+}
+.beisu-box >>> .el-input__inner{
+  height:20px;
+  line-height:20px;
+  padding-right:0;
+  width:72%;
+}
+.beisu-box >>> .el-input__suffix{
+  display: none;
+}
+.control-box{
+  position:absolute;
+  width:120px;
+  height:120px;
+  border:1px solid #ccc;
+  border-radius: 50%;
+  bottom:10px;
+  right:20px;
+  z-index:99;
+}
+.control-box .shang{
+  display: block;
+  margin:0 auto;
+  height:30px;
+  cursor:pointer;
+  animation: shake2 .2s infinite alternate ease-in-out;
+}
+.control-box .shang:hover{
+  animation: shake2 0s infinite alternate ease-in-out;
+}
+.control-box .zuo{
+  position:absolute;
+  left:0;
+  top:45px;
+  width:30px;
+  cursor:pointer;
+  animation: shake .2s infinite alternate ease-in-out;
+}
+.control-box .zuo:hover{
+  animation: shake 0s infinite alternate ease-in-out;
+}
+.control-box .xia{
+  position:absolute;
+  left:45px;
+  bottom:0;
+  cursor:pointer;
+  width:30px;
+  animation: shake2 .2s infinite alternate ease-in-out;
+}
+.control-box .xia:hover{
+  animation: shake2 0s infinite alternate ease-in-out;
+}
+.control-box .you{
+  position:absolute;
+  right:0;
+  top:45px;
+  width:30px;
+  cursor:pointer;
+  animation: shake .2s infinite alternate ease-in-out;
+}
+.control-box .you:hover{
+  animation: shake 0s infinite alternate ease-in-out;
+}
+.control-box-inner{
+  width:60px;
+  height:60px;
+  position:absolute;
+  top:30px;
+  left:30px;
+  border-radius:50%;
+
+}
+.control-box-inner .enlarge{
+  width:48%;
+  display: inline-block;
+  border-right:1px solid #ccc;
+  height:100%;
+  background: rgba(255,255,255,0.5);
+  opacity:0.5;
+  border-top-left-radius: 60px;
+  border-bottom-left-radius: 60px;
+  font-size:12px;
+  cursor:pointer;
+}
+.control-box-inner .enlarge:hover{
+  background: #169fe6;
+}
+.control-box-inner .enlarge span{
+  position:relative;
+  display: block;
+  width:13px;
+  height:30px;
+  top:14px;
+  left:13px;
+}
+.control-box-inner .shrink{
+  width:48%;
+  display:inline-block;
+  height:100%;
+  font-size:12px;
+  background: rgba(255,255,255,0.5);
+  opacity:0.5;
+  border-top-right-radius: 60px;
+  border-bottom-right-radius: 60px;
+  cursor:pointer;
+}
+.control-box-inner .shrink:hover{
+  background: #169fe6;
+}
+.control-box-inner .shrink span{
+  position:relative;
+  display: block;
+  width:13px;
+  height:30px;
+  top:14px;
+  left:3px;
+
+}
+@keyframes shake{
+  0% {
+    transform:translateX(-2px)
+  }
+  100% {
+    transfrom:translateX(2px)
+  }
+}
+@keyframes shake2{
+  0% {
+    transform:translateY(-2px)
+  }
+  100% {
+    transfrom:translateY(2px)
+  }
+}
+.loading{
+  width:50px;
+  height:50px;
+  display: block;
+  margin:30px auto 0;
+}
+.change-time-box{
+  width:263px;
+  margin:20px auto;
+  padding-bottom:20px;
+}
+.jietu{
+  position:absolute;
+  bottom:58px;
+  right:140px;
+  width:18px;
+  height:18px;
+
+}
+#rate{
+  width:75px;
+}
+.beisu-box{
+  position:absolute;
+  bottom:50px;
+  right:170px;
+  color:#fff;
+}
+
+</style>