20260314:1、网关设备支持接收子设备上报数据调试;

2、网关设备新增轮询管理页面(支持统一管理子设备轮询任务);
3、历史记录-网关设备新增子设备下拉框;
4、网关子设备不再展示到设备管理页面,入口设备管理-网关设备子设备-详情
This commit is contained in:
George 2026-03-14 15:13:05 +08:00
parent 9536ae19b1
commit afdadf84b9
20 changed files with 463 additions and 103 deletions

View File

@ -77,4 +77,14 @@ public class DeviceReportBo {
private String sources; private String sources;
/**
* 网关子设备BO
*/
private GwDeviceBo gwDeviceBo;
/**
* 设备名称
*/
private String deviceName;
} }

View File

@ -109,8 +109,13 @@ public class DataHandlerImpl implements IDataHandler {
pushObj.put("sources", bo.getSources()); pushObj.put("sources", bo.getSources());
messageBo.setMessage(JSON.toJSONString(pushObj)); messageBo.setMessage(JSON.toJSONString(pushObj));
remoteManager.pushCommon(messageBo); remoteManager.pushCommon(messageBo);
if (!Objects.isNull(bo.getGwDeviceBo())) { if (!Objects.isNull(bo.getGwDeviceBo())
&& !bo.getSerialNumber().equalsIgnoreCase(bo.getGwDeviceBo().getSerialNumber())) {
pushObj.put("subSerialNumber", bo.getSerialNumber());
pushObj.put("subDeviceName", bo.getDeviceName());
pushObj.put("subSlaveId", bo.getSlaveId());
messageBo.setTopic(topicsUtils.buildTopic(bo.getGwDeviceBo().getProductId(), bo.getGwDeviceBo().getSerialNumber(), TopicType.WS_SERVICE_INVOKE)); messageBo.setTopic(topicsUtils.buildTopic(bo.getGwDeviceBo().getProductId(), bo.getGwDeviceBo().getSerialNumber(), TopicType.WS_SERVICE_INVOKE));
messageBo.setMessage(JSON.toJSONString(pushObj));
remoteManager.pushCommon(messageBo); remoteManager.pushCommon(messageBo);
} }
// 上报属性给小度音箱,接入小度音箱后可放开 // 上报属性给小度音箱,接入小度音箱后可放开

View File

@ -16,14 +16,12 @@ import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.*; import com.fastbee.common.enums.*;
import com.fastbee.common.exception.ServiceException; import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.gateway.mq.TopicsUtils; import com.fastbee.common.utils.gateway.mq.TopicsUtils;
import com.fastbee.iot.cache.IOtaTaskCache; import com.fastbee.iot.cache.IOtaTaskCache;
import com.fastbee.iot.data.executor.PowerOnTriggerTaskExecutor; import com.fastbee.iot.data.executor.PowerOnTriggerTaskExecutor;
import com.fastbee.iot.data.service.IDataHandler; import com.fastbee.iot.data.service.IDataHandler;
import com.fastbee.iot.data.service.IDeviceReportMessageService; import com.fastbee.iot.data.service.IDeviceReportMessageService;
import com.fastbee.iot.data.service.IFirmwareTaskDetailService; import com.fastbee.iot.data.service.IFirmwareTaskDetailService;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.FunctionLog; import com.fastbee.iot.domain.FunctionLog;
import com.fastbee.iot.domain.Product; import com.fastbee.iot.domain.Product;
import com.fastbee.iot.domain.ProductAuthorize; import com.fastbee.iot.domain.ProductAuthorize;
@ -43,8 +41,6 @@ import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -297,6 +293,7 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
report.setUserId(message.getUserId()); report.setUserId(message.getUserId());
report.setUserName(message.getUserName()); report.setUserName(message.getUserName());
report.setDeviceName(message.getDeviceName()); report.setDeviceName(message.getDeviceName());
report.setSlaveId(message.getSlaveId());
report.setSources(message.getSources()); report.setSources(message.getSources());
report.setGwDeviceBo(message.getGwDeviceBo()); report.setGwDeviceBo(message.getGwDeviceBo());
// 属性上报执行规则引擎 // 属性上报执行规则引擎

View File

@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.annotation.SysProtocol; import com.fastbee.common.annotation.SysProtocol;
import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.core.mq.DeviceReport; import com.fastbee.common.core.mq.DeviceReport;
import com.fastbee.common.core.mq.GwDeviceBo;
import com.fastbee.common.core.mq.MQSendMessageBo; import com.fastbee.common.core.mq.MQSendMessageBo;
import com.fastbee.common.core.mq.SubDeviceBo; import com.fastbee.common.core.mq.SubDeviceBo;
import com.fastbee.common.core.mq.message.DeviceData; import com.fastbee.common.core.mq.message.DeviceData;
@ -111,6 +112,10 @@ public class ModbusToJsonHPProtocolService implements IProtocol {
result.add(simpleItem); result.add(simpleItem);
} }
} else { } else {
GwDeviceBo gwDeviceBo = new GwDeviceBo();
gwDeviceBo.setProductId(deviceData.getProductId());
gwDeviceBo.setSerialNumber(deviceData.getSerialNumber());
reportMessage.setGwDeviceBo(gwDeviceBo);
for (Map.Entry<String, Object> entry : values.entrySet()) { for (Map.Entry<String, Object> entry : values.entrySet()) {
String slaveKey = entry.getKey(); String slaveKey = entry.getKey();
Integer slaveId = StringUtils.matcherNum(slaveKey); Integer slaveId = StringUtils.matcherNum(slaveKey);
@ -146,6 +151,8 @@ public class ModbusToJsonHPProtocolService implements IProtocol {
report.setSerialNumber(vo.getSubDeviceNo()); report.setSerialNumber(vo.getSubDeviceNo());
report.setProductId(vo.getSubProductId()); report.setProductId(vo.getSubProductId());
report.setClientId(vo.getSubDeviceNo()); report.setClientId(vo.getSubDeviceNo());
report.setDeviceName(vo.getSubDeviceName());
report.setSlaveId(vo.getSlaveId());
break; break;
} }
} }

