Browse Source

feat: dashboard device

lihao16 7 tháng trước cách đây
mục cha
commit
63eefd40f1
24 tập tin đã thay đổi với 981 bổ sung52 xóa
  1. 2 1
      smsb-admin/src/main/resources/application-dev.yml
  2. 4 3
      smsb-extend/smsb-snailjob-server/src/main/resources/application-dev.yml
  3. 36 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbDeviceErrorRecordController.java
  4. 5 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbDeviceErrorRecord.java
  5. 52 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbDeviceOnlineSummary.java
  6. 5 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbDeviceErrorRecordBo.java
  7. 22 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbDeviceOnlineSummaryBo.java
  8. 37 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmLevelVo.java
  9. 28 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmNumTopVo.java
  10. 5 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceErrorRecordVo.java
  11. 58 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceOnlineSummaryVo.java
  12. 37 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceErrorRecordMapper.java
  13. 15 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceOnlineSummaryMapper.java
  14. 20 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceErrorRecordService.java
  15. 68 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceOnlineSummaryService.java
  16. 54 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceErrorRecordServiceImpl.java
  17. 127 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceOnlineSummaryServiceImpl.java
  18. 80 6
      smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceErrorRecordMapper.xml
  19. 7 0
      smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceOnlineSummaryMapper.xml
  20. 97 0
      smsb-modules/smsb-job/src/main/java/org/dromara/job/snailjob/DeviceOnlineSummaryExecutor.java
  21. 20 7
      smsb-modules/smsb-job/src/main/java/org/dromara/job/snailjob/DeviceStatusReviewExecutor.java
  22. 19 0
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/handler/ConnectServerHandler.java
  23. 24 0
      smsb-plus-ui/src/api/smsb/device/errorRecord.ts
  24. 159 35
      smsb-plus-ui/src/views/smsb/dashboard/device.vue

+ 2 - 1
smsb-admin/src/main/resources/application-dev.yml

@@ -23,7 +23,8 @@ snail-job:
   # token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
   token: "SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj"
   server:
-    host: 117.73.13.40
+    # host: 117.73.13.40
+    host: 127.0.0.1
     port: 17888
   # 详见 script/sql/snail_job.sql `sj_namespace` 表
   #namespace: ${spring.profiles.active}

+ 4 - 3
smsb-extend/smsb-snailjob-server/src/main/resources/application-dev.yml

@@ -2,10 +2,11 @@ spring:
   datasource:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://117.73.3.135:3306/smsb-plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+    # url: jdbc:mysql://117.73.3.135:3306/smsb-plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+    url: jdbc:mysql://127.0.0.1:3306/smsb-plus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
-    # password: password
-    password: Hycpb@123
+    password: password
+    # password: Hycpb@123
     hikari:
       connection-timeout: 30000
       validation-timeout: 5000

+ 36 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbDeviceErrorRecordController.java

@@ -3,6 +3,8 @@ package com.inspur.device.controller;
 import java.util.List;
 
 import com.inspur.device.domain.vo.DeviceAlarmCountVo;
+import com.inspur.device.domain.vo.DeviceAlarmLevelVo;
+import com.inspur.device.domain.vo.DeviceAlarmNumTopVo;
 import lombok.RequiredArgsConstructor;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.*;
@@ -108,4 +110,38 @@ public class SmsbDeviceErrorRecordController extends BaseController {
     public R<DeviceAlarmCountVo> countByType(@RequestParam String startTime, @RequestParam String endTime) {
         return R.ok(smsbDeviceErrorRecordService.countByType(startTime, endTime));
     }
+
+    /**
+     * 设备统计-告警级别统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/count/byLevel")
+    public R<DeviceAlarmLevelVo> countByLevel(@RequestParam String startTime, @RequestParam String endTime) {
+        return R.ok(smsbDeviceErrorRecordService.countByLevel(startTime, endTime));
+    }
+
+
+    /**
+     * 设备统计-告警数量TOP5
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/num/top")
+    public R<DeviceAlarmNumTopVo> alarmNumTop(@RequestParam String startTime, @RequestParam String endTime) {
+        return R.ok(smsbDeviceErrorRecordService.alarmNumTop(startTime, endTime));
+    }
+
+    /**
+     * 设备统计-告警数量TOP5
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/onlineTime/top")
+    public R<DeviceAlarmNumTopVo> onlineTimeTop(@RequestParam String startTime, @RequestParam String endTime) {
+        return R.ok(smsbDeviceErrorRecordService.onlineTimeTop(startTime, endTime));
+    }
 }

+ 5 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbDeviceErrorRecord.java

@@ -61,5 +61,10 @@ public class SmsbDeviceErrorRecord {
     @TableField(fill = FieldFill.INSERT)
     private Date createTime;
 
+    /**
+     * 在线时长
+     */
+    private Integer onlineDuration;
+
 
 }

+ 52 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/SmsbDeviceOnlineSummary.java

@@ -0,0 +1,52 @@
+package com.inspur.device.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 设备在线时长统计对象 smsb_device_online_summary
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+@Data
+@EqualsAndHashCode
+@TableName("smsb_device_online_summary")
+public class SmsbDeviceOnlineSummary {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 设备ID
+     */
+    private Long deviceId;
+
+    /**
+     * 当日在线时长
+     */
+    private Long duration;
+
+    /**
+     * 统计日期
+     */
+    private Date countTime;
+
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+
+
+}

+ 5 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbDeviceErrorRecordBo.java

@@ -51,5 +51,10 @@ public class SmsbDeviceErrorRecordBo extends BaseEntity {
     @NotNull(message = "告警类型不能为空", groups = {AddGroup.class, EditGroup.class})
     private Long errorType;
 
+    /**
+     * 在线时长
+     */
+    private Integer onlineDuration;
+
 
 }

+ 22 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbDeviceOnlineSummaryBo.java

