lihao16 пре 7 месеци
родитељ
комит
7b5408d6c4
16 измењених фајлова са 269 додато и 84 уклоњено
  1. 0 7
      smsb-admin/src/main/java/org/dromara/web/controller/AuthController.java
  2. 17 0
      smsb-common/smsb-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageContentDto.java
  3. 14 0
      smsb-common/smsb-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageContentTypeDto.java
  4. 7 0
      smsb-modules/smsb-device/pom.xml
  5. 2 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/constants/DeviceConstants.java
  6. 11 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceService.java
  7. 2 6
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbProductTypeService.java
  8. 53 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceServiceImpl.java
  9. 11 16
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbProductServiceImpl.java
  10. 14 17
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbProductTypeServiceImpl.java
  11. 31 0
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/DeviceController.java
  12. 5 0
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/message/push/PushMessageType.java
  13. 18 5
      smsb-modules/smsb-source/src/main/java/com/inspur/source/controller/SmsbFrontController.java
  14. 7 0
      smsb-plus-ui/src/api/smsb/device/device.ts
  15. 14 10
      smsb-plus-ui/src/utils/sse.ts
  16. 63 23
      smsb-plus-ui/src/views/smsb/device/index.vue

+ 0 - 7
smsb-admin/src/main/java/org/dromara/web/controller/AuthController.java

@@ -100,13 +100,6 @@ public class AuthController {
         // 登录
         LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
 
-        Long userId = LoginHelper.getUserId();
-        scheduledExecutorService.schedule(() -> {
-            SseMessageDto dto = new SseMessageDto();
-            dto.setMessage("欢迎登录smsb-plus浪潮屏媒安播云平台");
-            dto.setUserIds(List.of(userId));
-            SseMessageUtils.publishMessage(dto);
-        }, 5, TimeUnit.SECONDS);
         return R.ok(loginVo);
     }
 

+ 17 - 0
smsb-common/smsb-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageContentDto.java

@@ -0,0 +1,17 @@
+package org.dromara.common.sse.dto;
+
+
+import lombok.Data;
+
+/**
+ * sse消息内容
+ * @author lihao16
+ */
+@Data
+public class SseMessageContentDto {
+
+    private String msgType;
+
+    private String msgData;
+
+}

+ 14 - 0
smsb-common/smsb-common-sse/src/main/java/org/dromara/common/sse/dto/SseMessageContentTypeDto.java

@@ -0,0 +1,14 @@
+package org.dromara.common.sse.dto;
+
+/**
+ * sse message content type dto
+ * @author lihao16
+ */
+public class SseMessageContentTypeDto {
+
+    /**
+     * 设备截屏回传
+     */
+    public static final String SCREENSHOT = "screenshot";
+
+}

+ 7 - 0
smsb-modules/smsb-device/pom.xml

@@ -120,6 +120,13 @@
             <version>1.2.2</version>
         </dependency>
 
+        <!-- 阿里JSON解析器 -->
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.4</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 2 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/constants/DeviceConstants.java

@@ -35,6 +35,8 @@ public class DeviceConstants {
     /** 设备删除 已删除 */
     public static final Integer DEVICE_DEL_FLAG_YES = 1;
 
+    /** 设备截屏 Redis key */
+    public static final String REDIS_SCREENSHOT_KEY = "global:device:screenshot:";
 
 
 }

+ 11 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceService.java

@@ -5,8 +5,10 @@ import com.inspur.device.domain.bo.SmsbDeviceBo;
 import com.inspur.device.domain.vo.DeviceStatisticsVo;
 import com.inspur.device.domain.vo.SmsbDeviceRunInfoVo;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.Collection;
 import java.util.List;
@@ -120,4 +122,13 @@ public interface ISmsbDeviceService {
      * @return
      */
     TableDataInfo<SmsbDeviceVo> queryPageListByTenantId(SmsbDeviceBo bo, PageQuery pageQuery);
+
+    /**
+     * 前端设备上传截图
+     * @param identifier
+     * @param timestamp
+     * @param file
+     * @return
+     */
+    R<Void> screenshotUpload(String identifier, Long timestamp, MultipartFile file);
 }