View File

@ -391,6 +391,8 @@ public class ModbusProtocol implements IProtocol {
report.setSubDeviceBo(vo); report.setSubDeviceBo(vo);
report.setSerialNumber(vo.getSubDeviceNo()); report.setSerialNumber(vo.getSubDeviceNo());
report.setProductId(vo.getSubProductId()); report.setProductId(vo.getSubProductId());
report.setDeviceName(vo.getSubDeviceName());
report.setSlaveId(vo.getSlaveId());
break; break;
} }
} }

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.annotation.SysProtocol; import com.fastbee.common.annotation.SysProtocol;
import com.fastbee.common.constant.FastBeeConstant; import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.common.core.mq.DeviceReport; import com.fastbee.common.core.mq.DeviceReport;
import com.fastbee.common.core.mq.GwDeviceBo;
import com.fastbee.common.core.mq.MQSendMessageBo; import com.fastbee.common.core.mq.MQSendMessageBo;
import com.fastbee.common.core.mq.SubDeviceBo; import com.fastbee.common.core.mq.SubDeviceBo;
import com.fastbee.common.core.mq.message.DeviceData; import com.fastbee.common.core.mq.message.DeviceData;
@ -76,6 +77,10 @@ public class ModbusTcpProtocol implements IProtocol {
// report.setSources(hexDump); // report.setSources(hexDump);
// return report; // return report;
// } // }
GwDeviceBo gwDeviceBo = new GwDeviceBo();
gwDeviceBo.setProductId(deviceData.getProductId());
gwDeviceBo.setSerialNumber(deviceData.getSerialNumber());
report.setGwDeviceBo(gwDeviceBo);
matchSubDev(report, deviceData.getSerialNumber(), slaveId); matchSubDev(report, deviceData.getSerialNumber(), slaveId);
String serialNumber = report.getSerialNumber(); String serialNumber = report.getSerialNumber();
Long productId = report.getProductId(); Long productId = report.getProductId();
@ -366,6 +371,8 @@ public class ModbusTcpProtocol implements IProtocol {
report.setSubDeviceBo(vo); report.setSubDeviceBo(vo);
report.setSerialNumber(vo.getSubDeviceNo()); report.setSerialNumber(vo.getSubDeviceNo());
report.setProductId(vo.getSubProductId()); report.setProductId(vo.getSubProductId());
report.setDeviceName(vo.getSubDeviceName());
report.setSlaveId(vo.getSlaveId());
break; break;
} }
} }

View File

@ -142,12 +142,16 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter {
reportBo.setThingsModelSimpleItem(report.getThingsModelSimpleItem()); reportBo.setThingsModelSimpleItem(report.getThingsModelSimpleItem());
reportBo.setPlatformDate(DateUtils.getNowDate()); reportBo.setPlatformDate(DateUtils.getNowDate());
reportBo.setSerialNumber(report.getClientId()); reportBo.setSerialNumber(report.getClientId());
reportBo.setProductId(report.getProductId());
reportBo.setServerType(ServerType.TCP); reportBo.setServerType(ServerType.TCP);
reportBo.setReplyMessage(report.getReplyMessage()); reportBo.setReplyMessage(report.getReplyMessage());
reportBo.setIsReply(report.getIsReply()); reportBo.setIsReply(report.getIsReply());
reportBo.setStatus(report.getStatus()); reportBo.setStatus(report.getStatus());
reportBo.setProtocolCode(FastBeeConstant.PROTOCOL.ModbusRtu); reportBo.setProtocolCode(FastBeeConstant.PROTOCOL.ModbusRtu);
reportBo.setSources(report.getSources()); reportBo.setSources(report.getSources());
reportBo.setSlaveId(report.getSlaveId());
reportBo.setGwDeviceBo(report.getGwDeviceBo());
reportBo.setDeviceName(report.getDeviceName());
MessageProducer.sendPublishMsg(reportBo); MessageProducer.sendPublishMsg(reportBo);
} }

