Browse Source

基础版本1

xu 1 week ago
parent
commit
acf296c638

+ 16 - 0
server/api/v1/admin/device.go

@@ -43,6 +43,22 @@ func (da *DeviceApi) QueryDeviceList(c *gin.Context) {
 	}, "获取成功", c)
 }
 
+func (da *DeviceApi) GenerateDeviceFile(c *gin.Context) {
+	id, err := strconv.Atoi(c.Query("id"))
+	if err != nil {
+		logger.Get().Error("GenerateDeviceFile ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	data, err := deviceService.GenerateDeviceFile(id)
+	if err != nil {
+		logger.Get().Error("GenerateDeviceFile ---- " + err.Error())
+		response.FailWithMessage("设备文件生成失败", c)
+		return
+	}
+	response.OkWithData(data, c)
+}
+
 func (da *DeviceApi) CreateDevice(c *gin.Context) {
 	var device dao.Device
 	err := c.ShouldBindJSON(&device)

+ 65 - 0
server/api/v1/admin/tunnel.go

@@ -3,6 +3,7 @@ package admin
 import (
 	"github.com/gin-gonic/gin"
 	"server/dao"
+	"server/model/common/request"
 	"server/model/common/response"
 	"server/utils/logger"
 	"strconv"
@@ -20,6 +21,38 @@ func (ta *TunnelApi) QueryAllTunnels(c *gin.Context) {
 	response.OkWithData(tunnels, c)
 }
 
+func (ta *TunnelApi) QueryTunnelList(c *gin.Context) {
+	var info request.TunnelSearch
+	err := c.ShouldBindJSON(&info)
+	if err != nil {
+		logger.Get().Error("QueryTunnelList ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	list, total, err := tunnelService.QueryTunnelList(info)
+	if err != nil {
+		logger.Get().Error("QueryTunnelList ---- " + err.Error())
+		response.FailWithMessage("设备获取列表错误", c)
+		return
+	}
+	response.OkWithDetailed(response.PageResult{
+		List:     list,
+		Total:    total,
+		Page:     info.PageInfo.Page,
+		PageSize: info.PageInfo.PageSize,
+	}, "获取成功", c)
+}
+
+func (ta *TunnelApi) QueryNoRegionTunnels(c *gin.Context) {
+	tunnels, err := tunnelService.QueryNoRegionTunnels()
+	if err != nil {
+		logger.Get().Error("QueryNoRegionTunnels ---- " + err.Error())
+		response.FailWithMessage("获取未分配区域隧道失败", c)
+		return
+	}
+	response.OkWithData(tunnels, c)
+}
+
 func (ta *TunnelApi) CreateTunnel(c *gin.Context) {
 	var tunnel dao.Tunnel
 	if err := c.ShouldBindJSON(&tunnel); err != nil {
@@ -52,6 +85,38 @@ func (ta *TunnelApi) UpdateTunnel(c *gin.Context) {
 	response.OkWithMessage("更新隧道成功", c)
 }
 
+func (ta *TunnelApi) UpdateTunnelsRegion(c *gin.Context) {
+	var info request.RegionTunnelsId
+	if err := c.ShouldBindJSON(&info); err != nil {
+		logger.Get().Error("UpdateTunnelsRegion ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := tunnelService.UpdateTunnelsRegion(info.TunnelIds, info.RegionId)
+	if err != nil {
+		logger.Get().Error("UpdateTunnelsRegion ---- " + err.Error())
+		response.FailWithMessage("更新隧道所属区域失败", c)
+		return
+	}
+	response.OkWithMessage("更新隧道所属区域成功", c)
+}
+
+func (ta *TunnelApi) UpdateTunnelTime(c *gin.Context) {
+	var tunnelTime dao.TunnelTime
+	if err := c.ShouldBindJSON(&tunnelTime); err != nil {
+		logger.Get().Error("UpdateTunnelTime ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := tunnelService.UpdateTunnelTime(tunnelTime)
+	if err != nil {
+		logger.Get().Error("UpdateTunnelTime ---- " + err.Error())
+		response.FailWithMessage("更新隧道定时失败", c)
+		return
+	}
+	response.OkWithMessage("更新隧道定时成功", c)
+}
+
 func (ta *TunnelApi) DeleteTunnel(c *gin.Context) {
 	id, err := strconv.Atoi(c.Query("id"))
 	if err != nil {

+ 1 - 1
server/config.yaml

@@ -220,7 +220,7 @@ zap:
     show-line: true
     log-in-console: true
 mqtt:
-    server: "tcp://106.52.134.22:1883"
+    server: "tcp://118.253.180.18:1883"
     id: "mini_program_service_v20230426"
     user: "admin"
     password: "admin"

+ 14 - 2
server/dao/device.go

@@ -6,8 +6,15 @@ type Device struct {
 	global.GVA_MODEL
 	Sn          string      `json:"sn" gorm:"comment:设备sn"`
 	Name        string      `json:"name" gorm:"comment:名称"`
+	Address     string      `json:"address" gorm:"comment:设备地址"`
+	TaskTime    uint        `json:"taskTime" gorm:"comment:采集时间"`
+	WaitTime    uint        `json:"waitTime" gorm:"comment:延迟时间"`
+	RadarId     int8        `json:"radarId" gorm:"comment:雷达id"`
+	SerialId    int8        `json:"serialId" gorm:"comment:串口id"`
 	Genre       int         `json:"genre" gorm:"index;comment:类型id"`
 	DeviceGenre DeviceGenre `json:"deviceGenre" gorm:"foreignKey:Genre;"`
+	TunnelId    int         `json:"tunnelId" gorm:"column:tunnel_id"`
+	Tunnel      Tunnel      `json:"tunnel" gorm:"foreignKey:TunnelId"`
 }
 
 func (Device) TableName() string {
@@ -15,7 +22,7 @@ func (Device) TableName() string {
 }
 
 func QueryAllDevices() (devices []Device, err error) {
-	err = global.GVA_DB.Model(Device{}).Find(&devices).Error
+	err = global.GVA_DB.Model(Device{}).Preload("DeviceGenre").Preload("Tunnel").Find(&devices).Error
 	return
 }
 
@@ -37,7 +44,12 @@ func QueryDeviceList(sn, name string, genre, limit, offset int) (devices []Devic
 	if err != nil {
 		return
 	}
-	err = db.Limit(limit).Offset(offset).Preload("DeviceGenre").Find(&devices).Error
+	err = db.Limit(limit).Offset(offset).Preload("DeviceGenre").Preload("Tunnel").Find(&devices).Error
+	return
+}
+
+func QueryDeviceByTunnelId(id int) (devices []Device, err error) {
+	err = global.GVA_DB.Where("tunnel_id =?", id).Preload("DeviceGenre").Preload("Tunnel").Find(&devices).Error
 	return
 }
 

+ 3 - 2
server/dao/device_genre.go

@@ -4,7 +4,8 @@ import "server/global"
 
 type DeviceGenre struct {
 	global.GVA_MODEL
-	Name string `json:"name" gorm:"comment:类型名称"`
+	Name string `json:"name" form:"name" gorm:"comment:类型名称"`
+	Type string `json:"type" form:"type" gorm:"comment:硬件类型"`
 }
 
 func (DeviceGenre) TableName() string {
@@ -12,7 +13,7 @@ func (DeviceGenre) TableName() string {
 }
 
 func QueryAllDeviceGenres() (genres []DeviceGenre, err error) {
-	err = global.GVA_DB.Create(DeviceGenre{}).Find(&genres).Error
+	err = global.GVA_DB.Model(DeviceGenre{}).Find(&genres).Error
 	return
 }
 

+ 2 - 6
server/dao/region.go

@@ -5,7 +5,8 @@ import "server/global"
 type Region struct {
 	global.GVA_MODEL
 	Name    string   `json:"name" form:"name" gorm:"comment:名称;column:name"`
-	Tunnels []Tunnel `json:"tunnels" form:"tunnels" gorm:"-"`
+	Remarks string   `json:"remarks" form:"remarks" gorm:"comment"`
+	Tunnels []Tunnel `json:"tunnels" form:"tunnels" gorm:"foreignKey:RegionId"`
 }
 
 func (Region) TableName() string {
@@ -17,11 +18,6 @@ func QueryAllRegions() (regions []Region, err error) {
 	return regions, err
 }
 
-func QueryRegionByID(id int) (region Region, err error) {
-	err = global.GVA_DB.Where("id = ?", id).Preload("Tunnels").First(&region).Error
-	return region, err
-}
-
 func (r Region) CreateRegion() error {
 	return global.GVA_DB.Create(&r).Error
 }

+ 58 - 5
server/dao/tunnel.go

@@ -1,11 +1,21 @@
 package dao
 
-import "server/global"
+import (
+	"gorm.io/gorm"
+	"server/global"
+)
 
 type Tunnel struct {
 	global.GVA_MODEL
-	Name  string    `json:"name" gorm:"comment:名称"`
-	Users []SysUser `json:"users" gorm:"many2many:user_tunnel"`
+	TunnelSn   string     `json:"tunnelSn" gorm:"comment:隧道序号"`
+	Name       string     `json:"name" gorm:"comment:名称"`
+	Tactics    int        `json:"tactics" gorm:"comment:策略"`
+	RegionId   int        `json:"regionId" gorm:"column:region_id"`
+	Region     Region     `json:"region" gorm:"foreignKey:RegionId"` // 关系
+	TimeId     int        `json:"timeId" gorm:"comment:定时id"`
+	TunnelTime TunnelTime `json:"tunnelTime" gorm:"foreignKey:TimeId"`
+	Users      []SysUser  `json:"users" gorm:"many2many:user_tunnel"`
+	Devices    []Device   `json:"devices" gorm:"foreignKey:TunnelId"`
 }
 
 func (Tunnel) TableName() string {
@@ -13,18 +23,61 @@ func (Tunnel) TableName() string {
 }
 
 func QueryAllTunnels() (tunnels []Tunnel, err error) {
-	err = global.GVA_DB.Find(&tunnels).Error
+	err = global.GVA_DB.Find(&tunnels).Preload("Users").Preload("TunnelTime").Preload("Devices").Preload("Region").Error
 	return tunnels, err
 }
 
+func QueryTunnelList(name string, regionId, limit, offset int) (tunnels []Tunnel, total int64, err error) {
+	db := global.GVA_DB.Model(Tunnel{})
+
+	if name != "" {
+		db = db.Where("name LIKE ?", "%"+name+"%")
+	}
+
+	if regionId != 0 {
+		db = db.Where("region_id = ?", regionId)
+	}
+	err = db.Count(&total).Error
+	if err != nil {
+		return
+	}
+	err = db.Limit(limit).Offset(offset).Preload("Users").Preload("TunnelTime").Preload("Devices").Preload("Region").Find(&tunnels).Error
+	return
+}
+
+func QueryNoRegionTunnels() (tunnels []Tunnel, err error) {
+	err = global.GVA_DB.Where("region_id =?", 0).Find(&tunnels).Error
+	return
+}
+
 func (t Tunnel) CreateTunnel() error {
-	return global.GVA_DB.Create(&t).Error
+	err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
+		err := tx.Create(&t).Error
+		if err != nil {
+			return err
+		}
+		time := TunnelTime{
+			TunnelSn:  t.TunnelSn,
+			StartTime: "00:00",
+			EndTime:   "00:00",
+		}
+		err = tx.Create(&time).Error
+		if err != nil {
+			return err
+		}
+		return nil
+	})
+	return err
 }
 
 func (t Tunnel) UpdateTunnel() error {
 	return global.GVA_DB.Where("id = ?", t.ID).Updates(&t).Error
 }
 
+func UpdateTunnelsRegion(tunnelIds []int, regionId int) error {
+	return global.GVA_DB.Model(&Tunnel{}).Where("id in (?)", tunnelIds).Update("region_id", regionId).Error
+}
+
 func DeleteTunnel(id int) error {
 	return global.GVA_DB.Unscoped().Where("id = ?", id).Delete(&Tunnel{}).Error
 }

+ 28 - 0
server/dao/tunnelTime.go

@@ -0,0 +1,28 @@
+package dao
+
+import "server/global"
+
+type TunnelTime struct {
+	global.GVA_MODEL
+	TunnelSn   string `json:"tunnelSn" gorm:"comment:隧道序号"`
+	StartTime  string `json:"startTime" gorm:"comment:开始时间"`
+	EndTime    string `json:"endTime" gorm:"comment:结束时间"`
+	IsComplete bool   `json:"isComplete" gorm:"comment:是否完成"`
+}
+
+func (TunnelTime) TableName() string {
+	return "tunnel_time"
+}
+
+func (tt TunnelTime) CreateTunnelTime() error {
+	err := global.GVA_DB.Create(&tt).Error
+	return err
+}
+
+func (tt TunnelTime) UpdateTunnelTime() error {
+	return global.GVA_DB.Select("start_time", "end_time", "is_complete").Where("id = ?", tt.ID).Updates(&tt).Error
+}
+
+func TunnelTimeOk(sn string) error {
+	return global.GVA_DB.Model(&TunnelTime{}).Where("tunnel_sn = ?", sn).Update("is_complete", true).Error
+}

+ 1 - 0
server/initialize/gorm.go

@@ -48,6 +48,7 @@ func RegisterTables() {
 
 		dao.Region{},
 		dao.Tunnel{},
+		dao.TunnelTime{},
 		dao.Device{},
 		dao.DeviceGenre{},
 	)

+ 6 - 0
server/model/common/request/common.go

@@ -35,6 +35,12 @@ type DeviceSearch struct {
 type TunnelSearch struct {
 	PageInfo PageInfo `json:"pageInfo" form:"pageInfo"` // 分页信息
 	Name     string   `json:"name" form:"name"`         //隧道名称
+	RegionId int      `json:"regionId" form:"regionId"`
+}
+
+type RegionTunnelsId struct {
+	TunnelIds []int `json:"tunnelIds" form:"tunnelIds"`
+	RegionId  int   `json:"regionId" form:"regionId"`
 }
 
 type Empty struct{}

+ 22 - 0
server/model/common/response/common.go

@@ -6,3 +6,25 @@ type PageResult struct {
 	Page     int         `json:"page"`
 	PageSize int         `json:"pageSize"`
 }
+
+type EnvDev struct {
+	Sn       string `json:"sn"`       //设备SN
+	Address  string `json:"Address"`  //设备地址
+	Name     string `json:"name"`     //设备名称
+	TaskTime uint   `json:"taskTime"` //设备采集周期,单位分钟
+	WaitTime uint   `json:"waitTime"` //等待时间,单位秒
+	SerialId int8   `json:"serialId"` //串口id
+}
+
+type LampDev struct {
+	Sn       string `json:"sn"`       //设备SN
+	Address  string `json:"address"`  //设备地址
+	Name     string `json:"name"`     //设备名称
+	RadarId  int8   `json:"radarId"`  //雷达id(用于在雷达监测到来车后,控制雷达在的那一路的单灯控制器)
+	SerialId int8   `json:"serialId"` // 串口id
+}
+
+type DeviceFileData struct {
+	EnvDev  []EnvDev  `json:"envDev"`
+	LampDev []LampDev `json:"lampDev"`
+}

+ 1 - 0
server/router/admin/device.go

@@ -20,5 +20,6 @@ func (dr *DeviceRouter) InitDeviceRouter(Router *gin.RouterGroup) {
 	}
 	{
 		deviceRouterWithoutRecord.GET("queryAllDevices", deviceRouterApi.QueryAllDevices)
+		deviceRouterWithoutRecord.GET("generateDeviceFile", deviceRouterApi.GenerateDeviceFile)
 	}
 }

+ 4 - 0
server/router/admin/tunnel.go

@@ -13,11 +13,15 @@ func (tr *TunnelRouter) InitTunnelRouter(Router *gin.RouterGroup) {
 	tunnelRouterWithoutRecord := Router.Group("tunnel")
 	tunnelRouterApi := v1.ApiGroupApp.AdminApiGroup.TunnelApi
 	{
+		tunnelRouter.POST("queryTunnelList", tunnelRouterApi.QueryTunnelList)
 		tunnelRouter.POST("createTunnel", tunnelRouterApi.CreateTunnel)
 		tunnelRouter.PUT("updateTunnel", tunnelRouterApi.UpdateTunnel)
+		tunnelRouter.PUT("updateTunnelsRegion", tunnelRouterApi.UpdateTunnelsRegion)
+		tunnelRouter.PUT("updateTunnelTime", tunnelRouterApi.UpdateTunnelTime)
 		tunnelRouter.DELETE("deleteTunnel", tunnelRouterApi.DeleteTunnel)
 	}
 	{
 		tunnelRouterWithoutRecord.GET("queryAllTunnels", tunnelRouterApi.QueryAllTunnels)
+		tunnelRouterWithoutRecord.GET("queryNoRegionTunnels", tunnelRouterApi.QueryNoRegionTunnels)
 	}
 }

+ 35 - 0
server/service/admin/device.go

@@ -3,6 +3,7 @@ package admin
 import (
 	"server/dao"
 	"server/model/common/request"
+	"server/model/common/response"
 )
 
 type DeviceService struct{}
@@ -17,6 +18,40 @@ func (ds *DeviceService) QueryDeviceList(info request.DeviceSearch) ([]dao.Devic
 	return dao.QueryDeviceList(info.Sn, info.Name, info.Genre, limit, offset)
 }
 
+func (ds *DeviceService) GenerateDeviceFile(id int) (data response.DeviceFileData, err error) {
+	devices, err := dao.QueryDeviceByTunnelId(id)
+	if err != nil {
+		return data, err
+	}
+
+	for _, d := range devices {
+		if d.DeviceGenre.Type == "环境设备" {
+			envDev := response.EnvDev{
+				Sn:       d.Sn,
+				Name:     d.Name,
+				Address:  d.Address,
+				TaskTime: d.TaskTime,
+				WaitTime: d.WaitTime,
+				SerialId: d.SerialId,
+			}
+
+			data.EnvDev = append(data.EnvDev, envDev)
+		} else if d.DeviceGenre.Type == "开关设备" {
+			lampDev := response.LampDev{
+				Sn:       d.Sn,
+				Name:     d.Name,
+				Address:  d.Address,
+				RadarId:  d.RadarId,
+				SerialId: d.SerialId,
+			}
+
+			data.LampDev = append(data.LampDev, lampDev)
+		}
+	}
+
+	return data, nil
+}
+
 func (ds *DeviceService) CreateDevice(device dao.Device) error {
 	return device.CreateDevice()
 }

+ 4 - 9
server/service/admin/mqtt.go

@@ -42,8 +42,7 @@ func GetHandler() *MqttHandler {
 }
 
 func (o *MqttHandler) SubscribeTopics() {
-	mqtt.GetMQTTMgr().Subscribe("mini/#", mqtt.AtLeastOnce, o.HandlerData)
-	mqtt.GetMQTTMgr().Subscribe("$SYS/broker/log/N", mqtt.AtLeastOnce, o.HandlerData)
+	mqtt.GetMQTTMgr().Subscribe("smart_tunnel/#", mqtt.AtLeastOnce, o.HandlerData)
 }
 
 func (o *MqttHandler) HandlerData(m mqtt.Message) {
@@ -79,20 +78,16 @@ func (o *MqttHandler) Handler() interface{} {
 		}
 
 		_, topic, err := parseTopic(m.Topic())
+		fmt.Println(m.Topic())
+		fmt.Println(m.PayloadString())
 		if err != nil {
 			//logger.Logger.Errorf("parseTopic err")
 			continue
 		}
 
 		switch topic {
-		case protocol.TopicSwitchControlAck:
+		case protocol.TopicTunnelControlAck:
 
-		case protocol.TopicSwitchControlDg:
-		case protocol.TopicIlluminance:
-
-			logger.Get().Debugf("MqttHandler Illuminance = %s", m.PayloadString())
-			//TODO: 更新光照
-			//protocol.SetCurrentIlluminance(m.PayloadString())
 		}
 
 	}

+ 35 - 1
server/service/admin/tunnel.go

@@ -1,6 +1,10 @@
 package admin
 
-import "server/dao"
+import (
+	"fmt"
+	"server/dao"
+	"server/model/common/request"
+)
 
 type TunnelService struct{}
 
@@ -8,6 +12,16 @@ func (ts *TunnelService) QueryAllTunnels() ([]dao.Tunnel, error) {
 	return dao.QueryAllTunnels()
 }
 
+func (ts *TunnelService) QueryTunnelList(info request.TunnelSearch) ([]dao.Tunnel, int64, error) {
+	limit := info.PageInfo.PageSize
+	offset := info.PageInfo.PageSize * (info.PageInfo.Page - 1)
+	return dao.QueryTunnelList(info.Name, info.RegionId, limit, offset)
+}
+
+func (ts *TunnelService) QueryNoRegionTunnels() ([]dao.Tunnel, error) {
+	return dao.QueryNoRegionTunnels()
+}
+
 func (ts *TunnelService) CreateTunnel(tunnel dao.Tunnel) error {
 	return tunnel.CreateTunnel()
 }
@@ -16,6 +30,26 @@ func (ts *TunnelService) UpdateTunnel(tunnel dao.Tunnel) error {
 	return tunnel.UpdateTunnel()
 }
 
+func (ts *TunnelService) UpdateTunnelsRegion(tunnelIds []int, regionId int) error {
+	return dao.UpdateTunnelsRegion(tunnelIds, regionId)
+}
+
+func (ts *TunnelService) UpdateTunnelTime(time dao.TunnelTime) error {
+	time.IsComplete = false
+	return time.UpdateTunnelTime()
+}
+
+func (ts *TunnelService) TunnelTimeOk(sn string) error {
+	return dao.TunnelTimeOk(sn)
+}
+
 func (ts *TunnelService) DeleteTunnel(id int) error {
+	devices, err := dao.QueryDeviceByTunnelId(id)
+	if err != nil {
+		return err
+	}
+	if len(devices) > 0 {
+		return fmt.Errorf("存在数据不能删除")
+	}
 	return dao.DeleteTunnel(id)
 }

+ 1 - 4
server/utils/protocol/protocol.go

@@ -1,8 +1,5 @@
 package protocol
 
 const (
-	TopicSwitchControlCmd = "switch_control/cmd"
-	TopicSwitchControlAck = "switch_control/ack"
-	TopicSwitchControlDg  = "switch_control/dg"
-	TopicIlluminance      = "illuminance/data"
+	TopicTunnelControlAck = "tunnel_control/ack"
 )

+ 60 - 0
web/src/api/device.js

@@ -14,3 +14,63 @@ export const queryDeviceList = (data) => {
     data
   })
 }
+
+export const generateDeviceFile = (id) => {
+  return service({
+    url: `/device/generateDeviceFile?id=${id}`,
+    method: 'get',
+  })
+}
+
+export const createDevice = (data) => {
+  return service({
+    url: '/device/createDevice',
+    method: 'post',
+    data
+  })
+}
+
+export const updateDevice = (data) => {
+  return service({
+    url: '/device/updateDevice',
+    method: 'put',
+    data
+  })
+}
+
+export const deleteDevice = (id) => {
+  return service({
+    url: `/device/deleteDevice?id=${id}`,
+    method: 'delete'
+  })
+}
+
+export const queryAllDeviceGenres = () => {
+  return service({
+    url: '/deviceGenre/queryAllDeviceGenres',
+    method: 'get'
+  })
+}
+
+export const createDeviceGenre = (data) => {
+  return service({
+    url: '/deviceGenre/createDeviceGenre',
+    method: 'post',
+    data
+  })
+}
+
+export const updateDeviceGenre = (data) => {
+  return service({
+    url: '/deviceGenre/updateDeviceGenre',
+    method: 'put',
+    data
+  })
+}
+
+export const deleteDeviceGenre = (id) => {
+  return service({
+    url: `/deviceGenre/deleteDeviceGenre?id=${id}`,
+    method: 'delete'
+  })
+}

+ 31 - 0
web/src/api/region.js

@@ -0,0 +1,31 @@
+import service from '@/utils/request'
+
+export const queryAllRegions = () => {
+  return service({
+    url: '/region/queryAllRegions',
+    method: 'get'
+  })
+}
+
+export const createRegion = (data) => {
+  return service({
+    url: '/region/createRegion',
+    method: 'post',
+    data
+  })
+}
+
+export const updateRegion = (data) => {
+  return service({
+    url: '/region/updateRegion',
+    method: 'put',
+    data
+  })
+}
+
+export const deleteRegion = (id) => {
+  return service({
+    url: `/region/deleteRegion?id=${id}`,
+    method: 'delete'
+  })
+}

+ 62 - 0
web/src/api/tunnel.js

@@ -0,0 +1,62 @@
+import service from '@/utils/request'
+
+export const queryAllTunnels = () => {
+  return service({
+    url: '/tunnel/queryAllTunnels',
+    method: 'get'
+  })
+}
+
+export const queryTunnelList = (data) => {
+  return service({
+    url: '/tunnel/queryTunnelList',
+    method: 'post',
+    data
+  })
+}
+
+export const queryNoRegionTunnels = () => {
+  return service({
+    url: '/tunnel/queryNoRegionTunnels',
+    method: 'get'
+  })
+}
+
+export const createTunnel = (data) => {
+  return service({
+    url: '/tunnel/createTunnel',
+    method: 'post',
+    data
+  })
+}
+
+export const updateTunnel = (data) => {
+  return service({
+    url: '/tunnel/updateTunnel',
+    method: 'put',
+    data
+  })
+}
+
+export const updateTunnelsRegion = (data) => {
+  return service({
+    url: '/tunnel/updateTunnelsRegion',
+    method: 'put',
+    data
+  })
+}
+
+export const updateTunnelTime = (data) => {
+  return service({
+    url: '/tunnel/updateTunnelTime',
+    method: 'put',
+    data
+  })
+}
+
+export const deleteTunnel = (id) => {
+  return service({
+    url: `/tunnel/deleteTunnel?id=${id}`,
+    method: 'delete'
+  })
+}

BIN
web/src/assets/logo.png


+ 763 - 11
web/src/view/admin/device/device.vue

@@ -1,34 +1,786 @@
 <template>
   <el-container>
-    <el-header>
-      <el-form v-model="deviceSearch">
-        <el-form-item>
+    <el-header class="gva-search-box">
+      <el-form
+        v-model="deviceSearch"
+        :inline="true"
+        style="line-height: 75px"
+      >
+        <el-form-item label="名称">
           <el-input
             v-model="deviceSearch.name"
           />
         </el-form-item>
-        <el-form-item>
+        <el-form-item label="序号">
           <el-input
             v-model="deviceSearch.sn"
           />
         </el-form-item>
+        <el-form-item label="类型">
+          <el-select
+            v-model.number="deviceSearch.genre"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in deviceGenre"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            @click="getData"
+          >查询</el-button>
+        </el-form-item>
       </el-form>
     </el-header>
-    <el-main>
-      ti
+    <el-main class="gva-table-box">
+      <div style="margin: 0 0 20px 0 ">
+        <el-button
+          type="success"
+          @click="isDeviceAddDialog"
+        >添加</el-button>
+        <el-divider direction="vertical" />
+        <el-button
+          type="primary"
+          @click="deviceGenreDrawer = true"
+        >类型操作</el-button>
+      </div>
+      <el-table
+        :data="deviceData"
+        style="width: 100%;min-height:600px"
+      >
+        <el-table-column
+          type="selection"
+          width="55"
+          align="center"
+        />
+        <el-table-column
+          prop="name"
+          label="名称"
+          align="center"
+        />
+        <el-table-column
+          prop="sn"
+          label="序号"
+          align="center"
+        />
+        <el-table-column
+          prop="deviceGenre.name"
+          label="类型"
+          align="center"
+        />
+        <el-table-column
+          prop="tunnel.name"
+          label="隧道"
+          align="center"
+        />
+        <el-table-column
+          label="操作"
+          align="center"
+        >
+          <template #default="scope">
+            <el-button
+              type="primary"
+              @click="isDeviceEditDialog(scope.row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              type="danger"
+              @click="deviceDelete(scope.row)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="gva-pagination">
+        <el-pagination
+          :current-page="deviceSearch.pageInfo.page"
+          :page-size="deviceSearch.pageInfo.pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
     </el-main>
+    <!--    设备类型操作-->
+    <el-drawer
+      v-model="deviceGenreDrawer"
+      title="设备类型操作"
+      direction="rtl"
+      size="30%"
+    >
+      <div style="margin: 0 0 20px 0">
+        <el-button
+          type="success"
+          @click="deviceGenreAddDialog=true"
+        >添加</el-button>
+      </div>
+      <el-table
+        :data="deviceGenre"
+        style="width: 100%"
+      >
+        <el-table-column
+          prop="name"
+          label="名称"
+        />
+        <el-table-column
+          prop="type"
+          label="硬件类型"
+        />
+        <el-table-column label="操作">
+          <template #default="scope">
+            <el-button
+              size="small"
+              @click="isDeviceGenreEditDialog(scope.row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              size="small"
+              type="danger"
+              @click="deviceGenreDelete(scope.row)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-drawer>
+    <!--    设备类型新增-->
+    <el-dialog
+      v-model="deviceGenreAddDialog"
+      title="设备类型新增"
+      width="300"
+      align-center
+    >
+      <el-form
+        ref="deviceGenreRef"
+        :model="deviceGenreData"
+        :rules="deviceGenreRules"
+      >
+        <el-form-item
+          label="类型名称"
+          prop="name"
+        >
+          <el-input
+            v-model="deviceGenreData.name"
+            label="类型名称"
+          />
+        </el-form-item>
+        <el-form-item
+          label="硬件类型"
+          prop="type"
+        >
+          <el-select
+            v-model="deviceGenreData.type"
+            style="width: 240px"
+          >
+            <el-option
+              v-for="item in genreOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deviceGenreAddDialog = false;deviceGenreRef.resetFields();">取消</el-button>
+          <el-button
+            type="primary"
+            @click="deviceGenreAdd"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    设备类型修改-->
+    <el-dialog
+      v-model="deviceGenreEditDialog"
+      title="设备类型修改"
+      width="300"
+      align-center
+    >
+      <el-form
+        ref="deviceGenreRef"
+        :model="deviceGenreData"
+        :rules="deviceGenreRules"
+      >
+        <el-form-item
+          label="类型名称"
+          prop="name"
+        >
+          <el-input
+            v-model="deviceGenreData.name"
+            label="类型名称"
+          />
+        </el-form-item>
+        <el-form-item
+          label="硬件类型"
+          prop="type"
+        >
+          <el-select
+            v-model="deviceGenreData.type"
+            style="width: 240px"
+          >
+            <el-option
+              v-for="item in genreOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deviceGenreEditDialog = false;deviceGenreRef.resetFields();">取消</el-button>
+          <el-button
+            type="primary"
+            @click="deviceGenreEdit"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    设备添加-->
+    <el-dialog
+      v-model="deviceAddDialog"
+      title="设备添加"
+      width="500"
+      align-center
+    >
+      <el-form
+        ref="deviceRef"
+        :model="device"
+        :rules="deviceRules"
+        style="line-height: 75px"
+      >
+        <el-form-item
+          label="名称"
+          prop="name"
+        >
+          <el-input
+            v-model="device.name"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="序号"
+          prop="sn"
+        >
+          <el-input
+            v-model="device.sn"
+            disabled
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="类型"
+          prop="genre"
+        >
+          <el-select
+            v-model.number="device.genre"
+            style="width: 240px"
+            @change="changDevice"
+          >
+            <el-option
+              v-for="item in deviceGenre"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="隧道"
+          prop="tunnel"
+        >
+          <el-select
+            v-model.number="device.tunnelId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in tunnel"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="设备地址"
+          prop="address"
+        >
+          <el-input
+            v-model="device.address"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '环境设备'"
+          label="采集时间"
+          prop="taskTime"
+        >
+          <el-input
+            v-model="device.taskTime"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '环境设备'"
+          label="延迟时间"
+          prop="waitTime"
+        >
+          <el-input
+            v-model="device.waitTime"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '开关设备'"
+          label="雷达编号"
+          prop="radarId"
+        >
+          <el-input
+            v-model="device.radarId"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="串口编号"
+          prop="serialId"
+        >
+          <el-input
+            v-model="device.serialId"
+            style="width: 200px;"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deviceAddDialog = false;deviceRef.resetFields();">取消</el-button>
+          <el-button
+            type="primary"
+            @click="deviceAdd"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    设备修改-->
+    <el-dialog
+      v-model="deviceEditDialog"
+      title="设备修改"
+      width="500"
+      align-center
+    >
+      <el-form
+        ref="deviceRef"
+        :model="device"
+        :rules="deviceRules"
+        style="line-height: 75px"
+      >
+        <el-form-item
+          label="名称"
+          prop="name"
+        >
+          <el-input
+            v-model="device.name"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="序号"
+          prop="sn"
+        >
+          <el-input
+            v-model="device.sn"
+            disabled
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="类型"
+          prop="genre"
+        >
+          <el-select
+            v-model.number="device.genre"
+            style="width: 240px"
+            @change="changDevice"
+          >
+            <el-option
+              v-for="item in deviceGenre"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="隧道"
+          prop="tunnel"
+        >
+          <el-select
+            v-model.number="device.tunnelId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in tunnel"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          label="设备地址"
+          prop="address"
+        >
+          <el-input
+            v-model="device.address"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '环境设备'"
+          label="采集时间"
+          prop="taskTime"
+        >
+          <el-input
+            v-model="device.taskTime"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '环境设备'"
+          label="延迟时间"
+          prop="waitTime"
+        >
+          <el-input
+            v-model="device.waitTime"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          v-if="isDevice === '开关设备'"
+          label="雷达编号"
+          prop="radarId"
+        >
+          <el-input
+            v-model="device.radarId"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item
+          label="串口编号"
+          prop="serialId"
+        >
+          <el-input
+            v-model="device.serialId"
+            style="width: 200px;"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deviceEditDialog = false;deviceRef.resetFields();">取消</el-button>
+          <el-button
+            type="primary"
+            @click="deviceEdit"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
   </el-container>
 </template>
 
 <script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  createDevice,
+  createDeviceGenre, deleteDevice,
+  deleteDeviceGenre,
+  queryAllDeviceGenres,
+  queryDeviceList, updateDevice,
+  updateDeviceGenre
+} from '@/api/device'
+import { queryAllTunnels } from '@/api/tunnel'
+
+// 搜索条件
+const deviceSearch = ref({
+  pageInfo: {
+    page: 1,
+    pageSize: 10
+  },
+  name: '',
+  sn: '',
+  genre: undefined
+})
+
+const total = ref(0)
+
+// 分页
+const handleSizeChange = (val) => {
+  deviceSearch.value.pageInfo.pageSize = val
+  getData()
+}
 
-const deviceSearch = ref(
+const handleCurrentChange = (val) => {
+  deviceSearch.value.pageInfo.page = val
+  getData()
+}
+
+const deviceGenre = ref()
+const deviceData = ref()
+const tunnel = ref()
+
+const getData = async() => {
+  await queryAllDeviceGenres().then(res => {
+    deviceGenre.value = res.data
+    device.value.genre = res.data[0].ID
+  })
+  await queryAllTunnels().then(res => {
+    tunnel.value = res.data
+  })
+  await queryDeviceList(deviceSearch.value).then(res => {
+    deviceData.value = res.data.list
+    console.log(deviceData.value)
+    deviceSearch.value.pageInfo.page = res.data.page
+    deviceSearch.value.pageInfo.pageSize = res.data.pageSize
+    total.value = res.data.total
+  })
+}
+
+// 设备类型 --------------------------------------------------------------------
+const deviceGenreDrawer = ref(false)
+const deviceGenreData = ref({
+  id: 0,
+  name: '',
+  type: ''
+})
+
+const genreOptions = [
+  {
+    value: '环境设备',
+    label: '环境设备',
+  },
   {
-    name: '',
-    sn: '',
-    genre: 0
+    value: '开关设备',
+    label: '开关设备',
   }
-)
+]
+
+const deviceGenreRef = ref(null)
+const deviceGenreRules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '硬件类型不能为空', trigger: 'blur' }]
+})
+
+// 添加
+const deviceGenreAddDialog = ref(false)
+
+const deviceGenreAdd = () => {
+  deviceGenreRef.value.validate(async(valid) => {
+    if (valid) {
+      deviceGenreData.value.id = 0
+      await createDeviceGenre(deviceGenreData.value).then(res => {
+        deviceGenreAddDialog.value = false
+        if (res.code === 0) {
+          ElMessage.success('添加成功')
+        }
+        getData()
+      })
+    } else {
+      console.log('验证失败,请检查输入')
+      return false
+    }
+  })
+}
+
+// 修改
+const deviceGenreEditDialog = ref(false)
+
+const isDeviceGenreEditDialog = (val) => {
+  deviceGenreEditDialog.value = true
+  deviceGenreData.value = val
+}
+const deviceGenreEdit = async() => {
+  deviceGenreRef.value.validate(async(valid) => {
+    if (valid) {
+      await updateDeviceGenre(deviceGenreData.value).then(res => {
+        deviceGenreEditDialog.value = false
+        if (res.code === 0) {
+          ElMessage.success('修改成功')
+        }
+        getData()
+      })
+    } else {
+      console.log('验证失败,请检查输入')
+      return false
+    }
+  })
+}
+// 删除
+const deviceGenreDelete = async(val) => {
+  ElMessageBox.confirm(
+    '将永久删除改数据,请问继续吗?',
+    '删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+      await deleteDeviceGenre(val.ID).then(res => {
+        if (res.code === 0) {
+          ElMessage.success('删除成功')
+        }
+        getData()
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '删除已取消',
+      })
+    })
+}
+
+// 设备 -------------------------------------------------------------------
+// 初始数据
+const device = ref({
+  id: 0,
+  name: '',
+  sn: '',
+  tunnelId: undefined,
+  genre: undefined,
+  address: '',
+  taskTime: 0,
+  waitTime: 0,
+  radarId: 0,
+  serialId: 1
+})
+
+const isDevice = ref('环境设备')
+
+const changDevice = (item) => {
+  isDevice.value = deviceGenre.value.find(item => item.ID === device.value.genre).type
+  if (isDevice.value === '环境设备') {
+    device.value.taskTime = 0
+    device.value.waitTime = 0
+  } else if (isDevice.value === '开关设备') {
+    device.value.radarId = 0
+  }
+}
+
+const deviceRef = ref(null)
+const deviceRules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  sn: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  genre: [{ required: true, message: '设备类型不能为空', trigger: 'blur' }],
+  address: [{ required: true, message: '设备地址不能为空', trigger: 'blur' }],
+  serialId: [{ required: true, message: '串口编号不能为空', trigger: 'blur' }],
+})
+
+// 新增
+const deviceAddDialog = ref(false)
+
+const isDeviceAddDialog = () => {
+  deviceAddDialog.value = true
+  device.value.sn = 'LC' + Date.now()
+}
+const deviceAdd = async() => {
+  deviceRef.value.validate(async(valid) => {
+    if (valid) {
+      device.value.id = 0
+      await createDevice(device.value).then(res => {
+        deviceAddDialog.value = false
+        if (res.code === 0) {
+          ElMessage.success('添加成功')
+        }
+        getData()
+      })
+    } else {
+      console.log('验证失败,请检查输入')
+      return false
+    }
+  })
+}
+// 修改
+const deviceEditDialog = ref(false)
+
+const isDeviceEditDialog = (val) => {
+  deviceEditDialog.value = true
+  device.value = val
+}
+
+const deviceEdit = async() => {
+  deviceRef.value.validate(async(valid) => {
+    if (valid) {
+      await updateDevice(device.value).then(res => {
+        deviceEditDialog.value = false
+        if (res.code === 0) {
+          ElMessage.success('修改成功')
+        }
+        getData()
+      })
+    } else {
+      console.log('验证失败,请检查输入')
+      return false
+    }
+  })
+}
+
+// 删除
+const deviceDelete = (val) => {
+  ElMessageBox.confirm(
+    '将永久删除改数据,请问继续吗?',
+    '删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+      await deleteDevice(val.ID).then(res => {
+        if (res.code === 0) {
+          ElMessage.success('删除成功')
+        }
+        getData()
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '删除已取消',
+      })
+    })
+}
+
+onMounted(() => {
+  getData()
+})
 </script>
 
 <style scoped lang="scss">

+ 209 - 0
web/src/view/admin/region/region.vue

@@ -0,0 +1,209 @@
+<template>
+  <el-container>
+    <el-header class="gva-search-box">
+      <div style="line-height: 60px">
+        <el-button
+          type="success"
+          @click="regionAddDialog = true"
+        >添加</el-button>
+      </div>
+    </el-header>
+    <el-main class="gva-table-box">
+      <el-table
+        :data="filterRegion"
+        style="min-height: 700px"
+      >
+        <el-table-column
+          prop="name"
+          label="名称"
+        />
+        <el-table-column
+          prop="remarks"
+          label="备注"
+        />
+        <el-table-column
+          label="操作"
+          width="280"
+          fixed="right"
+        >
+          <template #header>
+            <el-input
+              v-model="search"
+              placeholder="搜索"
+            />
+          </template>
+          <template #default="scope">
+            <el-button
+              type="primary"
+              @click="regionEditDialog = true; regionData = {...scope.row}"
+            >
+              编辑
+            </el-button>
+            <el-button
+              type="danger"
+              @click="regionDelete(scope.row.ID)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-main>
+    <!--    地区新增-->
+    <el-dialog
+      v-model="regionAddDialog"
+      title="地区新增"
+      width="300"
+      align-center
+    >
+      <el-form v-model="regionData">
+        <el-form-item
+          label="名称"
+          prop="name"
+        >
+          <el-input v-model="regionData.name" />
+        </el-form-item>
+        <el-form-item
+          label="备注"
+          prop="remarks"
+        >
+          <el-input v-model="regionData.remarks" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="regionAddDialog = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="regionAdd"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    地区修改-->
+    <el-dialog
+      v-model="regionEditDialog"
+      title="地区新增"
+      width="300"
+      align-center
+    >
+      <el-form v-model="regionData">
+        <el-form-item
+          label="名称"
+          prop="name"
+        >
+          <el-input v-model="regionData.name" />
+        </el-form-item>
+        <el-form-item
+          label="备注"
+          prop="remarks"
+        >
+          <el-input v-model="regionData.remarks" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="regionEditDialog = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="regionEdit"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </el-container>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { createRegion, deleteRegion, queryAllRegions, updateRegion } from '@/api/region'
+
+const region = ref()
+
+const getData = async() => {
+  await queryAllRegions().then(res => {
+    region.value = res.data
+  })
+}
+
+const search = ref('')
+const filterRegion = computed(() => {
+  // 确保 region.value 存在并且是一个数组
+  const regionData = region.value || []
+
+  return regionData.filter(data =>
+    !search.value ||
+      data.name.toLowerCase().includes(search.value.toLowerCase())
+  )
+})
+
+// 初始数据
+const regionData = ref({
+  id: 0,
+  name: '',
+  remarks: ''
+})
+
+// 新增
+const regionAddDialog = ref(false)
+
+const regionAdd = async() => {
+  await createRegion(regionData.value).then(res => {
+    regionAddDialog.value = false
+    if (res.code === 0) {
+      ElMessage.success('添加成功')
+    }
+    getData()
+  })
+}
+
+// 修改
+const regionEditDialog = ref(false)
+
+const regionEdit = async() => {
+  await updateRegion(regionData.value).then(res => {
+    regionEditDialog.value = false
+    if (res.code === 0) {
+      ElMessage.success('修改成功')
+    }
+    getData()
+  })
+}
+
+// 删除
+
+const regionDelete = (val) => {
+  ElMessageBox.confirm(
+    '将永久删除改数据,请问继续吗?',
+    '删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+      await deleteRegion(val).then(res => {
+        if (res.code === 0) {
+          ElMessage.success('删除成功')
+        }
+        getData()
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '删除已取消',
+      })
+    })
+}
+
+onMounted(() => {
+  getData()
+})
+</script>

