20260101:

1、设备调试页面钩子函数,写入redis缓存标识,可通过切面推送错误信息到前端页面,离开页面取消标识;
2、数据记录页面一笔数据统一创建日期;优化设备日志保存从for单个改为批量
3、信息栏添加pdf文档下载
This commit is contained in:
George 2026-01-01 22:31:56 +08:00
parent 8a87041e71
commit 44df912fc2
23 changed files with 538 additions and 179 deletions

View File

@ -70,4 +70,10 @@ public class CacheConstants {
* 设备OTA升级实时数据 * 设备OTA升级实时数据
*/ */
public static final String DEVICE_OTA_DATA = "device:ota:"; public static final String DEVICE_OTA_DATA = "device:ota:";
/**
* 设备调试日志记录标志 redis key
*/
public static final String DEVICE_DEBUG_USER_KEY = "device:debug:user:";
} }

View File

@ -120,9 +120,10 @@ public class CaculateUtils {
*/ */
public static List<String> suffixHandle(String exeStr) { public static List<String> suffixHandle(String exeStr) {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
Stack stack = new Stack(); //Stack stack = new Stack();
Deque<Character> stack = new ArrayDeque<>();
char[] exeChars = exeStr.toCharArray(); char[] exeChars = exeStr.toCharArray();
List<String> res = new ArrayList<String>(); List<String> res = new ArrayList<>();
for (char x : exeChars) { for (char x : exeChars) {
// 判断是不是操作符号 // 判断是不是操作符号
if (symbol.indexOf(x) > -1) { if (symbol.indexOf(x) > -1) {
@ -224,7 +225,7 @@ public class CaculateUtils {
|| key.contains("[") || key.contains("]") || key.contains("*") || key.contains("+") || key.contains("[") || key.contains("]") || key.contains("*") || key.contains("+")
|| key.contains("?") || key.contains("^") || key.contains("$") || key.contains("|")) { || key.contains("?") || key.contains("^") || key.contains("$") || key.contains("|")) {
// 转义正则表达式特殊字符 // 转义正则表达式特殊字符
String escapedKey = java.util.regex.Pattern.quote(key); String escapedKey = Pattern.quote(key);
str = str.replaceAll(escapedKey, value); str = str.replaceAll(escapedKey, value);
} else { } else {
str = str.replaceAll(key, value); str = str.replaceAll(key, value);

View File

@ -2,21 +2,22 @@ package com.fastbee.iot.data.aspect;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.constant.CacheConstants;
import com.fastbee.common.core.device.DeviceAndProtocol; import com.fastbee.common.core.device.DeviceAndProtocol;
import com.fastbee.common.core.mq.DeviceReportBo; import com.fastbee.common.core.mq.DeviceReportBo;
import com.fastbee.common.core.mq.message.DeviceData; import com.fastbee.common.core.mq.message.DeviceData;
import com.fastbee.common.core.mq.message.DeviceMessage; import com.fastbee.common.core.mq.message.DeviceMessage;
import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.enums.DeviceLogTypeEnum; import com.fastbee.common.enums.DeviceLogTypeEnum;
import com.fastbee.common.enums.TopicType; import com.fastbee.common.enums.TopicType;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; 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.domain.Device; import com.fastbee.iot.domain.*;
import com.fastbee.iot.domain.DeviceLog;
import com.fastbee.iot.domain.Product;
import com.fastbee.iot.domain.ProductAuthorize;
import com.fastbee.iot.service.IDeviceService; import com.fastbee.iot.service.IDeviceService;
import com.fastbee.iot.service.IDeviceUserService;
import com.fastbee.iot.service.IProductAuthorizeService; import com.fastbee.iot.service.IProductAuthorizeService;
import com.fastbee.iot.service.IProductService; import com.fastbee.iot.service.IProductService;
import com.fastbee.iot.tdengine.service.ILogService; import com.fastbee.iot.tdengine.service.ILogService;
@ -94,6 +95,9 @@ public class DeviceDebugLogAspect {
@Resource @Resource
private TopicsUtils topicsUtils; private TopicsUtils topicsUtils;
@Resource
private RedisCache redisCache;
/** /**
* 拦截数据解析方法 * 拦截数据解析方法
*/ */
@ -184,36 +188,6 @@ public class DeviceDebugLogAspect {
}, "记录协议解析错误日志失败"); }, "记录协议解析错误日志失败");
} }
/**
* 根据设备编号获取协议编码(有缓存)
*/
private String getProtocolCodeBySerialNumber(String serialNumber) {
try {
DeviceAndProtocol deviceAndProtocol =
deviceService.selectProtocolBySerialNumber(serialNumber);
if (deviceAndProtocol != null) {
return deviceAndProtocol.getProtocolCode();
}
} catch (Exception ex) {
log.warn("获取协议编码失败: {}, 设备编号: {}", ex.getMessage(), serialNumber);
}
return null;
}
/**
* 根据产品ID获取协议编码(有缓存)
*/
private String getProtocolCodeByProductId(Long productId) {
try {
if (productId != null) {
return productService.getProtocolByProductId(productId);
}
} catch (Exception ex) {
log.warn("根据产品ID获取协议编码失败: {}, 产品ID: {}", ex.getMessage(), productId);
}
return null;
}
/** /**
* 根据设备编号获取产品ID(有缓存) * 根据设备编号获取产品ID(有缓存)
*/ */
@ -245,6 +219,12 @@ public class DeviceDebugLogAspect {
log.warn("设备编号为空,跳过错误日志记录"); log.warn("设备编号为空,跳过错误日志记录");
return; return;
} }
// 检查用户是否在数据调试页面
if (!isUserInDebugPage(context.getSerialNumber())) {
return;
}
saveErrorLog(context); saveErrorLog(context);
} catch (Exception ex) { } catch (Exception ex) {
log.error("{}: {}", errorLogPrefix, ex.getMessage(), ex); log.error("{}: {}", errorLogPrefix, ex.getMessage(), ex);
@ -318,6 +298,50 @@ public class DeviceDebugLogAspect {
String errorMsg = e.getMessage(); String errorMsg = e.getMessage();
return StringUtils.isEmpty(errorMsg) ? e.getClass().getName() : errorMsg; return StringUtils.isEmpty(errorMsg) ? e.getClass().getName() : errorMsg;
} }
@Resource
private IDeviceUserService deviceUserService;
/**
* 检查当前用户是否在数据调试页面
* @return true-在调试页面,false-不在调试页面
*/
private boolean isUserInDebugPage(String serialNumber) {
// 1、从Redis中获取用户ID
// 2、从SecurityContext中获取用户ID
try {
Long userId = SecurityUtils.getUserId();
if (userId == null) {
return false;
}
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
String flag = redisCache.getCacheObject(cacheKey);
return "1".equals(flag);
} catch (Exception e) {
// 如果获取用户信息失败,默认不记录日志,避免影响正常功能
log.debug("无法从SecurityContext获取用户ID(可能在异步线程中)");
}
// 3、从数据库中获取用户ID
try {
// 获取设备信息
Device device = getDeviceBySerialNumber(serialNumber);
if (device == null || device.getTenantId() == null) {
return false;
}
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + device.getTenantId();
String flag = redisCache.getCacheObject(cacheKey);
if ("1".equals(flag)) {
log.debug("检测到设备[{}]的关联用户[{}]开启了调试标志", serialNumber, device.getTenantId());
return true;
}
return false;
} catch (Exception e) {
log.warn("检查设备用户调试状态失败: {}, 设备编号: {}", e.getMessage(), serialNumber);
return false;
}
}
/** /**
* 检查设备是否已授权 * 检查设备是否已授权
@ -377,13 +401,13 @@ public class DeviceDebugLogAspect {
} }
// 构建并保存设备日志 // 构建并保存设备日志
DeviceLog deviceLog = buildDeviceLog(device, context); // DeviceLog deviceLog = buildDeviceLog(device, context);
logService.saveDeviceLog(deviceLog); // logService.saveDeviceLog(deviceLog);
if (log.isDebugEnabled()) { // if (log.isDebugEnabled()) {
log.debug("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}", // log.debug("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}",
serialNumber, context.getIdentity(), // serialNumber, context.getIdentity(),
StringUtils.substring(context.getErrorMsg(), 0, MAX_ERROR_MSG_DISPLAY_LENGTH)); // StringUtils.substring(context.getErrorMsg(), 0, MAX_ERROR_MSG_DISPLAY_LENGTH));
} // }
// 推送错误信息到前端页面 // 推送错误信息到前端页面
pushErrorToFrontend(serialNumber, productId, context); pushErrorToFrontend(serialNumber, productId, context);

View File

@ -11,7 +11,6 @@ import com.fastbee.common.enums.ServerType;
import com.fastbee.common.enums.TopicType; import com.fastbee.common.enums.TopicType;
import com.fastbee.common.constant.HttpStatus; import com.fastbee.common.constant.HttpStatus;
import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils; 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.common.utils.gateway.protocol.ByteUtils; import com.fastbee.common.utils.gateway.protocol.ByteUtils;
@ -134,6 +133,12 @@ public class PowerOnTriggerTaskExecutor {
return; return;
} }
try {
Thread.sleep(1000); // 延迟1秒,等待设备上线
} catch (InterruptedException e) {
log.error("设备上电触发任务执行失败", e);
}
String pendingKey = RedisKeyBuilder.buildPowerOnTriggerPendingKey(serialNumber); String pendingKey = RedisKeyBuilder.buildPowerOnTriggerPendingKey(serialNumber);
// 关键:使用原子删除操作来防止竞态条件 // 关键:使用原子删除操作来防止竞态条件
// 只有第一个成功删除 pendingKey 的线程才能继续执行任务 // 只有第一个成功删除 pendingKey 的线程才能继续执行任务
@ -300,10 +305,6 @@ public class PowerOnTriggerTaskExecutor {
continue; continue;
} }
// 记录指令到Redis缓存(用于轮询任务管理)
String runtimeCacheKey = RedisKeyBuilder.buildModbusRuntimeCacheKey(serialNumber);
redisCache.zSetAdd(runtimeCacheKey, command, DateUtils.getTimestampSeconds());
// 记录发送时间 // 记录发送时间
long sendTime = System.currentTimeMillis(); long sendTime = System.currentTimeMillis();
boolean responseReceived; boolean responseReceived;

View File

@ -15,11 +15,11 @@ import org.quartz.JobDataMap;
import org.quartz.JobKey; import org.quartz.JobKey;
import org.quartz.Scheduler; import org.quartz.Scheduler;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List; import java.util.List;
/** /**
@ -30,13 +30,13 @@ import java.util.List;
@Service @Service
@Slf4j @Slf4j
public class DeviceJobServiceImpl implements IDeviceJobService { public class DeviceJobServiceImpl implements IDeviceJobService {
@Autowired @Resource
private Scheduler scheduler; private Scheduler scheduler;
@Autowired @Resource
private DeviceJobMapper jobMapper; private DeviceJobMapper jobMapper;
@Autowired @Resource
private SysJobMapper sysJobMapper; private SysJobMapper sysJobMapper;

View File

@ -145,10 +145,10 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
queryParam.setSerialNumber(serialNumber); queryParam.setSerialNumber(serialNumber);
List<ProductAuthorize> authorizeList = productAuthorizeService.selectProductAuthorizeList(queryParam); List<ProductAuthorize> authorizeList = productAuthorizeService.selectProductAuthorizeList(queryParam);
if (authorizeList == null || authorizeList.isEmpty()) { if (authorizeList == null || authorizeList.isEmpty()) {
log.warn("TCP数据上报-授权码校验失败: 设备[{}]未绑定授权码,拒绝数据上报", serialNumber); //log.warn("TCP数据上报-授权码校验失败: 设备[{}]未绑定授权码,拒绝数据上报", serialNumber);
return; // 拒绝数据上报 return; // 拒绝数据上报
} }
log.debug("TCP数据上报-授权码校验通过: 设备[{}]已绑定授权码", serialNumber); //log.debug("TCP数据上报-授权码校验通过: 设备[{}]已绑定授权码", serialNumber);
} }
//同步设备状态 //同步设备状态
@ -285,10 +285,8 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
private void processNoSub(DeviceReport message, String topicName) { private void processNoSub(DeviceReport message, String topicName) {
if (message.getServerType().equals(ServerType.MQTT)) { if (message.getServerType().equals(ServerType.MQTT)) {
//处理topic以prop结尾上报的数据 (属性) //处理topic以prop结尾上报的数据 (属性)
if (message.getServerType().equals(ServerType.MQTT)) { if (!topicName.endsWith(TopicType.PROPERTY_POST.getTopicSuffix())) {
if (!topicName.endsWith(TopicType.PROPERTY_POST.getTopicSuffix())) { return;
return;
}
} }
} }
ReportDataBo report = new ReportDataBo(); ReportDataBo report = new ReportDataBo();

View File

@ -1,12 +1,14 @@
package com.fastbee.controller.device; package com.fastbee.controller.device;
import com.fastbee.common.annotation.Log; import com.fastbee.common.annotation.Log;
import com.fastbee.common.constant.CacheConstants;
import com.fastbee.common.constant.HttpStatus; import com.fastbee.common.constant.HttpStatus;
import com.fastbee.common.core.controller.BaseController; import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.core.domain.entity.SysRole; import com.fastbee.common.core.domain.entity.SysRole;
import com.fastbee.common.core.domain.model.LoginUser; import com.fastbee.common.core.domain.model.LoginUser;
import com.fastbee.common.core.page.TableDataInfo; import com.fastbee.common.core.page.TableDataInfo;
import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.enums.BusinessType; import com.fastbee.common.enums.BusinessType;
import com.fastbee.common.exception.ServiceException; import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.MessageUtils; import com.fastbee.common.utils.MessageUtils;
@ -34,6 +36,7 @@ import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -49,6 +52,8 @@ public class DeviceController extends BaseController {
@Autowired @Autowired
private IDeviceService deviceService; private IDeviceService deviceService;
@Autowired
private RedisCache redisCache;
/** /**
* 查询设备列表 * 查询设备列表
@ -453,4 +458,39 @@ public class DeviceController extends BaseController {
return rspData; return rspData;
} }
/**
* 开启设备调试日志记录
*/
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@PostMapping("/debug/enable")
@ApiOperation("开启设备调试日志记录")
public AjaxResult enableDebugLog() {
try {
Long userId = getUserId();
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
// 设置标志,过期时间30分钟(防止页面异常关闭导致标志未清除)
redisCache.setCacheObject(cacheKey, "1", 30, TimeUnit.MINUTES);
return AjaxResult.success("已开启调试日志记录");
} catch (Exception e) {
return AjaxResult.error("开启调试日志记录失败:" + e.getMessage());
}
}
/**
* 关闭设备调试日志记录
*/
@PreAuthorize("@ss.hasPermi('iot:device:list')")
@PostMapping("/debug/disable")
@ApiOperation("关闭设备调试日志记录")
public AjaxResult disableDebugLog() {
try {
Long userId = getUserId();
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
redisCache.deleteObject(cacheKey);
return AjaxResult.success("已关闭调试日志记录");
} catch (Exception e) {
return AjaxResult.error("关闭调试日志记录失败:" + e.getMessage());
}
}
} }