+ 2 - 6
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbProductTypeService.java

@@ -1,16 +1,12 @@
 package com.inspur.device.service;
 
-import com.inspur.device.domain.SmsbProductType;
-import com.inspur.device.domain.constants.SmsbSystemParamMap;
-import com.inspur.device.domain.vo.SmsbProductTypeVo;
 import com.inspur.device.domain.bo.SmsbProductTypeBo;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
+import com.inspur.device.domain.vo.SmsbProductTypeVo;
 import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 
-import java.time.LocalDate;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 屏幕类型Service接口

+ 53 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceServiceImpl.java

@@ -1,6 +1,7 @@
 package com.inspur.device.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -21,17 +22,29 @@ import com.inspur.device.service.ISmsbDeviceManufacturerService;
 import com.inspur.device.service.ISmsbDeviceService;
 import com.inspur.device.service.ISmsbProductService;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.oss.core.OssClient;
+import org.dromara.common.oss.entity.UploadResult;
+import org.dromara.common.oss.factory.OssFactory;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.sse.dto.SseMessageContentDto;
+import org.dromara.common.sse.dto.SseMessageContentTypeDto;
+import org.dromara.common.sse.dto.SseMessageDto;
+import org.dromara.common.sse.utils.SseMessageUtils;
 import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.CachePut;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -277,4 +290,44 @@ public class SmsbDeviceServiceImpl implements ISmsbDeviceService {
             .set(SmsbDevice::getLastOnline, updateResult.getLastOnline()));
         return updateResult;
     }
+
+    @Override
+    public R<Void> screenshotUpload(String identifier, Long timestamp, MultipartFile file) {
+        // 根据identifier获取设备信息
+        SmsbDeviceVo smsbDeviceVo = getDeviceByIdentifier(identifier);
+        String redisKey = DeviceConstants.REDIS_SCREENSHOT_KEY + smsbDeviceVo.getId() + ":" + timestamp;
+        // 根据key获取存储的userId
+        String redisValue = RedisUtils.getCacheObject(redisKey);
+        if (StringUtils.isEmpty(redisValue)) {
+            return R.ok("超过一分钟,暂不处理");
+        }
+        Long userId = Long.parseLong(redisValue);
+        // 文件上传
+        String originalfileName = file.getOriginalFilename();
+        String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
+        OssClient storage = OssFactory.instance();
+        UploadResult uploadResult;
+        try {
+            uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
+        } catch (IOException e) {
+            return R.fail(Integer.parseInt(ResultCodeEnum.UPLOAD_FAIL.getValue()), ResultCodeEnum.UPLOAD_FAIL.getMessage());
+        }
+        String imageUrl = uploadResult.getUrl();
+        // 保存到redis
+        RedisUtils.setObjectIfExists(redisKey, imageUrl, Duration.ofMinutes(1));
+        // 发送sse消息,前端展示
+        sendSseMessage(userId, imageUrl);
+        return R.ok();
+    }
+
+    private void sendSseMessage(Long userId, String imageUrl) {
+        SseMessageContentDto messageContentDto = new SseMessageContentDto();
+        messageContentDto.setMsgType(SseMessageContentTypeDto.SCREENSHOT);
+        messageContentDto.setMsgData(imageUrl);
+        SseMessageDto messageDto = new SseMessageDto();
+        String message = JSON.toJSONString(messageContentDto);
+        messageDto.setMessage(message);
+        messageDto.setUserIds(List.of(userId));
+        SseMessageUtils.publishMessage(messageDto);
+    }
 }

+ 11 - 16
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbProductServiceImpl.java

@@ -1,38 +1,33 @@
 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.google.common.math.IntMath;
-import com.inspur.device.domain.SmsbProductType;
+import com.inspur.device.domain.SmsbProduct;
+import com.inspur.device.domain.bo.SmsbProductBo;
 import com.inspur.device.domain.constants.ResultCodeEnum;
-import com.inspur.device.domain.constants.SmsbSystemParamMap;
 import com.inspur.device.domain.vo.SmsbProductTypeVo;