View File

@ -2,16 +2,14 @@ package com.fastbee.iot.domain;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fastbee.common.annotation.Excel; import com.fastbee.common.annotation.Excel;
import com.fastbee.common.core.domain.BaseEntity; import com.fastbee.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Transient;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -239,6 +237,11 @@ public class Device extends BaseEntity {
@TableField(exist = false) @TableField(exist = false)
private Boolean showChild; private Boolean showChild;
@Transient
@ApiModelProperty("查询入口(1=设备管理列表 2=数据中心 3=全部设备)")
@TableField(exist = false)
private Integer entrance;
@TableField(exist = false) @TableField(exist = false)
private List<Device> subDeviceList; private List<Device> subDeviceList;

View File

@ -0,0 +1,42 @@
package com.fastbee.iot.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 设备查询入口枚举,由前端传入,控制不同页面的设备过滤策略
*/
@AllArgsConstructor
@Getter
public enum DeviceEntrance {
/**
* 设备管理列表 —— 过滤子设备(device_type=4),只展示直连/网关/监控设备
*/
DEVICE_MANAGE(1, "设备管理列表"),
/**
* 数据中心(历史数据) —— 主设备下拉,子设备通过网关接口单独加载
*/
DATA_CENTER(2, "数据中心"),
/**
* 设备全量查询 —— 不做任何过滤,返回所有设备
*/
DEVICE_ALL(3, "全部设备");
private final int code;
private final String name;
public static DeviceEntrance fromCode(Integer code) {
if (code == null) {
return null;
}
for (DeviceEntrance value : DeviceEntrance.values()) {
if (value.code == code) {
return value;
}
}
return null;
}
}

View File

@ -1125,67 +1125,9 @@ public class DeviceServiceImpl implements IDeviceService {
} }
} }
} else if (DeviceType.GATEWAY.getCode() == device.getDeviceType()) { } else if (DeviceType.GATEWAY.getCode() == device.getDeviceType()) {
// LambdaQueryWrapper<ProductSubGateway> queryWrapper = new LambdaQueryWrapper<>();
// queryWrapper.eq(ProductSubGateway::getGwProductId, product.getProductId());
// 处理子设备添加和子设备轮询
ProductSubGateway productSubGateway = new ProductSubGateway();
productSubGateway.setGwProductId(product.getProductId());
List<ProductSubGatewayVO> productSubGatewayList = productSubGatewayMapper.selectListVO(productSubGateway);
if (CollectionUtils.isEmpty(productSubGatewayList)) {
return;
}
productSubGatewayList.sort(Comparator.comparing(ProductSubGatewayVO::getSlaveId));
List<Long> subProductIdList = productSubGatewayList.stream().map(ProductSubGatewayVO::getSubProductId)
.collect(Collectors.toList());
LambdaQueryWrapper<ProductModbusJob> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ProductModbusJob::getProductId, subProductIdList);
List<ProductModbusJob> productModbusJobList = productModbusJobMapper.selectList(queryWrapper);
Map<Long, List<ProductModbusJob>> productModbusJobMap = productModbusJobList.stream()
.collect(Collectors.groupingBy(ProductModbusJob::getProductId));
// 新增子设备、绑定子设备、子设备生成轮询
for (ProductSubGatewayVO productSubGatewayVO : productSubGatewayList) {
Device addDevice = new Device();
addDevice.setDeviceName(
productSubGatewayVO.getSubProductName() + "_" + productSubGatewayVO.getSlaveId());
addDevice.setProductId(productSubGatewayVO.getSubProductId());
addDevice.setProductName(productSubGatewayVO.getSubProductName());
String serialNumber = this.generationDeviceNum(1);
addDevice.setSerialNumber(serialNumber);
addDevice.setTenantId(device.getTenantId());
addDevice.setTenantName(device.getTenantName());
addDevice.setCreateBy(device.getCreateBy());
addDevice.setFirmwareVersion(new BigDecimal(1));
addDevice.setRssi(0);
addDevice.setIsShadow(0);
addDevice.setLocationWay(productSubGatewayVO.getSubLocationWay());
addDevice.setImgUrl(product.getImgUrl());
addDevice.setCreateTime(new Date());
deviceMapper.insertDevice(addDevice);
SubGateway subGateway = new SubGateway();
subGateway.setGwDeviceId(device.getDeviceId());
subGateway.setSubDeviceId(addDevice.getDeviceId());
subGateway.setSlaveId(productSubGatewayVO.getSlaveId());
subGateway.setCreateBy(addDevice.getCreateBy());
subGateway.setCreateTime(new Date());
subGatewayMapper.insertGateway(subGateway);
List<ProductModbusJob> productModbusList = productModbusJobMap
.get(productSubGatewayVO.getSubProductId());
if (!CollectionUtils.isEmpty(productModbusList)) {
for (ProductModbusJob productModbusJob : productModbusList) {
ModbusJob modbusJob = new ModbusJob();
modbusJob.setJobName(productModbusJob.getJobName());
modbusJob.setSubDeviceId(addDevice.getDeviceId());
modbusJob.setSubSerialNumber(addDevice.getSerialNumber());
modbusJob.setCommand(productModbusJob.getCommand());
modbusJob.setStatus(productModbusJob.getStatus());
modbusJob.setRemark(productModbusJob.getRemark());
modbusJobService.insertModbusJob(modbusJob);
if (FastBeeConstant.PROTOCOL.ModbusTcp.equals(product.getProtocolCode())) { if (FastBeeConstant.PROTOCOL.ModbusTcp.equals(product.getProtocolCode())) {
redisCache.getCacheModbusTcpId(device.getSerialNumber()); redisCache.getCacheModbusTcpId(device.getSerialNumber());
} }
}
}
}
} }
} }