View File

@ -50,7 +50,7 @@ public class MessageAdapter implements MessageDecoder, MessageEncoder {
Long productId = null; Long productId = null;
String devNum = new String(ByteBufUtil.getBytes(buf)); String devNum = new String(ByteBufUtil.getBytes(buf));
String dump = ByteBufUtil.hexDump(buf); String dump = ByteBufUtil.hexDump(buf);
log.info("{}=>上报hex数据:{}", clientId, dump); log.info("{}上报hex数据:{}", clientId, dump);
//这里兼容一下TCP整包发送的数据(整包==设备编号,数据一起发送) //这里兼容一下TCP整包发送的数据(整包==设备编号,数据一起发送)
if (clientId == null) { if (clientId == null) {

View File

@ -127,7 +127,6 @@ public class NettyConfig {
case COAP: case COAP:
this.name = name != null ? name : ServerType.COAP.name(); this.name = name != null ? name : ServerType.COAP.name();
this.server = server; this.server = server;
;
this.server.config = this; this.server.config = this;
break; break;
default: default:

View File

@ -5,6 +5,7 @@ import com.fastbee.base.session.Session;
import com.fastbee.base.session.SessionManager; import com.fastbee.base.session.SessionManager;
import com.fastbee.base.util.AttributeUtils; import com.fastbee.base.util.AttributeUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
@ -49,6 +50,15 @@ public class TCPMessageAdapter extends ChannelInboundHandlerAdapter {
ByteBuf buf = (ByteBuf) msg; ByteBuf buf = (ByteBuf) msg;
/*session存储*/ /*session存储*/
Session session = getSession(ctx); Session session = getSession(ctx);
// 临时记录设备原始数据日志
// 针对特定设备记录原始数据日志
String serialNumber = session.getClientId();
if ("D3C611176213".equalsIgnoreCase(serialNumber) || "DEE3FBBF0132".equalsIgnoreCase(serialNumber)) {
ByteBuf copy = buf.duplicate();
byte[] source = new byte[buf.writerIndex()];
copy.readBytes(source);
log.warn("=>设备[{}]原始数据:{}", serialNumber, ByteBufUtil.hexDump(source));
}
session.access(); session.access();
/*将设备端消息传递给下一个处理器*/ /*将设备端消息传递给下一个处理器*/
ctx.fireChannelRead(Packet.of(session, buf)); ctx.fireChannelRead(Packet.of(session, buf));

View File

@ -71,7 +71,8 @@ import java.math.BigDecimal;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.fastbee.common.utils.SecurityUtils.getLoginUser; import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
@ -498,7 +499,7 @@ public class DeviceServiceImpl implements IDeviceService {
@DSTransactional @DSTransactional
public List<ThingsModelSimpleItem> reportDeviceThingsModelValue(ThingsModelValuesInput input, int type, boolean isShadow) { public List<ThingsModelSimpleItem> reportDeviceThingsModelValue(ThingsModelValuesInput input, int type, boolean isShadow) {
String key = RedisKeyBuilder.buildTSLVCacheKey(input.getProductId(), input.getDeviceNumber()); String key = RedisKeyBuilder.buildTSLVCacheKey(input.getProductId(), input.getDeviceNumber());
Map<String, String> maps = new HashMap<String, String>(); Map<String, String> maps = new HashMap<>();
List<ThingsModelSimpleItem> list = new ArrayList<>(); List<ThingsModelSimpleItem> list = new ArrayList<>();
//属性存储集合 //属性存储集合
@ -507,6 +508,8 @@ public class DeviceServiceImpl implements IDeviceService {
List<FunctionLog> functionLogList = new ArrayList<>(); List<FunctionLog> functionLogList = new ArrayList<>();
//事件存储集合 //事件存储集合
List<EventLog> eventLogList = new ArrayList<>(); List<EventLog> eventLogList = new ArrayList<>();
// 获取当前时间
Date nowDate = DateUtils.getNowDate();
for (ThingsModelSimpleItem item : input.getThingsModelValueRemarkItem()) { for (ThingsModelSimpleItem item : input.getThingsModelValueRemarkItem()) {
String identity = item.getId(); String identity = item.getId();
String serialNumber = input.getDeviceNumber(); String serialNumber = input.getDeviceNumber();
@ -538,13 +541,7 @@ public class DeviceServiceImpl implements IDeviceService {
// 解析公式中的其他物模型标识符(格式:{identifier} 或直接使用标识符) // 解析公式中的其他物模型标识符(格式:{identifier} 或直接使用标识符)
String formula = thingModels.getFormula(); String formula = thingModels.getFormula();
// 匹配 {identifier} 格式的占位符 // 匹配 {identifier} 格式的占位符
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}"); Set<String> referencedIdentifiers = parseFormulaPlaceholders(formula);
java.util.regex.Matcher matcher = pattern.matcher(formula);
Set<String> referencedIdentifiers = new HashSet<>();
while (matcher.find()) {
String identifier = matcher.group(1);
referencedIdentifiers.add(identifier);
}
// 获取引用的其他物模型的值 // 获取引用的其他物模型的值
for (String refIdentifier : referencedIdentifiers) { for (String refIdentifier : referencedIdentifiers) {
@ -561,8 +558,7 @@ public class DeviceServiceImpl implements IDeviceService {
} else { } else {
// 如果引用的物模型值不存在,使用0作为默认值 // 如果引用的物模型值不存在,使用0作为默认值
params.put("{" + refIdentifier + "}", "0"); params.put("{" + refIdentifier + "}", "0");
log.warn("=>计算公式引用的物模型值不存在[{}],标识符:[{}],使用默认值0", log.warn("=>计算公式引用的物模型值不存在[{}],标识符:[{}],使用默认值0", input.getProductId(), refIdentifier);
input.getProductId(), refIdentifier);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("=>获取计算公式引用的物模型值失败[{}],标识符:[{}],错误:{}", log.error("=>获取计算公式引用的物模型值失败[{}],标识符:[{}],错误:{}",
@ -641,7 +637,7 @@ public class DeviceServiceImpl implements IDeviceService {
deviceLog.setLogValue(value); deviceLog.setLogValue(value);
deviceLog.setRemark(item.getRemark()); deviceLog.setRemark(item.getRemark());
deviceLog.setIdentity(id); deviceLog.setIdentity(id);
deviceLog.setCreateTime(DateUtils.getNowDate()); deviceLog.setCreateTime(nowDate);
deviceLog.setUserId(device.getTenantId()); deviceLog.setUserId(device.getTenantId());
deviceLog.setUserName(device.getTenantName()); deviceLog.setUserName(device.getTenantName());
deviceLog.setTenantId(device.getTenantId()); deviceLog.setTenantId(device.getTenantId());
@ -654,7 +650,7 @@ public class DeviceServiceImpl implements IDeviceService {
case SERVICE: case SERVICE:
if (1 == thingModels.getIsHistory()) { if (1 == thingModels.getIsHistory()) {
FunctionLog function = new FunctionLog(); FunctionLog function = new FunctionLog();
function.setCreateTime(DateUtils.getNowDate()); function.setCreateTime(nowDate);
function.setFunValue(value); function.setFunValue(value);
function.setSerialNumber(input.getDeviceNumber()); function.setSerialNumber(input.getDeviceNumber());
function.setIdentify(id); function.setIdentify(id);
@ -677,7 +673,7 @@ public class DeviceServiceImpl implements IDeviceService {
event.setIsMonitor(0); event.setIsMonitor(0);
event.setUserId(device.getTenantId()); event.setUserId(device.getTenantId());
event.setUserName(device.getTenantName()); event.setUserName(device.getTenantName());
event.setCreateTime(DateUtils.getNowDate()); event.setCreateTime(nowDate);
// 1=影子模式,2=在线模式,3=其他 // 1=影子模式,2=在线模式,3=其他
event.setMode(2); event.setMode(2);
eventLogList.add(event); eventLogList.add(event);
@ -687,11 +683,12 @@ public class DeviceServiceImpl implements IDeviceService {
} }
// 缓存最新一条数据到redis // 缓存最新一条数据到redis
redisCache.hashPutAll(key, maps); redisCache.hashPutAll(key, maps);
//存储历史数据 // 存储历史数据——批量
if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow) { if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow) {
for (DeviceLog deviceLog : deviceLogList) { // for (DeviceLog deviceLog : deviceLogList) {
logService.saveDeviceLog(deviceLog); // logService.saveDeviceLog(deviceLog);
} // }
logService.saveDeviceLogBatch(deviceLogList);
} }
//指令存储,影子模式不存储 //指令存储,影子模式不存储
if (!CollectionUtils.isEmpty(functionLogList) && !isShadow) { if (!CollectionUtils.isEmpty(functionLogList) && !isShadow) {
@ -720,7 +717,7 @@ public class DeviceServiceImpl implements IDeviceService {
Map<String, String> calculatedMaps = new HashMap<>(); Map<String, String> calculatedMaps = new HashMap<>();
List<DeviceLog> calculatedDeviceLogList = new ArrayList<>(); List<DeviceLog> calculatedDeviceLogList = new ArrayList<>();
calculateDependentThingsModels(input, updatedIdentifiers, key, calculatedMaps, calculatedDeviceLogList, type); calculateDependentThingsModels(input, updatedIdentifiers, nowDate, calculatedMaps, calculatedDeviceLogList, type);
// 将计算后的值更新到缓存 // 将计算后的值更新到缓存
if (!calculatedMaps.isEmpty()) { if (!calculatedMaps.isEmpty()) {
@ -731,9 +728,7 @@ public class DeviceServiceImpl implements IDeviceService {
// 保存计算型物模型的历史数据 // 保存计算型物模型的历史数据
if (!CollectionUtils.isEmpty(calculatedDeviceLogList)) { if (!CollectionUtils.isEmpty(calculatedDeviceLogList)) {
for (DeviceLog deviceLog : calculatedDeviceLogList) { logService.saveDeviceLogBatch(calculatedDeviceLogList);
logService.saveDeviceLog(deviceLog);
}
log.debug("=>自动计算依赖物模型,保存历史数据成功,设备编号:[{}],历史数据数量:[{}]", log.debug("=>自动计算依赖物模型,保存历史数据成功,设备编号:[{}],历史数据数量:[{}]",
input.getDeviceNumber(), calculatedDeviceLogList.size()); input.getDeviceNumber(), calculatedDeviceLogList.size());
} }
@ -743,6 +738,26 @@ public class DeviceServiceImpl implements IDeviceService {
return list; return list;
} }
/**
* 解析公式中的占位符标识符
*
* @param formula 计算公式
* @return 占位符标识符集合
*/
private Set<String> parseFormulaPlaceholders(String formula) {
Set<String> referencedIdentifiers = new HashSet<>();
if (StringUtils.isEmpty(formula)) {
return referencedIdentifiers;
}
Pattern pattern = Pattern.compile("\\{([^}]+)}");
Matcher matcher = pattern.matcher(formula);
while (matcher.find()) {
String identifier = matcher.group(1);
referencedIdentifiers.add(identifier);
}
return referencedIdentifiers;
}
/** /**
* 查询设备列表 * 查询设备列表
* *
@ -1947,14 +1962,14 @@ public class DeviceServiceImpl implements IDeviceService {
* *
* @param input 设备上报数据输入 * @param input 设备上报数据输入
* @param updatedIdentifiers 已更新的物模型标识符集合(设备上报的) * @param updatedIdentifiers 已更新的物模型标识符集合(设备上报的)
* @param key Redis缓存key * @param nowDate 数据记录时间
* @param maps 物模型值映射(用于批量更新缓存) * @param maps 物模型值映射(用于批量更新缓存)
* @param deviceLogList 设备日志列表(用于存储历史数据) * @param deviceLogList 设备日志列表(用于存储历史数据)
* @param type 物模型类型(1=属性,2=功能,3=事件) * @param type 物模型类型(1=属性,2=功能,3=事件)
*/ */
private void calculateDependentThingsModels(ThingsModelValuesInput input, private void calculateDependentThingsModels(ThingsModelValuesInput input,
Set<String> updatedIdentifiers, Set<String> updatedIdentifiers,
String key, Date nowDate,
Map<String, String> maps, Map<String, String> maps,
List<DeviceLog> deviceLogList, List<DeviceLog> deviceLogList,
int type) { int type) {
@ -1991,13 +2006,7 @@ public class DeviceServiceImpl implements IDeviceService {
} }
// 检查计算公式是否引用了已更新的物模型 // 检查计算公式是否引用了已更新的物模型
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}"); Set<String> referencedIdentifiers = parseFormulaPlaceholders(formula);
java.util.regex.Matcher matcher = pattern.matcher(formula);
Set<String> referencedIdentifiers = new HashSet<>();
while (matcher.find()) {
String identifier = matcher.group(1);
referencedIdentifiers.add(identifier);
}
// 检查是否至少引用了一个已更新的物模型 // 检查是否至少引用了一个已更新的物模型
boolean hasDependency = false; boolean hasDependency = false;
@ -2074,7 +2083,7 @@ public class DeviceServiceImpl implements IDeviceService {
deviceLog.setLogValue(calculatedValue); deviceLog.setLogValue(calculatedValue);
deviceLog.setRemark("自动计算"); // 标记为自动计算 deviceLog.setRemark("自动计算"); // 标记为自动计算
deviceLog.setIdentity(dependentIdentifier); deviceLog.setIdentity(dependentIdentifier);
deviceLog.setCreateTime(DateUtils.getNowDate()); deviceLog.setCreateTime(nowDate);
deviceLog.setUserId(device.getTenantId()); deviceLog.setUserId(device.getTenantId());
deviceLog.setUserName(device.getTenantName()); deviceLog.setUserName(device.getTenantName());
deviceLog.setTenantId(device.getTenantId()); deviceLog.setTenantId(device.getTenantId());

View File

@ -627,8 +627,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
public IdentifierVO revertObjectOrArrayIdentifier(String identifier) { public IdentifierVO revertObjectOrArrayIdentifier(String identifier) {
IdentifierVO identifierVO = new IdentifierVO(); IdentifierVO identifierVO = new IdentifierVO();
if (identifier.startsWith("array_")) { if (identifier.startsWith("array_")) {
int i1 = org.apache.commons.lang3.StringUtils.ordinalIndexOf(identifier, "_", 1); int i1 = StringUtils.ordinalIndexOf(identifier, "_", 1);
int i2 = org.apache.commons.lang3.StringUtils.ordinalIndexOf(identifier, "_", 2); int i2 = StringUtils.ordinalIndexOf(identifier, "_", 2);
identifierVO.setIdentifier(identifier.substring(i2 + 1)); identifierVO.setIdentifier(identifier.substring(i2 + 1));
identifierVO.setIndex(Integer.parseInt(identifier.substring(++i1, i2))); identifierVO.setIndex(Integer.parseInt(identifier.substring(++i1, i2)));
} else { } else {

View File

@ -27,6 +27,10 @@ public interface ILogService {
* 保存设备日志 * 保存设备日志
**/ **/
int saveDeviceLog(DeviceLog deviceLog); int saveDeviceLog(DeviceLog deviceLog);
/**
* 批量保存设备日志
**/
void saveDeviceLogBatch(List<DeviceLog> deviceLogList);
/** /**
* 批量保存日志 * 批量保存日志

View File

@ -14,10 +14,11 @@ import com.fastbee.iot.mapper.DeviceLogMapper;
import com.fastbee.iot.model.MonitorModel; import com.fastbee.iot.model.MonitorModel;
import com.fastbee.iot.tdengine.service.model.TdLogDto; import com.fastbee.iot.tdengine.service.model.TdLogDto;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -33,11 +34,11 @@ import java.util.stream.Collectors;
@ConditionalOnProperty(name = "spring.datasource.dynamic.datasource.taos.enabled", havingValue = "false") @ConditionalOnProperty(name = "spring.datasource.dynamic.datasource.taos.enabled", havingValue = "false")
@Service("MySql") @Service("MySql")
public class MySqlLogServiceImpl implements ILogService { public class MySqlLogServiceImpl implements ILogService {
@Autowired @Resource
private DeviceLogMapper deviceLogMapper; private DeviceLogMapper deviceLogMapper;
@Autowired @Resource
private EventLogMapper eventLogMapper; private EventLogMapper eventLogMapper;
@Autowired @Resource
private FunctionLogMapper functionLogMapper; private FunctionLogMapper functionLogMapper;
@ -55,6 +56,14 @@ public class MySqlLogServiceImpl implements ILogService {
return deviceLogMapper.insertDeviceLog(deviceLog); return deviceLogMapper.insertDeviceLog(deviceLog);
} }
@Override
public void saveDeviceLogBatch(List<DeviceLog> deviceLogList) {
if (CollectionUtils.isEmpty(deviceLogList)) {
return;
}
deviceLogMapper.saveBatch(deviceLogList);
}
@Override @Override
public int saveBatch(TdLogDto dto) { public int saveBatch(TdLogDto dto) {
return deviceLogMapper.saveBatch(dto.getList()); return deviceLogMapper.saveBatch(dto.getList());

View File

@ -23,6 +23,7 @@ import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.util.CollectionUtils;
/** /**
* 类名: TdengineLogServiceImpl * 类名: TdengineLogServiceImpl
@ -62,6 +63,52 @@ public class TdengineLogServiceImpl implements ILogService {
return tDDeviceLogMapper.save(dbName, deviceLog); return tDDeviceLogMapper.save(dbName, deviceLog);
} }
/***
* 批量保存设备日志
* @param deviceLogList 设备日志列表
*/
@Override
public void saveDeviceLogBatch(List<DeviceLog> deviceLogList) {
if (CollectionUtils.isEmpty(deviceLogList)) {
log.debug("=>批量保存设备日志到TDengine:日志列表为空,跳过保存");
return;
}
try {
// 按设备编号分组
Map<String, List<DeviceLog>> groupedBySerialNumber = deviceLogList.stream()
.filter(log -> log.getSerialNumber() != null && !log.getSerialNumber().isEmpty())
.collect(Collectors.groupingBy(DeviceLog::getSerialNumber));
if (groupedBySerialNumber.isEmpty()) {
log.warn("=>批量保存设备日志到TDengine:没有有效的设备编号,跳过保存");
return;
}
// 为每个设备批量保存日志
int totalSaved = 0;
for (Map.Entry<String, List<DeviceLog>> entry : groupedBySerialNumber.entrySet()) {
String serialNumber = entry.getKey();
List<DeviceLog> logs = entry.getValue();
try {
TdLogDto dto = new TdLogDto(serialNumber, logs);
int saved = tDDeviceLogMapper.saveBatch(dbName, dto);
totalSaved += saved;
log.debug("=>批量保存设备日志到TDengine成功,设备编号:[{}],保存{}条记录", serialNumber, saved);
} catch (Exception e) {
log.error("=>批量保存设备日志到TDengine失败,设备编号:[{}],共{}条记录,错误:{}",
serialNumber, logs.size(), e.getMessage(), e);
// 继续处理其他设备的日志,不中断整个流程
}
}
} catch (Exception e) {
log.error("=>批量保存设备日志到TDengine时发生异常,共{}条记录,错误:{}",
deviceLogList.size(), e.getMessage(), e);
throw new RuntimeException("批量保存设备日志到TDengine失败:" + e.getMessage(), e);
}
}
/** /**
* 批量保存日志 * 批量保存日志
*/ */

View File

@ -32,9 +32,9 @@ public class SysNotice extends BaseEntity {
private String noticeTitle; private String noticeTitle;
/** /**
* 公告类型(1通知 2公告) * 公告类型(1通知 2公告 3文档)
*/ */
@ApiModelProperty("公告类型(1通知 2公告)") @ApiModelProperty("公告类型(1通知 2公告 3文档)")
private String noticeType; private String noticeType;
/** /**
@ -43,6 +43,12 @@ public class SysNotice extends BaseEntity {
@ApiModelProperty("公告内容") @ApiModelProperty("公告内容")
private String noticeContent; private String noticeContent;
/**
* 文件路径(文档类型使用)
*/
@ApiModelProperty("文件路径(文档类型使用)")
private String filePath;
/** /**
* 公告状态(0正常 1关闭) * 公告状态(0正常 1关闭)
*/ */
@ -84,6 +90,14 @@ public class SysNotice extends BaseEntity {
this.noticeContent = noticeContent; this.noticeContent = noticeContent;
} }
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getStatus() { public String getStatus() {
return status; return status;
} }
@ -99,6 +113,7 @@ public class SysNotice extends BaseEntity {
.append("noticeTitle", getNoticeTitle()) .append("noticeTitle", getNoticeTitle())
.append("noticeType", getNoticeType()) .append("noticeType", getNoticeType())
.append("noticeContent", getNoticeContent()) .append("noticeContent", getNoticeContent())
.append("filePath", getFilePath())
.append("status", getStatus()) .append("status", getStatus())
.append("createBy", getCreateBy()) .append("createBy", getCreateBy())
.append("createTime", getCreateTime()) .append("createTime", getCreateTime())

View File

@ -9,6 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="noticeTitle" column="notice_title" /> <result property="noticeTitle" column="notice_title" />
<result property="noticeType" column="notice_type" /> <result property="noticeType" column="notice_type" />
<result property="noticeContent" column="notice_content" /> <result property="noticeContent" column="notice_content" />
<result property="filePath" column="file_path" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="createBy" column="create_by" /> <result property="createBy" column="create_by" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
@ -18,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectNoticeVo"> <sql id="selectNoticeVo">
select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, file_path, status, create_by, create_time, update_by, update_time, remark
from sys_notice from sys_notice
</sql> </sql>
@ -47,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeTitle != null and noticeTitle != '' ">notice_title, </if> <if test="noticeTitle != null and noticeTitle != '' ">notice_title, </if>
<if test="noticeType != null and noticeType != '' ">notice_type, </if> <if test="noticeType != null and noticeType != '' ">notice_type, </if>
<if test="noticeContent != null and noticeContent != '' ">notice_content, </if> <if test="noticeContent != null and noticeContent != '' ">notice_content, </if>
<if test="filePath != null and filePath != '' ">file_path, </if>
<if test="status != null and status != '' ">status, </if> <if test="status != null and status != '' ">status, </if>
<if test="remark != null and remark != ''">remark,</if> <if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
@ -55,6 +57,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeTitle != null and noticeTitle != ''">#{noticeTitle}, </if> <if test="noticeTitle != null and noticeTitle != ''">#{noticeTitle}, </if>
<if test="noticeType != null and noticeType != ''">#{noticeType}, </if> <if test="noticeType != null and noticeType != ''">#{noticeType}, </if>
<if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if> <if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if>
<if test="filePath != null and filePath != ''">#{filePath}, </if>
<if test="status != null and status != ''">#{status}, </if> <if test="status != null and status != ''">#{status}, </if>
<if test="remark != null and remark != ''">#{remark},</if> <if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
@ -68,6 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="noticeTitle != null and noticeTitle != ''">notice_title = #{noticeTitle}, </if> <if test="noticeTitle != null and noticeTitle != ''">notice_title = #{noticeTitle}, </if>
<if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if> <if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if>
<if test="noticeContent != null">notice_content = #{noticeContent}, </if> <if test="noticeContent != null">notice_content = #{noticeContent}, </if>
<if test="filePath != null">file_path = #{filePath}, </if>
<if test="status != null and status != ''">status = #{status}, </if> <if test="status != null and status != ''">status = #{status}, </if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
update_time = sysdate() update_time = sysdate()

View File

@ -61,3 +61,19 @@ export function delPreferences(data) {
method: 'DELETE' method: 'DELETE'
}); });
} }
// 开启设备调试日志记录
export function enableDebugLog() {
return request({
url: '/iot/device/debug/enable',
method: 'post',
});
}
// 关闭设备调试日志记录
export function disableDebugLog() {
return request({
url: '/iot/device/debug/disable',
method: 'post',
});
}

View File

@ -12,8 +12,8 @@
</el-form-item> </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__">
<span style="font-weight: bold; color: #409EFF;">全部勾选</span> <span style="font-weight: bold; color: #409EFF;">{{ devIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option> </el-option>
<el-option v-for="(item, index) in devIdentifierList" :key="index" :label="item.modelName" :value="item.identifier"></el-option> <el-option v-for="(item, index) in devIdentifierList" :key="index" :label="item.modelName" :value="item.identifier"></el-option>
</el-select> </el-select>
@ -80,8 +80,8 @@
</el-form-item> </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="sceneQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" multiple collapse-tags @change="handleSceneIdentifierChange"> <el-select style="width: 100%" v-model="sceneQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" multiple collapse-tags @change="handleSceneIdentifierChange">
<el-option label="全部勾选" value="__selectAll__"> <el-option label="全部勾选/反选" value="__selectAll__">
<span style="font-weight: bold; color: #409EFF;">全部勾选</span> <span style="font-weight: bold; color: #409EFF;">{{ sceneIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option> </el-option>
<el-option v-for="(item, index) in sceneIdentifierList" :key="index" :label="item.sourceName" :value="item.identifier"></el-option> <el-option v-for="(item, index) in sceneIdentifierList" :key="index" :label="item.sourceName" :value="item.identifier"></el-option>
</el-select> </el-select>
@ -270,6 +270,20 @@ export default {
const end = start + this.scenePageSize; const end = start + this.scenePageSize;
return this.sceneTableComTemp.slice(start, end); return this.sceneTableComTemp.slice(start, end);
}, },
// 判断设备变量是否全部选中
devIsAllSelected() {
if (this.devIdentifierList.length === 0) return false;
const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
return allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
},
// 判断场景变量是否全部选中
sceneIsAllSelected() {
if (this.sceneIdentifierList.length === 0) return false;
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
return allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
},
}, },
methods: { methods: {
// 获取设备列表 // 获取设备列表
@ -292,12 +306,22 @@ export default {
}, },
// 设备变量选择变化 // 设备变量选择变化
handleDevIdentifierChange(val) { handleDevIdentifierChange(val) {
// 如果选择了"全部勾选",则选中所有变量 // 如果选择了"全部勾选",则切换全选/全不选
const index = val.indexOf('__selectAll__'); const index = val.indexOf('__selectAll__');
if (index > -1) { if (index > -1) {
const allIdentifiers = this.devIdentifierList.map(item => item.identifier); const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
// 判断当前是否已经全部选中(基于当前值,不是新值)
const isAllSelected = allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
this.$nextTick(() => { this.$nextTick(() => {
this.devQueryParams.identifiers = [...allIdentifiers]; if (isAllSelected) {
// 如果已经全部选中,则清空选择(反选)
this.devQueryParams.identifiers = [];
} else {
// 如果没有全部选中,则选中所有(勾选)
this.devQueryParams.identifiers = [...allIdentifiers];
}
}); });
} }
}, },
@ -398,12 +422,22 @@ export default {
}, },
// 场景变量选择变化 // 场景变量选择变化
handleSceneIdentifierChange(val) { handleSceneIdentifierChange(val) {
// 如果选择了"全部勾选",则选中所有变量 // 如果选择了"全部勾选",则切换全选/全不选
const index = val.indexOf('__selectAll__'); const index = val.indexOf('__selectAll__');
if (index > -1) { if (index > -1) {
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier); const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
// 判断当前是否已经全部选中(基于当前值,不是新值)
const isAllSelected = allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
this.$nextTick(() => { this.$nextTick(() => {
this.sceneQueryParams.identifiers = [...allIdentifiers]; if (isAllSelected) {
// 如果已经全部选中,则清空选择(反选)
this.sceneQueryParams.identifiers = [];
} else {
// 如果没有全部选中,则选中所有(勾选)
this.sceneQueryParams.identifiers = [...allIdentifiers];
}
}); });
} }
}, },

View File

@ -177,24 +177,31 @@
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8"> <el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
<el-card shadow="hover" class="message-card"> <el-card shadow="hover" class="message-card">
<!-- 信息栏 --> <!-- 信息栏 -->
<h3 style="font-weight: bold; margin-top: 10px"> <h3 style="font-weight: bold; margin-top: 10px">
<i class="el-icon-s-order"></i> <i class="el-icon-s-order"></i>
{{ $t('home.information') }} {{ $t('home.information') }}
</h3> </h3>
<div class="notice-bar" :class="{ animating: animate }" v-for="item in noticeList" :key="item.noticeId"> <div class="notice-bar" :class="{ animating: animate }">
<div style="cursor: pointer; display: table; width: 100%; line-height: 36px" @click="openDetail(item.noticeId)" v-for="item in noticeList" :key="item.noticeId"> <div
<div style="display: table-cell; padding-right: 10px"> style="cursor: pointer; display: table; width: 100%; line-height: 36px"
<el-tag size="mini" effect="dark" type="warning" v-if="item.noticeType == 2">公告</el-tag> v-for="item in noticeList"
<el-tag size="mini" effect="dark" v-else>信息</el-tag> :key="item.noticeId"
{{ item.noticeTitle }} @click="openDetail(item.noticeId)"
</div> :class="{ 'document-notice': item.noticeType == 3 }"
<div style="display: table-cell; width: 90px; font-size: 14px"> >
<i class="el-icon-time"></i> <div style="display: table-cell; padding-right: 10px">
{{ parseTime(item.createTime, '{y}-{m}-{d}') }} <el-tag size="mini" effect="dark" type="warning" v-if="item.noticeType == 2">公告</el-tag>
</div> <el-tag size="mini" effect="dark" type="danger" v-else-if="item.noticeType == 3">文档</el-tag>
</div> <el-tag size="mini" effect="dark" v-else>信息</el-tag>
{{ item.noticeTitle }}
</div>
<div style="display: table-cell; width: 90px; font-size: 14px">
<i class="el-icon-time"></i>
{{ parseTime(item.createTime, '{y}-{m}-{d}') }}
</div>
</div> </div>
</div>
</el-card> </el-card>
</el-col> </el-col>
@ -238,7 +245,14 @@
<span style="margin-left: 20px">{{ notice.createTime }}</span> <span style="margin-left: 20px">{{ notice.createTime }}</span>
</div> </div>
<div v-loading="loading" class="content"> <div v-loading="loading" class="content">
<div v-html="notice.noticeContent"></div> <!-- 文档类型显示下载链接 -->
<div v-if="notice.noticeType == '3' && notice.filePath" style="text-align: center; padding: 40px;">
<el-button type="primary" size="medium" @click="downloadFile(notice.filePath)">
<i class="el-icon-download"></i> 下载文档
</el-button>
</div>
<!-- 其他类型显示内容 -->
<div v-else v-html="notice.noticeContent"></div>
</div> </div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="closeDetail">{{ $t('home.close') }}</el-button> <el-button type="primary" @click="closeDetail">{{ $t('home.close') }}</el-button>
@ -393,7 +407,7 @@ export default {
version: '3.8.0', version: '3.8.0',
animate: true, animate: true,
duration: 10, // 公告滚动一次的时长(单位:秒) duration: 10, // 公告滚动一次的时长(单位:秒)
interval: 2000, // 公告间隔时间(单位:毫秒) interval: 5000, // 公告间隔时间(单位:毫秒)
// 服务器信息 // 服务器信息
server: { server: {
jvm: { jvm: {
@ -455,7 +469,7 @@ export default {
this.getMqttStats(); this.getMqttStats();
this.statisticMqtt(); this.statisticMqtt();
// 设置天气自动刷新(每30分钟刷新一次) // 设置天气自动刷新(每30分钟刷新一次)
this.weatherTimer = setInterval(() => { this.weatherTimer = setInterval(() => {
this.fetchWeather(); this.fetchWeather();
@ -662,7 +676,7 @@ export default {
async (position) => { async (position) => {
const lat = position.coords.latitude; const lat = position.coords.latitude;
const lon = position.coords.longitude; const lon = position.coords.longitude;
try { try {
// 使用OpenStreetMap的逆地理编码获取城市名称 // 使用OpenStreetMap的逆地理编码获取城市名称
const response = await axios.get(`https://nominatim.openstreetmap.org/reverse`, { const response = await axios.get(`https://nominatim.openstreetmap.org/reverse`, {
@ -675,12 +689,12 @@ export default {
}, },
timeout: 8000 timeout: 8000
}); });
if (response.data && response.data.address) { if (response.data && response.data.address) {
const address = response.data.address; const address = response.data.address;
// 优先使用城市,如果没有则使用区县、镇等 // 优先使用城市,如果没有则使用区县、镇等
let cityName = address.city || address.town || address.village || address.county || address.district; let cityName = address.city || address.town || address.village || address.county || address.district;
// 如果是中国,处理城市名称 // 如果是中国,处理城市名称
if (address.country_code === 'cn') { if (address.country_code === 'cn') {
// 如果城市名不包含"市",添加"市"后缀 // 如果城市名不包含"市",添加"市"后缀
@ -695,7 +709,7 @@ export default {
} }
} }
} }
if (cityName) { if (cityName) {
// 保存定位到的城市 // 保存定位到的城市
localStorage.setItem('weatherCity', cityName); localStorage.setItem('weatherCity', cityName);
@ -708,7 +722,7 @@ export default {
} catch (err) { } catch (err) {
// 逆地理编码失败,使用默认城市 // 逆地理编码失败,使用默认城市
} }
// 如果逆地理编码失败,使用默认城市 // 如果逆地理编码失败,使用默认城市
this.fetchWeather(); this.fetchWeather();
}, },
@ -776,11 +790,11 @@ export default {
}, },
timeout: 8000 timeout: 8000
}); });
if (geoResponse.data && geoResponse.data.length > 0) { if (geoResponse.data && geoResponse.data.length > 0) {
const lat = parseFloat(geoResponse.data[0].lat); const lat = parseFloat(geoResponse.data[0].lat);
const lon = parseFloat(geoResponse.data[0].lon); const lon = parseFloat(geoResponse.data[0].lon);
const weatherResponse = await axios.get(`https://api.open-meteo.com/v1/forecast`, { const weatherResponse = await axios.get(`https://api.open-meteo.com/v1/forecast`, {
params: { params: {
latitude: lat, latitude: lat,
@ -792,12 +806,12 @@ export default {
}, },
timeout: 8000 timeout: 8000
}); });
if (weatherResponse.data && weatherResponse.data.current) { if (weatherResponse.data && weatherResponse.data.current) {
const current = weatherResponse.data.current; const current = weatherResponse.data.current;
const daily = weatherResponse.data.daily; const daily = weatherResponse.data.daily;
const weatherCode = this.translateWeatherCode(current.weather_code); const weatherCode = this.translateWeatherCode(current.weather_code);
return { return {
success: true, success: true,
city: city, city: city,
@ -875,7 +889,7 @@ export default {
} }
} }
} }
this.isLoading = false; this.isLoading = false;
}, },
// 翻译天气类型(英文转中文) // 翻译天气类型(英文转中文)
@ -1050,6 +1064,16 @@ export default {
this.noticeList = response.rows.splice(0, 6); this.noticeList = response.rows.splice(0, 6);
}); });
}, },
// 下载文件
downloadFile(filePath) {
const url = process.env.VUE_APP_BASE_API + filePath;
const link = document.createElement('a');
link.href = url;
link.download = filePath.substring(filePath.lastIndexOf('/') + 1);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
// 打开信息详情 // 打开信息详情
openDetail(id) { openDetail(id) {
this.open = true; this.open = true;
@ -1396,7 +1420,7 @@ export default {
bmapInstance.enableScrollWheelZoom(true); bmapInstance.enableScrollWheelZoom(true);
bmapInstance.enableDoubleClickZoom(true); bmapInstance.enableDoubleClickZoom(true);
bmapInstance.enableDragging(true); bmapInstance.enableDragging(true);
// 设置中心点和缩放级别 // 设置中心点和缩放级别
const centerPoint = new BMap.Point(mapConfig.center[0], mapConfig.center[1]); const centerPoint = new BMap.Point(mapConfig.center[0], mapConfig.center[1]);
bmapInstance.setCenter(centerPoint); bmapInstance.setCenter(centerPoint);
@ -2943,11 +2967,44 @@ $aqi-unhealthy-color: #f44336;
line-height: 20px; line-height: 20px;
} }
} }
.notice-bar { // 文档类型公告的特殊样式
overflow: hidden; .document-notice {
position: relative; background-color: #ffebee !important; // 浅红色背景
border-left: 4px solid #f44336 !important; // 红色左边框
margin: 5px 0;
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
background-color: #ffcdd2 !important; // 悬停时加深背景色
transform: translateX(5px);
}
.el-tag {
background-color: #f44336 !important;
border-color: #f44336 !important;
}
} }
// 公告栏容器样式调整
.notice-bar {
overflow: hidden;
position: relative;
// 确保每个公告项有适当间距
& > div {
margin: 4px 0;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.3s ease;
&:hover {
background-color: #f5f5f5;
}
}
}
.animating { .animating {
animation: scroll 10s linear infinite; animation: scroll 10s linear infinite;
} }
</style> </style>

View File

@ -161,17 +161,19 @@
<pagination style="margin-bottom: 20px" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" :pageSizes="[12, 24, 36, 60]" @pagination="getList" /> <pagination style="margin-bottom: 20px" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" :pageSizes="[12, 24, 36, 60]" @pagination="getList" />
</div> </div>
<div v-if="showType == 'card'"> <div v-if="showType == 'card'">
<el-row :gutter="20" v-loading="loading"> <el-row :gutter="20" v-loading="loading" style="display: flex; flex-wrap: wrap;">
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in deviceList" :key="index" style="margin-bottom: 20px; text-align: center"> <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in deviceList" :key="index" style="margin-bottom: 20px; text-align: center; display: flex;">
<el-card :body-style="{ padding: '20px' }" shadow="always"> <el-card :body-style="{ padding: '20px', display: 'flex', flexDirection: 'column', height: '100%' }" shadow="always" style="width: 100%; display: flex; flex-direction: column;">
<el-row type="flex" :gutter="10" justify="space-between"> <el-row type="flex" :gutter="10" justify="space-between">
<el-col :span="20" style="text-align: left"> <el-col :span="20" style="text-align: left">
<el-link type="" :underline="false" @click="handleDeviceDetail(item)" style="font-weight: bold; font-size: 16px; line-height: 32px"> <el-link type="" :underline="false" @click="handleDeviceDetail(item)" style="font-weight: bold; font-size: 16px; line-height: 32px; display: flex; align-items: center; width: 100%;">
<el-tooltip class="item" effect="dark" :content="$t('device.index.105953-38')" placement="top-start"> <el-tooltip class="item" effect="dark" :content="$t('device.index.105953-38')" placement="top-start">
<svg-icon icon-class="share" style="font-size: 20px" v-if="item.isOwner != 1" /> <svg-icon icon-class="share" style="font-size: 20px; flex-shrink: 0;" v-if="item.isOwner != 1" />
</el-tooltip>
<svg-icon icon-class="device" style="flex-shrink: 0;" v-if="item.isOwner == 1" />
<el-tooltip :content="item.deviceName" placement="top" :disabled="!item.deviceName || item.deviceName.length <= 20">
<span style="margin: 0 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;">{{ item.deviceName }}</span>
</el-tooltip> </el-tooltip>
<svg-icon icon-class="device" v-if="item.isOwner == 1" />
<span style="margin: 0 5px">{{ item.deviceName }}</span>
<!-- <el-tag size="mini" type="info">Ver {{item.firmwareVersion}}</el-tag>--> <!-- <el-tag size="mini" type="info">Ver {{item.firmwareVersion}}</el-tag>-->
<!-- <el-text v-if="item.protocolCode" type="info" size="mini" style="font-size: 14px; color: #ccc">{{ item.protocolCode }}</el-text> --> <!-- <el-text v-if="item.protocolCode" type="info" size="mini" style="font-size: 14px; color: #ccc">{{ item.protocolCode }}</el-text> -->
</el-link> </el-link>
@ -189,38 +191,38 @@
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="10"> <el-row :gutter="10" style="flex: 1; min-height: 0;">
<el-col :span="17"> <el-col :span="17" style="display: flex; flex-direction: column;">
<div style="text-align: left; line-height: 40px; white-space: nowrap"> <div style="text-align: left; line-height: 28px; min-height: 28px; word-wrap: break-word; overflow-wrap: break-word; flex-shrink: 0;">
<dict-tag :options="dict.type.iot_device_status" :value="item.status" size="mini" style="display: inline-block" /> <dict-tag :options="dict.type.iot_device_status" :value="item.status" size="mini" style="display: inline-block; margin-bottom: 4px; vertical-align: middle;" />
<span style="display: inline-block; margin: 0 10px"> <span style="display: inline-block; margin: 0 5px 4px 5px; vertical-align: middle;">
<!-- <el-tag type="success" size="small" v-if="item.isShadow == 1">影子</el-tag> <el-tag type="primary" size="mini" v-if="item.protocolCode" style="margin-bottom: 4px;">{{ item.protocolCode }}</el-tag>
<el-tag type="info" size="small" v-else>影子</el-tag> -->
<el-tag type="primary" size="mini" v-if="item.protocolCode">{{ item.protocolCode }}</el-tag>
</span> </span>
<el-tag type="primary" size="mini" v-if="item.transport">{{ item.transport }}</el-tag> <el-tag type="primary" size="mini" v-if="item.transport" style="margin-bottom: 4px;">{{ item.transport }}</el-tag>
<el-tag type="danger" size="mini" style="display: inline-block; margin: 0 0 0 10px" v-if="item.alertCount"> <el-tag type="danger" size="mini" style="display: inline-block; margin: 0 0 4px 5px;" v-if="item.alertCount">
{{ $t('device.index.105953-53') }}{{ item.alertCount.unprocessedCount }} {{ $t('device.index.105953-53') }}{{ item.alertCount.unprocessedCount }}
</el-tag> </el-tag>
<!-- <dict-tag :options="dict.type.iot_location_way" :value="item.locationWay" size="small" style="display:inline-block;" /> -->
<!-- <dict-tag :options="dict.type.iot_transport_type" :value="item.transport" size="small" style="display: inline-block" /> -->
</div> </div>
<el-descriptions :column="1" size="mini" style="white-space: nowrap"> <el-descriptions :column="1" size="mini" style="margin-top: 8px; flex: 1;">
<el-descriptions-item :label="$t('device.index.105953-20')"> <el-descriptions-item :label="$t('device.index.105953-20')">
{{ item.serialNumber }} <el-tooltip :content="item.serialNumber" placement="top" :disabled="!item.serialNumber || item.serialNumber.length <= 15">
<span style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ item.serialNumber }}</span>
</el-tooltip>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('device.index.105953-39')"> <el-descriptions-item :label="$t('device.index.105953-39')">
{{ item.productName }} <el-tooltip :content="item.productName" placement="top" :disabled="!item.productName || item.productName.length <= 20">
<span style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ item.productName }}</span>
</el-tooltip>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('device.index.105953-32')"> <el-descriptions-item :label="$t('device.index.105953-32')">
{{ parseTime(item.activeTime, '{y}-{m}-{d}') }} {{ parseTime(item.activeTime, '{y}-{m}-{d}') }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-col> </el-col>
<el-col :span="7"> <el-col :span="7" style="flex-shrink: 0;">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<el-image <el-image
style="width: 100%; height: 100px; border-radius: 10px" style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
lazy lazy
:preview-src-list="[baseUrl + item.imgUrl]" :preview-src-list="[baseUrl + item.imgUrl]"
:src="baseUrl + item.imgUrl" :src="baseUrl + item.imgUrl"
@ -228,21 +230,21 @@
v-if="item.imgUrl != null && item.imgUrl != ''" v-if="item.imgUrl != null && item.imgUrl != ''"
></el-image> ></el-image>
<el-image <el-image
style="width: 100%; height: 100px; border-radius: 10px" style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
:preview-src-list="[require('@/assets/images/gateway.png')]" :preview-src-list="[require('@/assets/images/gateway.png')]"
:src="require('@/assets/images/gateway.png')" :src="require('@/assets/images/gateway.png')"
fit="cover" fit="cover"
v-else-if="item.deviceType == 2" v-else-if="item.deviceType == 2"
></el-image> ></el-image>
<el-image <el-image
style="width: 100%; height: 100px; border-radius: 10px" style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
:preview-src-list="[require('@/assets/images/video.png')]" :preview-src-list="[require('@/assets/images/video.png')]"
:src="require('@/assets/images/video.png')" :src="require('@/assets/images/video.png')"
fit="cover" fit="cover"
v-else-if="item.deviceType == 3" v-else-if="item.deviceType == 3"
></el-image> ></el-image>
<el-image <el-image
style="width: 100%; height: 100px; border-radius: 10px" style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
:preview-src-list="[require('@/assets/images/product.png')]" :preview-src-list="[require('@/assets/images/product.png')]"
:src="require('@/assets/images/product.png')" :src="require('@/assets/images/product.png')"
fit="cover" fit="cover"
@ -251,14 +253,14 @@
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-button-group style="margin-top: 15px"> <el-button-group style="margin-top: 15px; width: 100%; display: flex; justify-content: center;">
<el-button type="danger" size="mini" style="padding: 5px 10px" icon="el-icon-delete" @click="handleDelete(item)" v-hasPermi="['iot:device:remove']"> <el-button type="danger" size="mini" style="padding: 5px 10px; flex: 1;" icon="el-icon-delete" @click="handleDelete(item)" v-hasPermi="['iot:device:remove']">
{{ $t('device.index.105953-35') }} {{ $t('device.index.105953-35') }}
</el-button> </el-button>
<el-button type="primary" size="mini" style="padding: 5px 15px" icon="el-icon-view" @click="handleEditDevice(item, 'basic')" v-hasPermi="['iot:device:query']"> <el-button type="primary" size="mini" style="padding: 5px 15px; flex: 1;" icon="el-icon-view" @click="handleEditDevice(item, 'basic')" v-hasPermi="['iot:device:query']">
{{ $t('device.index.105953-36') }} {{ $t('device.index.105953-36') }}
</el-button> </el-button>
<el-button type="success" size="mini" style="padding: 5px 15px" icon="el-icon-odometer" @click="handleRunDevice(item)" v-hasPermi="['iot:device:query']"> <el-button type="success" size="mini" style="padding: 5px 15px; flex: 1;" icon="el-icon-odometer" @click="handleRunDevice(item)" v-hasPermi="['iot:device:query']">
{{ $t('device.index.105953-40') }} {{ $t('device.index.105953-40') }}
</el-button> </el-button>
</el-button-group> </el-button-group>

View File

@ -375,7 +375,7 @@
<script> <script>
import { hex2int, int2hex, copyText, formatDate, deepClone } from '@/utils/common'; import { hex2int, int2hex, copyText, formatDate, deepClone } from '@/utils/common';
import { encode, decode, messagePost, addPreferences, editPreferences, delPreferences, preferencesList } from '@/api/iot/mqttTest'; import { encode, decode, messagePost, addPreferences, editPreferences, delPreferences, preferencesList, enableDebugLog, disableDebugLog } from '@/api/iot/mqttTest';
export default { export default {
props: { props: {
device: { device: {
@ -910,6 +910,22 @@ export default {
}); });
} }
}, },
/**开启调试日志记录 */
async enableDebugLogging() {
try {
await enableDebugLog();
} catch (err) {
console.warn('开启调试日志记录失败:', err);
}
},
/**关闭调试日志记录 */
async disableDebugLogging() {
try {
await disableDebugLog();
} catch (err) {
console.warn('关闭调试日志记录失败:', err);
}
},
}, },
mounted() { mounted() {
this.resetCreateForm(); this.resetCreateForm();
@ -917,8 +933,13 @@ export default {
this.$busEvent.$off('updateData', this.handleUpdateData); this.$busEvent.$off('updateData', this.handleUpdateData);
// 绑定新的监听器 // 绑定新的监听器
this.$busEvent.$on('updateData', this.handleUpdateData); this.$busEvent.$on('updateData', this.handleUpdateData);
// 开启调试日志记录
this.enableDebugLogging();
}, },
beforeDestroy() { beforeDestroy() {
// 关闭调试日志记录
this.disableDebugLogging();
// 移除所有事件监听器 // 移除所有事件监听器
this.$busEvent.$off('updateData', this.handleUpdateData); this.$busEvent.$off('updateData', this.handleUpdateData);
this.$busEvent.$off('updateMqttMessage'); this.$busEvent.$off('updateMqttMessage');

View File

@ -78,7 +78,7 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item :label="$t('system.notice.670989-5')" prop="noticeType"> <el-form-item :label="$t('system.notice.670989-5')" prop="noticeType">
<el-select v-model="form.noticeType" :placeholder="$t('system.notice.670989-6')" style="width: 100%"> <el-select v-model="form.noticeType" :placeholder="$t('system.notice.670989-6')" style="width: 100%" @change="handleNoticeTypeChange">
<el-option v-for="dict in dict.type.sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option> <el-option v-for="dict in dict.type.sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -90,7 +90,20 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <!-- 文档类型时显示文件上传 -->
<el-col :span="24" v-if="isDocumentType">
<el-form-item label="文档上传" prop="filePath">
<file-upload
v-model="form.filePath"
:file-type="['pdf', 'doc', 'docx']"
:file-size="50"
:limit="1"
:upload-file-url="getUploadUrl()"
></file-upload>
</el-form-item>
</el-col>
<!-- 非文档类型时显示内容编辑器 -->
<el-col :span="24" v-if="!isDocumentType">
<el-form-item :label="$t('system.notice.670989-7')"> <el-form-item :label="$t('system.notice.670989-7')">
<editor v-model="form.noticeContent" :min-height="192" /> <editor v-model="form.noticeContent" :min-height="192" />
</el-form-item> </el-form-item>
@ -108,9 +121,13 @@
<script> <script>
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice'; import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice';
import FileUpload from '@/components/FileUpload';
export default { export default {
name: 'Notice', name: 'Notice',
components: {
FileUpload
},
dicts: ['sys_notice_status', 'sys_notice_type'], dicts: ['sys_notice_status', 'sys_notice_type'],
data() { data() {
return { return {
@ -146,9 +163,31 @@ export default {
rules: { rules: {
noticeTitle: [{ required: true, message: this.$t('system.notice.670989-9'), trigger: 'blur' }], noticeTitle: [{ required: true, message: this.$t('system.notice.670989-9'), trigger: 'blur' }],
noticeType: [{ required: true, message: this.$t('system.notice.670989-10'), trigger: 'change' }], noticeType: [{ required: true, message: this.$t('system.notice.670989-10'), trigger: 'change' }],
filePath: [
{
validator: (rule, value, callback) => {
if (this.isDocumentType && !value) {
callback(new Error('文档类型必须上传文件'));
} else {
callback();
}
},
trigger: 'blur'
}
],
}, },
}; };
}, },
computed: {
// 判断是否为文档类型
isDocumentType() {
if (!this.form || this.form.noticeType === undefined || this.form.noticeType === null) {
return false;
}
// 兼容字符串和数字类型
return this.form.noticeType == '3';
}
},
created() { created() {
this.getList(); this.getList();
}, },
@ -174,10 +213,27 @@ export default {
noticeTitle: undefined, noticeTitle: undefined,
noticeType: undefined, noticeType: undefined,
noticeContent: undefined, noticeContent: undefined,
filePath: undefined,
status: '0', status: '0',
}; };
this.resetForm('form'); this.resetForm('form');
}, },
// 处理公告类型变化
handleNoticeTypeChange(value) {
// 切换类型时清空不相关的字段
const noticeType = String(value || '');
if (noticeType === '3') {
// 文档类型,清空内容
this.form.noticeContent = undefined;
// 触发文件路径验证
this.$nextTick(() => {
this.$refs.form && this.$refs.form.validateField('filePath');
});
} else {
// 非文档类型,清空文件路径
this.form.filePath = undefined;
}
},
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery() { handleQuery() {
this.queryParams.pageNum = 1; this.queryParams.pageNum = 1;
@ -244,6 +300,12 @@ export default {
}) })
.catch(() => {}); .catch(() => {});
}, },
/** 获取上传URL */
getUploadUrl() {
// 使用 window.location.origin 作为备用方案
const baseUrl = process.env.VUE_APP_BASE_API || '/prod-api';
return baseUrl + '/common/upload';
}
}, },
}; };
</script> </script>