+import com.inspur.device.domain.vo.SmsbProductVo;
+import com.inspur.device.mapper.SmsbProductMapper;
+import com.inspur.device.service.ISmsbProductService;
 import com.inspur.device.service.ISmsbProductTypeService;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import lombok.RequiredArgsConstructor;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.tenant.core.TenantEntity;
-import org.springframework.beans.BeanUtils;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.CachePut;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
-import com.inspur.device.domain.bo.SmsbProductBo;
-import com.inspur.device.domain.vo.SmsbProductVo;
-import com.inspur.device.domain.SmsbProduct;
-import com.inspur.device.mapper.SmsbProductMapper;
-import com.inspur.device.service.ISmsbProductService;
 
-import java.time.LocalDate;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Collection;
 import java.util.UUID;
 
 /**

+ 14 - 17
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbProductTypeServiceImpl.java

@@ -1,31 +1,28 @@
 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.SmsbProductType;
+import com.inspur.device.domain.bo.SmsbProductTypeBo;
 import com.inspur.device.domain.constants.ResultCodeEnum;
-import com.inspur.device.domain.constants.SmsbSystemParamMap;
-import com.inspur.device.domain.vo.SmsbProductVo;
-import org.dromara.common.core.constant.CacheNames;
+import com.inspur.device.domain.vo.SmsbProductTypeVo;
+import com.inspur.device.mapper.SmsbProductTypeMapper;
+import com.inspur.device.service.ISmsbProductTypeService;
+import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
-import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.BeanUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.CachePut;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
-import com.inspur.device.domain.bo.SmsbProductTypeBo;
-import com.inspur.device.domain.vo.SmsbProductTypeVo;
-import com.inspur.device.domain.SmsbProductType;
-import com.inspur.device.mapper.SmsbProductTypeMapper;
-import com.inspur.device.service.ISmsbProductTypeService;
 
-import java.time.LocalDate;
-import java.util.*;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
 
 /**
  * 屏幕类型Service业务层处理

+ 31 - 0
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/DeviceController.java

@@ -2,6 +2,7 @@ package com.inspur.netty.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import com.inspur.device.domain.bo.SmsbOtaRecordBo;
+import com.inspur.device.domain.constants.DeviceConstants;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
 import com.inspur.device.service.ISmsbDeviceService;
 import com.inspur.device.service.ISmsbOtaRecordService;
@@ -14,10 +15,13 @@ import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.idempotent.annotation.RepeatSubmit;
 import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.Duration;
 import java.util.List;
 
 /**
@@ -112,6 +116,33 @@ public class DeviceController {
         return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
     }
 
+    /**
+     * 设备待机
+     *
+     * @param deviceId
+     * @return
+     */
+    @GetMapping("/screenshot/{deviceId}")
+    public R<String> screenshot(@PathVariable Long deviceId) {
+        // 查询设备信息
+        SmsbDeviceVo deviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
+        if (deviceVo == null) {
+            return R.fail("设备不存在");
+        }
+        // 组装重启命令
+        Long timestamp = System.currentTimeMillis();
+        String screenshotCmd = deviceVo.getIdentifier() + PushMessageType.CONTROL_SCREENSHOT.getValue() + "/" + timestamp + NettyConstants.DATA_PACK_SEPARATOR;
+        boolean isSend = PushMsgUtil.sendV2(deviceVo.getIdentifier(), screenshotCmd);
+        // 如果消息发送成功 缓存Rediskey
+        if (isSend) {
+            String redisKey = DeviceConstants.REDIS_SCREENSHOT_KEY + deviceId + ":" + timestamp;
+            String redisValue = LoginHelper.getUserId().toString();
+            RedisUtils.setCacheObject(redisKey, redisValue, Duration.ofMinutes(1));
+        }
+        return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
+
+    }
+
     /**
      * 新增发布升级
      */

+ 5 - 0
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/message/push/PushMessageType.java