@@ -0,0 +1,22 @@
+package com.inspur.device.domain.bo;
+
+import com.inspur.device.domain.SmsbDeviceOnlineSummary;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+/**
+ * 设备在线时长统计业务对象 smsb_device_online_summary
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = SmsbDeviceOnlineSummary.class, reverseConvertGenerate = false)
+public class SmsbDeviceOnlineSummaryBo extends BaseEntity {
+
+    private Long id;
+
+}

+ 37 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmLevelVo.java

@@ -0,0 +1,37 @@
+package com.inspur.device.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+
+/**
+ * 设备统计 - 告警级别统计
+ *
+ * @author lihao16
+ */
+@Data
+public class DeviceAlarmLevelVo {
+    /**
+     * SQL 查询
+     */
+    private String alarmDate;
+
+    private Integer normalAlarm;
+
+    private Integer dangerAlarm;
+
+    private Integer todayAlarm;
+
+    /**
+     * 返回前端
+     */
+    private List<String> alarmDateList;
+
+    private List<Integer> normalAlamList;
+
+    private List<Integer> dangerAlamList;
+
+    private Integer totalNum;
+
+}

+ 28 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmNumTopVo.java

@@ -0,0 +1,28 @@
+package com.inspur.device.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 设备告警数量TOP
+ * @author lihao16
+ */
+@Data
+public class DeviceAlarmNumTopVo {
+
+    /** SQL 查询 */
+    private String deviceName;
+
+    private Integer alarmNum;
+
+    private Integer onlineTime;
+
+    /** 接口返回 */
+    private List<String> deviceNameList;
+
+    private List<Integer> alarmNumList;
+
+    private List<Integer> onlineTimeList;
+
+}

+ 5 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceErrorRecordVo.java

@@ -64,5 +64,10 @@ public class SmsbDeviceErrorRecordVo implements Serializable {
      */
     private Date createTime;
 
+    /**
+     * 在线时长
+     */
+    private Integer onlineDuration;
+
 
 }

+ 58 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceOnlineSummaryVo.java

@@ -0,0 +1,58 @@
+package com.inspur.device.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.inspur.device.domain.SmsbDeviceOnlineSummary;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 设备在线时长统计视图对象 smsb_device_online_summary
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = SmsbDeviceOnlineSummary.class)
+public class SmsbDeviceOnlineSummaryVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @ExcelProperty(value = "主键ID")
+    private Long id;
+
+    /**
+     * 设备ID
+     */
+    @ExcelProperty(value = "设备ID")
+    private Long deviceId;
+
+    /**
+     * 当日在线时长
+     */
+    @ExcelProperty(value = "当日在线时长")
+    private Long duration;
+
+    /**
+     * 统计日期
+     */
+    @ExcelProperty(value = "统计日期")
+    private Date countTime;
+
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+
+
+}

+ 37 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceErrorRecordMapper.java

@@ -2,10 +2,13 @@ package com.inspur.device.mapper;
 
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
 import com.inspur.device.domain.vo.DeviceAlarmCountVo;
+import com.inspur.device.domain.vo.DeviceAlarmLevelVo;
+import com.inspur.device.domain.vo.DeviceAlarmNumTopVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -23,4 +26,38 @@ public interface SmsbDeviceErrorRecordMapper extends BaseMapperPlus<SmsbDeviceEr
      * @return
      */
     DeviceAlarmCountVo selectCountByType(@Param("startTime") String startTime, @Param("endTime") String endTime);
+
+    /**
+     * 设备统计-告警级别统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    List<DeviceAlarmLevelVo> selectCountByLevel(@Param("startTime") String startTime, @Param("endTime") String endTime);
+
+    /**
+     * 设备统计-告警数量Top5
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    List<DeviceAlarmNumTopVo> selectAlarmNumTop(@Param("startTime") String startTime, @Param("endTime") String endTime);
+
+    /**
+     * 统计当天在线时长
+     * @param deviceId
+     * @param todayBegin
+     * @param todayEnd
+     * @return
+     */
+    Integer countOnlineDuration(@Param("deviceId") Long deviceId, @Param("todayBegin") Date todayBegin,
+                                @Param("todayEnd") Date todayEnd);
+
+    /**
+     * 统计在线时长Top5
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    List<DeviceAlarmNumTopVo> selectOnlineTimeTop(@Param("startTime") String startTime, @Param("endTime") String endTime);
 }

+ 15 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceOnlineSummaryMapper.java

@@ -0,0 +1,15 @@
+package com.inspur.device.mapper;
+
+import com.inspur.device.domain.SmsbDeviceOnlineSummary;
+import com.inspur.device.domain.vo.SmsbDeviceOnlineSummaryVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * 设备在线时长统计Mapper接口
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+public interface SmsbDeviceOnlineSummaryMapper extends BaseMapperPlus<SmsbDeviceOnlineSummary, SmsbDeviceOnlineSummaryVo> {
+
+}

+ 20 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceErrorRecordService.java

@@ -2,6 +2,8 @@ package com.inspur.device.service;
 
 import com.inspur.device.domain.bo.SmsbDeviceErrorRecordBo;
 import com.inspur.device.domain.vo.DeviceAlarmCountVo;
+import com.inspur.device.domain.vo.DeviceAlarmLevelVo;
+import com.inspur.device.domain.vo.DeviceAlarmNumTopVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -74,4 +76,22 @@ public interface ISmsbDeviceErrorRecordService {
      * @return
      */
     DeviceAlarmCountVo countByType(String startTime, String endTime);
