20260426:条件触发任务
This commit is contained in:
parent
d433c25f7b
commit
49cd12e1e3
@ -0,0 +1,218 @@
|
||||
package com.fastbee.iot.data.executor;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.common.constant.FastBeeConstant;
|
||||
import com.fastbee.common.core.mq.message.DeviceMessage;
|
||||
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.iot.data.service.IDeviceMessageService;
|
||||
import com.fastbee.iot.domain.ModbusJob;
|
||||
import com.fastbee.iot.service.IModbusJobService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 条件触发任务执行器(cycleType=4)
|
||||
* <p>
|
||||
* 每次设备上报属性/功能数据时,检查是否存在满足条件的 Modbus 任务,
|
||||
* 满足则立即下发对应的 Modbus 指令(与上电触发使用同一套指令下发通道)。
|
||||
* <p>
|
||||
* 条件存储在 {@code iot_modbus_job.remark} 字段中,JSON 格式:
|
||||
* <pre>{"identifier":"temp","operator":">","value":"50"}</pre>
|
||||
* 支持的 operator:{@code = != > < >= <=}
|
||||
*
|
||||
* @author Sumuxi
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ConditionTriggerTaskExecutor {
|
||||
|
||||
@Resource
|
||||
private IModbusJobService modbusJobService;
|
||||
|
||||
@Resource
|
||||
private IDeviceMessageService deviceMessageService;
|
||||
|
||||
/**
|
||||
* 异步检查并执行条件触发任务。
|
||||
* 由 {@code DataHandlerImpl.reportData()} 在每次属性/功能上报后调用。
|
||||
*
|
||||
* @param serialNumber 上报数据的设备编号(子设备或直连设备)
|
||||
* @param thingsModelSimpleItems 本次上报的物模型值列表
|
||||
*/
|
||||
@Async(FastBeeConstant.TASK.TRIGGER_POWER_ON_TASK_ASYNC)
|
||||
public void checkAndTrigger(String serialNumber, List<ThingsModelSimpleItem> thingsModelSimpleItems) {
|
||||
if (StringUtils.isEmpty(serialNumber) || CollectionUtils.isEmpty(thingsModelSimpleItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询该设备所有状态正常的条件触发任务
|
||||
ModbusJob query = new ModbusJob();
|
||||
query.setSubSerialNumber(serialNumber);
|
||||
query.setStatus("0");
|
||||
query.setCycleType(4);
|
||||
List<ModbusJob> jobList = modbusJobService.selectModbusJobList(query);
|
||||
if (CollectionUtils.isEmpty(jobList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ModbusJob job : jobList) {
|
||||
try {
|
||||
if (isConditionMet(job, thingsModelSimpleItems)) {
|
||||
dispatchCommand(serialNumber, job);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[条件触发] 执行异常,设备:{},任务ID:{}", serialNumber, job.getTaskId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断本次上报数据是否满足任务配置的条件。
|
||||
* <p>
|
||||
* 支持两种 remark 格式:
|
||||
* <ul>
|
||||
* <li>多条件:{@code {"logic":"and","conditions":[{...},{...}]}}</li>
|
||||
* </ul>
|
||||
* logic 为 "or" 时任一条件满足即触发;默认("and")要求全部满足。
|
||||
*/
|
||||
private boolean isConditionMet(ModbusJob job, List<ThingsModelSimpleItem> thingsModelSimpleItems) {
|
||||
String remark = job.getRemark();
|
||||
if (StringUtils.isEmpty(remark)) {
|
||||
log.warn("[条件触发] 任务 {} remark 为空,跳过", job.getTaskId());
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
try {
|
||||
json = JSON.parseObject(remark);
|
||||
} catch (Exception e) {
|
||||
log.warn("[条件触发] 任务 {} remark 解析失败:{}", job.getTaskId(), remark);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONArray conditions = json.getJSONArray("conditions");
|
||||
if (conditions == null || conditions.isEmpty()) {
|
||||
log.warn("[条件触发] 任务 {} conditions 为空", job.getTaskId());
|
||||
return false;
|
||||
}
|
||||
String logic = json.getString("logic");
|
||||
boolean isOr = "or".equalsIgnoreCase(logic);
|
||||
for (int i = 0; i < conditions.size(); i++) {
|
||||
JSONObject cond = conditions.getJSONObject(i);
|
||||
boolean met = evaluateSingleCondition(cond, thingsModelSimpleItems, job.getTaskId());
|
||||
if (isOr && met) return true;
|
||||
if (!isOr && !met) return false;
|
||||
}
|
||||
return !isOr; // AND:全部通过; OR:无一通过
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估单个条件对象(含 identifier / operator / value 字段)。
|
||||
*/
|
||||
private boolean evaluateSingleCondition(JSONObject condition,
|
||||
List<ThingsModelSimpleItem> items,
|
||||
Long taskId) {
|
||||
String identifier = condition.getString("identifier");
|
||||
String operator = condition.getString("operator");
|
||||
String condValue = condition.getString("value");
|
||||
|
||||
if (StringUtils.isEmpty(identifier) || StringUtils.isEmpty(operator) || condValue == null) {
|
||||
log.warn("[条件触发] 任务 {} 条件配置不完整:{}", taskId, condition);
|
||||
return false;
|
||||
}
|
||||
|
||||
String reportedValue = null;
|
||||
for (ThingsModelSimpleItem item : items) {
|
||||
if (identifier.equals(item.getId())) {
|
||||
reportedValue = item.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (reportedValue == null) {
|
||||
// 本次上报不含该属性,条件视为不满足
|
||||
return false;
|
||||
}
|
||||
|
||||
return evaluate(reportedValue, operator, condValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件表达式计算。
|
||||
* 优先尝试数值比较,失败则降级为字符串比较。
|
||||
*/
|
||||
private boolean evaluate(String actual, String operator, String expected) {
|
||||
try {
|
||||
BigDecimal a = new BigDecimal(actual.trim());
|
||||
BigDecimal b = new BigDecimal(expected.trim());
|
||||
int cmp = a.compareTo(b);
|
||||
switch (operator) {
|
||||
case "=": return cmp == 0;
|
||||
case "!=": return cmp != 0;
|
||||
case ">": return cmp > 0;
|
||||
case "<": return cmp < 0;
|
||||
case ">=": return cmp >= 0;
|
||||
case "<=": return cmp <= 0;
|
||||
default:
|
||||
log.warn("[条件触发] 不支持的操作符:{}", operator);
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 非数值,降级为字符串等于/不等于
|
||||
switch (operator) {
|
||||
case "=": return actual.equals(expected);
|
||||
case "!=": return !actual.equals(expected);
|
||||
default:
|
||||
log.warn("[条件触发] 非数值不支持操作符 {},actual={}", operator, actual);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下发 Modbus 指令(HEX 格式)。
|
||||
* 复用与上电触发相同的消息通道。
|
||||
*/
|
||||
private void dispatchCommand(String serialNumber, ModbusJob job) {
|
||||
String command = job.getCommand();
|
||||
if (StringUtils.isEmpty(command)) {
|
||||
log.warn("[条件触发] 任务 {} 指令为空,跳过下发", job.getTaskId());
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceMessage msg = new DeviceMessage();
|
||||
msg.setSerialNumber(serialNumber);
|
||||
msg.setTopicName("/function/get");
|
||||
msg.setDataType("hex");
|
||||
msg.setMessage(command);
|
||||
|
||||
try {
|
||||
deviceMessageService.messagePost(msg);
|
||||
// 记录执行时间
|
||||
updateExecuteTime(job.getTaskId());
|
||||
log.info("[条件触发] 下发成功,设备:{},任务ID:{},指令:{}", serialNumber, job.getTaskId(), command);
|
||||
} catch (Exception e) {
|
||||
log.error("[条件触发] 下发异常,设备:{},任务ID:{}", serialNumber, job.getTaskId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务最近执行时间。
|
||||
*/
|
||||
private void updateExecuteTime(Long taskId) {
|
||||
if (taskId == null) return;
|
||||
ModbusJob update = new ModbusJob();
|
||||
update.setTaskId(taskId);
|
||||
update.setExecuteTime(new Date());
|
||||
modbusJobService.updateModbusJob(update);
|
||||
}
|
||||
}
|
||||
@ -89,6 +89,7 @@ public class SceneContext {
|
||||
/**
|
||||
* 处理规则脚本
|
||||
* 静默时间:silent,在静默时间内即使满足触发条件也不执行动作
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private boolean process(String json) throws InterruptedException {
|
||||
@ -96,7 +97,8 @@ public class SceneContext {
|
||||
ScriptTemplate scriptTemplate = JSON.parseObject(json, ScriptTemplate.class);
|
||||
String requestId = "scene/" + scriptTemplate.getChainName();
|
||||
scene_logger.info("[{}]=====+>场景联动规则开始执行!", requestId);
|
||||
scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, JSON.toJSONString(scriptTemplate), JSON.toJSONString(this));
|
||||
scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, JSON.toJSONString(scriptTemplate),
|
||||
JSON.toJSONString(this));
|
||||
// MDC.put("id", scriptTemplate.getChainName());
|
||||
// 触发器
|
||||
if (scriptTemplate.getPurpose() == 2) {
|
||||
@ -513,7 +515,8 @@ public class SceneContext {
|
||||
alertPushParams.setDeviceId(device.getDeviceId());
|
||||
alertPushParams.setDeviceName(device.getDeviceName());
|
||||
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
|
||||
// List<DeviceUser> deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId());
|
||||
// List<DeviceUser> deviceUserList =
|
||||
// deviceUserService.getDeviceUserAndShare(device.getDeviceId());
|
||||
// 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
|
||||
Set<Long> userIdSet = new HashSet<>();
|
||||
try {
|
||||
@ -547,7 +550,8 @@ public class SceneContext {
|
||||
return alertPushParams;
|
||||
}
|
||||
|
||||
private AlertLog getTerminalUserAlertLog(SceneTerminalUserVO sceneTerminalUserVO, Device device, SceneThingsModelItem sceneThingsModelItem) {
|
||||
private AlertLog getTerminalUserAlertLog(SceneTerminalUserVO sceneTerminalUserVO, Device device,
|
||||
SceneThingsModelItem sceneThingsModelItem) {
|
||||
AlertLog alertLog = new AlertLog();
|
||||
alertLog.setAlertName("设备告警");
|
||||
alertLog.setAlertLevel(1L);
|
||||
|
||||
@ -17,6 +17,7 @@ import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.date.LocalDateTimeUtils;
|
||||
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
|
||||
import com.fastbee.iot.cache.SceneModelTagCache;
|
||||
import com.fastbee.iot.data.executor.ConditionTriggerTaskExecutor;
|
||||
import com.fastbee.iot.data.service.IDataHandler;
|
||||
import com.fastbee.iot.data.service.IMqttMessagePublish;
|
||||
import com.fastbee.iot.data.service.IRuleEngine;
|
||||
@ -59,6 +60,8 @@ public class DataHandlerImpl implements IDataHandler {
|
||||
@Resource
|
||||
private IRuleEngine ruleEngine;
|
||||
@Resource
|
||||
private ConditionTriggerTaskExecutor conditionTriggerExecutor;
|
||||
@Resource
|
||||
private MqttRemoteManager remoteManager;
|
||||
@Resource
|
||||
private TopicsUtils topicsUtils;
|
||||
@ -100,6 +103,8 @@ public class DataHandlerImpl implements IDataHandler {
|
||||
// todo 只有设备上报进入规则引擎流程
|
||||
if (bo.isRuleEngine() && !bo.getSerialNumber().startsWith("server-")) {
|
||||
ruleEngine.ruleMatch(bo);
|
||||
// 条件触发任务检查:根据本次上报值决定是否下发 Modbus 指令
|
||||
conditionTriggerExecutor.checkAndTrigger(bo.getSerialNumber(), result);
|
||||
}
|
||||
// 发送至前端
|
||||
PushMessageBo messageBo = new PushMessageBo();
|
||||
|
||||
@ -84,13 +84,13 @@ public class ModbusJob extends BaseEntity {
|
||||
private String remarkStr;
|
||||
|
||||
/**
|
||||
* 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发)
|
||||
* 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发,4-条件触发)
|
||||
*/
|
||||
@ApiModelProperty("时间周期类型")
|
||||
private Integer cycleType;
|
||||
|
||||
/**
|
||||
* 执行时间(仅设备上电触发任务使用,记录任务执行的时间)
|
||||
* 执行时间(仅设备上电触发/条件触发任务使用,记录最近一次执行的时间)
|
||||
*/
|
||||
@ApiModelProperty("执行时间")
|
||||
private Date executeTime;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.fastbee.iot.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.common.core.device.DeviceAndProtocol;
|
||||
import com.fastbee.common.core.mq.SubDeviceBo;
|
||||
import com.fastbee.common.exception.ServiceException;
|
||||
@ -69,9 +70,13 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
List<ModbusJob> modbusJobList = modbusJobMapper.selectModbusJobList(modbusJob);
|
||||
if (!CollectionUtils.isEmpty(modbusJobList)) {
|
||||
for (ModbusJob job : modbusJobList) {
|
||||
// 直接使用数据库中的 cycleType 判断
|
||||
if (job.getCycleType() != null && job.getCycleType() == 3) {
|
||||
Integer ct = job.getCycleType();
|
||||
if (ct != null && ct == 3) {
|
||||
job.setRemarkStr("设备上电触发");
|
||||
} else if (ct != null && ct == 4) {
|
||||
// 条件触发:remark 是 JSON 对象,解析后展示为可读描述
|
||||
String condDesc = buildConditionDesc(job.getRemark());
|
||||
job.setRemarkStr("条件触发:" + condDesc);
|
||||
} else {
|
||||
job.setRemarkStr(JobCronUtils.handleCronCycle(1, job.getRemark()).getDesc());
|
||||
job.setCycleType(1); // 周期循环(默认)
|
||||
@ -83,13 +88,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是设备上电触发(直接使用 cycleType 判断)
|
||||
* 检查是否是非定时触发类型(上电触发 cycleType=3 或条件触发 cycleType=4)
|
||||
* 这两种类型不需要创建 Quartz 定时任务,而是通过事件驱动触发。
|
||||
*
|
||||
* @param cycleType 时间周期类型
|
||||
* @return true表示是设备上电触发
|
||||
* @return true 表示不需要创建定时任务
|
||||
*/
|
||||
private boolean isPowerOnTrigger(Integer cycleType) {
|
||||
return cycleType != null && cycleType == 3;
|
||||
private boolean isNonScheduleTrigger(Integer cycleType) {
|
||||
return cycleType != null && (cycleType == 3 || cycleType == 4);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,7 +113,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
}
|
||||
|
||||
// 查询关联的网关设备
|
||||
DeviceAndProtocol deviceAndProtocol = deviceService.selectProtocolBySerialNumber(modbusJob.getSubSerialNumber());
|
||||
DeviceAndProtocol deviceAndProtocol = deviceService
|
||||
.selectProtocolBySerialNumber(modbusJob.getSubSerialNumber());
|
||||
// 如果是网关子设备
|
||||
if (deviceAndProtocol.getDeviceType() == DeviceType.SUB_GATEWAY.getCode() ||
|
||||
deviceAndProtocol.getDeviceType() == DeviceType.GATEWAY.getCode()) {
|
||||
@ -129,14 +136,12 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
deviceJob.setSerialNumber(deviceAndProtocol.getGwSerialNumber());
|
||||
deviceJob.setProductId(deviceAndProtocol.getGwProductId());
|
||||
deviceJob.setCreateTime(DateUtils.getNowDate());
|
||||
// 处理时间转换
|
||||
// 检查是否是设备上电触发(使用 cycleType 判断)
|
||||
if (isPowerOnTrigger(modbusJob.getCycleType())) {
|
||||
// 设备上电触发不需要 cron 表达式,设置为null
|
||||
// DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
|
||||
// 上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||
if (isNonScheduleTrigger(modbusJob.getCycleType())) {
|
||||
deviceJob.setCronExpression(null);
|
||||
} else {
|
||||
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
|
||||
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1,
|
||||
deviceJob.getRemark());
|
||||
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
||||
}
|
||||
deviceJobService.insertJob(deviceJob);
|
||||
@ -150,7 +155,7 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
DeviceJob deviceJob = new DeviceJob();
|
||||
deviceJob.setDeviceId(deviceAndProtocol.getDeviceId()); // 用网关自己的deviceId
|
||||
buildSaveDeviceJob(modbusJob, deviceAndProtocol, deviceJob);
|
||||
} else {
|
||||
} else {
|
||||
throw new ServiceException("请先绑定网关");
|
||||
}
|
||||
} else {
|
||||
@ -200,7 +205,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
if (scheduleChanged) {
|
||||
// 计算新的 remark / cycleType(允许只传 cycleType 或只传 remark)
|
||||
String newRemark = modbusJob.getRemark() != null ? modbusJob.getRemark() : existingJob.getRemark();
|
||||
Integer newCycleType = modbusJob.getCycleType() != null ? modbusJob.getCycleType() : existingJob.getCycleType();
|
||||
Integer newCycleType = modbusJob.getCycleType() != null ? modbusJob.getCycleType()
|
||||
: existingJob.getCycleType();
|
||||
if (newCycleType == null) {
|
||||
newCycleType = 1;
|
||||
}
|
||||
@ -228,15 +234,20 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
create.setSerialNumber(oldDeviceJob.getSerialNumber());
|
||||
create.setProductId(oldDeviceJob.getProductId());
|
||||
create.setJobType(5);
|
||||
create.setJobName(StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
|
||||
create.setJobGroup(StringUtils.isNotEmpty(oldDeviceJob.getJobGroup()) ? oldDeviceJob.getJobGroup() : "MODBUS");
|
||||
create.setMisfirePolicy(StringUtils.isNotEmpty(oldDeviceJob.getMisfirePolicy()) ? oldDeviceJob.getMisfirePolicy() : "2");
|
||||
create.setConcurrent(StringUtils.isNotEmpty(oldDeviceJob.getConcurrent()) ? oldDeviceJob.getConcurrent() : "1");
|
||||
create.setJobName(
|
||||
StringUtils.isNotEmpty(oldDeviceJob.getJobName()) ? oldDeviceJob.getJobName() : "modbus poll");
|
||||
create.setJobGroup(
|
||||
StringUtils.isNotEmpty(oldDeviceJob.getJobGroup()) ? oldDeviceJob.getJobGroup() : "MODBUS");
|
||||
create.setMisfirePolicy(
|
||||
StringUtils.isNotEmpty(oldDeviceJob.getMisfirePolicy()) ? oldDeviceJob.getMisfirePolicy()
|
||||
: "2");
|
||||
create.setConcurrent(
|
||||
StringUtils.isNotEmpty(oldDeviceJob.getConcurrent()) ? oldDeviceJob.getConcurrent() : "1");
|
||||
create.setIsAdvance(oldDeviceJob.getIsAdvance() != null ? oldDeviceJob.getIsAdvance() : 0);
|
||||
create.setStatus("0");
|
||||
create.setRemark(newRemark);
|
||||
|
||||
if (isPowerOnTrigger(newCycleType)) {
|
||||
if (isNonScheduleTrigger(newCycleType)) {
|
||||
create.setCronExpression(null);
|
||||
} else {
|
||||
SceneModelTagCycleVO vo = JobCronUtils.handleCronCycle(newCycleType, newRemark);
|
||||
@ -258,13 +269,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
int rows = modbusJobMapper.updateModbusJob(modbusJob);
|
||||
|
||||
// 如发生重绑且旧 jobId 无引用,则删除旧 DeviceJob(同时会清理 quartz trigger/job)
|
||||
if (scheduleChanged && oldDeviceJobId != null && modbusJob.getJobId() != null && !oldDeviceJobId.equals(modbusJob.getJobId())) {
|
||||
if (scheduleChanged && oldDeviceJobId != null && modbusJob.getJobId() != null
|
||||
&& !oldDeviceJobId.equals(modbusJob.getJobId())) {
|
||||
ModbusJob q = new ModbusJob();
|
||||
q.setJobId(oldDeviceJobId);
|
||||
List<ModbusJob> left = modbusJobMapper.selectModbusJobList(q);
|
||||
if (CollectionUtils.isEmpty(left)) {
|
||||
try {
|
||||
deviceJobService.deleteJobByIds(new Long[]{oldDeviceJobId});
|
||||
deviceJobService.deleteJobByIds(new Long[] { oldDeviceJobId });
|
||||
} catch (SchedulerException e) {
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
@ -332,7 +344,8 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
case GATEWAY:
|
||||
// 网关设备需要查询所有子设备的modbus任务
|
||||
List<SubDeviceBo> subDeviceList = subGatewayService.getSubDeviceListByGw(serialNumber);
|
||||
List<String> serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo).collect(Collectors.toList());
|
||||
List<String> serialNumberList = subDeviceList.stream().map(SubDeviceBo::getSubDeviceNo)
|
||||
.collect(Collectors.toList());
|
||||
// 把网关自身SN也加入查询
|
||||
serialNumberList.add(serialNumber);
|
||||
modbusJob.setSubDeviceList(serialNumberList);
|
||||
@ -360,9 +373,10 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
|
||||
/**
|
||||
* 组装并保存设备任务
|
||||
* @param modbusJob 待新增的modbus任务
|
||||
* @param deviceAndProtocol 设备协议
|
||||
* @param deviceJob 设备任务
|
||||
*
|
||||
* @param modbusJob 待新增的modbus任务
|
||||
* @param deviceAndProtocol 设备协议
|
||||
* @param deviceJob 设备任务
|
||||
*/
|
||||
private void buildSaveDeviceJob(ModbusJob modbusJob, DeviceAndProtocol deviceAndProtocol, DeviceJob deviceJob) {
|
||||
deviceJob.setRemark(modbusJob.getRemark());
|
||||
@ -379,13 +393,10 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
deviceJob.setSerialNumber(deviceAndProtocol.getSerialNumber()); // 网关自己的SN
|
||||
deviceJob.setProductId(deviceAndProtocol.getProductId());
|
||||
deviceJob.setCreateTime(DateUtils.getNowDate());
|
||||
// 检查是否是设备上电触发(使用 cycleType 判断)
|
||||
if (isPowerOnTrigger(modbusJob.getCycleType())) {
|
||||
// 设备上电触发不需要 cron 表达式,设置为null
|
||||
// DeviceJobServiceImpl.insertJob 会检查并跳过创建定时任务
|
||||
// 上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||
if (isNonScheduleTrigger(modbusJob.getCycleType())) {
|
||||
deviceJob.setCronExpression(null);
|
||||
} else {
|
||||
// 处理时间转换
|
||||
SceneModelTagCycleVO sceneModelTagCycleVO = JobCronUtils.handleCronCycle(1, deviceJob.getRemark());
|
||||
deviceJob.setCronExpression(sceneModelTagCycleVO.getCron());
|
||||
}
|
||||
@ -400,4 +411,48 @@ public class ModbusJobServiceImpl implements IModbusJobService {
|
||||
modbusJob.setJobId(job.getJobId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将条件触发的 remark JSON 转换为可读描述,用于列表展示。
|
||||
* 示例:{"identifier":"temperature","operator":">","value":"50"} → "temperature >
|
||||
* 50"
|
||||
*/
|
||||
private String buildConditionDesc(String remark) {
|
||||
if (StringUtils.isEmpty(remark)) {
|
||||
return "(条件未配置)";
|
||||
}
|
||||
try {
|
||||
JSONObject json = JSONObject.parseObject(remark);
|
||||
// 多条件格式
|
||||
if (json.containsKey("conditions")) {
|
||||
com.alibaba.fastjson2.JSONArray conditions = json.getJSONArray("conditions");
|
||||
if (conditions == null || conditions.isEmpty()) {
|
||||
return "(条件未配置)";
|
||||
}
|
||||
String logic = json.getString("logic");
|
||||
String sep = "or".equalsIgnoreCase(logic) ? " 或 " : " 且 ";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < conditions.size(); i++) {
|
||||
JSONObject cond = conditions.getJSONObject(i);
|
||||
String id = cond.getString("identifier");
|
||||
String op = cond.getString("operator");
|
||||
String val = cond.getString("value");
|
||||
if (i > 0)
|
||||
sb.append(sep);
|
||||
sb.append(id).append(op).append(val);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
// 单条件旧格式
|
||||
String id = json.getString("identifier");
|
||||
String op = json.getString("operator");
|
||||
String val = json.getString("value");
|
||||
if (StringUtils.isEmpty(id) || StringUtils.isEmpty(op) || val == null) {
|
||||
return remark;
|
||||
}
|
||||
return id + " " + op + " " + val;
|
||||
} catch (Exception e) {
|
||||
return remark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
<el-tag v-if="scope.row.cycleType === 1" type="primary" size="small">周期循环</el-tag>
|
||||
<el-tag v-else-if="scope.row.cycleType === 2" type="warning" size="small">自定义时间段</el-tag>
|
||||
<el-tag v-else-if="scope.row.cycleType === 3" type="success" size="small">设备上电触发</el-tag>
|
||||
<el-tag v-else-if="scope.row.cycleType === 4" type="danger" size="small">条件触发</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -83,7 +84,11 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" prop="remarkStr"></el-table-column>
|
||||
<el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" min-width="200" class-name="modbus-task-schedule-col">
|
||||
<template slot-scope="scope">
|
||||
<span class="modbus-task-schedule-text">{{ formatModbusScheduleDisplay(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.device-modbus-task.384302-60')" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)" v-hasPermi="['modbus:job:edit']">编辑</el-button>
|
||||
@ -331,6 +336,84 @@
|
||||
<i class="el-icon-question" style="color: #909399; margin-left: 5px;"></i>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<!-- 条件触发 -->
|
||||
<el-radio :label="4" style="display: block; margin-top: 10px;">
|
||||
<span>条件触发</span>
|
||||
<el-tooltip placement="right">
|
||||
<div slot="content">
|
||||
当设备上报的指定属性满足条件时,自动下发本条指令
|
||||
</div>
|
||||
<i class="el-icon-question" style="color: #909399; margin-left: 5px;"></i>
|
||||
</el-tooltip>
|
||||
<!-- 条件配置区域(多条件) -->
|
||||
<div v-show="createForm.cycleType === 4" style="margin-top: 10px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
|
||||
<!-- 多条件逻辑选择(≥2条时显示) -->
|
||||
<div v-if="createForm.conditions.length > 1" style="margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
|
||||
<span style="font-size:12px; color:#606266;">条件关系:</span>
|
||||
<el-radio-group v-model="createForm.conditionLogic" size="mini">
|
||||
<el-radio-button label="and">全部满足(且)</el-radio-button>
|
||||
<el-radio-button label="or">任一满足(或)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<!-- 条件行列表 -->
|
||||
<div
|
||||
v-for="(cond, idx) in createForm.conditions"
|
||||
:key="idx"
|
||||
style="display: flex; align-items: flex-end; gap: 6px; margin-bottom: 6px;"
|
||||
>
|
||||
<!-- 多条件连接词标签 -->
|
||||
<span v-if="idx > 0" style="font-size:12px; color:#409EFF; white-space: nowrap; padding-bottom: 6px; min-width: 30px;">
|
||||
{{ createForm.conditionLogic === 'or' ? '或' : '且' }}
|
||||
</span>
|
||||
<span v-else style="min-width: 30px;"></span>
|
||||
<el-select
|
||||
v-model="cond.identifier"
|
||||
size="small"
|
||||
placeholder="监听属性"
|
||||
filterable
|
||||
style="flex: 2;"
|
||||
>
|
||||
<el-option
|
||||
v-for="prop in conditionProperties"
|
||||
:key="prop.id"
|
||||
:label="prop.name + '(' + prop.id + ')' + (prop.isCalculated ? ' [计算]' : '')"
|
||||
:value="prop.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="cond.operator" size="small" style="flex: 1.2;">
|
||||
<el-option label="等于(=)" value="=" />
|
||||
<el-option label="不等于(!=)" value="!=" />
|
||||
<el-option label="大于(>)" value=">" />
|
||||
<el-option label="小于(<)" value="<" />
|
||||
<el-option label="大于等于(>=)" value=">=" />
|
||||
<el-option label="小于等于(<=)" value="<=" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="cond.value"
|
||||
size="small"
|
||||
placeholder="阈值"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
<el-button
|
||||
v-if="createForm.conditions.length > 1"
|
||||
type="danger"
|
||||
icon="el-icon-minus"
|
||||
size="mini"
|
||||
circle
|
||||
@click.stop="removeCondition(idx)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 添加条件按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
plain
|
||||
@click.stop="addCondition"
|
||||
style="margin-top: 2px;"
|
||||
>添加条件</el-button>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@ -363,6 +446,26 @@ import { parseTime } from '@/utils/ruoyi';
|
||||
import { encode } from '@/api/iot/mqttTest';
|
||||
import { listJob, getJob, delJob, addJob, updateJob, getSlaveId } from '@/api/iot/modbusJob';
|
||||
import { listGateway } from '@/api/iot/gateway';
|
||||
import { listByProductIdAndSerialNumber, cacheJsonThingsModel } from '@/api/iot/model';
|
||||
|
||||
/**
|
||||
* 计算型物模型且公式含跨从机引用 {slaveId:N.xxx}(与后端 CalculateUtils 一致)。
|
||||
* 条件触发依赖与上报同一链路的物模型值,跨设备计算量为查询/聚合侧数据,不纳入可选监听属性。
|
||||
*/
|
||||
function isCrossDeviceCalculatedModel(isCalculated, formula) {
|
||||
const ic = isCalculated === 1 || isCalculated === '1';
|
||||
if (!ic) {
|
||||
return false;
|
||||
}
|
||||
if (formula == null) {
|
||||
return false;
|
||||
}
|
||||
const s = String(formula);
|
||||
if (s.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
return /\{slaveId:\d+\.[^}]+}/.test(s);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'device-modbus-task',
|
||||
@ -515,6 +618,8 @@ export default {
|
||||
},
|
||||
cycles1: [{ interval: '300', time: '', week: '', day: '' }],
|
||||
cycles2: [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }],
|
||||
// 条件触发:当前产品的属性列表({id, name} 结构)
|
||||
conditionProperties: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@ -522,7 +627,7 @@ export default {
|
||||
showExecuteTime(row) {
|
||||
const t = row.cycleType;
|
||||
if (t === 1 || t === 2) return true;
|
||||
if (t === 3) return row.status === '1';
|
||||
if (t === 3 || t === 4) return !!row.executeTime;
|
||||
return false;
|
||||
},
|
||||
// 回填时间周期(remark -> cycles1/cycles2)
|
||||
@ -672,7 +777,7 @@ export default {
|
||||
/**计算定时时间*/
|
||||
let cycle = '';
|
||||
|
||||
// 如果是设备上电触发,不需要 cron 表达式
|
||||
// 设备上电触发(3)和条件触发(4)不需要 cron 表达式
|
||||
if (this.createForm.cycleType === 3) {
|
||||
this.createForm.subDeviceId = this.device.deviceId;
|
||||
this.createForm.subSerialNumber = this.device.serialNumber;
|
||||
@ -683,6 +788,31 @@ export default {
|
||||
this.editDialog = false;
|
||||
return;
|
||||
}
|
||||
if (this.createForm.cycleType === 4) {
|
||||
const validConds = (this.createForm.conditions || []).filter(
|
||||
(c) => c.identifier && c.operator && c.value !== undefined && c.value !== ''
|
||||
);
|
||||
if (validConds.length === 0) {
|
||||
this.$message({ type: 'warning', message: '条件触发:至少填写一条完整的条件(属性 + 操作符 + 阈值)' });
|
||||
return;
|
||||
}
|
||||
this.createForm.subDeviceId = this.device.deviceId;
|
||||
this.createForm.subSerialNumber = this.device.serialNumber;
|
||||
this.createForm.command = this.createCode;
|
||||
// 多条件 JSON 存入 remark(统一使用新格式,后端向下兼容旧格式)
|
||||
this.createForm.remark = JSON.stringify({
|
||||
logic: this.createForm.conditionLogic || 'and',
|
||||
conditions: validConds.map((c) => ({
|
||||
identifier: c.identifier,
|
||||
operator: c.operator,
|
||||
value: String(c.value),
|
||||
})),
|
||||
});
|
||||
this.createForm.cronExpression = '';
|
||||
this.submitForm();
|
||||
this.editDialog = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有的周期循环和自定义时间段逻辑
|
||||
const c = this.cycles1.map((item) => {
|
||||
@ -707,9 +837,72 @@ export default {
|
||||
this.editDialog = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载属性列表供条件触发下拉使用。
|
||||
* 主设备:listByProductIdAndSerialNumber,含本设备链路上可用的属性与本机/公式内自引用型计算量;
|
||||
* 排除公式中含 {slaveId:N.xxx} 的跨设备计算物模型(与上报值不同路,无法作为条件比较)。
|
||||
* 网关模式:另追加子产品物模型(cacheJson),同样按公式过滤跨设备计算量。
|
||||
*/
|
||||
loadConditionProperties() {
|
||||
if (!this.device || !this.device.productId) return;
|
||||
const allProperties = [];
|
||||
const seen = new Set();
|
||||
|
||||
/** 统一格式并追加到列表(去重) */
|
||||
const mergeItem = (id, name, isCalculated) => {
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
allProperties.push({ id, name, isCalculated: !!isCalculated });
|
||||
};
|
||||
|
||||
// 1. 主设备:产品级 + 设备级计算物模型(type=1 属性),过滤跨设备计算量
|
||||
const mainPromise = listByProductIdAndSerialNumber({
|
||||
productId: this.device.productId,
|
||||
serialNumber: this.device.serialNumber,
|
||||
pageSize: 999,
|
||||
pageNum: 1,
|
||||
}).then((res) => {
|
||||
(res.rows || []).forEach((item) => {
|
||||
// type=1 为属性,只取属性,不取功能/事件
|
||||
if (item.type === 1) {
|
||||
if (isCrossDeviceCalculatedModel(item.isCalculated, item.formula)) {
|
||||
return;
|
||||
}
|
||||
mergeItem(item.identifier, item.modelName, item.isCalculated === 1);
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
// 2. 网关模式:追加子产品物模型,过滤跨设备计算量
|
||||
const subPromises = [];
|
||||
if (this.isGatewayMode && this.subDeviceList && this.subDeviceList.length > 0) {
|
||||
const subProductIds = [...new Set(
|
||||
this.subDeviceList.map((s) => s.subProductId).filter(Boolean)
|
||||
)];
|
||||
subProductIds.forEach((pid) => {
|
||||
subPromises.push(
|
||||
cacheJsonThingsModel(pid).then((res) => {
|
||||
const data = res.data ? JSON.parse(res.data) : {};
|
||||
(data.properties || []).forEach((p) => {
|
||||
if (isCrossDeviceCalculatedModel(p.isCalculated, p.formula)) {
|
||||
return;
|
||||
}
|
||||
mergeItem(p.id, p.name, p.isCalculated === 1);
|
||||
});
|
||||
}).catch(() => {})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([mainPromise, ...subPromises]).then(() => {
|
||||
this.conditionProperties = allProperties;
|
||||
});
|
||||
},
|
||||
|
||||
/**打开编辑框 */
|
||||
openEdit() {
|
||||
this.resetCreateForm();
|
||||
this.loadConditionProperties();
|
||||
// 网关设备(deviceType=2)直接轮询:跳过从机地址校验,由用户手动填写
|
||||
if (this.device && this.device.deviceType === 2) {
|
||||
this.createForm.path = '';
|
||||
@ -747,7 +940,9 @@ export default {
|
||||
setValue16: '0000',
|
||||
setValueSwitch: 'Dec',
|
||||
status: '0',
|
||||
cycleType: 1, // 1周期,2自定义
|
||||
cycleType: 1, // 1周期,2自定义,3上电触发,4条件触发
|
||||
conditionLogic: 'and',
|
||||
conditions: [{ identifier: '', operator: '>', value: '' }],
|
||||
};
|
||||
this.createCode = '';
|
||||
},
|
||||
@ -886,6 +1081,8 @@ export default {
|
||||
listGateway({ gwDeviceId: this.device.deviceId, pageSize: 999 })
|
||||
.then((res) => {
|
||||
this.subDeviceList = res.rows || [];
|
||||
// 子设备加载完成后,刷新条件触发属性下拉(合并子设备物模型)
|
||||
this.loadConditionProperties();
|
||||
})
|
||||
.finally(() => {
|
||||
this.subListLoading = false;
|
||||
@ -936,6 +1133,7 @@ export default {
|
||||
// 编辑轮询任务(修改时间周期/指令等)
|
||||
handleEdit(row) {
|
||||
this.resetCreateForm();
|
||||
this.loadConditionProperties();
|
||||
this.editName = true;
|
||||
getJob(row.taskId).then((res) => {
|
||||
const data = (res && res.data) || {};
|
||||
@ -945,11 +1143,100 @@ export default {
|
||||
// 回填周期
|
||||
const cycleType = data.cycleType != null ? data.cycleType : 1;
|
||||
this.createForm.cycleType = cycleType;
|
||||
this.parseRemarkToCycles(cycleType, data.remark);
|
||||
// 回填条件触发配置(兼容新多条件格式和旧单条件格式)
|
||||
if (cycleType === 4 && data.remark) {
|
||||
try {
|
||||
const parsed = JSON.parse(data.remark);
|
||||
if (parsed.conditions && Array.isArray(parsed.conditions)) {
|
||||
// 新多条件格式
|
||||
this.createForm.conditionLogic = parsed.logic || 'and';
|
||||
this.createForm.conditions = parsed.conditions.map((c) => ({
|
||||
identifier: c.identifier || '',
|
||||
operator: c.operator || '>',
|
||||
value: c.value !== undefined ? c.value : '',
|
||||
}));
|
||||
} else {
|
||||
// 旧单条件格式
|
||||
this.createForm.conditionLogic = 'and';
|
||||
this.createForm.conditions = [{
|
||||
identifier: parsed.identifier || '',
|
||||
operator: parsed.operator || '>',
|
||||
value: parsed.value !== undefined ? parsed.value : '',
|
||||
}];
|
||||
}
|
||||
} catch (e) {
|
||||
this.createForm.conditionLogic = 'and';
|
||||
this.createForm.conditions = [{ identifier: '', operator: '>', value: '' }];
|
||||
}
|
||||
} else {
|
||||
this.parseRemarkToCycles(cycleType, data.remark);
|
||||
}
|
||||
// 打开弹窗
|
||||
this.editDialog = true;
|
||||
});
|
||||
},
|
||||
/** 列表「定时时间」列:条件触发时避免展示原始 JSON */
|
||||
formatModbusScheduleDisplay(row) {
|
||||
const ct = row.cycleType;
|
||||
if (ct === 3) {
|
||||
return '设备上电触发';
|
||||
}
|
||||
if (ct === 4) {
|
||||
return this.formatConditionTriggerRemark(row);
|
||||
}
|
||||
if (row.remarkStr != null && row.remarkStr !== '') {
|
||||
return row.remarkStr;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
/** 将条件触发的 remark JSON 转为可读句,如:条件触发:T > 20 且 OP3 < 11 */
|
||||
formatConditionTriggerRemark(row) {
|
||||
let raw = row.remark;
|
||||
if (raw == null || String(raw).trim() === '') {
|
||||
const rs = row.remarkStr;
|
||||
if (typeof rs === 'string' && rs.indexOf('条件触发:') === 0) {
|
||||
raw = rs.replace(/^条件触发:\s*/, '');
|
||||
}
|
||||
}
|
||||
if (raw == null || String(raw).trim() === '') {
|
||||
return '条件触发:—';
|
||||
}
|
||||
const str = String(raw).trim();
|
||||
if (str.charAt(0) !== '{' && str.charAt(0) !== '[') {
|
||||
return '条件触发:' + (str.length > 120 ? str.slice(0, 120) + '…' : str);
|
||||
}
|
||||
try {
|
||||
const obj = JSON.parse(str);
|
||||
if (obj.conditions && Array.isArray(obj.conditions) && obj.conditions.length) {
|
||||
const isOr = obj.logic && String(obj.logic).toLowerCase() === 'or';
|
||||
const sep = isOr ? ' 或 ' : ' 且 ';
|
||||
const parts = obj.conditions
|
||||
.map((c) => {
|
||||
if (!c) {
|
||||
return '';
|
||||
}
|
||||
const id = c.identifier != null ? c.identifier : '';
|
||||
const op = c.operator != null ? c.operator : '';
|
||||
const val = c.value != null ? c.value : '';
|
||||
if (id === '' && op === '' && val === '') {
|
||||
return '';
|
||||
}
|
||||
return `${id} ${op} ${val}`.replace(/\s+/g, ' ').trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
return '条件触发:' + (parts.length ? parts.join(sep) : '—');
|
||||
}
|
||||
if (obj.identifier != null || obj.operator) {
|
||||
const id = obj.identifier != null ? obj.identifier : '';
|
||||
const op = obj.operator != null ? obj.operator : '';
|
||||
const val = obj.value != null ? obj.value : '';
|
||||
return '条件触发:' + `${id} ${op} ${val}`.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
} catch (e) {
|
||||
/* 保持下方兜底 */
|
||||
}
|
||||
return '条件触发:' + (str.length > 100 ? str.slice(0, 100) + '…' : str);
|
||||
},
|
||||
// 格式化显示CRON描述
|
||||
formatCronDisplay(item) {
|
||||
let result = '';
|
||||
@ -1024,9 +1311,21 @@ export default {
|
||||
this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }];
|
||||
} else if (val === 3) {
|
||||
// 设备上电触发,不需要时间周期配置
|
||||
// 可以清空或保留,但不使用
|
||||
} else if (val === 4) {
|
||||
// 条件触发,不需要时间周期配置
|
||||
if (!this.createForm.conditions || this.createForm.conditions.length === 0) {
|
||||
this.createForm.conditions = [{ identifier: '', operator: '>', value: '' }];
|
||||
}
|
||||
}
|
||||
},
|
||||
/** 条件触发:新增一行条件 */
|
||||
addCondition() {
|
||||
this.createForm.conditions.push({ identifier: '', operator: '>', value: '' });
|
||||
},
|
||||
/** 条件触发:删除指定行条件 */
|
||||
removeCondition(idx) {
|
||||
this.createForm.conditions.splice(idx, 1);
|
||||
},
|
||||
handleCustomIntervalDelete(index) {
|
||||
this.cycles2.splice(index, 1);
|
||||
},
|
||||
@ -1081,6 +1380,15 @@ $right-btn-color: #1890ff;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.modbus-task-schedule-text {
|
||||
white-space: normal;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sub-device-side-panel {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 4px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user