@@ -52,6 +52,11 @@ public enum PushMessageType {
      */
     CONTROL_STANDBY("/standby"),
 
+    /**
+     * 设备截屏
+     */
+    CONTROL_SCREENSHOT("/screenshot"),
+
     /**
      * 调节设备音量
      */

+ 18 - 5
smsb-modules/smsb-source/src/main/java/com/inspur/source/controller/SmsbFrontController.java

@@ -2,8 +2,8 @@ package com.inspur.source.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
 import com.inspur.device.domain.vo.SmsbOtaRecordVo;
+import com.inspur.device.service.ISmsbDeviceService;
 import com.inspur.device.service.ISmsbOtaRecordService;
-import com.inspur.source.domain.vo.FrontItemInfoVO;
 import com.inspur.source.domain.vo.FrontPushInfoVo;
 import com.inspur.source.service.ISmsbItemPushService;
 import jakarta.validation.constraints.NotNull;
@@ -11,10 +11,8 @@ import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 前端设备接口
@@ -32,6 +30,9 @@ public class SmsbFrontController {
     @Autowired
     private ISmsbOtaRecordService iSmsbOtaRecordService;
 
+    @Autowired
+    private ISmsbDeviceService smsbDeviceService;
+
     /**
      * 根据设备identifier 获取该设备最新内容下发记录
      *
@@ -69,4 +70,16 @@ public class SmsbFrontController {
         return iSmsbOtaRecordService.deviceCheckOta(identifier);
     }
 
+    /**
+     * 前端设备上传截图
+     *
+     * @param identifier 主键
+     */
+    @SaIgnore
+    @GetMapping("/screenshot/upload")
+    public R<Void> screenshotUpload(@RequestParam("identifier") String identifier,@RequestParam("timestamp") Long timestamp,
+                                               @RequestBody MultipartFile file) {
+        return smsbDeviceService.screenshotUpload(identifier,timestamp, file);
+    }
+
 }

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

@@ -123,6 +123,13 @@ export const startStream = (id: string | number) => {
   });
 };
 
