4 Incheckningar eef53bf7b3 ... c530d8eb5e

Upphovsman SHA1 Meddelande Datum
  huangyan c530d8eb5e 更新监控页面模板和SSE连接,支持JSK、PF和PAU设备类型,并优化Dockerfile 1 vecka sedan
  huangyan 5b625a8462 feat: 新增新风机组监控页面模板和Dockerfile 1 vecka sedan
  huangyan 133b33d355 楼控组态图-测试 2 veckor sedan
  huangyan b379bfe59f 温控面板 2 veckor sedan

+ 60 - 0
Dockerfile

@@ -0,0 +1,60 @@
+# 第一阶段:构建阶段,使用官方 Golang 镜像编译程序
+FROM golang:1.23-alpine AS builder
+
+# 安装必要的构建工具(git 用于获取 GitHub 依赖,ca-certificates 用于 HTTPS 连接)
+RUN apk add --no-cache git ca-certificates
+
+# 设置工作目录
+WORKDIR /app
+
+# 设置 Go 环境变量
+ENV CGO_ENABLED=0 \
+    GOOS=linux \
+    GOARCH=amd64 \
+    GOPROXY=https://goproxy.cn,direct
+
+# 复制 go.mod 和 go.sum 文件
+COPY go.mod go.sum ./
+
+# 下载依赖
+RUN go mod download
+
+# 复制源代码
+COPY . .
+
+# 编译应用程序
+RUN go build -ldflags="-w -s" -o city_chips ./cmd/server
+
+# 第二阶段:运行阶段,使用极简的 alpine 镜像
+FROM alpine:latest
+
+# 安装必要的运行时依赖
+RUN apk --no-cache add ca-certificates tzdata && \
+    addgroup -g 1001 -S appgroup && \
+    adduser -u 1001 -S appuser -G appgroup
+
+# 设置时区
+ENV TZ=Asia/Shanghai
+
+# 设置工作目录
+WORKDIR /app
+
+# 从构建阶段复制二进制文件
+COPY --from=builder /app/city_chips ./
+
+# 复制配置文件和模板文件
+COPY --from=builder /app/config ./config
+COPY --from=builder /app/templates ./templates
+
+# 创建日志目录
+RUN mkdir -p storage/logs && \
+    chown -R appuser:appgroup /app
+
+# 切换到非 root 用户
+USER appuser
+
+# 暴露应用端口(根据配置文件,默认是 8000)
+EXPOSE 8000
+
+# 运行应用程序
+CMD ["./city_chips"]

+ 1 - 1
cmd/server/wire/wire_gen.go

@@ -57,7 +57,7 @@ func NewWire(viperViper *viper.Viper, logger *log.Logger) (*gin.Engine, func(),
 	energyService := service.NewEnergyService(serviceService, energyRepository, viperViper, client)
 	energyHandler := handler.NewEnergyHandler(handlerHandler, energyService, viperViper)
 	intelligentBuildingControlRepository := repository.NewIntelligentBuildingControlRepository(repositoryRepository)
-	intelligentBuildingControlService := service.NewIntelligentBuildingControlService(serviceService, intelligentBuildingControlRepository)
+	intelligentBuildingControlService := service.NewIntelligentBuildingControlService(serviceService, intelligentBuildingControlRepository, viperViper)
 	intelligentBuildingControlHandler := handler.NewIntelligentBuildingControlHandler(handlerHandler, intelligentBuildingControlService, viperViper)
 	temperatureRepository := repository.NewTemperatureRepository(repositoryRepository)
 	temperatureService := service.NewTemperatureService(serviceService, temperatureRepository, viperViper, client)

+ 2 - 1
config/local.yml

@@ -52,6 +52,7 @@ hikvision:
     doorStates: "/artemis/api/nms/v1/online/acs_device/get" #查询门禁设备状态接口
     acsdoorstates: "/artemis/api/acs/v1/door/states" #门禁点状态查询
     eventLogs: "/artemis/api/scpms/v2/eventLogs/searches" #入侵报警事件日志查询
+    iasDeviceSearch: "/api/resource/v2/iasDevice/search" #查询入侵报警主机列表v2
     regionsRoot: "/artemis/api/resource/v1/regions/root" #获取区域树接口
     regionsSubRegions: "/artemis/api/resource/v2/regions/subRegions" #根据区域编号获取下一级区域列表v2
     vqdList: "/artemis/api/nms/v1/vqd/list" #根据监控点列表查询视频质量诊断结果
@@ -94,7 +95,7 @@ Modbus:
   url: "tcp://"
 #楼控系统
 obix:
-  baseUrl: "https://10.1.201.253/obix/config"
+  baseUrl: "https://182.43.247.65:8098/obix/config"
   username: "obix"
   password: "Obix123456"
 #照明 系统

+ 35 - 13
internal/handler/hikvision.go

@@ -320,19 +320,28 @@ func (h *HikvisionHandler) GetAccess(ctx *gin.Context) {
 		devices = append(devices, inspection)
 		alarmList = append(alarmList, alarm)
 	}
-
-	m["DeviceCount"] = rand.Intn(500) //设备总数
-	m["Online"] = rand.Intn(100)      //在线
-	m["Abnormal"] = rand.Intn(100)    //异常
-	m["Fault"] = rand.Intn(100)       //故障
-	m["Offline"] = rand.Intn(100)     //离线
-	m["Attendance"] = rand.Intn(100)  //出勤率
-	m["Invasio1"] = invasio1          //入侵事件1
-	m["Invasio2"] = invasio2          //入侵事件2
-	m["DailyTotal"] = DailyTotal      //每日统计
-	m["Cumulative"] = Cumulative      //累计统计
-	m["AlarmList"] = alarmList        //实时告警与通知
-	m["devices"] = devices            //设备列表
+	err, result := h.hikvisionService.GetAcsDeviceSearch("1", "1000", "")
+	if err != nil {
+		resp.HandleError(ctx, 1201, "获取设备列表失败", err)
+		return
+	}
+	err, resource := h.hikvisionService.DeviceResource("door")
+	if err != nil {
+		resp.HandleError(ctx, 1201, "获取设备资源失败", err)
+		return
+	}
+	m["DeviceCount"] = resource.Data.Total //设备总数
+	m["Online"] = rand.Intn(100)           //在线
+	m["Abnormal"] = rand.Intn(100)         //异常
+	m["Fault"] = rand.Intn(100)            //故障
+	m["Offline"] = rand.Intn(100)          //离线
+	m["Attendance"] = rand.Intn(100)       //出勤率
+	m["Invasio1"] = invasio1               //入侵事件1
+	m["Invasio2"] = invasio2               //入侵事件2
+	m["DailyTotal"] = DailyTotal           //每日统计
+	m["Cumulative"] = Cumulative           //累计统计
+	m["AlarmList"] = alarmList             //实时告警与通知
+	m["devices"] = result.Data             //设备列表
 	resp.HandleSuccess(ctx, m)
 }
 
@@ -829,3 +838,16 @@ func (h *HikvisionHandler) GetAcsDoorStates(ctx *gin.Context) {
 		}
 	}
 }
+
+// GetIasDeviceSearch 查询入侵报警主机列表v2
+func (h *HikvisionHandler) GetIasDeviceSearch(ctx *gin.Context) {
+	pageNo := ctx.Query("pageNo")
+	pageSize := ctx.Query("pageSize")
+	name := ctx.Query("name")
+	err, search := h.hikvisionService.GetIasDeviceSearch(pageNo, pageSize, name)
+	if err != nil {
+		resp.HandleError(ctx, 1203, "控制门禁失败", err.Error())
+		return
+	}
+	resp.HandleSuccess(ctx, search.Data)
+}

+ 307 - 62
internal/handler/intelligentbuildingcontrol.go