View File

@ -398,6 +398,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if> <if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if>
<where> <where>
and d.gw_dev_code is null and d.gw_dev_code is null
<if test="entrance != null and entrance == 1">
and (p.device_type is null or p.device_type != 4)
</if>
<if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if> <if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if>
<if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if> <if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if>
<if test="productId != null "> and d.product_id = #{productId}</if> <if test="productId != null "> and d.product_id = #{productId}</if>
@ -881,6 +884,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if> <if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if>
<where> <where>
and d.gw_dev_code is null and d.gw_dev_code is null
<if test="entrance != null and entrance == 1">
and (p.device_type is null or p.device_type != 4)
</if>
<if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if> <if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if>
<if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if> <if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if>
<if test="productId != null "> and d.product_id = #{productId}</if> <if test="productId != null "> and d.product_id = #{productId}</if>

View File

@ -104,6 +104,7 @@
"device.device-edit.148398-75": "video surveillance", "device.device-edit.148398-75": "video surveillance",
"device.device-edit.148398-76": "Data debugging", "device.device-edit.148398-76": "Data debugging",
"device.device-edit.148398-77": "Polling task", "device.device-edit.148398-77": "Polling task",
"device.device-edit.subDevicePollingManage": "Polling management",
"device.device-edit.148398-78": "Gateway", "device.device-edit.148398-78": "Gateway",
"device.device-edit.148398-79": "Cloud recording", "device.device-edit.148398-79": "Cloud recording",
"device.device-edit.148398-80": "Alert user", "device.device-edit.148398-80": "Alert user",

View File

@ -104,6 +104,7 @@
"device.device-edit.148398-75": "视频监控", "device.device-edit.148398-75": "视频监控",
"device.device-edit.148398-76": "数据调试", "device.device-edit.148398-76": "数据调试",
"device.device-edit.148398-77": "轮询任务", "device.device-edit.148398-77": "轮询任务",
"device.device-edit.subDevicePollingManage": "轮询管理",
"device.device-edit.148398-78": "网关", "device.device-edit.148398-78": "网关",
"device.device-edit.148398-79": "云端录像", "device.device-edit.148398-79": "云端录像",
"device.device-edit.148398-80": "告警用户", "device.device-edit.148398-80": "告警用户",

View File