+ 580 - 0
web/src/view/admin/tunnel/tunnel.vue

@@ -0,0 +1,580 @@
+<template>
+  <el-container>
+    <el-header class="gva-search-box">
+      <el-form
+        v-model="searchData"
+        :inline="true"
+        style="line-height: 75px"
+      >
+        <el-form-item label="名称">
+          <el-input
+            v-model="searchData.name"
+          />
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-select
+            v-model.number="searchData.regionId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in region"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            @click="getData"
+          >查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-header>
+    <el-main class="gva-table-box">
+      <div style="margin: 0 0 20px 0 ">
+        <el-button
+          type="success"
+          @click="tunnelAddDialog = true"
+        >添加</el-button>
+        <el-button
+          type="primary"
+          :disabled="tunnelIds.length === 0"
+          @click="allocationRegionDialog = true"
+        >分配</el-button>
+      </div>
+      <el-table
+        :data="tunnel"
+        style="width: 100%;min-height:600px"
+        row-key="id"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          type="selection"
+          width="55"
+          align="center"
+        />
+        <el-table-column
+          prop="name"
+          label="名称"
+          align="center"
+        />
+        <el-table-column
+          prop="region.name"
+          label="地区"
+          align="center"
+        />
+        <el-table-column
+          label="策略"
+          align="center"
+        >
+          <template #default="scope">
+            <el-select
+              v-model="scope.row.tactics"
+              style="width: 100px"
+              @change="changeTactics(scope.row)"
+            >
+              <el-option
+                v-for="item in tacticsOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          align="center"
+        >
+          <template #default="scope">
+            <el-button
+              type="primary"
+              @click="tunnelData = scope.row ; tunnelEditDialog = true"
+            >
+              修改
+            </el-button>
+            <el-button
+              type="danger"
+              @click="tunnelDelete(scope.row)"
+            >
+              删除
+            </el-button>
+            <el-button
+              type="primary"
+              @click="tunnelControlPanel(scope.row)"
+            >
+              控制面板
+            </el-button>
+            <el-button
+              type="primary"
+              @click="getDeviceFile(scope.row.ID)"
+            >
+              生成设备文件
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="gva-pagination">
+        <el-pagination
+          :current-page="searchData.pageInfo.page"
+          :page-size="searchData.pageInfo.pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </el-main>
+    <!--    隧道添加-->
+    <el-dialog
+      v-model="tunnelAddDialog"
+      title="隧道添加"
+      width="500"
+      align-center
+    >
+      <el-form
+        v-model="tunnelData"
+        style="line-height: 75px"
+      >
+        <el-form-item label="名称">
+          <el-input
+            v-model="tunnelData.name"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item label="地区">
+          <el-select
+            v-model.number="tunnelData.regionId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in region"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="tunnelAddDialog = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="tunnelAdd"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    隧道修改-->
+    <el-dialog
+      v-model="tunnelEditDialog"
+      title="隧道修改"
+      width="500"
+      align-center
+    >
+      <el-form
+        v-model="tunnelData"
+        style="line-height: 75px"
+      >
+        <el-form-item label="名称">
+          <el-input
+            v-model="tunnelData.name"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item label="地区">
+          <el-select
+            v-model.number="tunnelData.regionId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in region"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="tunnelEditDialog = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="tunnelEdit"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    隧道分配-->
+    <el-dialog
+      v-model="allocationRegionDialog"
+      title="隧道分配"
+      width="500"
+      align-center
+    >
+      <el-form>
+        <el-form-item label="地区">
+          <el-select
+            v-model.number="regionId"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in region"
+              :key="item.ID"
+              :label="item.name"
+              :value="item.ID"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="allocationRegionDialog = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="allocationRegion"
+          >
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!--    生成设备文件-->
+    <el-drawer
+      v-model="isDeviceFile"
+      title="设备"
+      direction="rtl"
+      size="80%"
+    >
+      <el-button @click="downloadDeviceFile">下载</el-button>
+    </el-drawer>
+    <!--    控制面板-->
+    <el-drawer
+      v-model="isControlPanel"
+      title="控制面板"
+      direction="rtl"
+      size="80%"
+    >
+      <el-collapse
+        v-model="activeNames"
+        @change="handleChange"
+      >
+        <el-collapse-item
+          title="开关"
+          name="1"
+        >
+          1
+        </el-collapse-item>
+        <el-collapse-item
+          title="定时"
+          name="2"
+        >
+          <div style="margin-bottom: 30px">
+            <span style="margin-right: 30px">开始:</span>
+            <el-time-select
+              v-model="tunnelTiming.startTime"
+              style="width: 150px"
+              start="00:00"
+              end="23:55"
+              step="00:05"
+              format="HH:mm"
+            />
+          </div>
+          <div style="margin-bottom: 30px">
+            <span style="margin-right: 30px">结束:</span>
+            <el-time-select
+              v-model="tunnelTiming.endTime"
+              style="width: 150px"
+              start="00:00"
+              end="23:55"
+              step="00:05"
+              format="HH:mm"
+            />
+          </div>
+          <div>
+            <el-button
+              type="success"
+              @click="editTunnelTiming"
+            >
+              存储数据
+            </el-button>
+            <span style="margin-left: 30px">
+              <el-tag
+                v-if="tunnelTimeData.tunnelTime.isComplete"
+                type="success"
+              >完成</el-tag>
+              <el-tag
+                v-if="!tunnelTimeData.tunnelTime.isComplete"
+                type="warning"
+              >正在发送</el-tag>
+            </span>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+    </el-drawer>
+  </el-container>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { queryAllRegions } from '@/api/region'
+import {
+  createTunnel,
+  deleteTunnel,
+  queryTunnelList,
+  updateTunnel,
+  updateTunnelsRegion,
+  updateTunnelTime
+} from '@/api/tunnel'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { generateDeviceFile } from '@/api/device'
+
+const searchData = ref({
+  pageInfo: {
+    page: 1,
+    pageSize: 10
+  },
+  name: '',
+  regionId: undefined
+})
+
+// 分页
+const handleSizeChange = (val) => {
+  searchData.value.pageInfo.pageSize = val
+  getData()
+}
+
+const handleCurrentChange = (val) => {
+  searchData.value.pageInfo.page = val
+  getData()
+}
+
+const total = ref(0)
+
+const region = ref()
+const tunnel = ref()
+
+const getData = async() => {
+  await queryAllRegions().then(res => {
+    region.value = res.data
+  })
+  await queryTunnelList(searchData.value).then(res => {
+    tunnel.value = res.data.list
+    total.value = res.data.total
+    searchData.value.pageInfo.page = res.data.page
+    searchData.value.pageInfo.pageSize = res.data.pageSize
+    console.log(tunnel.value)
+  })
+}
+
+// 新增
+
+const tunnelAddDialog = ref(false)
+
+const tunnelData = ref({
+  id: 0,
+  name: '',
+  regionId: undefined
+})
+
+const tunnelAdd = async() => {
+  await createTunnel(tunnelData.value).then(res => {
+    tunnelAddDialog.value = false
+    getData()
+  })
+}
+
+// 修改
+
+const tunnelEditDialog = ref(false)
+
+const tunnelEdit = async() => {
+  await updateTunnel(tunnelData.value).then(res => {
+    tunnelEditDialog.value = false
+    getData()
+  })
+}
+
+// 删除
+const tunnelDelete = (val) => {
+  ElMessageBox.confirm(
+    '将永久删除改数据,请问继续吗?',
+    '删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+      await deleteTunnel(val.ID).then(res => {
+        if (res.code === 0) {
+          ElMessage.success('删除成功')
+        }
+        getData()
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '删除已取消',
+      })
+    })
+}
+
+// 多选
+const tunnelIds = ref([])
+
+const handleSelectionChange = (val) => {
+  const array = []
+  for (const i in val) {
+    array.push(val[i].ID)
+  }
+  tunnelIds.value = array
+}
+
+// 分配
+
+const allocationRegionDialog = ref(false)
+const regionId = ref()
+const allocationRegion = async() => {
+  await updateTunnelsRegion({
+    tunnelIds: tunnelIds.value,
+    regionId: regionId.value
+  }).then(res => {
+    if (res.code === 0) {
+      ElMessage.success('分配成功')
+      tunnelIds.value = []
+      getData()
+    }
+    allocationRegionDialog.value = false
+  })
+}
+
+// 生成设备文件
+const isDeviceFile = ref(false)
+const fileData = ref()
+const getDeviceFile = async(val) => {
+  isDeviceFile.value = true
+  await generateDeviceFile(val).then(res => {
+    fileData.value = res.data
+  })
+}
+
+const downloadDeviceFile = () => {
+  // 将对象转换为 JSON 字符串
+  const jsonString = JSON.stringify(fileData.value, null, 2)
+
+  // 创建一个新的 Blob 对象
+  const blob = new Blob([jsonString], { type: 'application/json' })
+
+  // 创建一个隐藏的可下载链接
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+
+  // 设置链接的 href 和 download 属性
+  link.href = url
+  link.setAttribute('download', 'data.json') // 下载文件名
+
+  // 将链接添加到 DOM 中
+  document.body.appendChild(link)
+
+  // 触发点击事件,开始下载
+  link.click()
+
+  // 下载完成后移除链接并释放 URL 对象
+  link.remove()
+  window.URL.revokeObjectURL(url) // 清理资源
+}
+
+// 策略
+const tacticsOptions = [
+  {
+    value: 1,
+    label: '环境',
+  },
+  {
+    value: 2,
+    label: '雷达',
+  },
+  {
+    value: 3,
+    label: '定时',
+  }
+]
+
+const changeTactics = (val) => {
+  ElMessageBox.confirm(
+    '你确定修改当前策略吗?',
+    '警告',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+      await updateTunnel(val).then(res => {
+        if (res.code === 0) {
+          ElMessage.success('修改成功')
+        }
+        getData()
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消修改',
+      })
+    })
+}
+
+// 控制面板
+const tunnelTimeData = ref()
+const isControlPanel = ref(false)
+const tunnelControlPanel = (val) => {
+  isControlPanel.value = true
+  tunnelTimeData.value = val
+  tunnelTiming.value = val.tunnelTime
+  console.log(val)
+}
+
+const activeNames = ref(['1', '2'])
+const handleChange = (val) => {
+  console.log(val)
+}
+
+// 定时
+
+const tunnelTiming = ref({
+  startTime: '00:00',
+  endTime: '00:00'
+})
+
+const editTunnelTiming = async() => {
+  console.log(tunnelTiming.value)
+  await updateTunnelTime(tunnelTiming.value).then(res => {
+    if (res.code === 0) {
+      ElMessage.success('已发送')
+    }
+    getData()
+  })
+}
+
+onMounted(() => {
+  getData()
+})
+</script>