@@ -9,8 +9,10 @@ import (
 	"fmt"
 	"go.uber.org/zap"
 	"math/rand"
+	"net/http"
 	"regexp"
-	"sync"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/spf13/viper"
@@ -76,8 +78,10 @@ func (h *IntelligentBuildingControlHandler) GetPoint(ctx *gin.Context) {
 	floor := ctx.PostForm("floor")
 	section := ctx.PostForm("section")
 	device_name := ctx.PostForm("deviceName")
+	pageNum, err := strconv.Atoi(ctx.PostForm("pageNum"))
+	pageSize, err := strconv.Atoi(ctx.PostForm("pageSize"))
 	conds := make(map[string]any)
-	var pointType []model.PointType
+	//var pointType []model.PointType
 	if pointName != "" {
 		conds["point_name"] = pointName
 	}
@@ -97,56 +101,126 @@ func (h *IntelligentBuildingControlHandler) GetPoint(ctx *gin.Context) {
 		conds["device_name"] = device_name
 	}
 
-	baseUrl := h.conf.GetString("obix.baseUrl")
-	points, err := h.intelligentBuildingControlService.GetPoint(conds)
+	//baseUrl := h.conf.GetString("obix.baseUrl")
+	point, total, err := h.intelligentBuildingControlService.GetPoint(conds, pageNum, pageSize)
 	if err != nil {
 		resp.HandleError(ctx, 1201, "查询点位失败", nil)
 		return
 	}
+	//
+	//var wg sync.WaitGroup
+	//var mutex sync.Mutex // 保护切片并发写入
+	//m := make(map[string]string)
+	//sem := make(chan struct{}, 10) // 最大并发数为10
+	//
+	//for _, v := range *points {
+	//	url := baseUrl + v.FullPath
+	//	wg.Add(1)
+	//	sem <- struct{}{}
+	//	go func(url string, pointName string) {
+	//		defer func() {
+	//			<-sem
+	//			wg.Done()
+	//		}()
+	//
+	//		request, err := obix.SendSecureRequest(url, h.conf.GetString("obix.username"), h.conf.GetString("obix.password"))
+	//		if err != nil {
+	//			h.logger.Error("发送请求失败", zap.Error(err))
+	//			return
+	//		}
+	//
+	//		re := regexp.MustCompile(`val="([^"]+)"`)
+	//		matches := re.FindStringSubmatch(request)
+	//
+	//		mutex.Lock()
+	//		defer mutex.Unlock()
+	//
+	//		if len(matches) > 1 {
+	//			s := model.PointName[pointName]
+	//			m[s] = matches[1]
+	//			pointType = append(pointType, model.PointType{Type: m})
+	//		} else {
+	//			h.logger.Warn("未找到 val 值", zap.String("url", url))
+	//		}
+	//	}(url, v.PointName)
+	//}
+	//
+	//// 等待所有协程完成
+	//wg.Wait()
 
-	var wg sync.WaitGroup
-	var mutex sync.Mutex // 保护切片并发写入
-	m := make(map[string]string)
-	sem := make(chan struct{}, 10) // 最大并发数为10
+	// 统一返回结果
+	resp.PageHandleSuccess(ctx, point, total, pageNum, pageSize)
+}
 
+// GetGetPoint 获取点位设备数据
+func (h *IntelligentBuildingControlHandler) GetGetPoint(ctx *gin.Context) {
+	pointName := ctx.Query("pointName")
+	deviceType := ctx.Query("deviceType")
+	building := ctx.Query("building")
+	floor := ctx.Query("floor")
+	section := ctx.Query("section")
+	device_name := ctx.Query("deviceName")
+	conds := make(map[string]any)
+	if pointName != "" {
+		conds["point_name"] = pointName
+	}
+	if deviceType != "" {
+		conds["device_type"] = deviceType
+	}
+	if building != "" {
+		conds["building"] = building
+	}
+	if floor != "" {
+		conds["floor"] = floor
+	}
+	if section != "" {
+		conds["section"] = section
+	}
+	if device_name != "" {
+		conds["device_name"] = device_name
+	}
+	baseUrl := h.conf.GetString("obix.baseUrl")
+	m := make(map[string]any)
+	points, _, err := h.intelligentBuildingControlService.GetPoint(conds, 1, 100)
+	if err != nil {
+		resp.HandleError(ctx, 1201, "查询点位失败", nil)
+	}
 	for _, v := range *points {
 		url := baseUrl + v.FullPath
-		wg.Add(1)
-		sem <- struct{}{}
-		go func(url string, pointName string) {
-			defer func() {
-				<-sem
-				wg.Done()
-			}()
-
-			request, err := obix.SendSecureRequest(url, h.conf.GetString("obix.username"), h.conf.GetString("obix.password"))
-			if err != nil {
-				h.logger.Error("发送请求失败", zap.Error(err))
-				return
-			}
-
-			re := regexp.MustCompile(`val="([^"]+)"`)
-			matches := re.FindStringSubmatch(request)
-
-			mutex.Lock()
-			defer mutex.Unlock()
-
-			if len(matches) > 1 {
-				s := model.PointName[pointName]
-				m[s] = matches[1]
-				pointType = append(pointType, model.PointType{Type: m})
-			} else {
-				h.logger.Warn("未找到 val 值", zap.String("url", url))
+		request, err := obix.SendSecureRequest(url, h.conf.GetString("obix.username"), h.conf.GetString("obix.password"))
+		if err != nil {
+			h.logger.Error("发送请求失败", zap.Error(err))
+			return
+		}
+		re := regexp.MustCompile(`val="([^"]+)"`)
+		matches := re.FindStringSubmatch(request)
+		if len(matches) > 1 {
+			s := model.PointName[v.PointName]
+			if s != "" {
+				value := matches[1]
+				switch val := obix.DetectType(value).(type) {
+				case int:
+					m[s] = val
+				case float64:
+					m[s] = fmt.Sprintf("%.2f", val) // 保留两位小数输出
+				case bool:
+					if val {
+						m[s] = "是"
+					} else {
+						m[s] = "否"
+					}
+				default:
+					m[s] = value // 原样输出字符串
+				}
 			}
-		}(url, v.PointName)
+		} else {
+			h.logger.Warn("未找到 val 值", zap.String("url", url))
+		}
 	}
+	resp.HandleSuccess(ctx, m)
 
-	// 等待所有协程完成
-	wg.Wait()
-
-	// 统一返回结果
-	resp.HandleSuccess(ctx, pointType)
 }
+
 func (h *IntelligentBuildingControlHandler) GetGetPointSSE(ctx *gin.Context) {
 	// 设置响应头
 	ctx.Header("Content-Type", "text/event-stream")
@@ -198,7 +272,7 @@ func (h *IntelligentBuildingControlHandler) GetGetPointSSE(ctx *gin.Context) {
 			return
 		default:
 			m := make(map[string]any)
-			points, err := h.intelligentBuildingControlService.GetPoint(conds)
+			points, _, err := h.intelligentBuildingControlService.GetPoint(conds, 1, 100)
 			if err != nil {
 				resp.HandleError(ctx, 1201, "查询点位失败", nil)
 				conn = false
@@ -249,29 +323,143 @@ func (h *IntelligentBuildingControlHandler) GetGetPointSSE(ctx *gin.Context) {
 	}
 }
 
+//func (h *IntelligentBuildingControlHandler) GetGetPointSSE(ctx *gin.Context) {
+//	// 设置响应头
+//	ctx.Header("Content-Type", "text/event-stream")
+//	ctx.Header("Cache-Control", "no-cache")
+//	ctx.Header("Connection", "keep-alive")
+//
+//	// 监听客户端断开连接
+//	conn := true
+//	notify := ctx.Writer.CloseNotify()
+//
+//	type Response struct {
+//		RequestId string `json:"requestId,omitempty"`
+//		Code      int32  `json:"code,omitempty"`
+//		Msg       string `json:"msg,omitempty"`
+//		Data      any    `json:"data"`
+//	}
+//
+//	pointName := ctx.Query("pointName")
+//	deviceType := ctx.Query("deviceType")
+//	building := ctx.Query("building")
+//	floor := ctx.Query("floor")
+//	section := ctx.Query("section")
+//	device_name := ctx.Query("deviceName")
+//
+//	conds := make(map[string]any)
+//	if pointName != "" {
+//		conds["point_name"] = pointName
+//	}
+//	if deviceType != "" {
+//		conds["device_type"] = deviceType
+//	}
+//	if building != "" {
+//		conds["building"] = building
+//	}
+//	if floor != "" {
+//		conds["floor"] = floor
+//	}
+//	if section != "" {
+//		conds["section"] = section
+//	}
+//	if device_name != "" {
+//		conds["device_name"] = device_name
+//	}
+//
+//	baseUrl := h.conf.GetString("obix.baseUrl")
+//
+//	var response Response
+//	for conn {
+//		select {
+//		case <-notify:
+//			conn = false
+//			fmt.Println("客户端断开连接")
+//			return
+//		default:
+//			m := make(map[string]any)
+//			points, _, err := h.intelligentBuildingControlService.GetPoint(conds, 1, 100)
+//			if err != nil {
+//				resp.HandleError(ctx, 1201, "查询点位失败", nil)
+//				conn = false
+//				continue
+//			}
+//
+//			var wg sync.WaitGroup
+//			var mutex sync.Mutex
+//			sem := make(chan struct{}, 10) // 控制最大并发数为10
+//
+//			for _, v := range *points {
+//				select {
+//				case sem <- struct{}{}:
+//				default:
+//					<-sem
+//					sem <- struct{}{}
+//				}
+//				wg.Add(1)
+//				go func(v model.Point) {
+//					defer func() {
+//						<-sem
+//						wg.Done()
+//					}()
+//
+//					url := baseUrl + v.FullPath
+//					request, err := obix.SendSecureRequest(url, h.conf.GetString("obix.username"), h.conf.GetString("obix.password"))
+//					if err != nil {
+//						h.logger.Error("发送请求失败", zap.String("url", url), zap.Error(err))
+//						return
+//					}
+//
+//					re := regexp.MustCompile(`val="([^"]+)"`)
+//					matches := re.FindStringSubmatch(request)
+//
+//					if len(matches) > 1 {
+//						s := model.PointName[v.PointName]
+//						if s != "" {
+//							value := matches[1]
+//							result := obix.DetectType(value)
+//
+//							mutex.Lock()
+//							defer mutex.Unlock()
+//
+//							switch val := result.(type) {
+//							case int:
+//								m[s] = val
+//							case float64:
+//								m[s] = fmt.Sprintf("%.2f", val)
+//							case bool:
+//								if val {
+//									m[s] = "是"
+//								} else {
+//									m[s] = "否"
+//								}
+//							default:
+//								m[s] = value
+//							}
+//						}
+//					} else {
+//						h.logger.Warn("未找到 val 值", zap.String("url", url))
+//					}
+//				}(v)
+//			}
+//
+//			wg.Wait() // 等待所有 goroutine 完成
+//
+//			response.Code = 200
+//			response.RequestId = ctx.ClientIP()
+//			response.Msg = "success"
+//			response.Data = m
+//
+//			res, _ := json.Marshal(&response)
+//			fmt.Fprintf(ctx.Writer, "data: %s\n\n", string(res))
+//			ctx.Writer.Flush()
+//			time.Sleep(10 * time.Second)
+//		}
+//	}
+//}
+
 // GetPointType 获取点位类型
 func (h *IntelligentBuildingControlHandler) GetPointType(ctx *gin.Context) {
-	var tempS1 model.NumericPoint
-	//	var xmlData = `<?xml version="1.0" encoding="UTF-8"?>
-	//<real val="27.49811553955078" display="27.50 °C {ok}" unit="obix:units/celsius">
-	//	<str name="facets" val="units=u:celsius;°C;(K);+273.15;" display="units=°C"/>
-	//	<ref name="proxyExt" display="analogInput:1:Present Value:-1:REAL"/>
-	//	<real name="out" val="27.49811553955078" display="27.50 °C {ok}"/>
-	//	<ref name="tag" display="Point Tag"/>
-	//</real>`
-	//err2 := ctx.ShouldBindXML(&tempS1)
-	//err2 := xml.Unmarshal([]byte(xmlData), &tempS1)
-	//if err2 != nil {
-	//	resp.HandleError(ctx, 1201, "绑定XML失败", nil)
-	//	return
-	//}
-	request, err2 := obix.SendSecureRequest("https://10.1.201.253/obix/config/Drivers/BacnetNetwork/DDC_9B_1F_1a/points/PAU/PAU_9B_1F_1/PAUAlr1", h.conf.GetString("obix.username"), h.conf.GetString("obix.password"))
-	if err2 != nil {
-		resp.HandleError(ctx, 1201, "发送请求失败", nil)
-		return
-	}
-	fmt.Println(request, "===========")
-	fmt.Println(tempS1)
 	points, err := h.intelligentBuildingControlService.GetPointType()
 	if err != nil {
 		resp.HandleError(ctx, 1201, "查询点位类型失败", nil)
@@ -279,6 +467,8 @@ func (h *IntelligentBuildingControlHandler) GetPointType(ctx *gin.Context) {
 	}
 	resp.HandleSuccess(ctx, points)
 }
+
+// GetDeviceType 获取设备类型
 func (h *IntelligentBuildingControlHandler) GetDeviceType(ctx *gin.Context) {
 	points, err := h.intelligentBuildingControlService.DeviceType()
 	if err != nil {
@@ -291,3 +481,58 @@ func (h *IntelligentBuildingControlHandler) GetDeviceType(ctx *gin.Context) {
 	}
 	resp.HandleSuccess(ctx, m)
 }
+
+// GetDevices 获取设备列表
+func (h *IntelligentBuildingControlHandler) GetDevices(ctx *gin.Context) {
+	conds := make(map[string]any)
+	pageNum, err := strconv.Atoi(ctx.Query("pageNum"))
+	pageSize, err := strconv.Atoi(ctx.Query("pageSize"))
+	device_type := ctx.Query("device_type")
+	if len(device_type) == 0 || device_type == "" {
+		resp.HandleError(ctx, 1201, "设备类型不能为空", nil)
+	}
+	conds["device_type"] = device_type
+	if err != nil {
+		resp.HandleError(ctx, 1201, "获取分页参数失败", nil)
+		return
+	}
+	devices, total, err := h.intelligentBuildingControlService.GetDevices(conds, pageNum, pageSize)
+	if err != nil {
+		resp.HandleError(ctx, 1201, "查询设备类型失败", nil)
+		return
+	}
+	resp.PageHandleSuccess(ctx, devices, total, pageNum, pageSize)
+}
+
+// ConfigurationDiagram 获取设备组态图
+func (h *IntelligentBuildingControlHandler) ConfigurationDiagram(c *gin.Context) {
+	// 获取查询参数
+	//type-deviceName
+	id := c.Query("id")
+
+	if len(id) == 0 {
+		resp.HandleError(c, 1201, "设备名称不能为空", nil)
+		return
+	}
+	split := strings.Split(id, "-")
+	if len(split) != 2 {
+		resp.HandleError(c, 1201, "设备名称格式错误", nil)
+		return
+	}
+	deviceName := split[1]
+	types := split[0]
+	switch types {
+	case "PAU":
+		c.HTML(http.StatusOK, "PAU.html", gin.H{
+			"deviceName": deviceName,
+		})
+	case "JSK":
+		c.HTML(http.StatusOK, "JSK.html", gin.H{
+			"deviceName": deviceName,
+		})
+	case "PF":
+		c.HTML(http.StatusOK, "PF.html", gin.H{
+			"deviceName": deviceName,
+		})
+	}
+}

+ 24 - 15
internal/model/intelligentbuildingcontrol.go

@@ -39,21 +39,30 @@ type PointType struct {
 
 // PointName 设备名称对应表
 var PointName = map[string]string{
-	"TempS1":   "送风温度",   //
-	"HumS1":    "送风湿度",   //
-	"TempH1":   "回风温度",   //
-	"TempX1":   "新风温度",   //
-	"CO2":      "回风二氧化碳", //
-	"AHUSts1":  "运行状态",   //
-	"AHUAlr1":  "故障报警",   //
-	"AHUAorm1": "自动状态",   //
-	"FJDPS1":   "风机压差",   //
-	"FDKG1":    "防冻开关",   //
-	"Sorw1":    "冬夏季模式",  //
-	"XFLev1":   "新风阀反馈",  //
-	"HFLev1":   "回风阀反馈",  //
-	"SFLev1":   "水阀反馈",
-	"GlwDPS1":  "过滤网压差", //
+	"TempS1":    "送风温度",
+	"HumS1":     "送风湿度",
+	"TempH1":    "回风温度",
+	"TempX1":    "新风温度",
+	"CO2":       "回风二氧化碳",
+	"AHUSts1":   "运行状态",
+	"AHUAlr1":   "故障报警",
+	"AHUAorm1":  "自动状态",
+	"FJDPS1":    "风机压差",
+	"FDKG1":     "防冻开关",
+	"Sorw1":     "冬夏季模式",
+	"XFLev1":    "新风阀反馈",
+	"HFLev1":    "回风阀反馈",
+	"SFLev1":    "水阀反馈",
+	"GlwDPS1":   "过滤网压差",
+	"PumbSts1":  "泵运行状态1",
+	"PumbAlr1":  "泵故障报警1",
+	"PumbSts2":  "泵运行状态2",
+	"PumbAlr2":  "泵故障报警2",
+	"PumbAorm1": "泵自动状态1",
+	"YWG1":      "超高液位",
+	"PFSts1":    "运行状态",
+	"PFAlr1":    "故障报警",
+	"PFAorm1":   "自动状态",
 }
 
 // RealPoint TempS1 点位表

+ 57 - 3
internal/repository/intelligentbuildingcontrol.go

@@ -8,10 +8,12 @@ import (
 
 type IntelligentBuildingControlRepository interface {
 	GetIntelligentBuildingControl(ctx context.Context, id int64) (*model.IntelligentBuildingControl, error)
-	GetPoint(conds map[string]any) (*[]model.Point, error)
+	GetPoint(conds map[string]any, pageNum, pageSize int) (*[]model.Point, int64, error)
 	GetPointType() ([]string, error)
 	DeviceType() ([]string, error)
 	DeviceCount() (int64, error)
+	GetPopintList(conds map[string]any, pageNum, pageSize int) ([]string, int64, error)
+	GetPointAll(conds map[string]any) (*[]model.Point, error)
 }
 
 func NewIntelligentBuildingControlRepository(
@@ -62,9 +64,61 @@ func (r *intelligentBuildingControlRepository) GetIntelligentBuildingControl(ctx
 }
 
 // GetPoint 根据点位数据获取点位信息
-func (r *intelligentBuildingControlRepository) GetPoint(conds map[string]any) (*[]model.Point, error) {
+func (r *intelligentBuildingControlRepository) GetPoint(conds map[string]any, pageNum, pageSize int) (*[]model.Point, int64, error) {
 	var points []model.Point
-	points, err := helper.QueryByConditions[model.Point](r.db, conds)
+	points, total, err := helper.QueryByConditions[model.Point](r.db, conds, pageNum, pageSize)
+	if err != nil {
+		return &points, 0, err
+	}
+	return &points, total, nil
+}
+
+//	func (r *intelligentBuildingControlRepository) GetPopintList() ([]string, error) {
+//		var names []string
+//		err := r.db.Model(&model.Point{}).Select("device_name").Group("device_name").Find(&names).Error
+//		if err != nil {
+//			return nil, err
+//		}
+//		return names, nil
+//	}
+func (r *intelligentBuildingControlRepository) GetPopintList(conds map[string]any, pageNum, pageSize int) ([]string, int64, error) {
+	var names []string
+	var total int64
+
+	db := r.db.Model(&model.Point{})
+
+	// 应用查询条件
+	for k, v := range conds {
+		if v != nil {
+			db = db.Where(k+" = ?", v)
+		}
+	}
+
+	// 查询总数(去重后的)
+	err := db.Select("device_name").Group("device_name").Count(&total).Error
+	if err != nil {
+		return nil, 0, err
+	}
+
+	// 分页查询
+	err = db.Select("device_name").
+		Group("device_name").
+		Order("device_name ASC").
+		Offset((pageNum - 1) * pageSize).
+		Limit(pageSize).
+		Find(&names).Error
+
+	if err != nil {
+		return nil, 0, err
+	}
+
+	return names, total, nil
+}
+
+// GetPointAll 获取所有点位表
+func (r *intelligentBuildingControlRepository) GetPointAll(conds map[string]any) (*[]model.Point, error) {
+	var points []model.Point
+	points, err := helper.QueryByCondition[model.Point](r.db, conds)
 	if err != nil {
 		return &points, err
 	}

+ 4 - 1
internal/server/http.go

@@ -30,7 +30,7 @@ func NewServerHTTP(
 	)
 	// 使用 HTTP/2 必须启用 HTTPS
 	//r.RunTLS(":443", "cert.pem", "key.pem")
-	r.LoadHTMLGlob("templates/h5player.html")
+	r.LoadHTMLGlob("templates/*.html")
 	r.Static("/static", "./templates/static")
 
 	//出入口控制系统
@@ -141,6 +141,9 @@ func NewServerHTTP(
 		inte.GET("/pointSSE", intell.GetGetPointSSE)
 		inte.GET("/pointType", intell.GetPointType)
 		inte.GET("/deviceType", intell.GetDeviceType)
+		inte.GET("/getPoint", intell.GetGetPoint)
+		inte.GET("/getDevices", intell.GetDevices)
+		inte.GET("/configurationDiagram", intell.ConfigurationDiagram)
 	}
 	//温控
 	temper := r.Group("/temperature")

+ 29 - 0
internal/service/hikvision.go

@@ -34,6 +34,7 @@ type HikvisionService interface {
 	GetAcsDoorStates(indexCodes []string) (error, hikvisionOpenAPIGo.Result)
 	GetDoorDoControl(doorIndexCodes []string, controlType int) (error, hikvisionOpenAPIGo.Result)
 	GetEventLogs(pageNo, pageSize, srcType, startTime, endTime, srcName string, eventType int) (error, hikvisionOpenAPIGo.Result)
+	GetIasDeviceSearch(pageNo, pageSize, name string) (error, hikvisionOpenAPIGo.Result)
 }
 
 func NewHikvisionService(service *Service, hikvisionRepository repository.HikvisionRepository, conf *viper.Viper) HikvisionService {
@@ -698,3 +699,31 @@ func (s *hikvisionService) GetEventLogs(pageNo, pageSize, srcType, startTime, en
 	}
 	return nil, hikvision
 }
+
+// GetIasDeviceSearch 查询入侵报警主机列表v2
+func (s *hikvisionService) GetIasDeviceSearch(pageNo, pageSize, name string) (error, hikvisionOpenAPIGo.Result) {
+	//var cameraSearch model.CameraSearch
+	m := make(map[string]any)
+	if pageNo != "" {
+		m["pageNo"] = pageNo
+	} else {
+		m["pageNo"] = "1"
+	}
+	if pageSize != "" {
+		m["pageSize"] = pageSize
+	} else {
+		m["pageSize"] = s.conf.GetString("hikvision.pageSize")
+	}
+	if name != "" {
+		m["name"] = name
+	}
+	// 获取门禁状态
+	hikvision, err := s.Hikvision(s.conf.GetString("hikvision.api.iasDeviceSearch"), m, 15)
+	if err != nil {
+		return errors.New("获取入侵报警主机列表失败"), hikvisionOpenAPIGo.Result{}
+	}
+	if hikvision.Code != "0" {
+		return errors.New("获取入侵报警主机列表失败"), hikvisionOpenAPIGo.Result{}
+	}
+	return nil, hikvision
+}

+ 103 - 5
internal/service/intelligentbuildingcontrol.go

@@ -3,30 +3,41 @@ package service
 import (
 	"city_chips/internal/model"
 	"city_chips/internal/repository"
+	"city_chips/pkg/helper/obix"
 	"context"
+	"fmt"
+	"github.com/pkg/errors"
+	"github.com/spf13/viper"
+	"go.uber.org/zap"
+	"regexp"
+	"sync"
 )
 
 type IntelligentBuildingControlService interface {
 	GetIntelligentBuildingControl(ctx context.Context, id int64) (*model.IntelligentBuildingControl, error)
-	GetPoint(conds map[string]any) (*[]model.Point, error)
+	GetPoint(conds map[string]any, pageNum, pageSize int) (*[]model.Point, int64, error)
 	GetPointType() ([]string, error)
 	DeviceType() ([]string, error)
 	DeviceCount() (int64, error)
+	GetDevices(conds map[string]any, pageNum, pageSize int) (map[string]any, int64, error)
 }
 
 func NewIntelligentBuildingControlService(
 	service *Service,
 	intelligentBuildingControlRepository repository.IntelligentBuildingControlRepository,
+	conf *viper.Viper,
 ) IntelligentBuildingControlService {
 	return &intelligentBuildingControlService{
 		Service:                              service,
 		intelligentBuildingControlRepository: intelligentBuildingControlRepository,
+		conf:                                 conf,
 	}
 }
 
 type intelligentBuildingControlService struct {
 	*Service
 	intelligentBuildingControlRepository repository.IntelligentBuildingControlRepository
+	conf                                 *viper.Viper
 }
 
 // DeviceCount 获取设备总数
@@ -59,14 +70,101 @@ func (s *intelligentBuildingControlService) GetPointType() ([]string, error) {
 }
 
 // GetPoint 获取点位列表
-func (s *intelligentBuildingControlService) GetPoint(conds map[string]any) (*[]model.Point, error) {
-	points, err := s.intelligentBuildingControlRepository.GetPoint(conds)
+func (s *intelligentBuildingControlService) GetPoint(conds map[string]any, pageNum, pageSize int) (*[]model.Point, int64, error) {
+	points, total, err := s.intelligentBuildingControlRepository.GetPoint(conds, pageNum, pageSize)
 	if err != nil {
-		return &[]model.Point{}, err
+		return &[]model.Point{}, 0, err
 	}
-	return points, err
+	return points, total, err
 }
 
 func (s *intelligentBuildingControlService) GetIntelligentBuildingControl(ctx context.Context, id int64) (*model.IntelligentBuildingControl, error) {
 	return s.intelligentBuildingControlRepository.GetIntelligentBuildingControl(ctx, id)
 }
+
+// GetDevices 获取设备列表(支持设备 & 点位双重并发)
+func (s *intelligentBuildingControlService) GetDevices(conds map[string]any, pageNum, pageSize int) (map[string]any, int64, error) {
+	var total int64
+	baseUrl := s.conf.GetString("obix.baseUrl")
+	device := make(map[string]any)
+	list, total, err := s.intelligentBuildingControlRepository.GetPopintList(conds, pageNum, pageSize)
+	if err != nil {
+		return nil, 0, errors.New("获取设备列表失败")
+	}
+
+	var wg sync.WaitGroup
+	var mu sync.Mutex // 保护 device 的写入
+	var all *[]model.Point
+	for _, v := range list {
+		wg.Add(1)
+		go func(deviceName string) {
+			defer wg.Done()
+
+			all, err = s.intelligentBuildingControlRepository.GetPointAll(map[string]any{"device_name": deviceName})
+			if err != nil {
+				s.logger.Error("获取设备点位列表失败", zap.String("device", deviceName), zap.Error(err))
+				return
+			}
+
+			m := make(map[string]any)
+			var innerWg sync.WaitGroup
+			var innerMu sync.Mutex // 保护 m 的写入
+
+			for _, vl := range *all {
+				innerWg.Add(1)
+				go func(vl model.Point) {
+					defer innerWg.Done()
+
+					url := baseUrl + vl.FullPath
+					request, err := obix.SendSecureRequest(url, s.conf.GetString("obix.username"), s.conf.GetString("obix.password"))
+					if err != nil {
+						s.logger.Error("发送请求失败", zap.String("url", url), zap.Error(err))
+						return
+					}
+
+					re := regexp.MustCompile(`val="([^"]+)"`)
+					matches := re.FindStringSubmatch(request)
+					if len(matches) > 1 {
+						key := model.PointName[vl.PointName]
+						if key != "" {
+							value := matches[1]
+							switch val := obix.DetectType(value).(type) {
+							case int:
+								innerMu.Lock()
+								m[key] = val
+								innerMu.Unlock()
+							case float64:
+								innerMu.Lock()
+								m[key] = fmt.Sprintf("%.2f", val)
+								innerMu.Unlock()
+							case bool:
+								innerMu.Lock()
+								if val {
+									m[key] = "是"
+								} else {
+									m[key] = "否"
+								}
+								innerMu.Unlock()
+							default:
+								innerMu.Lock()
+								m[key] = value
+								innerMu.Unlock()
+							}
+						}
+					} else {
+						s.logger.Warn("未找到 val 值", zap.String("url", url))
+					}
+				}(vl)
+			}
+
+			innerWg.Wait()
+
+			mu.Lock()
+			device[deviceName] = m
+			mu.Unlock()
+		}(v)
+	}
+
+	wg.Wait()
+	return device, total, nil
+}

+ 16 - 2
pkg/helper/dbutils/dbutils.go

@@ -2,9 +2,23 @@ package helper
 
 import "gorm.io/gorm"
 
-//通用多条查询
-func QueryByConditions[T any](db *gorm.DB, conditions map[string]interface{}) ([]T, error) {
+// QueryByConditions 通用多条查询
+func QueryByCondition[T any](db *gorm.DB, conditions map[string]interface{}) ([]T, error) {
 	var results []T
 	tx := db.Where(conditions).Find(&results)
 	return results, tx.Error
 }
+
+// QueryByConditions 分页查询
+func QueryByConditions[T any](db *gorm.DB, conditions map[string]interface{}, pageNum, pageSize int) ([]T, int64, error) {
+	var results []T
+	var total int64
+
+	tx := db.Where(conditions).Model(&results).Count(&total)
+	if tx.Error != nil {
+		return nil, 0, tx.Error
+	}
+
+	tx = db.Where(conditions).Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&results)
+	return results, total, tx.Error
+}

+ 15 - 0
pkg/helper/resp/resp.go

@@ -10,6 +10,14 @@ type response struct {
 	Message string      `json:"message"`
 	Data    interface{} `json:"data"`
 }
+type pageResponse struct {
+	Code     int         `json:"code"`
+	Message  string      `json:"message"`
+	PageNum  int         `json:"pageNum"`
+	PageSize int         `json:"pageSize"`
+	Total    int64       `json:"total"`
+	Data     interface{} `json:"data"`
+}
 
 func HandleSuccess(ctx *gin.Context, data interface{}) {
 	if data == nil {
@@ -18,6 +26,13 @@ func HandleSuccess(ctx *gin.Context, data interface{}) {
 	resp := response{Code: 200, Message: "success", Data: data}
 	ctx.JSON(http.StatusOK, resp)
 }
+func PageHandleSuccess(ctx *gin.Context, data interface{}, total int64, pageNum, pageSize int) {
+	if data == nil {
+		data = map[string]string{}
+	}
+	resp := pageResponse{Code: 200, Message: "success", Data: data, PageNum: pageNum, PageSize: pageSize, Total: total}
+	ctx.JSON(http.StatusOK, resp)
+}
 
 func HandleError(ctx *gin.Context, code int, message string, data interface{}) {
 	if data == nil {

+ 407 - 0
templates/JSK.html

@@ -0,0 +1,407 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>集水坑监控</title>
+  <script src="/static/css/tailwindcss.css"></script>
+
+  <script>
+    tailwind.config = {
+      theme: {
+        extend: {
+          colors: {
+            primary: '#0f766e',
+            secondary: '#0d9488',
+            accent: '#14b8a6',
+            neutral: '#134e4a',
+          },
+          fontFamily: {
+            sans: ['Inter', 'system-ui', 'sans-serif'],
+          },
+        },
+      }
+    }
+  </script>
+
+  <style type="text/tailwindcss">
+    @layer utilities {
+      .card-shadow {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+      }
+      .status-normal {
+        @apply text-emerald-500;
+      }
+      .status-warning {
+        @apply text-amber-500;
+      }
+      .status-alarm {
+        @apply text-rose-500;
+      }
+      .data-update {
+        animation: pulse 1s ease-in-out;
+      }
+      @keyframes pulse {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0.6; }
+      }
+    }
+  </style>
+</head>
+
+<body class="bg-gradient-to-br from-primary to-neutral text-gray-800 min-h-screen p-3">
+<div class="container mx-auto max-w-7xl">
+  <!-- 头部信息 - 更紧凑 -->
+  <header class="mb-4">
+    <div class="flex justify-between items-center">
+      <h1 class="text-[clamp(1.2rem,2vw,1.6rem)] font-bold text-white">{{.deviceName}}</h1>
+      <div class="flex items-center bg-white/10 px-2 py-1 rounded-lg">
+        <i class="fa fa-refresh mr-1 text-white text-sm"></i>
+        <span id="connection-status" class="text-white text-xs">连接中...</span>
+      </div>
+    </div>
+    <div class="h-1 w-full bg-gradient-to-r from-accent to-transparent rounded-full mt-1"></div>
+  </header>
+
+  <!-- 流程图区域 - 调整大小 -->
+  <div class="bg-white/90 rounded-lg p-4 mb-4 card-shadow">
+    <h2 class="text-base font-semibold mb-2 flex items-center">
+      <i class="fa fa-sitemap text-primary mr-1"></i>系统流程图
+    </h2>
+    <div class="flex items-center justify-between">
+<!--      <div class="bg-blue-100 px-2 py-1 rounded text-primary font-medium text-sm">-->
+<!--        <i class="fa fa-arrow-right mr-1"></i> 新风-->
+<!--      </div>-->
+      <img src="/static/images/JSK.png" alt="新风机组流程图" class="max-w-[80%] h-auto rounded shadow-sm">
+<!--      <div class="bg-green-100 px-2 py-1 rounded text-green-600 font-medium text-sm">-->
+<!--        送风 <i class="fa fa-arrow-right ml-1"></i>-->
+<!--      </div>-->
+    </div>
+  </div>
+
+  <!-- 主要内容区域 - 紧凑布局 -->
+  <div id="main-content" class="space-y-4">
+    <!-- 数据卡片区域 - 更密的网格 -->
+    <div id="data-cards" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
+      <!-- 数据卡片将在这里动态生成 -->
+    </div>
+
+    <!-- 状态和控制参数合并区域 - 节省空间 -->
+    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+      <!-- 状态参数区域 -->
+      <div id="status-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-tachometer mr-1"></i>状态参数
+        </h2>
+        <div id="status-parameters" class="grid grid-cols-1 sm:grid-cols-2 gap-2">
+          <!-- 状态参数将在这里动态生成 -->
+        </div>
+      </div>
+
+      <!-- 控制参数区域 -->
+      <div id="control-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-sliders mr-1"></i>控制参数
+        </h2>
+        <div id="control-parameters" class="space-y-2">
+          <!-- 控制参数将在这里动态生成 -->
+        </div>
+      </div>
+    </div>
+
+    <!-- 数据更新日志 - 缩小高度 -->
+    <div id="log-section" class="bg-white/90 rounded-lg p-3 card-shadow">
+      <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+        <i class="fa fa-history mr-1"></i>数据更新日志
+      </h2>
+      <div id="update-log" class="text-xs text-gray-600 max-h-20 overflow-y-auto space-y-0.5">
+        <p class="text-gray-500">等待数据更新...</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  // 数据类型配置 - 定义不同字段的显示和交互方式
+  const fieldConfig = {
+    // 温度类字段
+    "新风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "blue" },
+    "送风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "green" },
+    "回风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "purple" },
+
+    // 湿度类字段
+    "送风湿度": { type: "humidity", unit: "%", icon: "fa-tint", color: "blue" },
+
+    // 二氧化碳
+    "回风二氧化碳": { type: "co2", unit: "ppm", icon: "fa-leaf", color: "green" },
+
+    // 阀门反馈
+    "回风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "gray" },
+    "新风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "blue" },
+    "水阀反馈": { type: "valve", unit: "%", icon: "fa-tint", color: "cyan" },
+
+    // 模式类(开关)
+    "冬夏季模式": { type: "mode", trueText: "夏季模式", falseText: "冬季模式", section: "control" },
+
+    // 状态类
+    "运行状态": { type: "status", trueText: "运行中", falseText: "停止", trueClass: "status-normal", falseClass: "status-warning" },
+    "故障报警": { type: "status", trueText: "故障", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "自动状态": { type: "status", trueText: "自动", falseText: "手动", trueClass: "status-normal", falseClass: "status-warning" },
+    "过滤网压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "防冻开关": { type: "status", trueText: "触发", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "风机压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true }
+  };
+
+  // 连接SSE并处理数据
+  function connectSSE() {
+    const statusElement = document.getElementById('connection-status');
+    const logElement = document.getElementById('update-log');
+    const dataCardsContainer = document.getElementById('data-cards');
+    const statusParametersContainer = document.getElementById('status-parameters');
+    const controlParametersContainer = document.getElementById('control-parameters');
+    const statusSection = document.getElementById('status-section');
+    const controlSection = document.getElementById('control-section');
+
+    // 跟踪已创建的元素,避免重复创建
+    const createdElements = new Set();
+
+    // 更新连接状态
+    function updateConnectionStatus(status, isConnected) {
+      statusElement.textContent = status;
+      if (isConnected) {
+        statusElement.classList.remove('text-amber-400', 'text-rose-400');
+        statusElement.classList.add('text-emerald-400');
+      } else if (status.includes('连接中')) {
+        statusElement.classList.remove('text-emerald-400', 'text-rose-400');
+        statusElement.classList.add('text-amber-400');
+      } else {
+        statusElement.classList.remove('text-emerald-400', 'text-amber-400');
+        statusElement.classList.add('text-rose-400');
+      }
+    }
+
+    // 添加日志记录
+    function addLogEntry(message) {
+      const now = new Date();
+      const timeString = now.toLocaleTimeString();
+      const logEntry = document.createElement('p');
+      logEntry.innerHTML = `<span class="text-primary">[${timeString}]</span> ${message}`;
+
+      // 移除"等待数据更新..."提示
+      if (logElement.querySelector('.text-gray-500')) {
+        logElement.innerHTML = '';
+      }
+
+      logElement.appendChild(logEntry);
+      logElement.scrollTop = logElement.scrollHeight;
+    }
+
+    // 创建或更新数据卡片 - 更紧凑的卡片
+    function createOrUpdateDataCard(field, value) {
+      const config = fieldConfig[field] || { type: "generic", unit: "", icon: "fa-dashboard", color: "gray" };
+      const elementId = `card-${field.replace(/\s+/g, '-')}`;
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          valueElement.textContent = `${value}${config.unit}`;
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的数据卡片
+      const card = document.createElement('div');
+      card.id = elementId;
+      card.className = "bg-white/90 rounded-lg p-2 card-shadow transform transition-all hover:scale-[1.02]";
+
+      // 确定图标颜色类
+      const iconColorClass = config.color === 'blue' ? 'text-blue-600' :
+              config.color === 'green' ? 'text-green-600' :
+                      config.color === 'purple' ? 'text-purple-600' :
+                              config.color === 'cyan' ? 'text-cyan-600' :
+                                      'text-gray-600';
+
+      const bgColorClass = config.color === 'blue' ? 'bg-blue-100' :
+              config.color === 'green' ? 'bg-green-100' :
+                      config.color === 'purple' ? 'bg-purple-100' :
+                              config.color === 'cyan' ? 'bg-cyan-100' :
+                                      'bg-gray-100';
+
+      card.innerHTML = `
+          <div class="flex justify-between items-start">
+            <div>
+              <p class="text-gray-500 text-xs">${field}</p>
+              <h3 id="${elementId}-value" class="text-lg font-bold mt-0.5">${value}${config.unit}</h3>
+            </div>
+            <div class="${bgColorClass} p-1.5 rounded-full">
+              <i class="fa ${config.icon} ${iconColorClass} text-sm"></i>
+            </div>
+          </div>
+        `;
+
+      dataCardsContainer.appendChild(card);
+      createdElements.add(elementId);
+    }
+
+    // 创建或更新状态参数 - 更紧凑的参数项
+    function createOrUpdateStatusParameter(field, value) {
+      const config = fieldConfig[field] || { type: "generic", trueText: "是", falseText: "否" };
+      const elementId = `status-${field.replace(/\s+/g, '-')}`;
+
+      // 确定应该显示在哪个区域
+      const targetContainer = config.section === "control" ? controlParametersContainer : statusParametersContainer;
+      const targetSection = config.section === "control" ? controlSection : statusSection;
+
+      // 显示对应的区域
+      targetSection.classList.remove('hidden');
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          if (config.type === "status") {
+            const isTrue = value === "是";
+            const displayValue = config.invert ? !isTrue : isTrue;
+            const text = displayValue ? config.trueText : config.falseText;
+
+            valueElement.textContent = text;
+            valueElement.className = `px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}`;
+          } else if (config.type === "mode") {
+            const isChecked = value === "是";
+            valueElement.checked = isChecked;
+          } else {
+            valueElement.textContent = value;
+          }
+
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的参数元素
+      const parameter = document.createElement('div');
+      parameter.id = elementId;
+
+      if (config.type === "status") {
+        const isTrue = value === "是";
+        const displayValue = config.invert ? !isTrue : isTrue;
+        const text = displayValue ? config.trueText : config.falseText;
+
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}">
+              ${text}
+            </span>
+          `;
+      } else if (config.type === "mode") {
+        const isChecked = value === "是";
+
+        parameter.className = "flex justify-between items-center border-b border-gray-100 pb-2";
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <label class="inline-flex items-center cursor-pointer">
+              <input type="checkbox" id="${elementId}-value" ${isChecked ? 'checked' : ''} class="sr-only peer">
+              <div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
+              <span class="ml-2 text-xs font-medium">${isChecked ? config.trueText : config.falseText}</span>
+            </label>
+          `;
+
+        // 添加模式切换事件
+        setTimeout(() => {
+          const checkbox = document.getElementById(`${elementId}-value`);
+          if (checkbox) {
+            checkbox.addEventListener('change', function() {
+              const newValue = this.checked ? "是" : "否";
+              const displayText = this.checked ? config.trueText : config.falseText;
+              this.nextElementSibling.nextElementSibling.textContent = displayText;
+
+              addLogEntry(`${field}已切换为${displayText}`);
+              // 这里可以添加发送到服务器的逻辑
+            });
+          }
+        }, 0);
+      } else {
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="text-base font-semibold">${value}</span>
+          `;
+      }
+
+      targetContainer.appendChild(parameter);
+      createdElements.add(elementId);
+    }
+
+    // 判断字段应该显示的位置
+    function isStatusOrControlField(field) {
+      const config = fieldConfig[field];
+      return config && (config.type === "status" || config.type === "mode");
+    }
+
+    // 处理接收到的SSE数据
+    function handleSSEData(data) {
+      addLogEntry(`接收${Object.keys(data).length}个字段数据`);
+
+      // 处理每个返回的字段
+      Object.keys(data).forEach(field => {
+        const value = data[field];
+
+        // 根据字段类型决定显示方式和位置
+        if (isStatusOrControlField(field)) {
+          createOrUpdateStatusParameter(field, value);
+        } else {
+          createOrUpdateDataCard(field, value);
+        }
+      });
+    }
+
+    // 初始化连接状态
+    updateConnectionStatus('连接中...', false);
+    let urls ="./pointSSE?deviceName="+{{.deviceName}}
+
+    // 实际SSE连接代码
+    const eventSource = new EventSource(urls);
+
+    eventSource.onopen = () => {
+      updateConnectionStatus('已连接', true);
+      addLogEntry('SSE连接已建立');
+    };
+
+    eventSource.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data);
+        if (data.code === 200 && data.data) {
+          handleSSEData(data.data);
+        }
+      } catch (error) {
+        addLogEntry(`数据解析错误: ${error.message}`);
+        console.error('SSE数据解析错误:', error);
+      }
+    };
+
+    eventSource.onerror = (error) => {
+      updateConnectionStatus('连接错误', false);
+      addLogEntry('SSE连接发生错误,正在重试...');
+      console.error('SSE错误:', error);
+    };
+
+  }
+
+  // 页面加载完成后连接SSE
+  document.addEventListener('DOMContentLoaded', connectSSE);
+</script>
+</body>
+
+</html>

+ 407 - 0
templates/PAU.html

@@ -0,0 +1,407 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>新风机组监控</title>
+  <script src="/static/css/tailwindcss.css"></script>
+
+  <script>
+    tailwind.config = {
+      theme: {
+        extend: {
+          colors: {
+            primary: '#0f766e',
+            secondary: '#0d9488',
+            accent: '#14b8a6',
+            neutral: '#134e4a',
+          },
+          fontFamily: {
+            sans: ['Inter', 'system-ui', 'sans-serif'],
+          },
+        },
+      }
+    }
+  </script>
+
+  <style type="text/tailwindcss">
+    @layer utilities {
+      .card-shadow {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+      }
+      .status-normal {
+        @apply text-emerald-500;
+      }
+      .status-warning {
+        @apply text-amber-500;
+      }
+      .status-alarm {
+        @apply text-rose-500;
+      }
+      .data-update {
+        animation: pulse 1s ease-in-out;
+      }
+      @keyframes pulse {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0.6; }
+      }
+    }
+  </style>
+</head>
+
+<body class="bg-gradient-to-br from-primary to-neutral text-gray-800 min-h-screen p-3">
+<div class="container mx-auto max-w-7xl">
+  <!-- 头部信息 - 更紧凑 -->
+  <header class="mb-4">
+    <div class="flex justify-between items-center">
+      <h1 class="text-[clamp(1.2rem,2vw,1.6rem)] font-bold text-white">{{.deviceName}}</h1>
+      <div class="flex items-center bg-white/10 px-2 py-1 rounded-lg">
+        <i class="fa fa-refresh mr-1 text-white text-sm"></i>
+        <span id="connection-status" class="text-white text-xs">连接中...</span>
+      </div>
+    </div>
+    <div class="h-1 w-full bg-gradient-to-r from-accent to-transparent rounded-full mt-1"></div>
+  </header>
+
+  <!-- 流程图区域 - 调整大小 -->
+  <div class="bg-white/90 rounded-lg p-4 mb-4 card-shadow">
+    <h2 class="text-base font-semibold mb-2 flex items-center">
+      <i class="fa fa-sitemap text-primary mr-1"></i>系统流程图
+    </h2>
+    <div class="flex items-center justify-between">
+<!--      <div class="bg-blue-100 px-2 py-1 rounded text-primary font-medium text-sm">-->
+<!--        <i class="fa fa-arrow-right mr-1"></i> 新风-->
+<!--      </div>-->
+      <img src="/static/images/PAU.png" alt="新风机组流程图" class="max-w-[80%] h-auto rounded shadow-sm">
+<!--      <div class="bg-green-100 px-2 py-1 rounded text-green-600 font-medium text-sm">-->
+<!--        送风 <i class="fa fa-arrow-right ml-1"></i>-->
+<!--      </div>-->
+    </div>
+  </div>
+
+  <!-- 主要内容区域 - 紧凑布局 -->
+  <div id="main-content" class="space-y-4">
+    <!-- 数据卡片区域 - 更密的网格 -->
+    <div id="data-cards" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
+      <!-- 数据卡片将在这里动态生成 -->
+    </div>
+
+    <!-- 状态和控制参数合并区域 - 节省空间 -->
+    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+      <!-- 状态参数区域 -->
+      <div id="status-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-tachometer mr-1"></i>状态参数
+        </h2>
+        <div id="status-parameters" class="grid grid-cols-1 sm:grid-cols-2 gap-2">
+          <!-- 状态参数将在这里动态生成 -->
+        </div>
+      </div>
+
+      <!-- 控制参数区域 -->
+      <div id="control-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-sliders mr-1"></i>控制参数
+        </h2>
+        <div id="control-parameters" class="space-y-2">
+          <!-- 控制参数将在这里动态生成 -->
+        </div>
+      </div>
+    </div>
+
+    <!-- 数据更新日志 - 缩小高度 -->
+    <div id="log-section" class="bg-white/90 rounded-lg p-3 card-shadow">
+      <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+        <i class="fa fa-history mr-1"></i>数据更新日志
+      </h2>
+      <div id="update-log" class="text-xs text-gray-600 max-h-20 overflow-y-auto space-y-0.5">
+        <p class="text-gray-500">等待数据更新...</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  // 数据类型配置 - 定义不同字段的显示和交互方式
+  const fieldConfig = {
+    // 温度类字段
+    "新风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "blue" },
+    "送风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "green" },
+    "回风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "purple" },
+
+    // 湿度类字段
+    "送风湿度": { type: "humidity", unit: "%", icon: "fa-tint", color: "blue" },
+
+    // 二氧化碳
+    "回风二氧化碳": { type: "co2", unit: "ppm", icon: "fa-leaf", color: "green" },
+
+    // 阀门反馈
+    "回风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "gray" },
+    "新风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "blue" },
+    "水阀反馈": { type: "valve", unit: "%", icon: "fa-tint", color: "cyan" },
+
+    // 模式类(开关)
+    "冬夏季模式": { type: "mode", trueText: "夏季模式", falseText: "冬季模式", section: "control" },
+
+    // 状态类
+    "运行状态": { type: "status", trueText: "运行中", falseText: "停止", trueClass: "status-normal", falseClass: "status-warning" },
+    "故障报警": { type: "status", trueText: "故障", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "自动状态": { type: "status", trueText: "自动", falseText: "手动", trueClass: "status-normal", falseClass: "status-warning" },
+    "过滤网压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "防冻开关": { type: "status", trueText: "触发", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "风机压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true }
+  };
+
+  // 连接SSE并处理数据
+  function connectSSE() {
+    const statusElement = document.getElementById('connection-status');
+    const logElement = document.getElementById('update-log');
+    const dataCardsContainer = document.getElementById('data-cards');
+    const statusParametersContainer = document.getElementById('status-parameters');
+    const controlParametersContainer = document.getElementById('control-parameters');
+    const statusSection = document.getElementById('status-section');
+    const controlSection = document.getElementById('control-section');
+
+    // 跟踪已创建的元素,避免重复创建
+    const createdElements = new Set();
+
+    // 更新连接状态
+    function updateConnectionStatus(status, isConnected) {
+      statusElement.textContent = status;
+      if (isConnected) {
+        statusElement.classList.remove('text-amber-400', 'text-rose-400');
+        statusElement.classList.add('text-emerald-400');
+      } else if (status.includes('连接中')) {
+        statusElement.classList.remove('text-emerald-400', 'text-rose-400');
+        statusElement.classList.add('text-amber-400');
+      } else {
+        statusElement.classList.remove('text-emerald-400', 'text-amber-400');
+        statusElement.classList.add('text-rose-400');
+      }
+    }
+
+    // 添加日志记录
+    function addLogEntry(message) {
+      const now = new Date();
+      const timeString = now.toLocaleTimeString();
+      const logEntry = document.createElement('p');
+      logEntry.innerHTML = `<span class="text-primary">[${timeString}]</span> ${message}`;
+
+      // 移除"等待数据更新..."提示
+      if (logElement.querySelector('.text-gray-500')) {
+        logElement.innerHTML = '';
+      }
+
+      logElement.appendChild(logEntry);
+      logElement.scrollTop = logElement.scrollHeight;
+    }
+
+    // 创建或更新数据卡片 - 更紧凑的卡片
+    function createOrUpdateDataCard(field, value) {
+      const config = fieldConfig[field] || { type: "generic", unit: "", icon: "fa-dashboard", color: "gray" };
+      const elementId = `card-${field.replace(/\s+/g, '-')}`;
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          valueElement.textContent = `${value}${config.unit}`;
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的数据卡片
+      const card = document.createElement('div');
+      card.id = elementId;
+      card.className = "bg-white/90 rounded-lg p-2 card-shadow transform transition-all hover:scale-[1.02]";
+
+      // 确定图标颜色类
+      const iconColorClass = config.color === 'blue' ? 'text-blue-600' :
+              config.color === 'green' ? 'text-green-600' :
+                      config.color === 'purple' ? 'text-purple-600' :
+                              config.color === 'cyan' ? 'text-cyan-600' :
+                                      'text-gray-600';
+
+      const bgColorClass = config.color === 'blue' ? 'bg-blue-100' :
+              config.color === 'green' ? 'bg-green-100' :
+                      config.color === 'purple' ? 'bg-purple-100' :
+                              config.color === 'cyan' ? 'bg-cyan-100' :
+                                      'bg-gray-100';
+
+      card.innerHTML = `
+          <div class="flex justify-between items-start">
+            <div>
+              <p class="text-gray-500 text-xs">${field}</p>
+              <h3 id="${elementId}-value" class="text-lg font-bold mt-0.5">${value}${config.unit}</h3>
+            </div>
+            <div class="${bgColorClass} p-1.5 rounded-full">
+              <i class="fa ${config.icon} ${iconColorClass} text-sm"></i>
+            </div>
+          </div>
+        `;
+
+      dataCardsContainer.appendChild(card);
+      createdElements.add(elementId);
+    }
+
+    // 创建或更新状态参数 - 更紧凑的参数项
+    function createOrUpdateStatusParameter(field, value) {
+      const config = fieldConfig[field] || { type: "generic", trueText: "是", falseText: "否" };
+      const elementId = `status-${field.replace(/\s+/g, '-')}`;
+
+      // 确定应该显示在哪个区域
+      const targetContainer = config.section === "control" ? controlParametersContainer : statusParametersContainer;
+      const targetSection = config.section === "control" ? controlSection : statusSection;
+
+      // 显示对应的区域
+      targetSection.classList.remove('hidden');
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          if (config.type === "status") {
+            const isTrue = value === "是";
+            const displayValue = config.invert ? !isTrue : isTrue;
+            const text = displayValue ? config.trueText : config.falseText;
+
+            valueElement.textContent = text;
+            valueElement.className = `px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}`;
+          } else if (config.type === "mode") {
+            const isChecked = value === "是";
+            valueElement.checked = isChecked;
+          } else {
+            valueElement.textContent = value;
+          }
+
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的参数元素
+      const parameter = document.createElement('div');
+      parameter.id = elementId;
+
+      if (config.type === "status") {
+        const isTrue = value === "是";
+        const displayValue = config.invert ? !isTrue : isTrue;
+        const text = displayValue ? config.trueText : config.falseText;
+
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}">
+              ${text}
+            </span>
+          `;
+      } else if (config.type === "mode") {
+        const isChecked = value === "是";
+
+        parameter.className = "flex justify-between items-center border-b border-gray-100 pb-2";
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <label class="inline-flex items-center cursor-pointer">
+              <input type="checkbox" id="${elementId}-value" ${isChecked ? 'checked' : ''} class="sr-only peer">
+              <div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
+              <span class="ml-2 text-xs font-medium">${isChecked ? config.trueText : config.falseText}</span>
+            </label>
+          `;
+
+        // 添加模式切换事件
+        setTimeout(() => {
+          const checkbox = document.getElementById(`${elementId}-value`);
+          if (checkbox) {
+            checkbox.addEventListener('change', function() {
+              const newValue = this.checked ? "是" : "否";
+              const displayText = this.checked ? config.trueText : config.falseText;
+              this.nextElementSibling.nextElementSibling.textContent = displayText;
+
+              addLogEntry(`${field}已切换为${displayText}`);
+              // 这里可以添加发送到服务器的逻辑
+            });
+          }
+        }, 0);
+      } else {
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="text-base font-semibold">${value}</span>
+          `;
+      }
+
+      targetContainer.appendChild(parameter);
+      createdElements.add(elementId);
+    }
+
+    // 判断字段应该显示的位置
+    function isStatusOrControlField(field) {
+      const config = fieldConfig[field];
+      return config && (config.type === "status" || config.type === "mode");
+    }
+
+    // 处理接收到的SSE数据
+    function handleSSEData(data) {
+      addLogEntry(`接收${Object.keys(data).length}个字段数据`);
+
+      // 处理每个返回的字段
+      Object.keys(data).forEach(field => {
+        const value = data[field];
+
+        // 根据字段类型决定显示方式和位置
+        if (isStatusOrControlField(field)) {
+          createOrUpdateStatusParameter(field, value);
+        } else {
+          createOrUpdateDataCard(field, value);
+        }
+      });
+    }
+
+    // 初始化连接状态
+    updateConnectionStatus('连接中...', false);
+    let urls ="./pointSSE?deviceName="+{{.deviceName}}
+    // 实际SSE连接代码
+      console.log("url地址===========>",urls)
+    const eventSource = new EventSource(urls);
+
+    eventSource.onopen = () => {
+      updateConnectionStatus('已连接', true);
+      addLogEntry('SSE连接已建立');
+    };
+
+    eventSource.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data);
+        if (data.code === 200 && data.data) {
+          handleSSEData(data.data);
+        }
+      } catch (error) {
+        addLogEntry(`数据解析错误: ${error.message}`);
+        console.error('SSE数据解析错误:', error);
+      }
+    };
+
+    eventSource.onerror = (error) => {
+      updateConnectionStatus('连接错误', false);
+      addLogEntry('SSE连接发生错误,正在重试...');
+      console.error('SSE错误:', error);
+    };
+
+  }
+
+  // 页面加载完成后连接SSE
+  document.addEventListener('DOMContentLoaded', connectSSE);
+</script>
+</body>
+
+</html>

+ 406 - 0
templates/PF.html

@@ -0,0 +1,406 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>排风</title>
+  <script src="/static/css/tailwindcss.css"></script>
+
+  <script>
+    tailwind.config = {
+      theme: {
+        extend: {
+          colors: {
+            primary: '#0f766e',
+            secondary: '#0d9488',
+            accent: '#14b8a6',
+            neutral: '#134e4a',
+          },
+          fontFamily: {
+            sans: ['Inter', 'system-ui', 'sans-serif'],
+          },
+        },
+      }
+    }
+  </script>
+
+  <style type="text/tailwindcss">
+    @layer utilities {
+      .card-shadow {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+      }
+      .status-normal {
+        @apply text-emerald-500;
+      }
+      .status-warning {
+        @apply text-amber-500;
+      }
+      .status-alarm {
+        @apply text-rose-500;
+      }
+      .data-update {
+        animation: pulse 1s ease-in-out;
+      }
+      @keyframes pulse {
+        0%, 100% { opacity: 1; }
+        50% { opacity: 0.6; }
+      }
+    }
+  </style>
+</head>
+
+<body class="bg-gradient-to-br from-primary to-neutral text-gray-800 min-h-screen p-3">
+<div class="container mx-auto max-w-7xl">
+  <!-- 头部信息 - 更紧凑 -->
+  <header class="mb-4">
+    <div class="flex justify-between items-center">
+      <h1 class="text-[clamp(1.2rem,2vw,1.6rem)] font-bold text-white">{{.deviceName}}</h1>
+      <div class="flex items-center bg-white/10 px-2 py-1 rounded-lg">
+        <i class="fa fa-refresh mr-1 text-white text-sm"></i>
+        <span id="connection-status" class="text-white text-xs">连接中...</span>
+      </div>
+    </div>
+    <div class="h-1 w-full bg-gradient-to-r from-accent to-transparent rounded-full mt-1"></div>
+  </header>
+
+  <!-- 流程图区域 - 调整大小 -->
+  <div class="bg-white/90 rounded-lg p-4 mb-4 card-shadow">
+    <h2 class="text-base font-semibold mb-2 flex items-center">
+      <i class="fa fa-sitemap text-primary mr-1"></i>系统流程图
+    </h2>
+    <div class="flex items-center justify-between">
+<!--      <div class="bg-blue-100 px-2 py-1 rounded text-primary font-medium text-sm">-->
+<!--        <i class="fa fa-arrow-right mr-1"></i> 新风-->
+<!--      </div>-->
+      <img src="/static/images/PF.png" alt="新风机组流程图" class="max-w-[80%] h-auto rounded shadow-sm">
+<!--      <div class="bg-green-100 px-2 py-1 rounded text-green-600 font-medium text-sm">-->
+<!--        送风 <i class="fa fa-arrow-right ml-1"></i>-->
+<!--      </div>-->
+    </div>
+  </div>
+
+  <!-- 主要内容区域 - 紧凑布局 -->
+  <div id="main-content" class="space-y-4">
+    <!-- 数据卡片区域 - 更密的网格 -->
+    <div id="data-cards" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
+      <!-- 数据卡片将在这里动态生成 -->
+    </div>
+
+    <!-- 状态和控制参数合并区域 - 节省空间 -->
+    <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
+      <!-- 状态参数区域 -->
+      <div id="status-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-tachometer mr-1"></i>状态参数
+        </h2>
+        <div id="status-parameters" class="grid grid-cols-1 sm:grid-cols-2 gap-2">
+          <!-- 状态参数将在这里动态生成 -->
+        </div>
+      </div>
+
+      <!-- 控制参数区域 -->
+      <div id="control-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
+        <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+          <i class="fa fa-sliders mr-1"></i>控制参数
+        </h2>
+        <div id="control-parameters" class="space-y-2">
+          <!-- 控制参数将在这里动态生成 -->
+        </div>
+      </div>
+    </div>
+
+    <!-- 数据更新日志 - 缩小高度 -->
+    <div id="log-section" class="bg-white/90 rounded-lg p-3 card-shadow">
+      <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
+        <i class="fa fa-history mr-1"></i>数据更新日志
+      </h2>
+      <div id="update-log" class="text-xs text-gray-600 max-h-20 overflow-y-auto space-y-0.5">
+        <p class="text-gray-500">等待数据更新...</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+  // 数据类型配置 - 定义不同字段的显示和交互方式
+  const fieldConfig = {
+    // 温度类字段
+    "新风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "blue" },
+    "送风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "green" },
+    "回风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "purple" },
+
+    // 湿度类字段
+    "送风湿度": { type: "humidity", unit: "%", icon: "fa-tint", color: "blue" },
+
+    // 二氧化碳
+    "回风二氧化碳": { type: "co2", unit: "ppm", icon: "fa-leaf", color: "green" },
+
+    // 阀门反馈
+    "回风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "gray" },
+    "新风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "blue" },
+    "水阀反馈": { type: "valve", unit: "%", icon: "fa-tint", color: "cyan" },
+
+    // 模式类(开关)
+    "冬夏季模式": { type: "mode", trueText: "夏季模式", falseText: "冬季模式", section: "control" },
+
+    // 状态类
+    "运行状态": { type: "status", trueText: "运行中", falseText: "停止", trueClass: "status-normal", falseClass: "status-warning" },
+    "故障报警": { type: "status", trueText: "故障", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "自动状态": { type: "status", trueText: "自动", falseText: "手动", trueClass: "status-normal", falseClass: "status-warning" },
+    "过滤网压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "防冻开关": { type: "status", trueText: "触发", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
+    "风机压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true }
+  };
+
+  // 连接SSE并处理数据
+  function connectSSE() {
+    const statusElement = document.getElementById('connection-status');
+    const logElement = document.getElementById('update-log');
+    const dataCardsContainer = document.getElementById('data-cards');
+    const statusParametersContainer = document.getElementById('status-parameters');
+    const controlParametersContainer = document.getElementById('control-parameters');
+    const statusSection = document.getElementById('status-section');
+    const controlSection = document.getElementById('control-section');
+
+    // 跟踪已创建的元素,避免重复创建
+    const createdElements = new Set();
+
+    // 更新连接状态
+    function updateConnectionStatus(status, isConnected) {
+      statusElement.textContent = status;
+      if (isConnected) {
+        statusElement.classList.remove('text-amber-400', 'text-rose-400');
+        statusElement.classList.add('text-emerald-400');
+      } else if (status.includes('连接中')) {
+        statusElement.classList.remove('text-emerald-400', 'text-rose-400');
+        statusElement.classList.add('text-amber-400');
+      } else {
+        statusElement.classList.remove('text-emerald-400', 'text-amber-400');
+        statusElement.classList.add('text-rose-400');
+      }
+    }
+
+    // 添加日志记录
+    function addLogEntry(message) {
+      const now = new Date();
+      const timeString = now.toLocaleTimeString();
+      const logEntry = document.createElement('p');
+      logEntry.innerHTML = `<span class="text-primary">[${timeString}]</span> ${message}`;
+
+      // 移除"等待数据更新..."提示
+      if (logElement.querySelector('.text-gray-500')) {
+        logElement.innerHTML = '';
+      }
+
+      logElement.appendChild(logEntry);
+      logElement.scrollTop = logElement.scrollHeight;
+    }
+
+    // 创建或更新数据卡片 - 更紧凑的卡片
+    function createOrUpdateDataCard(field, value) {
+      const config = fieldConfig[field] || { type: "generic", unit: "", icon: "fa-dashboard", color: "gray" };
+      const elementId = `card-${field.replace(/\s+/g, '-')}`;
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          valueElement.textContent = `${value}${config.unit}`;
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的数据卡片
+      const card = document.createElement('div');
+      card.id = elementId;
+      card.className = "bg-white/90 rounded-lg p-2 card-shadow transform transition-all hover:scale-[1.02]";
+
+      // 确定图标颜色类
+      const iconColorClass = config.color === 'blue' ? 'text-blue-600' :
+              config.color === 'green' ? 'text-green-600' :
+                      config.color === 'purple' ? 'text-purple-600' :
+                              config.color === 'cyan' ? 'text-cyan-600' :
+                                      'text-gray-600';
+
+      const bgColorClass = config.color === 'blue' ? 'bg-blue-100' :
+              config.color === 'green' ? 'bg-green-100' :
+                      config.color === 'purple' ? 'bg-purple-100' :
+                              config.color === 'cyan' ? 'bg-cyan-100' :
+                                      'bg-gray-100';
+
+      card.innerHTML = `
+          <div class="flex justify-between items-start">
+            <div>
+              <p class="text-gray-500 text-xs">${field}</p>
+              <h3 id="${elementId}-value" class="text-lg font-bold mt-0.5">${value}${config.unit}</h3>
+            </div>
+            <div class="${bgColorClass} p-1.5 rounded-full">
+              <i class="fa ${config.icon} ${iconColorClass} text-sm"></i>
+            </div>
+          </div>
+        `;
+
+      dataCardsContainer.appendChild(card);
+      createdElements.add(elementId);
+    }
+
+    // 创建或更新状态参数 - 更紧凑的参数项
+    function createOrUpdateStatusParameter(field, value) {
+      const config = fieldConfig[field] || { type: "generic", trueText: "是", falseText: "否" };
+      const elementId = `status-${field.replace(/\s+/g, '-')}`;
+
+      // 确定应该显示在哪个区域
+      const targetContainer = config.section === "control" ? controlParametersContainer : statusParametersContainer;
+      const targetSection = config.section === "control" ? controlSection : statusSection;
+
+      // 显示对应的区域
+      targetSection.classList.remove('hidden');
+
+      // 如果元素已存在,只更新值
+      if (createdElements.has(elementId)) {
+        const valueElement = document.getElementById(`${elementId}-value`);
+        if (valueElement) {
+          if (config.type === "status") {
+            const isTrue = value === "是";
+            const displayValue = config.invert ? !isTrue : isTrue;
+            const text = displayValue ? config.trueText : config.falseText;
+
+            valueElement.textContent = text;
+            valueElement.className = `px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}`;
+          } else if (config.type === "mode") {
+            const isChecked = value === "是";
+            valueElement.checked = isChecked;
+          } else {
+            valueElement.textContent = value;
+          }
+
+          valueElement.classList.add('data-update');
+          setTimeout(() => valueElement.classList.remove('data-update'), 1000);
+        }
+        return;
+      }
+
+      // 创建新的参数元素
+      const parameter = document.createElement('div');
+      parameter.id = elementId;
+
+      if (config.type === "status") {
+        const isTrue = value === "是";
+        const displayValue = config.invert ? !isTrue : isTrue;
+        const text = displayValue ? config.trueText : config.falseText;
+
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}">
+              ${text}
+            </span>
+          `;
+      } else if (config.type === "mode") {
+        const isChecked = value === "是";
+
+        parameter.className = "flex justify-between items-center border-b border-gray-100 pb-2";
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <label class="inline-flex items-center cursor-pointer">
+              <input type="checkbox" id="${elementId}-value" ${isChecked ? 'checked' : ''} class="sr-only peer">
+              <div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
+              <span class="ml-2 text-xs font-medium">${isChecked ? config.trueText : config.falseText}</span>
+            </label>
+          `;
+
+        // 添加模式切换事件
+        setTimeout(() => {
+          const checkbox = document.getElementById(`${elementId}-value`);
+          if (checkbox) {
+            checkbox.addEventListener('change', function() {
+              const newValue = this.checked ? "是" : "否";
+              const displayText = this.checked ? config.trueText : config.falseText;
+              this.nextElementSibling.nextElementSibling.textContent = displayText;
+
+              addLogEntry(`${field}已切换为${displayText}`);
+              // 这里可以添加发送到服务器的逻辑
+            });
+          }
+        }, 0);
+      } else {
+        parameter.className = config.section === "control"
+                ? "flex justify-between items-center border-b border-gray-100 pb-2"
+                : "bg-gray-50 p-2.5 rounded text-sm";
+
+        parameter.innerHTML = `
+            <label class="font-medium text-sm">${field}</label>
+            <span id="${elementId}-value" class="text-base font-semibold">${value}</span>
+          `;
+      }
+
+      targetContainer.appendChild(parameter);
+      createdElements.add(elementId);
+    }
+
+    // 判断字段应该显示的位置
+    function isStatusOrControlField(field) {
+      const config = fieldConfig[field];
+      return config && (config.type === "status" || config.type === "mode");
+    }
+
+    // 处理接收到的SSE数据
+    function handleSSEData(data) {
+      addLogEntry(`接收${Object.keys(data).length}个字段数据`);
+
+      // 处理每个返回的字段
+      Object.keys(data).forEach(field => {
+        const value = data[field];
+
+        // 根据字段类型决定显示方式和位置
+        if (isStatusOrControlField(field)) {
+          createOrUpdateStatusParameter(field, value);
+        } else {
+          createOrUpdateDataCard(field, value);
+        }
+      });
+    }
+
+    // 初始化连接状态
+    updateConnectionStatus('连接中...', false);
+    let urls ="./pointSSE?deviceName="+{{.deviceName}}
+    // 实际SSE连接代码
+    const eventSource = new EventSource(urls);
+
+    eventSource.onopen = () => {
+      updateConnectionStatus('已连接', true);
+      addLogEntry('SSE连接已建立');
+    };
+
+    eventSource.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data);
+        if (data.code === 200 && data.data) {
+          handleSSEData(data.data);
+        }
+      } catch (error) {
+        addLogEntry(`数据解析错误: ${error.message}`);
+        console.error('SSE数据解析错误:', error);
+      }
+    };
+
+    eventSource.onerror = (error) => {
+      updateConnectionStatus('连接错误', false);
+      addLogEntry('SSE连接发生错误,正在重试...');
+      console.error('SSE错误:', error);
+    };
+
+  }
+
+  // 页面加载完成后连接SSE
+  document.addEventListener('DOMContentLoaded', connectSSE);
+</script>
+</body>
+
+</html>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
templates/static/css/tailwindcss.css


BIN
templates/static/images/JSK.png


BIN
templates/static/images/PAU.png


BIN
templates/static/images/PF.png


Vissa filer visades inte eftersom för många filer har ändrats