@ -10,6 +10,11 @@
<el-option v-for="(item, index) in devDeviceList" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option> <el-option v-for="(item, index) in devDeviceList" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="devSubDeviceList.length > 0" label="子设备" prop="subDeviceId">
<el-select style="width: 100%" v-model="devQueryParams.subDeviceId" placeholder="请选择子设备" filterable @change="handleDevSubDeviceChange">
<el-option v-for="item in devSubDeviceList" :key="item.subDeviceId" :label="item.subDeviceName + ' (从机' + item.slaveId + ')'" :value="item.subDeviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers"> <el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers">
<el-select style="width: 100%" v-model="devQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" filterable multiple collapse-tags @change="handleDevIdentifierChange"> <el-select style="width: 100%" v-model="devQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" filterable multiple collapse-tags @change="handleDevIdentifierChange">
<el-option label="全部勾选/反选" value="__selectAll__"> <el-option label="全部勾选/反选" value="__selectAll__">
@ -148,6 +153,7 @@
<script> <script>
import moment from 'moment'; import moment from 'moment';
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js'; import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
import { listGateway } from '@/api/iot/gateway.js';
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js'; import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
import { getDataCenterDeviceHistory, getDataCenterSceneHistory, exportDeviceHistory } from '@/api/data/center.js'; import { getDataCenterDeviceHistory, getDataCenterSceneHistory, exportDeviceHistory } from '@/api/data/center.js';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
@ -159,6 +165,7 @@ export default {
return { return {
activeTab: 'device', activeTab: 'device',
devDeviceList: [], // 设备-设备列表 devDeviceList: [], // 设备-设备列表
devSubDeviceList: [], // 网关子设备列表
devIdentifierList: [], devIdentifierList: [],
pickerOptions: { pickerOptions: {
shortcuts: [ shortcuts: [
@ -212,6 +219,7 @@ export default {
// 查询参数 // 查询参数
devQueryParams: { devQueryParams: {
deviceId: null, deviceId: null,
subDeviceId: null,
identifiers: [], identifiers: [],
dayDaterange: [new Date(new Date().getTime() - 3600 * 1000 * 2), new Date()], dayDaterange: [new Date(new Date().getTime() - 3600 * 1000 * 2), new Date()],
}, },
@ -299,6 +307,7 @@ export default {
getDevDeviceList() { getDevDeviceList() {
const params = { const params = {
showChild: true, showChild: true,
entrance: 2,
pageNum: 1, pageNum: 1,
pageSize: 9999, pageSize: 9999,
}; };
@ -311,7 +320,34 @@ export default {
// 选择设备后 // 选择设备后
handleDevDeviceChange(val) { handleDevDeviceChange(val) {
this.devQueryParams.identifiers = []; this.devQueryParams.identifiers = [];
this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = [];
this.devIdentifierList = [];
const device = this.devDeviceList.find((item) => item.deviceId === val);
if (device && device.deviceType === 2) {
this.getDevSubDeviceList(val);
} else {
this.getDevIdentifierList(val); this.getDevIdentifierList(val);
}
},
// 选择子设备后
handleDevSubDeviceChange(val) {
this.devQueryParams.identifiers = [];
this.devIdentifierList = [];
if (val) {
this.getDevIdentifierList(val);
}
},
// 获取网关子设备列表
getDevSubDeviceList(gwDeviceId) {
listGateway({ gwDeviceId: gwDeviceId, pageNum: 1, pageSize: 9999 }).then((res) => {
if (res.code === 200) {
this.devSubDeviceList = res.rows || [];
if (this.devSubDeviceList.length === 0) {
this.getDevIdentifierList(gwDeviceId);
}
}
});
}, },
// 设备变量选择变化 // 设备变量选择变化
handleDevIdentifierChange(val) { handleDevIdentifierChange(val) {
@ -342,21 +378,33 @@ export default {
}; };
listThingsModel(params).then((res) => { listThingsModel(params).then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.devIdentifierList = res.rows; this.devIdentifierList = res.rows || [];
} }
}); });
}, },
// 获取实际查询用的设备信息(子设备优先)
getQueryDevice() {
const device = this.devDeviceList.find((item) => item.deviceId === this.devQueryParams.deviceId);
if (!device) return null;
if (this.devSubDeviceList.length > 0 && this.devQueryParams.subDeviceId) {
const subDevice = this.devSubDeviceList.find((item) => item.subDeviceId === this.devQueryParams.subDeviceId);
if (subDevice) {
return { deviceId: subDevice.subDeviceId, serialNumber: subDevice.subDeviceNo };
}
}
return { deviceId: device.deviceId, serialNumber: device.serialNumber };
},
// 获取设备图形数据 // 获取设备图形数据
getDevChartDatas() { getDevChartDatas() {
this.loading = true; this.loading = true;
const devices = this.devDeviceList.find((item) => item.deviceId === this.devQueryParams.deviceId); const queryDevice = this.getQueryDevice();
const identifierList = this.devQueryParams.identifiers.map((item) => { const identifierList = this.devQueryParams.identifiers.map((item) => {
const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item); const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item);
return { identifier: identifiers.identifier, type: identifiers.type }; return { identifier: identifiers.identifier, type: identifiers.type };
}); });
const params = { const params = {
deviceId: devices.deviceId, deviceId: queryDevice.deviceId,
serialNumber: devices.serialNumber, serialNumber: queryDevice.serialNumber,
identifierList: identifierList, identifierList: identifierList,
beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'), beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'), endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
@ -376,19 +424,24 @@ export default {
}, },
// 搜索按钮操作 // 搜索按钮操作
handleDevQuery() { handleDevQuery() {
const isField = this.areAllFields(this.devQueryParams); if (!this.devQueryParams.deviceId || this.devQueryParams.identifiers.length === 0 || !this.devQueryParams.dayDaterange || this.devQueryParams.dayDaterange.length === 0) {
if (isField) { return;
this.getDevChartDatas();
} }
if (this.devSubDeviceList.length > 0 && !this.devQueryParams.subDeviceId) {
return;
}
this.getDevChartDatas();
}, },
// 重置按钮操作 // 重置按钮操作
handleDevResetQuery() { handleDevResetQuery() {
this.resetForm('devQueryForm'); this.resetForm('devQueryForm');
this.devQueryParams.identifiers = []; this.devQueryParams.identifiers = [];
this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = [];
this.devIdentifierList = [];
this.devDatas = []; this.devDatas = [];
this.devTableComTemp = []; this.devTableComTemp = [];
this.devTotal = 0; this.devTotal = 0;
this.handleDevQuery();
}, },
// 获取设备列表数据 // 获取设备列表数据
getDevTableList(e) { getDevTableList(e) {
@ -612,38 +665,38 @@ export default {
}, },
// 导出设备历史数据 // 导出设备历史数据
async handleDevExport() { async handleDevExport() {
const isField = this.areAllFields(this.devQueryParams); if (!this.devQueryParams.deviceId || this.devQueryParams.identifiers.length === 0 || !this.devQueryParams.dayDaterange || this.devQueryParams.dayDaterange.length === 0) {
if (!isField) {
this.$message.warning('请先选择设备、变量和时间范围'); this.$message.warning('请先选择设备、变量和时间范围');
return; return;
} }
if (this.devSubDeviceList.length > 0 && !this.devQueryParams.subDeviceId) {
this.$message.warning('请选择子设备');
return;
}
if (this.devDatas.length === 0) { if (this.devDatas.length === 0) {
this.$message.warning('没有数据可导出'); this.$message.warning('没有数据可导出');
return; return;
} }
try { try {
this.exportLoading = true; this.exportLoading = true;
const devices = this.devDeviceList.find((item) => item.deviceId === this.devQueryParams.deviceId); const queryDevice = this.getQueryDevice();
const identifierList = this.devQueryParams.identifiers.map((item) => { const identifierList = this.devQueryParams.identifiers.map((item) => {
const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item); const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item);
return { identifier: identifiers.identifier, type: identifiers.type }; return { identifier: identifiers.identifier, type: identifiers.type };
}); });
const params = { const params = {
deviceId: devices.deviceId, deviceId: queryDevice.deviceId,
serialNumber: devices.serialNumber, serialNumber: queryDevice.serialNumber,
identifierList: identifierList, identifierList: identifierList,
beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'), beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'), endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
}; };
const res = await exportDeviceHistory(params); const res = await exportDeviceHistory(params);
// 检查是否为Blob对象
if (res instanceof Blob) { if (res instanceof Blob) {
// 验证是否为有效的blob(不是错误JSON)
const clonedBlob = res.slice(); const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob); const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) { if (!isValidBlob) {
// 可能是错误响应,尝试解析
try { try {
const text = await res.text(); const text = await res.text();
const errorObj = JSON.parse(text); const errorObj = JSON.parse(text);
@ -654,13 +707,11 @@ export default {
return; return;
} }
} }
// 有效的Excel文件,直接下载 - 使用设备编号作为文件名 const deviceSerialNumber = queryDevice.serialNumber || '设备数据';
const deviceSerialNumber = devices.serialNumber || '设备数据';
const fileName = `${deviceSerialNumber}_${moment().format('YYYYMMDDHHmmss')}.xlsx`; const fileName = `${deviceSerialNumber}_${moment().format('YYYYMMDDHHmmss')}.xlsx`;
saveAs(res, fileName); saveAs(res, fileName);
this.$message.success('导出成功'); this.$message.success('导出成功');
} else { } else {
// 非Blob响应,可能是错误
this.$message.error('导出失败:服务器返回格式错误'); this.$message.error('导出失败:服务器返回格式错误');
} }
} catch (err) { } catch (err) {

View File

@ -175,6 +175,17 @@
<device-sub ref="deviceSub" :device="form"/> <device-sub ref="deviceSub" :device="form"/>
</el-tab-pane> </el-tab-pane>
<!-- 子设备轮询任务管理(仅网关设备 + Modbus协议显示) -->
<el-tab-pane
name="gwSubModbusTask"
:disabled="form.deviceId == 0"
v-if="form.deviceType == 2 && (form.protocolCode == 'MODBUS-RTU' || form.protocolCode == 'MODBUS-TCP')"
lazy
>
<span slot="label">{{$t('device.device-edit.subDevicePollingManage')}}</span>
<gw-sub-modbus-task ref="gwSubModbusTask" :device="form"/>
</el-tab-pane>
<el-tab-pane name="sipChannel" :disabled="form.deviceId == 0" v-if="form.deviceType === 3" lazy> <el-tab-pane name="sipChannel" :disabled="form.deviceId == 0" v-if="form.deviceType === 3" lazy>
<span slot="label">{{ $t('device.device-edit.148398-44') }}</span> <span slot="label">{{ $t('device.device-edit.148398-44') }}</span>
<channel ref="Channel" :device="form" @playerEvent="getPlayerData($event)"/> <channel ref="Channel" :device="form" @playerEvent="getPlayerData($event)"/>
@ -375,6 +386,7 @@ import deviceInlineVideo from './device-inline-video';
import deviceFuncLog from './device-functionlog'; import deviceFuncLog from './device-functionlog';
import deviceSub from './device-sub'; import deviceSub from './device-sub';
import deviceThingsModel from './device-things-model.vue'; import deviceThingsModel from './device-things-model.vue';
import gwSubModbusTask from './gw-sub-modbus-task';
import vueQr from 'vue-qr'; import vueQr from 'vue-qr';
import {loadMap} from '@/utils/map.js'; import {loadMap} from '@/utils/map.js';
import { import {
@ -424,6 +436,7 @@ export default {
deviceModbusTask, deviceModbusTask,
deviceInlineVideo, deviceInlineVideo,
deviceThingsModel, deviceThingsModel,
gwSubModbusTask,
}, },
watch: { watch: {
activeName(val) { activeName(val) {

View File

@ -219,9 +219,9 @@ export default {
}, },
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const deviceIds = row.deviceId || this.ids; const deviceIds = row.id || this.ids;
this.$modal this.$modal
.confirm(this.$t('device.device-sub.299018-26', [deviceIds])) .confirm(this.$t('device.device-sub.299018-25', [deviceIds]))
.then(function () { .then(function () {
return delGateway(deviceIds); return delGateway(deviceIds);
}) })

View File

@ -0,0 +1,260 @@
<template>
<div class="gw-sub-modbus-task">
<el-row :gutter="12" style="height: 100%">
<!-- 左侧:子设备列表 -->
<el-col :span="6">
<div class="sub-device-panel">
<div class="panel-title">
<i class="el-icon-connection"></i>
<span>子设备列表</span>
<el-tag size="mini" type="info" style="margin-left: 8px">{{ subDeviceList.length }}</el-tag>
</div>
<div v-loading="listLoading" class="sub-device-list">
<div
v-for="item in subDeviceList"
:key="item.subDeviceId"
class="sub-device-item"
:class="{ active: selectedSubDevice && selectedSubDevice.deviceId === item.subDeviceId }"
@click="handleSelectSubDevice(item)"
>
<div class="sub-device-name">
<i class="el-icon-cpu"></i>
{{ item.subDeviceName || item.subDeviceNo }}
</div>
<div class="sub-device-meta">
<span class="meta-item">
<i class="el-icon-document"></i>
从机地址:{{ item.slaveId || '-' }}
</span>
<span class="meta-item">
<el-badge
:type="item.status === 3 ? 'success' : 'info'"
is-dot
style="margin-right: 4px"
/>
{{ item.status === 3 ? '在线' : '离线' }}
</span>
</div>
<div class="sub-device-sn">{{ item.subDeviceNo }}</div>
</div>
<el-empty
v-if="!listLoading && subDeviceList.length === 0"
description="暂无子设备"
:image-size="80"
/>
</div>
</div>
</el-col>
<!-- 右侧:轮询任务 -->
<el-col :span="18">
<div class="task-panel">
<div v-if="selectedSubDevice" class="task-panel-header">
<span class="selected-device-info">
<i class="el-icon-right" style="color: #409eff"></i>
当前设备:<strong>{{ selectedSubDevice.deviceName }}</strong>
<el-tag size="mini" style="margin-left: 8px">SN: {{ selectedSubDevice.serialNumber }}</el-tag>
<el-tag size="mini" type="warning" style="margin-left: 4px">从机地址: {{ selectedSubDevice.slaveId }}</el-tag>
</span>
</div>
<device-modbus-task
v-if="selectedSubDevice"
ref="modbusTask"
:device="selectedSubDevice"
/>
<el-empty
v-else
description="请从左侧选择子设备以管理其轮询任务"
:image-size="120"
style="margin-top: 80px"
/>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { listGateway } from '@/api/iot/gateway';
import deviceModbusTask from './device-modbus-task';
export default {
name: 'gw-sub-modbus-task',
components: {
deviceModbusTask,
},
props: {
device: {
type: Object,
default: null,
},
},
data() {
return {
listLoading: false,
subDeviceList: [],
selectedSubDevice: null,
};
},
watch: {
device: {
handler(newVal) {
if (newVal && newVal.deviceId && newVal.deviceId != 0) {
this.selectedSubDevice = null;
this.loadSubDeviceList();
}
},
},
},
mounted() {
if (this.device && this.device.deviceId && this.device.deviceId != 0) {
this.loadSubDeviceList();
}
},
methods: {
loadSubDeviceList() {
this.listLoading = true;
listGateway({ gwDeviceId: this.device.deviceId, pageSize: 999 })
.then((response) => {
this.subDeviceList = response.rows || [];
// 默认选中第一个有从机地址的子设备
if (this.subDeviceList.length > 0 && !this.selectedSubDevice) {
this.handleSelectSubDevice(this.subDeviceList[0]);
}
})
.finally(() => {
this.listLoading = false;
});
},
handleSelectSubDevice(item) {
// 构造 device-modbus-task 组件所需的 device 对象
this.selectedSubDevice = {
deviceId: item.subDeviceId,
deviceName: item.subDeviceName || item.subDeviceNo,
serialNumber: item.subDeviceNo,
productId: item.subProductId,
protocolCode: this.device.protocolCode,
deviceType: 4, // SUB_GATEWAY
slaveId: item.slaveId,
};
},
},
};
</script>
<style lang="scss" scoped>
.gw-sub-modbus-task {
padding: 8px 0;
min-height: 500px;
}
.sub-device-panel {
border: 1px solid #e4e7ed;
border-radius: 4px;
height: 100%;
min-height: 500px;
display: flex;
flex-direction: column;
overflow: hidden;
.panel-title {
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
color: #303133;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
i {
margin-right: 6px;
color: #409eff;
}
}
.sub-device-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.sub-device-item {
padding: 10px 16px;
cursor: pointer;
border-left: 3px solid transparent;
transition: all 0.2s;
&:hover {
background: #f0f7ff;
}
&.active {
background: #ecf5ff;
border-left-color: #409eff;
.sub-device-name {
color: #409eff;
}
}
.sub-device-name {
font-size: 13px;
font-weight: 500;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
i {
margin-right: 4px;
font-size: 12px;
}
}
.sub-device-meta {
display: flex;
align-items: center;
gap: 10px;
margin-top: 4px;
.meta-item {
font-size: 11px;
color: #909399;
display: flex;
align-items: center;
}
}
.sub-device-sn {
font-size: 11px;
color: #c0c4cc;
margin-top: 2px;
font-family: monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.task-panel {
.task-panel-header {
padding: 8px 0 12px;
font-size: 13px;
color: #606266;
border-bottom: 1px dashed #e4e7ed;
margin-bottom: 12px;
.selected-device-info {
display: flex;
align-items: center;
gap: 4px;
strong {
color: #303133;
}
}
}
}
</style>

View File

@ -356,6 +356,7 @@ export default {
pageNum: 1, pageNum: 1,
pageSize: 12, pageSize: 12,
showChild: true, showChild: true,
entrance: 1,
deviceName: null, deviceName: null,
productId: null, productId: null,
groupId: null, groupId: null,

View File

@ -21,6 +21,9 @@
<div> <div>
{{ item.type == 0 ? $t('device.instruction-parsing.830424-1') : item.type == 1 ? $t('device.instruction-parsing.830424-2') : $t('device.instruction-parsing.830424-3') }} {{ item.type == 0 ? $t('device.instruction-parsing.830424-1') : item.type == 1 ? $t('device.instruction-parsing.830424-2') : $t('device.instruction-parsing.830424-3') }}
</div> </div>
<el-tag v-if="item.subDeviceName" size="mini" type="warning" style="margin-left: 8px;" :title="item.subSerialNumber">
{{ item.subDeviceName }} (从机{{ item.subSlaveId }})
</el-tag>
<div class="head-time">{{ item.time }}</div> <div class="head-time">{{ item.time }}</div>
<div class="analysis-btn right-btn" v-if="item.type != 0 && !item.analysis" v-loading="item.loading" @click.stop="analysisData(item, index)"> <div class="analysis-btn right-btn" v-if="item.type != 0 && !item.analysis" v-loading="item.loading" @click.stop="analysisData(item, index)">
{{ $t('device.instruction-parsing.830424-4') }} {{ $t('device.instruction-parsing.830424-4') }}
@ -534,6 +537,9 @@ export default {
analysis: false, analysis: false,
//解析的数据数组 //解析的数据数组
analysisList: data.message, analysisList: data.message,
subDeviceName: data.subDeviceName || '',
subSerialNumber: data.subSerialNumber || '',
subSlaveId: data.subSlaveId,
}); });
} }
}, },

View File

@ -54,7 +54,7 @@
:disabled="!item.enabled" :disabled="!item.enabled"
size="small" size="small"
@change="onCountChange(index)" @change="onCountChange(index)"
@input.native="onCountInput(index, $event)" @keyup.native="onCountKeyup(index, $event)"
/> />
</div> </div>
</el-col> </el-col>
@ -194,10 +194,12 @@ export default {
onCountChange(index) { onCountChange(index) {
this.rebuildDevices(index); this.rebuildDevices(index);
}, },
onCountInput(index, e) { onCountKeyup(index, e) {
this.$nextTick(() => { if (!e.target || e.target.tagName !== 'INPUT') return;
const val = parseInt(e.target.value);
if (isNaN(val) || val < 1 || val > 10) return;
this.subProductList[index].count = val;
this.rebuildDevices(index); this.rebuildDevices(index);
});
}, },
rebuildDevices(index) { rebuildDevices(index) {
const item = this.subProductList[index]; const item = this.subProductList[index];