+
+    /**
+     * 设备统计-告警级别统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    DeviceAlarmLevelVo countByLevel(String startTime, String endTime);
+
+    /**
+     * 设备统计-告警数量Top5
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    DeviceAlarmNumTopVo alarmNumTop(String startTime, String endTime);
+
+    DeviceAlarmNumTopVo onlineTimeTop(String startTime, String endTime);
 }

+ 68 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceOnlineSummaryService.java

@@ -0,0 +1,68 @@
+package com.inspur.device.service;
+
+import com.inspur.device.domain.bo.SmsbDeviceOnlineSummaryBo;
+import com.inspur.device.domain.vo.SmsbDeviceOnlineSummaryVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 设备在线时长统计Service接口
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+public interface ISmsbDeviceOnlineSummaryService {
+
+    /**
+     * 查询设备在线时长统计
+     *
+     * @param id 主键
+     * @return 设备在线时长统计
+     */
+    SmsbDeviceOnlineSummaryVo queryById(Long id);
+
+    /**
+     * 分页查询设备在线时长统计列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 设备在线时长统计分页列表
+     */
+    TableDataInfo<SmsbDeviceOnlineSummaryVo> queryPageList(SmsbDeviceOnlineSummaryBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询符合条件的设备在线时长统计列表
+     *
+     * @param bo 查询条件
+     * @return 设备在线时长统计列表
+     */
+    List<SmsbDeviceOnlineSummaryVo> queryList(SmsbDeviceOnlineSummaryBo bo);
+
+    /**
+     * 新增设备在线时长统计
+     *
+     * @param bo 设备在线时长统计
+     * @return 是否新增成功
+     */
+    Boolean insertByBo(SmsbDeviceOnlineSummaryBo bo);
+
+    /**
+     * 修改设备在线时长统计
+     *
+     * @param bo 设备在线时长统计
+     * @return 是否修改成功
+     */
+    Boolean updateByBo(SmsbDeviceOnlineSummaryBo bo);
+
+    /**
+     * 校验并批量删除设备在线时长统计信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+}

+ 54 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceErrorRecordServiceImpl.java

@@ -1,11 +1,14 @@
 package com.inspur.device.service.impl;
 
+import cn.hutool.core.collection.CollectionUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
 import com.inspur.device.domain.bo.SmsbDeviceErrorRecordBo;
 import com.inspur.device.domain.vo.DeviceAlarmCountVo;
+import com.inspur.device.domain.vo.DeviceAlarmLevelVo;
+import com.inspur.device.domain.vo.DeviceAlarmNumTopVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import com.inspur.device.mapper.SmsbDeviceErrorRecordMapper;
 import com.inspur.device.service.ISmsbDeviceErrorRecordService;
@@ -84,6 +87,7 @@ public class SmsbDeviceErrorRecordServiceImpl implements ISmsbDeviceErrorRecordS
         lqw.like(StringUtils.isNotBlank(bo.getDeviceName()), SmsbDeviceErrorRecord::getDeviceName, bo.getDeviceName());
         lqw.eq(bo.getErrorLevel() != null, SmsbDeviceErrorRecord::getErrorLevel, bo.getErrorLevel());
         lqw.eq(bo.getErrorType() != null, SmsbDeviceErrorRecord::getErrorType, bo.getErrorType());
+        lqw.orderByDesc(SmsbDeviceErrorRecord::getCreateTime);
         return lqw;
     }
 
@@ -153,4 +157,54 @@ public class SmsbDeviceErrorRecordServiceImpl implements ISmsbDeviceErrorRecordS
         dbResult.setAlarmCount(alarmCounts);
         return dbResult;
     }
+
+    @Override
+    public DeviceAlarmLevelVo countByLevel(String startTime, String endTime) {
+        DeviceAlarmLevelVo result = new DeviceAlarmLevelVo();
+        List<DeviceAlarmLevelVo> queryResult = baseMapper.selectCountByLevel(startTime, endTime);
+        if (CollectionUtil.isEmpty(queryResult)) {
+            return result;
+        }
+        List<String> alarmDateList = queryResult.stream().map(DeviceAlarmLevelVo::getAlarmDate).toList();
+        List<Integer> normalAlamList = queryResult.stream().map(DeviceAlarmLevelVo::getNormalAlarm).toList();
+        List<Integer> dangerAlamList = queryResult.stream().map(DeviceAlarmLevelVo::getDangerAlarm).toList();
+        Integer totalNum = queryResult.stream().mapToInt(DeviceAlarmLevelVo::getTodayAlarm).sum();
+        result.setAlarmDateList(alarmDateList);
+        result.setNormalAlamList(normalAlamList);
+        result.setDangerAlamList(dangerAlamList);
+        result.setTotalNum(totalNum);
+        return result;
+    }
+
+    @Override
+    public DeviceAlarmNumTopVo alarmNumTop(String startTime, String endTime) {
+        DeviceAlarmNumTopVo result = new DeviceAlarmNumTopVo();
+        startTime = startTime + " 00:00:00";
+        endTime = endTime + " 23:59:59";
+        List<DeviceAlarmNumTopVo> queryResult = baseMapper.selectAlarmNumTop(startTime, endTime);
+        if (CollectionUtil.isEmpty(queryResult)) {
+            return result;
+        }
+        List<String> deviceNameList = queryResult.stream().map(DeviceAlarmNumTopVo::getDeviceName).toList();
+        List<Integer> alarmNumList = queryResult.stream().map(DeviceAlarmNumTopVo::getAlarmNum).toList();
+        result.setAlarmNumList(alarmNumList);
+        result.setDeviceNameList(deviceNameList);
+        return result;
+    }
+
+    @Override
+    public DeviceAlarmNumTopVo onlineTimeTop(String startTime, String endTime) {
+        DeviceAlarmNumTopVo result = new DeviceAlarmNumTopVo();
+        startTime = startTime + " 00:00:00";
+        endTime = endTime + " 23:59:59";
+        List<DeviceAlarmNumTopVo> queryResult = baseMapper.selectOnlineTimeTop(startTime, endTime);
+        if (CollectionUtil.isEmpty(queryResult)) {
+            return result;
+        }
+        List<String> deviceNameList = queryResult.stream().map(DeviceAlarmNumTopVo::getDeviceName).toList();
+        List<Integer> onlineTimeList = queryResult.stream().map(DeviceAlarmNumTopVo::getOnlineTime).toList();
+        result.setDeviceNameList(deviceNameList);
+        result.setOnlineTimeList(onlineTimeList);
+        return result;
+    }
 }

+ 127 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceOnlineSummaryServiceImpl.java

@@ -0,0 +1,127 @@
+package com.inspur.device.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.inspur.device.domain.SmsbDeviceOnlineSummary;
+import com.inspur.device.domain.bo.SmsbDeviceOnlineSummaryBo;
+import com.inspur.device.domain.vo.SmsbDeviceOnlineSummaryVo;
+import com.inspur.device.mapper.SmsbDeviceOnlineSummaryMapper;
+import com.inspur.device.service.ISmsbDeviceOnlineSummaryService;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备在线时长统计Service业务层处理
+ *
+ * @author Hao Li
+ * @date 2025-03-26
+ */
+@RequiredArgsConstructor
+@Service
+public class SmsbDeviceOnlineSummaryServiceImpl implements ISmsbDeviceOnlineSummaryService {
+
+    private final SmsbDeviceOnlineSummaryMapper baseMapper;
+
+    /**
+     * 查询设备在线时长统计
+     *
+     * @param id 主键
+     * @return 设备在线时长统计
+     */
+    @Override
+    public SmsbDeviceOnlineSummaryVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 分页查询设备在线时长统计列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 设备在线时长统计分页列表
+     */
+    @Override
+    public TableDataInfo<SmsbDeviceOnlineSummaryVo> queryPageList(SmsbDeviceOnlineSummaryBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<SmsbDeviceOnlineSummary> lqw = buildQueryWrapper(bo);
+        Page<SmsbDeviceOnlineSummaryVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询符合条件的设备在线时长统计列表
+     *
+     * @param bo 查询条件
+     * @return 设备在线时长统计列表
+     */
+    @Override
+    public List<SmsbDeviceOnlineSummaryVo> queryList(SmsbDeviceOnlineSummaryBo bo) {
+        LambdaQueryWrapper<SmsbDeviceOnlineSummary> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<SmsbDeviceOnlineSummary> buildQueryWrapper(SmsbDeviceOnlineSummaryBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<SmsbDeviceOnlineSummary> lqw = Wrappers.lambdaQuery();
+        return lqw;
+    }
+
+    /**
+     * 新增设备在线时长统计
+     *
+     * @param bo 设备在线时长统计
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByBo(SmsbDeviceOnlineSummaryBo bo) {
+        SmsbDeviceOnlineSummary add = MapstructUtils.convert(bo, SmsbDeviceOnlineSummary.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改设备在线时长统计
+     *
+     * @param bo 设备在线时长统计
+     * @return 是否修改成功
+     */
+    @Override
+    public Boolean updateByBo(SmsbDeviceOnlineSummaryBo bo) {
+        SmsbDeviceOnlineSummary update = MapstructUtils.convert(bo, SmsbDeviceOnlineSummary.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(SmsbDeviceOnlineSummary entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 校验并批量删除设备在线时长统计信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+}

+ 80 - 6
smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceErrorRecordMapper.xml

@@ -4,13 +4,87 @@
     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.inspur.device.mapper.SmsbDeviceErrorRecordMapper">
     <select id="selectCountByType" resultType="com.inspur.device.domain.vo.DeviceAlarmCountVo">
+        SELECT IFNULL(SUM(CASE WHEN error_type = 1 THEN 1 ELSE 0 END), 0) AS onlineNum,
+               IFNULL(SUM(CASE WHEN error_type = 2 THEN 1 ELSE 0 END), 0) AS offlineNum,
+               IFNULL(SUM(CASE WHEN error_type = 3 THEN 1 ELSE 0 END), 0) AS bdNum,
+               IFNULL(SUM(CASE WHEN error_type = 4 THEN 1 ELSE 0 END), 0) AS thanNum
+        FROM smsb_device_error_record
+        where create_time between #{startTime} and #{endTime}
+    </select>
+
+    <select id="selectAlarmNumTop" resultType="com.inspur.device.domain.vo.DeviceAlarmNumTopVo">
         SELECT
-            IFNULL( SUM( CASE WHEN error_type = 1 THEN 1 ELSE 0 END ), 0 ) AS onlineNum,
-            IFNULL( SUM( CASE WHEN error_type = 2 THEN 1 ELSE 0 END ), 0 ) AS offlineNum,
-            IFNULL( SUM( CASE WHEN error_type = 3 THEN 1 ELSE 0 END ), 0 ) AS bdNum,
-            IFNULL( SUM( CASE WHEN error_type = 4 THEN 1 ELSE 0 END ), 0 ) AS thanNum
+            sd.name as deviceName,top.alarmNum
         FROM
-            smsb_device_error_record
-        where create_time between #{startTime} and #{endTime}
+            (SELECT
+                 device_id, COUNT(1) as alarmNum
+             FROM smsb_device_error_record
+             where
+                 create_time between #{startTime} and #{endTime}
+             GROUP BY device_id
+             ORDER BY alarmNum desc
+                LIMIT 5
+             ) top
+        INNER JOIN
+            smsb_device sd on top.device_id = sd.id
+        ORDER BY
+            top.alarmNum asc
+    </select>
+
+    <select id="selectOnlineTimeTop" resultType="com.inspur.device.domain.vo.DeviceAlarmNumTopVo">
+        SELECT
+            sd.name as deviceName, top.duration as onlineTime
+        FROM
+            (SELECT
+                 SUM(duration) as duration, device_id
+             FROM
+                 smsb_device_online_summary
+             where
+                 count_time between #{startTime} and #{endTime}
+             GROUP BY
+                 device_id
+             ORDER BY duration desc
+                LIMIT 5
+            ) top
+        INNER JOIN
+            smsb_device sd on top.device_id = sd.id
+        ORDER BY duration asc
+    </select>
+
+    <select id="selectCountByLevel" resultType="com.inspur.device.domain.vo.DeviceAlarmLevelVo">
+        SELECT
+            date_range.stat_date AS alarmDate,
+            COALESCE(SUM(t.error_level = 1), 0) AS normalAlarm,
+            COALESCE(SUM(t.error_level = 2), 0) AS dangerAlarm,
+            COALESCE(COUNT(t.id), 0) AS todayAlarm
+        FROM (
+            SELECT
+                DATE_ADD(#{startTime}, INTERVAL t4.num DAY) AS stat_date
+            FROM (
+                SELECT
+                    (t3.num * 1000 + t2.num * 100 + t1.num * 10 + t0.num) AS num
+                FROM
+                    (SELECT 0 num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t0,
+                    (SELECT 0 num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
+                    (SELECT 0 num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
+                    (SELECT 0 num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3
+            ) t4
+            WHERE DATE_ADD(#{startTime}, INTERVAL t4.num DAY) &lt;= #{endTime}
+        ) date_range
+        LEFT JOIN smsb_device_error_record t
+            ON DATE(t.create_time) = date_range.stat_date
+            AND t.error_level IN (1, 2)
+            AND t.create_time BETWEEN #{startTime} AND #{endTime} + INTERVAL 1 DAY - INTERVAL 1 SECOND
+        GROUP BY date_range.stat_date
+        ORDER BY date_range.stat_date
+    </select>
+
+    <select id="countOnlineDuration" resultType="java.lang.Integer">
+        SELECT
+            IFNULL(SUM(t.online_duration), 0) AS onlineDuration
+        FROM
+            smsb_device_error_record t
+        WHERE
+            t.device_id = #{deviceId} AND t.create_time BETWEEN #{todayBegin} AND #{todayEnd}
     </select>
 </mapper>

+ 7 - 0
smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceOnlineSummaryMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.inspur.device.mapper.SmsbDeviceOnlineSummaryMapper">
+
+</mapper>

+ 97 - 0
smsb-modules/smsb-job/src/main/java/org/dromara/job/snailjob/DeviceOnlineSummaryExecutor.java

@@ -0,0 +1,97 @@
+package org.dromara.job.snailjob;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateUtil;
+import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
+import com.aizuda.snailjob.client.model.ExecuteResult;
+import com.aizuda.snailjob.common.log.SnailJobLog;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.inspur.device.domain.SmsbDeviceErrorRecord;
+import com.inspur.device.domain.SmsbDeviceOnlineSummary;
+import com.inspur.device.domain.constants.DeviceConstants;
+import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
+import com.inspur.device.domain.vo.SmsbDeviceVo;
+import com.inspur.device.mapper.SmsbDeviceErrorRecordMapper;
+import com.inspur.device.mapper.SmsbDeviceMapper;
+import com.inspur.device.mapper.SmsbDeviceOnlineSummaryMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author opensnail
+ * @date 2024-05-17
+ */
+@Component
+@JobExecutor(name = "deviceOnlineSummary")
+public class DeviceOnlineSummaryExecutor {
+
+    @Autowired
+    private SmsbDeviceErrorRecordMapper smsbDeviceErrorRecordMapper;
+
+    @Autowired
+    private SmsbDeviceMapper smsbDeviceMapper;
+
+    @Autowired
+    private SmsbDeviceOnlineSummaryMapper smsbDeviceOnlineSummaryMapper;
+
+
+    public ExecuteResult jobExecute() {
+        SnailJobLog.REMOTE.info("deviceOnlineSummary job start...");
+        // 查询出所有设备
+        List<SmsbDeviceVo> smsbDeviceVoList = smsbDeviceMapper.selectVoList();
+        if (CollectionUtil.isEmpty(smsbDeviceVoList)) {
+            SnailJobLog.REMOTE.info("smsbDeviceVoList is empty");
+            return ExecuteResult.success("deviceOnlineSummary job execute success");
+        }
+        Date todayBegin = DateUtil.beginOfDay(new Date());
+        Date todayEnd = DateUtil.endOfDay(new Date());
+        for (SmsbDeviceVo smsbDeviceVo : smsbDeviceVoList) {
+            Long deviceId = smsbDeviceVo.getId();
+            SmsbDeviceOnlineSummary smsbDeviceOnlineSummary = new SmsbDeviceOnlineSummary();
+            smsbDeviceOnlineSummary.setDeviceId(deviceId);
+            smsbDeviceOnlineSummary.setCountTime(todayBegin);
+            smsbDeviceOnlineSummary.setTenantId(smsbDeviceVo.getTenantId());
+            Integer onlineDuration = 0;
+            // 查询出当天最后一天 上线/离线记录
+            LambdaQueryWrapper<SmsbDeviceErrorRecord> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+            lambdaQueryWrapper.eq(SmsbDeviceErrorRecord::getDeviceId, deviceId);
+            lambdaQueryWrapper.and(lqw -> lqw.eq(SmsbDeviceErrorRecord::getErrorType, DeviceConstants.DEVICE_ERROR_ONLINE)
+                .or().eq(SmsbDeviceErrorRecord::getErrorType, DeviceConstants.DEVICE_ERROR_OFFLINE));
+            lambdaQueryWrapper.between(SmsbDeviceErrorRecord::getCreateTime, todayBegin, todayEnd);
+            lambdaQueryWrapper.orderByDesc(SmsbDeviceErrorRecord::getCreateTime);
+            lambdaQueryWrapper.last(DeviceConstants.LIMIT_ONE);
+            SmsbDeviceErrorRecordVo lastOnOrOffRecord = smsbDeviceErrorRecordMapper.selectVoOne(lambdaQueryWrapper);
+            // 如果当天没有在离线记录,且设备在线,则当天在线时长为60*60*24
+            if (lastOnOrOffRecord == null) {
+                if (smsbDeviceVo.getOnlineStatus().equals(DeviceConstants.DEVICE_STATUS_ONLINE.longValue())) {
+                    onlineDuration = 60 * 60 * 24;
+                } else {
+                    onlineDuration = 0;
+                }
+                smsbDeviceOnlineSummary.setDuration(onlineDuration.longValue());
+                SnailJobLog.REMOTE.info("deviceOnlineSummary job deviceId : " + deviceId + ",onlineDuration : " + onlineDuration);
+                smsbDeviceOnlineSummaryMapper.insert(smsbDeviceOnlineSummary);
+                continue;
+            }
+            // 最后一条为离线 统计当天所有的online_duration
+            if (lastOnOrOffRecord.getErrorType().equals(DeviceConstants.DEVICE_ERROR_OFFLINE.longValue())) {
+                onlineDuration = smsbDeviceErrorRecordMapper.countOnlineDuration(deviceId, todayBegin, todayEnd);
+                smsbDeviceOnlineSummary.setDuration(onlineDuration.longValue());
+            } else {
+                // 最后一条为上线,则当天在线时长为0
+                Long durationEndDay = (todayEnd.getTime() - lastOnOrOffRecord.getCreateTime().getTime()) / 1000;
+                onlineDuration = smsbDeviceErrorRecordMapper.countOnlineDuration(deviceId, todayBegin, todayEnd);
+                smsbDeviceOnlineSummary.setDuration(durationEndDay + onlineDuration.longValue());
+            }
+            SnailJobLog.REMOTE.info("deviceOnlineSummary job deviceId : " + deviceId + ",onlineDuration : " + smsbDeviceOnlineSummary.getDuration());
+            smsbDeviceOnlineSummaryMapper.insert(smsbDeviceOnlineSummary);
+        }
+        SnailJobLog.REMOTE.info("deviceOnlineSummary job end...");
+        return ExecuteResult.success("deviceOnlineSummary job execute success");
+    }
+
+
+}

+ 20 - 7
smsb-modules/smsb-job/src/main/java/org/dromara/job/snailjob/DeviceStatusReviewExecutor.java

@@ -7,9 +7,9 @@ import com.aizuda.snailjob.common.log.SnailJobLog;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
 import com.inspur.device.domain.constants.DeviceConstants;
+import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
 import com.inspur.device.mapper.SmsbDeviceErrorRecordMapper;
-import com.inspur.device.mapper.SmsbDeviceMapper;
 import com.inspur.device.service.ISmsbDeviceService;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,8 +21,8 @@ import java.util.Date;
 import java.util.List;
 
 /**
- * @author opensnail
- * @date 2024-05-17
+ * @author Hao Li
+ * @date 2025-03-17
  */
 @Component
 @JobExecutor(name = "deviceStatusReview")
@@ -37,9 +37,6 @@ public class DeviceStatusReviewExecutor {
     @Autowired
     private ISmsbDeviceService smsbDeviceService;
 
-    @Autowired
-    private SmsbDeviceMapper smsbDeviceMapper;
-
     @Autowired
     private SmsbDeviceErrorRecordMapper smsbDeviceErrorRecordMapper;
 
@@ -89,11 +86,27 @@ public class DeviceStatusReviewExecutor {
         return ExecuteResult.success("deviceStatusReview job execute success");
     }
 
-    private static void buildErrorRecord(SmsbDeviceErrorRecord errorRecord, SmsbDeviceVo smsbDeviceVo, Integer errorLevel, Integer errorType) {
+    private void buildErrorRecord(SmsbDeviceErrorRecord errorRecord, SmsbDeviceVo smsbDeviceVo, Integer errorLevel, Integer errorType) {
         errorRecord.setDeviceId(smsbDeviceVo.getId());
         errorRecord.setDeviceName(smsbDeviceVo.getName());
         errorRecord.setErrorLevel(errorLevel);
         errorRecord.setErrorType(errorType);
         errorRecord.setTenantId(smsbDeviceVo.getTenantId());
+        // 如果不是离线 在线时长为0
+        if (!errorType.equals(DeviceConstants.DEVICE_ERROR_OFFLINE)) {
+            errorRecord.setOnlineDuration(0);
+        } else {
+            // 查询这个设备最后一次上线记录
+            SmsbDeviceErrorRecordVo lastOnlineRecord = smsbDeviceErrorRecordMapper.selectVoOne(new LambdaQueryWrapper<SmsbDeviceErrorRecord>()
+                .eq(SmsbDeviceErrorRecord::getDeviceId, smsbDeviceVo.getId())
+                .eq(SmsbDeviceErrorRecord::getErrorType, DeviceConstants.DEVICE_ERROR_ONLINE)
+                .orderByDesc(SmsbDeviceErrorRecord::getCreateTime)
+                .last(DeviceConstants.LIMIT_ONE));
+            if (lastOnlineRecord == null) {
+                errorRecord.setOnlineDuration(0);
+            } else {
+                errorRecord.setOnlineDuration((int) (System.currentTimeMillis() - lastOnlineRecord.getCreateTime().getTime()) / 1000);
+            }
+        }
     }
 }

+ 19 - 0
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/handler/ConnectServerHandler.java

@@ -1,9 +1,11 @@
 package com.inspur.netty.handler;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.inspur.device.domain.SmsbDevice;
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
 import com.inspur.device.domain.constants.DeviceConstants;
+import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
 import com.inspur.device.mapper.SmsbDeviceErrorRecordMapper;
 import com.inspur.device.mapper.SmsbDeviceMapper;
@@ -20,6 +22,7 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.utils.SpringUtils;
 import org.dromara.common.core.utils.StringUtils;
+import org.dromara.system.domain.SysOss;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.nio.charset.Charset;
@@ -126,6 +129,22 @@ public class ConnectServerHandler extends ChannelInboundHandlerAdapter {
         errorRecord.setErrorLevel(errorLevel);
         errorRecord.setErrorType(errorType);
         errorRecord.setTenantId(smsbDeviceVo.getTenantId());
+        // 如果不是离线 在线时长为0
+        if (!errorType.equals(DeviceConstants.DEVICE_ERROR_OFFLINE)) {
+            errorRecord.setOnlineDuration(0);
+        }else {
+            // 查询这个设备最后一次上线记录
+            SmsbDeviceErrorRecordVo lastOnlineRecord = smsbDeviceErrorRecordMapper.selectVoOne(new LambdaQueryWrapper<SmsbDeviceErrorRecord>()
+                .eq(SmsbDeviceErrorRecord::getDeviceId, smsbDeviceVo.getId())
+                .eq(SmsbDeviceErrorRecord::getErrorType, DeviceConstants.DEVICE_ERROR_ONLINE)
+                .orderByDesc(SmsbDeviceErrorRecord::getCreateTime)
+                .last(DeviceConstants.LIMIT_ONE));
+            if (lastOnlineRecord == null) {
+                errorRecord.setOnlineDuration(0);
+            }else {
+                errorRecord.setOnlineDuration((int) (System.currentTimeMillis() - lastOnlineRecord.getCreateTime().getTime()) / 1000);
+            }
+        }
     }
 
     @Override

+ 24 - 0
smsb-plus-ui/src/api/smsb/device/errorRecord.ts

@@ -24,6 +24,30 @@ export const alarmCountByType = (params?: any): AxiosPromise<DeviceErrorRecordVO
   });
 };
 
+export const alarmCountByLevel = (params?: any): AxiosPromise<DeviceErrorRecordVO> => {
+  return request({
+    url: '/device/errorRecord/count/byLevel',
+    method: 'get',
+    params: params
+  });
+};
+
+export const alarmNumTop = (params?: any): AxiosPromise<DeviceErrorRecordVO> => {
+  return request({
+    url: '/device/errorRecord/num/top',
+    method: 'get',
+    params: params
+  });
+};
+
+export const onlineTimeTop = (params?: any): AxiosPromise<DeviceErrorRecordVO> => {
+  return request({
+    url: '/device/errorRecord/onlineTime/top',
+    method: 'get',
+    params: params
+  });
+};
+
 /**
  * 查询设备告警记录详细
  * @param id

+ 159 - 35
smsb-plus-ui/src/views/smsb/dashboard/device.vue

@@ -4,10 +4,10 @@
       <el-card shadow="hover" style="margin-top: 10px">
         <el-row justify="end" align="middle">
           <el-col :span="19" style="text-align: right">
-            <el-radio-group v-model="timeRadio" size="large" @change="handleDateRangeChange">
-              <el-radio-button label="今日" value="today" />
-              <el-radio-button label="本周" value="week" />
-              <el-radio-button label="本月" value="month" />
+            <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange">
+<!--              <el-radio-button label="今日" value="today" />-->
+              <el-radio-button label="近7天" value="week" />
+              <el-radio-button label="近30天" value="month" />
             </el-radio-group>
           </el-col>
           <el-col :span="5" style="text-align: right">
@@ -17,7 +17,7 @@
               range-separator="-"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
-              style="margin-left: 10px; margin-right: 20px"
+              style="margin-left: 10px; margin-right: 30px"
             />
           </el-col>
         </el-row>
@@ -45,7 +45,7 @@
             <el-col :span="8">
               <el-card shadow="hover">
                 <h3>告警级别</h3>
-                <div class="chart-placeholder"></div>
+                <div ref="alarmLevel" class="chart-placeholder"></div>
               </el-card>
             </el-col>
             <el-col :span="8">
@@ -59,21 +59,13 @@
             <el-col :span="12">
               <el-card shadow="hover">
                 <h3>在线时长排行</h3>
-                <el-table :data="onlineTimeRanking" style="width: 100%">
-                  <el-table-column prop="rank" label="排名" width="60"></el-table-column>
-                  <el-table-column prop="device" label="设备名称"></el-table-column>
-                  <el-table-column prop="value" label="时长"></el-table-column>
-                </el-table>
+                <div ref="onlineTime" class="chart-placeholder"></div>
               </el-card>
             </el-col>
             <el-col :span="12">
               <el-card shadow="hover">
                 <h3>告警数量排行</h3>
-                <el-table :data="alarmRanking" style="width: 100%">
-                  <el-table-column prop="rank" label="排名" width="60"></el-table-column>
-                  <el-table-column prop="device" label="设备名称"></el-table-column>
-                  <el-table-column prop="value" label="告警数量"></el-table-column>
-                </el-table>
+                <div ref="alarmNum" class="chart-placeholder"></div>
               </el-card>
             </el-col>
           </el-row>
@@ -82,13 +74,13 @@
           <el-card shadow="hover">
             <h3>告警清单</h3>
             <el-table v-loading="loading" :data="alarmList" row-key="id">
-              <el-table-column label="设备名称" align="left" prop="deviceName" :show-overflow-tooltip="true" />
-              <el-table-column label="告警等级" align="center" prop="errorLevel" width="100">
+              <el-table-column label="设备名称" align="left" prop="deviceName" width="100" :show-overflow-tooltip="true" />
+              <el-table-column label="告警等级" align="center" prop="errorLevel" width="80">
                 <template #default="scope">
                   <dict-tag :options="smsb_device_error_level" :value="scope.row.errorLevel" />
                 </template>
               </el-table-column>
-              <el-table-column label="告警类型" align="center" prop="errorType" width="100">
+              <el-table-column label="告警类型" align="center" prop="errorType" width="80">
                 <template #default="scope">
                   <dict-tag :options="smsb_device_error_type" :value="scope.row.errorType" />
                 </template>
@@ -107,21 +99,24 @@ import { reactive } from 'vue';
 import { deviceStatistics } from '@/api/smsb/device/device';
 import * as echarts from 'echarts';
 import { DeviceErrorRecordQuery, DeviceErrorRecordVO } from '@/api/smsb/device/errorRecord_type';
-import { alarmCountByType, listDeviceErrorRecord } from '@/api/smsb/device/errorRecord';
+import { alarmCountByLevel, alarmCountByType, alarmNumTop, listDeviceErrorRecord, onlineTimeTop } from '@/api/smsb/device/errorRecord';
 
 const alarmList = ref<DeviceErrorRecordVO[]>([]);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { smsb_device_error_level, smsb_device_error_type } = toRefs<any>(proxy?.useDict('smsb_device_error_level', 'smsb_device_error_type'));
 const total = ref(0);
 const loading = ref(true);
-const timeRadio = ref('today');
+const timeRadio = ref('week');
 const dateRange = ref(['2025-01-01', '2025-01-01']);
 const typePie = ref();
+const alarmLevel = ref();
 const alarmCount = ref();
+const alarmNum = ref();
+const onlineTime = ref();
 const dialogData = reactive<DialogPageData<DeviceErrorRecordQuery>>({
   dialogQueryParams: {
     pageNum: 1,
-    pageSize: 11,
+    pageSize: 18,
     deviceId: undefined,
     params: {}
   }
@@ -135,18 +130,6 @@ const stats = reactive([
   { label: '待接入设备', value: 0, class: 'warning' }
 ]);
 
-const onlineTimeRanking = reactive([
-  { rank: 1, device: '设备A', value: '6000' },
-  { rank: 2, device: '设备B', value: '5900' },
-  { rank: 3, device: '设备C', value: '5800' }
-]);
-
-const alarmRanking = reactive([
-  { rank: 1, device: '设备A', value: '6000' },
-  { rank: 2, device: '设备B', value: '5900' },
-  { rank: 3, device: '设备C', value: '5800' }
-]);
-
 const handleDateRangeChange = () => {
   const rangeType = timeRadio.value;
   const today = new Date();
@@ -166,6 +149,9 @@ const handleDateRangeChange = () => {
   }
   dateRange.value = [formatDate(startDate), formatDate(endDate)];
   alarmCountData();
+  getAlarmLevel();
+  getAlarmNumTop();
+  getOnlineTimeTop();
 };
 
 const formatDate = (date: Date) => {
@@ -174,6 +160,88 @@ const formatDate = (date: Date) => {
   const day = String(date.getDate()).padStart(2, '0');
   return `${year}-${month}-${day}`;
 };
+const getOnlineTimeTop = async () => {
+  const params = {
+    startTime: dateRange.value[0],
+    endTime: dateRange.value[1]
+  };
+  const res = await onlineTimeTop(params);
+  const onlineTimeTopInstance = echarts.init(onlineTime.value, 'macaroons');
+  onlineTimeTopInstance.setOption({
+    title: {
+      text: ''
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {},
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      boundaryGap: [0, 0.01]
+    },
+    yAxis: {
+      type: 'category',
+      data: res.data.deviceNameList
+    },
+    series: [
+      {
+        name: '',
+        type: 'bar',
+        data: res.data.onlineTimeList
+      }
+    ]
+  });
+};
+const getAlarmNumTop = async () => {
+  const params = {
+    startTime: dateRange.value[0],
+    endTime: dateRange.value[1]
+  };
+  const res = await alarmNumTop(params);
+  const alarmNumTopInstance = echarts.init(alarmNum.value, 'macaroons');
+  alarmNumTopInstance.setOption({
+    title: {
+      text: ''
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {},
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      boundaryGap: [0, 0.01]
+    },
+    yAxis: {
+      type: 'category',
+      data: res.data.deviceNameList
+    },
+    series: [
+      {
+        name: '',
+        type: 'bar',
+        data: res.data.alarmNumList
+      }
+    ]
+  });
+};
 const alarmCountData = async () => {
   const params = {
     startTime: dateRange.value[0],
@@ -197,6 +265,59 @@ const alarmCountData = async () => {
     ]
   });
 };
+const getAlarmLevel = async () => {
+  const params = {
+    startTime: dateRange.value[0],
+    endTime: dateRange.value[1]
+  };
+  const res = await alarmCountByLevel(params);
+
+  const alarmLevelInstance = echarts.init(alarmLevel.value, 'macaroons');
+  alarmLevelInstance.setOption({
+    title: {
+      text: ''
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['普通', '紧急']
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    toolbox: {
+      feature: {
+        saveAsImage: {}
+      }
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: res.data.alarmDateList
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: '普通',
+        type: 'line',
+        stack: 'Total',
+        data: res.data.normalAlamList
+      },
+      {
+        name: '紧急',
+        type: 'line',
+        stack: 'Total',
+        data: res.data.dangerAlamList
+      }
+    ]
+  });
+};
 const getDeviceStatistics = async () => {
   const res = await deviceStatistics();
   stats.forEach((item) => {
@@ -233,7 +354,7 @@ const getDeviceStatistics = async () => {
       {
         name: '类型占比',
         type: 'pie',
-        radius: '60%',
+        radius: '65%',
         data: [
           { value: (res.data.onlineNum / res.data.totalNum) * 100, name: '在线' },
           { value: (res.data.offlineNum / res.data.totalNum) * 100, name: '离线' },
@@ -262,6 +383,9 @@ onMounted(() => {
   getDeviceStatistics();
   getAlarmList();
   alarmCountData();
+  getAlarmLevel();
+  getAlarmNumTop();
+  getOnlineTimeTop();
 });
 </script>