+export const getDeviceScreenshot = (id: string | number) => {
+  return request({
+    url: '/netty/device/screenshot/' + id,
+    method: 'get'
+  });
+};
+
 export const stopStream = (id: string | number) => {
   return request({
     url: '/stream/stop/' + id,

+ 14 - 10
smsb-plus-ui/src/utils/sse.ts

@@ -1,6 +1,7 @@
 import { getToken } from '@/utils/auth';
 import { ElNotification } from 'element-plus';
 import useNoticeStore from '@/store/modules/notice';
+import useScreenshotStore from '@/store/modules/screenshot';
 
 // 初始化
 export const initSSE = (url: any) => {
@@ -8,11 +9,8 @@ export const initSSE = (url: any) => {
     return;
   }
 
-  url = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID
-  const {
-    data,
-    error
-  } = useEventSource(url, [], {
+  url = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID;
+  const { data, error } = useEventSource(url, [], {
     autoReconnect: {
       retries: 10,
       delay: 3000,
@@ -26,20 +24,26 @@ export const initSSE = (url: any) => {
     console.log('SSE connection error:', error.value);
     error.value = null;
   });
-
+  // sse 接收到消息
   watch(data, () => {
     if (!data.value) return;
-    useNoticeStore().addNotice({
+    // console.log(data.value);
+    // 处理消息
+    const msg = JSON.parse(data.value);
+    if (msg.msgType === 'screenshot') {
+      useScreenshotStore().addScreenshot(msg.msgData);
+    }
+    /*useNoticeStore().addNotice({
       message: data.value,
       read: false,
       time: new Date().toLocaleString()
-    });
-    ElNotification({
+    });*/
+    /*ElNotification({
       title: '消息',
       message: data.value,
       type: 'success',
       duration: 3000
-    });
+    });*/
     data.value = null;
   });
 };

+ 63 - 23
smsb-plus-ui/src/views/smsb/device/index.vue

@@ -39,25 +39,26 @@
           </el-row>
         </el-card>
         <el-card shadow="hover" :style="{ marginTop: '10px', height: '60px' }">
-          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="40px">
             <el-form-item label="名称" prop="name">
-              <el-input v-model="queryParams.name" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
+              <el-input v-model="queryParams.name" style="width: 150px" placeholder="请输入设备名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             <el-form-item label="SN" prop="serialNumber">
-              <el-input v-model="queryParams.serialNumber" placeholder="请输入设备SN" clearable @keyup.enter="handleQuery" />
+              <el-input v-model="queryParams.serialNumber" style="width: 150px" placeholder="请输入设备SN" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             <el-form-item label="MAC" prop="mac">
-              <el-input v-model="queryParams.mac" placeholder="请输入设备MAC" clearable @keyup.enter="handleQuery" />
+              <el-input v-model="queryParams.mac" style="width: 150px" placeholder="请输入设备MAC" clearable @keyup.enter="handleQuery" />
             </el-form-item>
             <el-form-item>
               <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
               <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-              <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['smsb:device:add']">设备添加 </el-button>
-              <el-button type="success" plain icon="VideoPlay" :disabled="single" @click="startMonitor()">回采画面 </el-button>
-              <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['smsb:device:remove']"
+              <el-button v-hasPermi="['smsb:device:add']" type="primary" plain icon="Plus" @click="handleAdd">设备添加 </el-button>
+              <el-button type="warning" plain icon="Picture" :disabled="single" @click="screenShot()">回采画面 </el-button>
+              <el-button type="success" plain icon="VideoPlay" :disabled="single" @click="startMonitor()">回调视频 </el-button>
+              <el-button v-hasPermi="['smsb:device:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
                 >删除
               </el-button>
-              <el-button type="info" plain icon="Position" :disabled="single" @click="handleInfo()" v-hasPermi="['smsb:device:query']">
+              <el-button v-hasPermi="['smsb:device:query']" type="info" plain icon="Position" :disabled="single" @click="handleInfo()">
                 详情
               </el-button>
             </el-form-item>
@@ -101,19 +102,19 @@
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['smsb:device:edit']"></el-button>
+              <el-button v-hasPermi="['smsb:device:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
             </el-tooltip>
             <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['smsb:device:remove']"></el-button>
+              <el-button v-hasPermi="['smsb:device:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
             </el-tooltip>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
     </el-card>
     <!-- 添加或修改设备对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="850px" append-to-body>
+    <el-dialog v-model="dialog.visible" :title="dialog.title" width="850px" append-to-body>
       <el-form ref="deviceFormRef" :model="form" :rules="rules" label-width="78px">
         <el-row>
           <el-col :span="12">
@@ -189,7 +190,7 @@
       </template>
     </el-dialog>
     <!--设备详情弹窗-->
-    <el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="1000px" append-to-body>
+    <el-dialog v-model="viewDialog.visible" :title="viewDialog.title" width="1000px" append-to-body>
       <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClickTab">
         <el-tab-pane label="状态检测" name="info">
           <div>
@@ -217,7 +218,7 @@
         </el-tab-pane>
         <el-tab-pane label="报警信息" name="alarm">
           <el-table v-loading="loading" :data="alarmList" row-key="id">
-            <el-table-column label="主键ID" align="left" prop="id" v-if="true" />
+            <el-table-column v-if="true" label="主键ID" align="left" prop="id" />
             <el-table-column label="设备名称" align="left" prop="deviceName" :show-overflow-tooltip="true" />
             <el-table-column label="告警等级" align="center" prop="errorLevel" width="100">
               <template #default="scope">
@@ -233,9 +234,9 @@
           </el-table>
           <pagination
             v-show="alarmTotal > 0"
-            :total="alarmTotal"
             v-model:page="dialogQueryParams.pageNum"
             v-model:limit="dialogQueryParams.pageSize"
+            :total="alarmTotal"
             @pagination="getAlarmList"
           />
         </el-tab-pane>
@@ -246,7 +247,7 @@
         </div>
       </template>
     </el-dialog>
-    <el-dialog :title="watchDialog.title" v-model="watchDialog.visible" width="900px" append-to-body @closed="onDialogClosed">
+    <el-dialog v-model="watchDialog.visible" :title="watchDialog.title" width="900px" append-to-body @closed="onDialogClosed">
       <div v-if="watchDialog.visible" style="width: 100%; height: 500px">
         <video ref="flvPlayerRef" style="width: 100%; height: 100%" controls></video>
       </div>
@@ -256,6 +257,12 @@
         </div>
       </template>
     </el-dialog>
+    <el-dialog v-model="shotDialog.visible" :title="shotDialog.title" width="900px" append-to-body @closed="onDialogClosed">
+      <div style="text-align: center">
+        <!--        <image-preview :src="screenshotImageUrl" style="width: 40px; height: 40px; cursor: pointer" />-->
+        <el-image v-loading="screenshotLoading" :src="screenshotImageUrl" style="width: 600px; height: 600px" />
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -273,7 +280,8 @@ import {
   startStream,
   deviceStatistics,
   getDeviceRunInfo,
-  stopStream
+  stopStream,
+  getDeviceScreenshot
 } from '@/api/smsb/device/device';
 import { DeviceVO, DeviceQuery, DeviceForm, DeviceStatisticsVo } from '@/api/smsb/device/device_type';
 import { ProductVO } from '@/api/smsb/device/product_types';
@@ -286,7 +294,11 @@ import flvjs from 'flv.js';
 import { DeviceRunInfoVO } from '@/api/smsb/device/device_run_type';
 import { DeviceErrorRecordQuery, DeviceErrorRecordVO } from '@/api/smsb/device/errorRecord_type';
 import { listDeviceErrorRecord } from '@/api/smsb/device/errorRecord';
+import { storeToRefs } from 'pinia';
+import useScreenshotStore from '@/store/modules/screenshot';
 
+const screenshotStore = storeToRefs(useScreenshotStore());
+const screenshotImageUrl = ref<string>();
 const alarmList = ref<DeviceErrorRecordVO[]>([]);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { sys_device_online, smsb_yes_no, smsb_device_error_level, smsb_device_error_type } = toRefs<any>(
@@ -298,6 +310,7 @@ const productList = ref<ProductVO[]>([]);
 const manufacturerList = ref<DeviceManufacturerVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
+const screenshotLoading = ref(true);
 const showSearch = ref(true);
 const ids = ref<Array<string | number>>([]);
 const single = ref(true);
@@ -342,6 +355,10 @@ const watchDialog = reactive<DialogOption>({
   visible: false,
   title: ''
 });
+const shotDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
 const activeName = ref('info');
 
 const initFormData: DeviceForm = {
@@ -456,8 +473,8 @@ const cancel = () => {
   viewDialog.visible = false;
   watchDialog.visible = false;
   // 关闭播放器
-  if (flvPlayer) {
-    flvPlayer.destroy();
+  if (flvPlayer.value) {
+    flvPlayer.value.destroy();
   }
 };
 
@@ -577,6 +594,32 @@ const handleControl = async (type: string) => {
     proxy?.$modal.msgSuccess(resMsg);
   }
 };
+let intervalId = null;
+const screenShot = async (row?: DeviceVO) => {
+  const deviceId = row?.id || ids.value[0];
+  shotDialog.title = '回传画面';
+  shotDialog.visible = true;
+  const res = await getDeviceScreenshot(deviceId);
+
+  intervalId = setInterval(getScreenshot, 1000);
+};
+const getScreenshot = async () => {
+  screenshotImageUrl.value = screenshotStore.state.value.imageUrl;
+  console.log('device.vue screenshotImageUrl.value = ' + screenshotImageUrl.value);
+  if (screenshotImageUrl.value !== null) {
+    clearInterval(intervalId);
+    screenshotLoading.value = false;
+  }
+};
+// 对话框关闭时清理
+const onDialogClosed = () => {
+  destroyPlayer();
+  screenshotImageUrl.value = null;
+  screenshotStore.state.value.imageUrl = null;
+  screenshotLoading.value = true;
+  // 清除定时器
+  clearInterval(intervalId);
+};
 
 const startMonitor = async (row?: DeviceVO) => {
   try {
@@ -625,10 +668,7 @@ const destroyPlayer = () => {
     flvPlayer.value = null;
   }
 };
-// 对话框关闭时清理
-const onDialogClosed = () => {
-  destroyPlayer();
-};
+
 const stopMonitor = async () => {
   const res = await stopStream(streamDeviceId.value);
   watchDialog.visible = false;