20260315:1、网关产品支持一键导入子产品物模型;
2、网关设备支持跨子设备物模型计算公式; 3、历史数据支持网关设备数据分析
This commit is contained in:
parent
ec4f8be1ad
commit
90b078323b
@ -10,6 +10,7 @@ import com.fastbee.common.core.thingsModel.SceneThingsModelItem;
|
||||
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
|
||||
import com.fastbee.common.exception.ServiceException;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.SecurityUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.spring.SpringUtils;
|
||||
import com.fastbee.iot.data.service.IFunctionInvoke;
|
||||
@ -76,7 +77,8 @@ public class SceneContext {
|
||||
*/
|
||||
private List<SceneThingsModelItem> sceneThingsModelItems;
|
||||
|
||||
public SceneContext(String deviceNum, Long productId, int type, List<ThingsModelSimpleItem> thingsModelSimpleItems) {
|
||||
public SceneContext(String deviceNum, Long productId, int type,
|
||||
List<ThingsModelSimpleItem> thingsModelSimpleItems) {
|
||||
this.deviceNum = deviceNum;
|
||||
this.productId = productId;
|
||||
this.type = type;
|
||||
@ -142,6 +144,7 @@ public class SceneContext {
|
||||
|
||||
/***
|
||||
* 设备触发脚本处理
|
||||
*
|
||||
* @param scriptTemplate 解析后的Json脚本数据
|
||||
* @return
|
||||
*/
|
||||
@ -169,11 +172,14 @@ public class SceneContext {
|
||||
if (sceneThingsModelItems == null) {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, matchItem.getRemark());
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(),
|
||||
matchItem.getValue(), type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
deviceNum, matchItem.getRemark());
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
// 3.设备上报值匹配
|
||||
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue());
|
||||
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(),
|
||||
matchItem.getValue());
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -190,8 +196,10 @@ public class SceneContext {
|
||||
if (sceneThingsModelItems == null) {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, "");
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline",
|
||||
type == 5 ? "1" : "0", type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
deviceNum, "");
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
// 记录结果
|
||||
return true;
|
||||
@ -236,7 +244,8 @@ public class SceneContext {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num, "");
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
num, "");
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
resultCount++;
|
||||
}
|
||||
@ -249,6 +258,7 @@ public class SceneContext {
|
||||
|
||||
/***
|
||||
* 产品触发脚本处理
|
||||
*
|
||||
* @param scriptTemplate
|
||||
* @return
|
||||
*/
|
||||
@ -276,11 +286,14 @@ public class SceneContext {
|
||||
if (sceneThingsModelItems == null) {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, matchItem.getRemark());
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(),
|
||||
matchItem.getValue(), type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
deviceNum, matchItem.getRemark());
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
// 设备上报值匹配
|
||||
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue());
|
||||
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(),
|
||||
matchItem.getValue());
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -297,8 +310,10 @@ public class SceneContext {
|
||||
if (sceneThingsModelItems == null) {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, "");
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline",
|
||||
type == 5 ? "1" : "0", type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
deviceNum, "");
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
// 记录结果
|
||||
return true;
|
||||
@ -344,7 +359,8 @@ public class SceneContext {
|
||||
sceneThingsModelItems = new ArrayList<>();
|
||||
}
|
||||
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type,
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num, "");
|
||||
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
|
||||
num, "");
|
||||
sceneThingsModelItems.add(sceneItem);
|
||||
resultCount++;
|
||||
}
|
||||
@ -442,14 +458,17 @@ public class SceneContext {
|
||||
* @param sceneId 场景ID
|
||||
*/
|
||||
private void alert(Long sceneId, boolean isNotify) {
|
||||
Set<Long> sceneIdSet = sceneThingsModelItems.stream().map(SceneThingsModelItem::getSceneId).collect(Collectors.toSet());
|
||||
Set<Long> sceneIdSet = sceneThingsModelItems.stream().map(SceneThingsModelItem::getSceneId)
|
||||
.collect(Collectors.toSet());
|
||||
List<SceneTerminalUserVO> sceneTerminalUserVOList = sceneService.selectTerminalUserBySceneIds(sceneIdSet);
|
||||
Map<Long, SceneTerminalUserVO> sceneTerminalUserMap = sceneTerminalUserVOList.stream().collect(Collectors.toMap(SceneTerminalUserVO::getSceneId, Function.identity()));
|
||||
Map<Long, SceneTerminalUserVO> sceneTerminalUserMap = sceneTerminalUserVOList.stream()
|
||||
.collect(Collectors.toMap(SceneTerminalUserVO::getSceneId, Function.identity()));
|
||||
List<AlertLog> alertLogList = new ArrayList<>();
|
||||
for (SceneThingsModelItem sceneThingsModelItem : sceneThingsModelItems) {
|
||||
// 查询设备信息
|
||||
Device device = deviceService.selectDeviceBySerialNumber(sceneThingsModelItem.getDeviceNumber());
|
||||
Optional.ofNullable(device).orElseThrow(() -> new ServiceException("告警推送,设备不存在" + "[{" + sceneThingsModelItem.getDeviceNumber() + "}]"));
|
||||
Optional.ofNullable(device).orElseThrow(
|
||||
() -> new ServiceException("告警推送,设备不存在" + "[{" + sceneThingsModelItem.getDeviceNumber() + "}]"));
|
||||
// 判断是否是终端用户的场景
|
||||
SceneTerminalUserVO sceneTerminalUserVO = sceneTerminalUserMap.get(sceneId);
|
||||
if (1 == sceneTerminalUserVO.getTerminalUser()) {
|
||||
@ -469,7 +488,8 @@ public class SceneContext {
|
||||
// 开启通知发送
|
||||
if (isNotify) {
|
||||
AlertPushParams alertPushParams = buildAlertPushParams(device, sceneThingsModelItem);
|
||||
List<AlertNotifyTemplate> alertNotifyTemplateList = alertService.listAlertNotifyTemplate(alertSceneSendVO.getAlertId());
|
||||
List<AlertNotifyTemplate> alertNotifyTemplateList = alertService
|
||||
.listAlertNotifyTemplate(alertSceneSendVO.getAlertId());
|
||||
alertPushParams.setAlertName(alertSceneSendVO.getAlertName());
|
||||
for (AlertNotifyTemplate alertNotifyTemplate : alertNotifyTemplateList) {
|
||||
alertPushParams.setNotifyTemplateId(alertNotifyTemplate.getNotifyTemplateId());
|
||||
@ -493,14 +513,22 @@ public class SceneContext {
|
||||
alertPushParams.setDeviceName(device.getDeviceName());
|
||||
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
|
||||
// List<DeviceUser> deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId());
|
||||
// 多租户改版查询自己配置的告警用户
|
||||
// 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
|
||||
Set<Long> userIdSet = new HashSet<>();
|
||||
try {
|
||||
userIdSet.add(SecurityUtils.getUserId());
|
||||
} catch (Exception e) {
|
||||
userIdSet.add(device.getUserId() == null ? device.getTenantId() : device.getUserId());
|
||||
}
|
||||
DeviceAlertUser deviceAlertUser = new DeviceAlertUser();
|
||||
deviceAlertUser.setDeviceId(device.getDeviceId());
|
||||
List<DeviceAlertUser> deviceUserList = deviceAlertUserService.selectDeviceAlertUserList(deviceAlertUser);
|
||||
if (CollectionUtils.isNotEmpty(deviceUserList)) {
|
||||
alertPushParams.setUserPhoneSet(deviceUserList.stream().map(DeviceAlertUser::getPhoneNumber).filter(StringUtils::isNotEmpty).collect(Collectors.toSet()));
|
||||
alertPushParams.setUserIdSet(deviceUserList.stream().map(DeviceAlertUser::getUserId).collect(Collectors.toSet()));
|
||||
alertPushParams.setUserPhoneSet(deviceUserList.stream().map(DeviceAlertUser::getPhoneNumber)
|
||||
.filter(StringUtils::isNotEmpty).collect(Collectors.toSet()));
|
||||
userIdSet.addAll(deviceUserList.stream().map(DeviceAlertUser::getUserId).collect(Collectors.toSet()));
|
||||
}
|
||||
alertPushParams.setUserIdSet(userIdSet);
|
||||
String address;
|
||||
if (StringUtils.isNotEmpty(device.getNetworkAddress())) {
|
||||
address = device.getNetworkAddress();
|
||||
@ -546,7 +574,8 @@ public class SceneContext {
|
||||
* @return com.fastbee.iot.domain.AlertLog
|
||||
* @param: device
|
||||
*/
|
||||
private AlertLog getAlertLog(AlertSceneSendVO alertSceneSendVO, Device device, SceneThingsModelItem sceneThingsModelItem) {
|
||||
private AlertLog getAlertLog(AlertSceneSendVO alertSceneSendVO, Device device,
|
||||
SceneThingsModelItem sceneThingsModelItem) {
|
||||
AlertLog alertLog = new AlertLog();
|
||||
alertLog.setAlertName(alertSceneSendVO.getAlertName());
|
||||
alertLog.setAlertLevel(alertSceneSendVO.getAlertLevel());
|
||||
@ -609,14 +638,16 @@ public class SceneContext {
|
||||
// 比较值用英文中划线分割 -
|
||||
String[] triggerValues = triggerValue.split("-");
|
||||
if (isNumeric(value) && isNumeric(triggerValues[0]) && isNumeric(triggerValues[1])) {
|
||||
result = Double.parseDouble(value) >= Double.parseDouble(triggerValues[0]) && Double.parseDouble(value) <= Double.parseDouble(triggerValues[1]);
|
||||
result = Double.parseDouble(value) >= Double.parseDouble(triggerValues[0])
|
||||
&& Double.parseDouble(value) <= Double.parseDouble(triggerValues[1]);
|
||||
}
|
||||
break;
|
||||
case "notBetween":
|
||||
// 比较值用英文中划线分割 -
|
||||
String[] trigValues = triggerValue.split("-");
|
||||
if (isNumeric(value) && isNumeric(trigValues[0]) && isNumeric(trigValues[1])) {
|
||||
result = Double.parseDouble(value) <= Double.parseDouble(trigValues[0]) || Double.parseDouble(value) >= Double.parseDouble(trigValues[1]);
|
||||
result = Double.parseDouble(value) <= Double.parseDouble(trigValues[0])
|
||||
|| Double.parseDouble(value) >= Double.parseDouble(trigValues[1]);
|
||||
}
|
||||
break;
|
||||
case "contain":
|
||||
|
||||
@ -7,8 +7,10 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.iot.domain.ThingsModel;
|
||||
import com.fastbee.iot.model.AlertCountVO;
|
||||
import com.fastbee.iot.model.DeviceHistoryParam;
|
||||
import com.fastbee.iot.model.SubDeviceCompareParam;
|
||||
import com.fastbee.iot.model.ThingsModelLogCountVO;
|
||||
import com.fastbee.iot.model.dto.ThingsModelDTO;
|
||||
import com.fastbee.iot.model.param.DataCenterParam;
|
||||
@ -66,6 +68,184 @@ public class DataCenterController {
|
||||
return AjaxResult.success(jsonObject);
|
||||
}
|
||||
|
||||
@ApiOperation("网关产品-可导入的子产品物模型列表(按identifier去重)")
|
||||
@PreAuthorize("@ss.hasPermi('iot:model:list')")
|
||||
@GetMapping("/subProductThingsModel")
|
||||
public AjaxResult listSubProductThingsModel(@RequestParam Long gwProductId) {
|
||||
List<ThingsModel> list = dataCenterService.listSubProductThingsModelForImport(gwProductId);
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
@ApiOperation("网关产品-一键导入子产品物模型")
|
||||
@PreAuthorize("@ss.hasPermi('iot:model:add')")
|
||||
@PostMapping("/importSubProductThingsToGateway")
|
||||
public AjaxResult importSubProductThingsToGateway(@RequestBody Map<String, Long> body) {
|
||||
Long gwProductId = body != null ? body.get("gwProductId") : null;
|
||||
if (gwProductId == null) {
|
||||
return AjaxResult.error("请传入网关产品ID");
|
||||
}
|
||||
int count = dataCenterService.importSubProductThingsToGateway(gwProductId);
|
||||
return AjaxResult.success("导入成功,共 " + count + " 个物模型", count);
|
||||
}
|
||||
|
||||
@ApiOperation("网关计算物模型历史(查询时按公式计算,按分钟对齐,缺数沿用上一分钟)")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@PostMapping("/deviceHistoryCalculated")
|
||||
public AjaxResult deviceHistoryCalculated(@RequestBody DeviceHistoryParam deviceHistoryParam) {
|
||||
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||
return AjaxResult.error("请选择网关设备");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
|
||||
}
|
||||
List<JSONObject> list = dataCenterService.deviceHistoryCalculated(deviceHistoryParam);
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
@ApiOperation("导出网关计算物模型历史Excel")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@PostMapping("/deviceHistoryCalculated/export")
|
||||
public void exportDeviceHistoryCalculated(@RequestBody DeviceHistoryParam deviceHistoryParam,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.getWriter().write("请选择网关设备");
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
|
||||
}
|
||||
List<JSONObject> dataList = dataCenterService.deviceHistoryCalculated(deviceHistoryParam);
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
response.getWriter().write("没有数据可导出");
|
||||
return;
|
||||
}
|
||||
List<com.fastbee.iot.model.dto.ThingsModelDTO> modelList = dataCenterService
|
||||
.listGatewayCalculatedModels(deviceHistoryParam.getDeviceId());
|
||||
Map<String, String> identifierNameMap = new LinkedHashMap<>();
|
||||
for (com.fastbee.iot.model.dto.ThingsModelDTO m : modelList) {
|
||||
identifierNameMap.put(m.getIdentifier(), m.getModelName() != null ? m.getModelName() : m.getIdentifier());
|
||||
}
|
||||
List<List<String>> headList = new ArrayList<>();
|
||||
headList.add(Collections.singletonList("时间"));
|
||||
List<String> identifierList = new ArrayList<>();
|
||||
if (deviceHistoryParam.getIdentifierList() != null) {
|
||||
for (DeviceHistoryParam.IdentifierVO vo : deviceHistoryParam.getIdentifierList()) {
|
||||
identifierList.add(vo.getIdentifier());
|
||||
headList.add(Collections
|
||||
.singletonList(identifierNameMap.getOrDefault(vo.getIdentifier(), vo.getIdentifier())));
|
||||
}
|
||||
}
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
for (JSONObject jsonObject : dataList) {
|
||||
String time = jsonObject.keySet().iterator().next();
|
||||
Object dataValue = jsonObject.get(time);
|
||||
List<Object> rowData = new ArrayList<>();
|
||||
rowData.add(time);
|
||||
if (dataValue instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<JSONObject> identifierDataList = (List<JSONObject>) dataValue;
|
||||
Map<String, Object> valueMap = new HashMap<>();
|
||||
for (JSONObject identifierObj : identifierDataList) {
|
||||
for (Map.Entry<String, Object> entry : identifierObj.entrySet()) {
|
||||
valueMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
for (String id : identifierList) {
|
||||
rowData.add(valueMap.get(id));
|
||||
}
|
||||
}
|
||||
rows.add(rowData);
|
||||
}
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
String fileName = URLEncoder
|
||||
.encode(deviceHistoryParam.getSerialNumber() + "_calculated_"
|
||||
+ DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).head(headList).build();
|
||||
WriteSheet writeSheet = EasyExcel.writerSheet(0, "网关计算量历史").build();
|
||||
excelWriter.write(rows, writeSheet);
|
||||
excelWriter.finish();
|
||||
}
|
||||
|
||||
@ApiOperation("网关设备仅计算物模型列表(历史记录-网关计算量 变量下拉用)")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@GetMapping("/gatewayCalculatedModels")
|
||||
public AjaxResult listGatewayCalculatedModels(@RequestParam Long deviceId) {
|
||||
return AjaxResult.success(dataCenterService.listGatewayCalculatedModels(deviceId));
|
||||
}
|
||||
|
||||
@ApiOperation("多设备对比历史(支持多变量,按分钟对齐,缺数沿用上一分钟)")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@PostMapping("/subDeviceComparativeHistory")
|
||||
public AjaxResult subDeviceComparativeHistory(@RequestBody SubDeviceCompareParam param) {
|
||||
boolean hasIdentifier = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
|
||||
|| !StringUtils.isEmpty(param.getIdentifier());
|
||||
if (param.getGwDeviceId() == null || !hasIdentifier) {
|
||||
return AjaxResult.error("请选择网关设备和变量");
|
||||
}
|
||||
if (param.getType() == null) {
|
||||
param.setType(1);
|
||||
}
|
||||
List<JSONObject> list = dataCenterService.subDeviceComparativeHistory(param);
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
@ApiOperation("导出多设备对比历史Excel")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@PostMapping("/subDeviceComparativeHistory/export")
|
||||
public void exportSubDeviceComparativeHistory(@RequestBody SubDeviceCompareParam param,
|
||||
HttpServletResponse response) throws IOException {
|
||||
boolean hasIdentifier = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
|
||||
|| !StringUtils.isEmpty(param.getIdentifier());
|
||||
if (param.getGwDeviceId() == null || !hasIdentifier) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.getWriter().write("请选择网关设备和变量");
|
||||
return;
|
||||
}
|
||||
if (param.getType() == null) {
|
||||
param.setType(1);
|
||||
}
|
||||
List<JSONObject> dataList = dataCenterService.subDeviceComparativeHistory(param);
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
response.getWriter().write("没有数据可导出");
|
||||
return;
|
||||
}
|
||||
List<List<String>> headList = new ArrayList<>();
|
||||
headList.add(Collections.singletonList("时间"));
|
||||
Set<String> keys = new LinkedHashSet<>(dataList.get(0).keySet());
|
||||
keys.remove("time");
|
||||
for (String k : keys) {
|
||||
headList.add(Collections.singletonList(k));
|
||||
}
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
for (JSONObject row : dataList) {
|
||||
List<Object> r = new ArrayList<>();
|
||||
r.add(row.getString("time"));
|
||||
for (String k : keys) {
|
||||
r.add(row.get(k));
|
||||
}
|
||||
rows.add(r);
|
||||
}
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
String idPart = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
|
||||
? String.join("-", param.getIdentifierList()) : param.getIdentifier();
|
||||
String fileName = URLEncoder
|
||||
.encode("sub_compare_" + idPart + "_"
|
||||
+ DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).head(headList).build();
|
||||
WriteSheet writeSheet = EasyExcel.writerSheet(0, "多设备对比").build();
|
||||
excelWriter.write(rows, writeSheet);
|
||||
excelWriter.finish();
|
||||
}
|
||||
|
||||
@ApiOperation("查询场景变量历史数据")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@GetMapping("/sceneHistory")
|
||||
@ -138,7 +318,8 @@ public class DataCenterController {
|
||||
@ApiOperation("导出设备历史数据Excel")
|
||||
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||
@PostMapping("/deviceHistory/export")
|
||||
public void exportDeviceHistory(@RequestBody DeviceHistoryParam deviceHistoryParam, HttpServletResponse response) throws IOException {
|
||||
public void exportDeviceHistory(@RequestBody DeviceHistoryParam deviceHistoryParam, HttpServletResponse response)
|
||||
throws IOException {
|
||||
final String serialNumber = deviceHistoryParam.getSerialNumber();
|
||||
if (StringUtils.isEmpty(serialNumber)) {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
@ -178,11 +359,13 @@ public class DataCenterController {
|
||||
for (String identifier : identifierList) {
|
||||
ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
|
||||
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
|
||||
? thingsModel.getModelName() : identifier;
|
||||
? thingsModel.getModelName()
|
||||
: identifier;
|
||||
identifierNameMap.put(identifier, headerName);
|
||||
// 获取数据类型
|
||||
String dataType = "string"; // 默认字符串类型
|
||||
if (thingsModel != null && thingsModel.getDatatype() != null && StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
|
||||
if (thingsModel != null && thingsModel.getDatatype() != null
|
||||
&& StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
|
||||
dataType = thingsModel.getDatatype().getType();
|
||||
}
|
||||
identifierTypeMap.put(identifier, dataType);
|
||||
@ -220,11 +403,13 @@ public class DataCenterController {
|
||||
for (String identifier : identifierSet) {
|
||||
ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
|
||||
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
|
||||
? thingsModel.getModelName() : identifier;
|
||||
? thingsModel.getModelName()
|
||||
: identifier;
|
||||
identifierNameMap.put(identifier, headerName);
|
||||
// 获取数据类型
|
||||
String dataType = "string"; // 默认字符串类型
|
||||
if (thingsModel != null && thingsModel.getDatatype() != null && StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
|
||||
if (thingsModel != null && thingsModel.getDatatype() != null
|
||||
&& StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
|
||||
dataType = thingsModel.getDatatype().getType();
|
||||
}
|
||||
identifierTypeMap.put(identifier, dataType);
|
||||
@ -290,7 +475,9 @@ public class DataCenterController {
|
||||
// 设置响应头
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
String fileName = URLEncoder.encode(serialNumber + "_" + DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8").replaceAll("\\+", "%20");
|
||||
String fileName = URLEncoder
|
||||
.encode(serialNumber + "_" + DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
|
||||
.replaceAll("\\+", "%20");
|
||||
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||
|
||||
// 使用EasyExcel写入数据
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package com.fastbee.iot.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多设备对比查询参数(支持多变量,历史记录按分钟对齐,缺数沿用上一分钟最后值)
|
||||
* <p>
|
||||
* 传 identifierList 时支持多变量对比;仅传 identifier 时按单变量兼容。
|
||||
* </p>
|
||||
*
|
||||
* @author fastbee
|
||||
*/
|
||||
@Data
|
||||
public class SubDeviceCompareParam {
|
||||
|
||||
/** 网关设备ID */
|
||||
private Long gwDeviceId;
|
||||
/** 物模型标识符(单变量时使用,与 identifierList 二选一) */
|
||||
private String identifier;
|
||||
/** 物模型标识符列表(多变量对比时使用,优先于 identifier) */
|
||||
private List<String> identifierList;
|
||||
/** 指定参与对比的子设备ID列表;为空或 null 时表示全部子设备 */
|
||||
private List<Long> subDeviceIds;
|
||||
/** 类型 1-属性 2-功能 3-事件,对比仅支持属性 */
|
||||
private Integer type;
|
||||
private String beginTime;
|
||||
private String endTime;
|
||||
}
|
||||
@ -1,17 +1,16 @@
|
||||
package com.fastbee.iot.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.iot.domain.ThingsModel;
|
||||
import com.fastbee.iot.model.AlertCountVO;
|
||||
import com.fastbee.iot.model.DeviceHistoryParam;
|
||||
import com.fastbee.iot.model.HistoryModel;
|
||||
import com.fastbee.iot.model.SubDeviceCompareParam;
|
||||
import com.fastbee.iot.model.dto.ThingsModelDTO;
|
||||
import com.fastbee.iot.model.ThingsModelLogCountVO;
|
||||
import com.fastbee.iot.model.param.DataCenterParam;
|
||||
import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据中心服务类
|
||||
@ -30,6 +29,38 @@ public interface DataCenterService {
|
||||
*/
|
||||
List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam);
|
||||
|
||||
/**
|
||||
* 网关产品:查询绑定子产品的物模型列表(用于一键导入,按 identifier 去重)
|
||||
*
|
||||
* @param gwProductId 网关产品ID
|
||||
* @return 去重后的物模型列表
|
||||
*/
|
||||
List<ThingsModel> listSubProductThingsModelForImport(Long gwProductId);
|
||||
|
||||
/**
|
||||
* 网关产品:一键将绑定子产品的物模型导入到网关产品(按 identifier 去重,已存在则跳过)
|
||||
*
|
||||
* @param gwProductId 网关产品ID
|
||||
* @return 本次导入数量
|
||||
*/
|
||||
int importSubProductThingsToGateway(Long gwProductId);
|
||||
|
||||
/**
|
||||
* 网关计算物模型历史:仅查询时计算,按分钟对齐,缺数沿用上一分钟最后值;公式支持 {slaveId:N.identifier}
|
||||
*
|
||||
* @param deviceHistoryParam 网关设备ID、计算物模型 identifier 列表、时间范围
|
||||
* @return 与 deviceHistory 相同格式
|
||||
*/
|
||||
List<JSONObject> deviceHistoryCalculated(DeviceHistoryParam deviceHistoryParam);
|
||||
|
||||
/**
|
||||
* 多设备同一物理量对比历史:按分钟对齐,缺数沿用上一分钟最后值
|
||||
*
|
||||
* @param param 网关设备ID、identifier、时间范围
|
||||
* @return 每行格式:{ "time": "yyyy-MM-dd HH:mm:ss", "设备标签1": v1, "设备标签2": v2, ... }
|
||||
*/
|
||||
List<JSONObject> subDeviceComparativeHistory(SubDeviceCompareParam param);
|
||||
|
||||
/**
|
||||
* 查询场景变量的历史数据
|
||||
*
|
||||
@ -61,4 +92,12 @@ public interface DataCenterService {
|
||||
* @return com.fastbee.common.core.domain.AjaxResult
|
||||
*/
|
||||
List<ThingsModelLogCountVO> countThingsModelInvoke(DataCenterParam dataCenterParam);
|
||||
|
||||
/**
|
||||
* 网关设备仅计算物模型列表(用于历史记录-网关计算量 变量选择)
|
||||
*
|
||||
* @param gwDeviceId 网关设备ID
|
||||
* @return 仅计算物模型的 DTO 列表
|
||||
*/
|
||||
List<ThingsModelDTO> listGatewayCalculatedModels(Long gwDeviceId);
|
||||
}
|
||||
|
||||
@ -3,14 +3,19 @@ package com.fastbee.iot.service.impl;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.common.enums.scenemodel.SceneModelVariableTypeEnum;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.SecurityUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.CaculateUtils;
|
||||
import com.fastbee.iot.cache.SceneModelTagCache;
|
||||
import com.fastbee.iot.domain.*;
|
||||
import com.fastbee.iot.model.AlertCountVO;
|
||||
import com.fastbee.iot.model.DeviceHistoryParam;
|
||||
import com.fastbee.iot.model.HistoryModel;
|
||||
import com.fastbee.iot.model.SubDeviceCompareParam;
|
||||
import com.fastbee.iot.model.ThingsModelLogCountVO;
|
||||
import com.fastbee.iot.model.dto.ThingsModelDTO;
|
||||
import com.fastbee.iot.model.gateWay.ProductSubGatewayVO;
|
||||
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
|
||||
import com.fastbee.iot.model.param.DataCenterParam;
|
||||
import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
|
||||
import com.fastbee.iot.service.*;
|
||||
@ -20,6 +25,8 @@ import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -47,6 +54,17 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
private SceneModelTagCache sceneModelTagCache;
|
||||
@Resource
|
||||
private IAlertLogService alertLogService;
|
||||
@Resource
|
||||
private IProductSubGatewayService productSubGatewayService;
|
||||
@Resource
|
||||
private ISubGatewayService subGatewayService;
|
||||
@Resource
|
||||
private IProductService productService;
|
||||
@Resource
|
||||
private com.fastbee.iot.cache.ITSLCache itslCache;
|
||||
|
||||
/** 公式中跨设备引用:{slaveId:N.identifier} */
|
||||
private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam) {
|
||||
@ -66,9 +84,11 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
if (CollectionUtils.isEmpty(identifierVOList)) {
|
||||
return resultList;
|
||||
}
|
||||
List<String> identifierList = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
List<String> identifierList = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier)
|
||||
.collect(Collectors.toList());
|
||||
List<HistoryModel> historyModelList = new ArrayList<>();
|
||||
List<String> propertyIdentifierList = identifierVOList.stream().filter(t -> 1 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
List<String> propertyIdentifierList = identifierVOList.stream().filter(t -> 1 == t.getType())
|
||||
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
if (CollectionUtils.isNotEmpty(propertyIdentifierList)) {
|
||||
DeviceLog deviceLog = new DeviceLog();
|
||||
deviceLog.setIdentityList(propertyIdentifierList);
|
||||
@ -77,16 +97,19 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
deviceLog.setEndTime(deviceHistoryParam.getEndTime());
|
||||
historyModelList.addAll(deviceLogService.listHistory(deviceLog));
|
||||
}
|
||||
List<String> functionIdentifierList = identifierVOList.stream().filter(t -> 2 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
List<String> functionIdentifierList = identifierVOList.stream().filter(t -> 2 == t.getType())
|
||||
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
if (CollectionUtils.isNotEmpty(functionIdentifierList)) {
|
||||
FunctionLog functionLog = new FunctionLog();
|
||||
functionLog.setIdentifyList(functionIdentifierList);
|
||||
functionLog.setSerialNumber(deviceHistoryParam.getSerialNumber());
|
||||
functionLog.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime()));
|
||||
functionLog
|
||||
.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime()));
|
||||
functionLog.setEndTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getEndTime()));
|
||||
historyModelList.addAll(functionLogService.listHistory(functionLog));
|
||||
}
|
||||
List<String> eventIdentifierList = identifierVOList.stream().filter(t -> 3 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
List<String> eventIdentifierList = identifierVOList.stream().filter(t -> 3 == t.getType())
|
||||
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
|
||||
if (CollectionUtils.isNotEmpty(eventIdentifierList)) {
|
||||
EventLog eventLog = new EventLog();
|
||||
eventLog.setIdentityList(eventIdentifierList);
|
||||
@ -109,7 +132,8 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
for (Map.Entry<Date, List<HistoryModel>> entry : map.entrySet()) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
List<HistoryModel> value = entry.getValue();
|
||||
Map<String, HistoryModel> historyModelMap = value.stream().collect(Collectors.toMap(HistoryModel::getIdentity, Function.identity(), (o, n) -> n));
|
||||
Map<String, HistoryModel> historyModelMap = value.stream()
|
||||
.collect(Collectors.toMap(HistoryModel::getIdentity, Function.identity(), (o, n) -> n));
|
||||
List<JSONObject> jsonObjectList = new ArrayList<>();
|
||||
for (String identifier : identifierList) {
|
||||
JSONObject jsonObject1 = new JSONObject();
|
||||
@ -140,11 +164,14 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> sceneModelDataIdList = StringUtils.str2List(sceneHistoryParam.getIds(), ",", true, true);
|
||||
List<SceneModelData> sceneModelDataList = sceneModelDataService.selectSceneModelDataListByIds(sceneModelDataIdList.stream().map(Long::parseLong).collect(Collectors.toList()));
|
||||
List<Long> datasourceIdList = sceneModelDataList.stream().map(SceneModelData::getDatasourceId).distinct().collect(Collectors.toList());
|
||||
List<SceneModelData> sceneModelDataList = sceneModelDataService.selectSceneModelDataListByIds(
|
||||
sceneModelDataIdList.stream().map(Long::parseLong).collect(Collectors.toList()));
|
||||
List<Long> datasourceIdList = sceneModelDataList.stream().map(SceneModelData::getDatasourceId).distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (SceneModelVariableTypeEnum.THINGS_MODEL.getType().equals(sceneHistoryParam.getVariableType())) {
|
||||
List<ThingsModel> thingsModelList = thingsModelService.selectThingsModelListByModelIds(datasourceIdList);
|
||||
Map<Long, Integer> thingsModelMap = thingsModelList.stream().collect(Collectors.toMap(ThingsModel::getModelId, ThingsModel::getType));
|
||||
Map<Long, Integer> thingsModelMap = thingsModelList.stream()
|
||||
.collect(Collectors.toMap(ThingsModel::getModelId, ThingsModel::getType));
|
||||
DeviceHistoryParam deviceHistoryParam = new DeviceHistoryParam();
|
||||
List<DeviceHistoryParam.IdentifierVO> identifierVOList = new ArrayList<>();
|
||||
for (SceneModelData sceneModelData : sceneModelDataList) {
|
||||
@ -189,4 +216,354 @@ public class DataCenterServiceImpl implements DataCenterService {
|
||||
resultList.addAll(functionLogService.countThingsModelInvoke(dataCenterParam));
|
||||
return resultList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThingsModel> listSubProductThingsModelForImport(Long gwProductId) {
|
||||
ProductSubGateway query = new ProductSubGateway();
|
||||
query.setGwProductId(gwProductId);
|
||||
List<ProductSubGatewayVO> list = productSubGatewayService.selectProductSubGatewayList(query);
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Long> subProductIds = list.stream().map(ProductSubGatewayVO::getSubProductId).distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<String, ThingsModel> dedup = new LinkedHashMap<>();
|
||||
ThingsModel q = new ThingsModel();
|
||||
q.setLanguage(SecurityUtils.getLanguage());
|
||||
q.setType(1);
|
||||
for (Long subId : subProductIds) {
|
||||
q.setProductId(subId);
|
||||
List<ThingsModel> models = thingsModelService.selectThingsModelList(q);
|
||||
for (ThingsModel m : models) {
|
||||
dedup.putIfAbsent(m.getIdentifier(), m);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(dedup.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int importSubProductThingsToGateway(Long gwProductId) {
|
||||
Product gwProduct = productService.selectProductByProductId(gwProductId);
|
||||
if (gwProduct == null) {
|
||||
return 0;
|
||||
}
|
||||
List<ThingsModel> toImport = listSubProductThingsModelForImport(gwProductId);
|
||||
ThingsModel existQ = new ThingsModel();
|
||||
existQ.setProductId(gwProductId);
|
||||
existQ.setLanguage(SecurityUtils.getLanguage());
|
||||
Set<String> existingIds = thingsModelService.selectThingsModelList(existQ).stream()
|
||||
.map(ThingsModel::getIdentifier).collect(Collectors.toSet());
|
||||
int count = 0;
|
||||
for (ThingsModel m : toImport) {
|
||||
if (existingIds.contains(m.getIdentifier())) {
|
||||
continue;
|
||||
}
|
||||
ThingsModel add = new ThingsModel();
|
||||
add.setProductId(gwProductId);
|
||||
add.setProductName(gwProduct.getProductName());
|
||||
add.setModelName(m.getModelName());
|
||||
add.setIdentifier(m.getIdentifier());
|
||||
add.setType(m.getType());
|
||||
add.setDatatype(m.getDatatype());
|
||||
add.setSpecs(m.getSpecs());
|
||||
add.setFormula(m.getFormula());
|
||||
add.setIsCalculated(m.getIsCalculated() != null ? m.getIsCalculated() : 0);
|
||||
add.setIsChart(m.getIsChart() != null ? m.getIsChart() : 0);
|
||||
add.setIsHistory(m.getIsHistory() != null ? m.getIsHistory() : 1);
|
||||
add.setIsMonitor(m.getIsMonitor() != null ? m.getIsMonitor() : 0);
|
||||
add.setIsReadonly(m.getIsReadonly() != null ? m.getIsReadonly() : 1);
|
||||
add.setDeviceId(0L);
|
||||
add.setDelFlag("0");
|
||||
if (SecurityUtils.getLoginUser() != null && SecurityUtils.getLoginUser().getUser() != null) {
|
||||
if (SecurityUtils.getLoginUser().getUser().getDept() != null) {
|
||||
add.setTenantId(SecurityUtils.getLoginUser().getUser().getDept().getDeptUserId());
|
||||
add.setTenantName(SecurityUtils.getLoginUser().getUser().getDept().getDeptUserName());
|
||||
}
|
||||
}
|
||||
thingsModelService.insertThingsModel(add);
|
||||
existingIds.add(add.getIdentifier());
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
itslCache.setCacheThingsModelByProductId(gwProductId);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JSONObject> deviceHistoryCalculated(DeviceHistoryParam deviceHistoryParam) {
|
||||
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Device device = deviceService.selectDeviceByDeviceId(deviceHistoryParam.getDeviceId());
|
||||
if (device == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<DeviceHistoryParam.IdentifierVO> identifierVOList = deviceHistoryParam.getIdentifierList();
|
||||
if (CollectionUtils.isEmpty(identifierVOList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
ThingsModel queryModel = new ThingsModel();
|
||||
queryModel.setProductId(device.getProductId());
|
||||
queryModel.setIsCalculated(1);
|
||||
List<ThingsModel> allCalculated = thingsModelService.selectThingsModelList(queryModel);
|
||||
long gwDeviceId = device.getDeviceId();
|
||||
allCalculated = allCalculated.stream()
|
||||
.filter(m -> m.getDeviceId() == null || m.getDeviceId() == 0
|
||||
|| m.getDeviceId().longValue() == gwDeviceId)
|
||||
.collect(Collectors.toList());
|
||||
Set<String> calculatedIdentifiers = allCalculated.stream().map(ThingsModel::getIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
List<String> requestedIds = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier)
|
||||
.collect(Collectors.toList());
|
||||
requestedIds = requestedIds.stream().filter(calculatedIdentifiers::contains).collect(Collectors.toList());
|
||||
if (requestedIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
SubGateway subQ = new SubGateway();
|
||||
subQ.setGwDeviceId(device.getDeviceId());
|
||||
List<SubDeviceListVO> subDevices = subGatewayService.selectGatewayList(subQ);
|
||||
// String gwSn = device.getSerialNumber();
|
||||
List<HistoryModel> computedHistory = new ArrayList<>();
|
||||
for (String calcIdentifier : requestedIds) {
|
||||
ThingsModel model = allCalculated.stream().filter(m -> m.getIdentifier().equals(calcIdentifier)).findFirst()
|
||||
.orElse(null);
|
||||
if (model == null || StringUtils.isEmpty(model.getFormula())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> refs = parseCrossDeviceRefs(model.getFormula());
|
||||
if (refs.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Map<String, List<HistoryModel>> rawByRef = new LinkedHashMap<>();
|
||||
Set<Long> minutesWithData = new TreeSet<>();
|
||||
for (Map.Entry<String, String> e : refs.entrySet()) {
|
||||
String key = e.getKey();
|
||||
String ident = e.getValue();
|
||||
int slaveId = Integer.parseInt(key.split("\\.")[0].replace("slaveId:", ""));
|
||||
String subSn = subDevices.stream().filter(s -> s.getSlaveId() != null && s.getSlaveId() == slaveId)
|
||||
.findFirst().map(SubDeviceListVO::getSubDeviceNo).orElse(null);
|
||||
if (subSn == null) {
|
||||
continue;
|
||||
}
|
||||
DeviceLog dl = new DeviceLog();
|
||||
dl.setSerialNumber(subSn);
|
||||
dl.setIdentityList(Collections.singletonList(ident));
|
||||
dl.setBeginTime(deviceHistoryParam.getBeginTime());
|
||||
dl.setEndTime(deviceHistoryParam.getEndTime());
|
||||
List<HistoryModel> raw = deviceLogService.listHistory(dl);
|
||||
rawByRef.put(key, raw);
|
||||
for (HistoryModel h : raw) {
|
||||
if (h.getTime() != null) {
|
||||
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
|
||||
minutesWithData.add(minuteMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minutesWithData.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<Date> minuteSlotsOnlyWithData = new ArrayList<>();
|
||||
for (Long ms : minutesWithData) {
|
||||
minuteSlotsOnlyWithData.add(new Date(ms));
|
||||
}
|
||||
Map<String, LinkedHashMap<Date, String>> alignedByRef = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<HistoryModel>> entry : rawByRef.entrySet()) {
|
||||
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(entry.getValue(),
|
||||
minuteSlotsOnlyWithData);
|
||||
alignedByRef.put(entry.getKey(), aligned);
|
||||
}
|
||||
for (Date minute : minuteSlotsOnlyWithData) {
|
||||
Map<String, String> replaceMap = new HashMap<>();
|
||||
for (Map.Entry<String, LinkedHashMap<Date, String>> entry : alignedByRef.entrySet()) {
|
||||
String val = entry.getValue().get(minute);
|
||||
replaceMap.put("{" + entry.getKey() + "}", val != null ? val : "0");
|
||||
}
|
||||
try {
|
||||
String computed = CaculateUtils.execute(model.getFormula(), replaceMap).toString();
|
||||
HistoryModel hm = new HistoryModel();
|
||||
hm.setTime(minute);
|
||||
hm.setIdentity(calcIdentifier);
|
||||
hm.setValue(computed);
|
||||
computedHistory.add(hm);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
computedHistory.sort(Comparator.comparing(HistoryModel::getTime));
|
||||
return handleData(requestedIds, computedHistory);
|
||||
}
|
||||
|
||||
/** 多设备对比列名分隔符:设备标签:::变量标识,用于多变量时前端解析展示名 */
|
||||
private static final String SUB_COMPARE_COL_SEP = ":::";
|
||||
|
||||
@Override
|
||||
public List<JSONObject> subDeviceComparativeHistory(SubDeviceCompareParam param) {
|
||||
List<String> identifiers = resolveCompareIdentifiers(param);
|
||||
if (param.getGwDeviceId() == null || identifiers.isEmpty() || param.getType() == null
|
||||
|| param.getType() != 1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Device gw = deviceService.selectDeviceByDeviceId(param.getGwDeviceId());
|
||||
if (gw == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
SubGateway subQ = new SubGateway();
|
||||
subQ.setGwDeviceId(param.getGwDeviceId());
|
||||
List<SubDeviceListVO> subList = subGatewayService.selectGatewayList(subQ);
|
||||
if (CollectionUtils.isEmpty(subList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (param.getSubDeviceIds() != null && !param.getSubDeviceIds().isEmpty()) {
|
||||
subList = subList.stream()
|
||||
.filter(s -> param.getSubDeviceIds().contains(s.getSubDeviceId()))
|
||||
.collect(Collectors.toList());
|
||||
if (subList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
String gwSn = gw.getSerialNumber();
|
||||
Set<Long> minutesWithData = new TreeSet<>();
|
||||
Map<String, List<HistoryModel>> rawByDevice = new LinkedHashMap<>();
|
||||
for (SubDeviceListVO sub : subList) {
|
||||
String subSn = sub.getSubDeviceNo() != null ? sub.getSubDeviceNo() : (gwSn + "_" + sub.getSlaveId());
|
||||
String label = (sub.getSubDeviceName() != null ? sub.getSubDeviceName() : ("从机" + sub.getSlaveId())) + "(从机"
|
||||
+ sub.getSlaveId() + ")";
|
||||
DeviceLog dl = new DeviceLog();
|
||||
dl.setSerialNumber(subSn);
|
||||
dl.setIdentityList(identifiers);
|
||||
dl.setBeginTime(param.getBeginTime());
|
||||
dl.setEndTime(param.getEndTime());
|
||||
List<HistoryModel> raw = deviceLogService.listHistory(dl);
|
||||
rawByDevice.put(label, raw);
|
||||
for (HistoryModel h : raw) {
|
||||
if (h.getTime() != null) {
|
||||
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
|
||||
minutesWithData.add(minuteMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Date> minuteSlotsOnlyWithData = new ArrayList<>();
|
||||
for (Long ms : minutesWithData) {
|
||||
minuteSlotsOnlyWithData.add(new Date(ms));
|
||||
}
|
||||
boolean singleVar = (identifiers.size() == 1);
|
||||
Map<String, LinkedHashMap<Date, String>> deviceAligned = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<HistoryModel>> e : rawByDevice.entrySet()) {
|
||||
String label = e.getKey();
|
||||
List<HistoryModel> raw = e.getValue();
|
||||
if (singleVar) {
|
||||
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(raw, minuteSlotsOnlyWithData);
|
||||
deviceAligned.put(label, aligned);
|
||||
} else {
|
||||
Map<String, List<HistoryModel>> byIdentity = raw.stream()
|
||||
.filter(h -> h.getIdentity() != null)
|
||||
.collect(Collectors.groupingBy(HistoryModel::getIdentity));
|
||||
for (Map.Entry<String, List<HistoryModel>> entry : byIdentity.entrySet()) {
|
||||
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(entry.getValue(),
|
||||
minuteSlotsOnlyWithData);
|
||||
deviceAligned.put(label + SUB_COMPARE_COL_SEP + entry.getKey(), aligned);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<JSONObject> result = new ArrayList<>();
|
||||
for (Date minute : minuteSlotsOnlyWithData) {
|
||||
JSONObject row = new JSONObject();
|
||||
row.put("time", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, minute));
|
||||
for (Map.Entry<String, LinkedHashMap<Date, String>> e : deviceAligned.entrySet()) {
|
||||
row.put(e.getKey(), e.getValue().get(minute));
|
||||
}
|
||||
result.add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> resolveCompareIdentifiers(SubDeviceCompareParam param) {
|
||||
if (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty()) {
|
||||
return param.getIdentifierList();
|
||||
}
|
||||
if (param.getIdentifier() != null && !param.getIdentifier().isEmpty()) {
|
||||
return Collections.singletonList(param.getIdentifier());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThingsModelDTO> listGatewayCalculatedModels(Long gwDeviceId) {
|
||||
Device device = deviceService.selectDeviceByDeviceId(gwDeviceId);
|
||||
if (device == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
ThingsModel queryModel = new ThingsModel();
|
||||
queryModel.setProductId(device.getProductId());
|
||||
queryModel.setLanguage(SecurityUtils.getLanguage());
|
||||
queryModel.setIsCalculated(1);
|
||||
List<ThingsModel> list = thingsModelService.selectThingsModelList(queryModel);
|
||||
long gwId = device.getDeviceId();
|
||||
list = list.stream()
|
||||
.filter(m -> m.getDeviceId() == null || m.getDeviceId() == 0 || m.getDeviceId().longValue() == gwId)
|
||||
.collect(Collectors.toList());
|
||||
List<ThingsModelDTO> result = new ArrayList<>();
|
||||
for (ThingsModel m : list) {
|
||||
ThingsModelDTO dto = new ThingsModelDTO();
|
||||
dto.setModelId(m.getModelId());
|
||||
dto.setModelName(m.getModelName());
|
||||
dto.setIdentifier(m.getIdentifier());
|
||||
dto.setType(m.getType());
|
||||
dto.setProductId(m.getProductId());
|
||||
dto.setSerialNumber(device.getSerialNumber());
|
||||
result.add(dto);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> parseCrossDeviceRefs(String formula) {
|
||||
Map<String, String> refs = new LinkedHashMap<>();
|
||||
Matcher m = CROSS_DEVICE_REF.matcher(formula);
|
||||
while (m.find()) {
|
||||
String key = "slaveId:" + m.group(1) + "." + m.group(2);
|
||||
refs.put(key, m.group(2));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
private List<Date> buildMinuteSlots(String beginTime, String endTime) {
|
||||
Date start = DateUtils.parseDate(beginTime);
|
||||
Date end = DateUtils.parseDate(endTime);
|
||||
if (start == null || end == null || !start.before(end)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
long step = 60_000L;
|
||||
long startMs = (start.getTime() / step) * step;
|
||||
long endMs = (end.getTime() / step) * step;
|
||||
List<Date> slots = new ArrayList<>();
|
||||
for (long t = startMs; t <= endMs; t += step) {
|
||||
slots.add(new Date(t));
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private LinkedHashMap<Date, String> alignByMinuteWithCarryForward(List<HistoryModel> raw, List<Date> minuteSlots) {
|
||||
if (minuteSlots.isEmpty()) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
TreeMap<Long, String> byMinute = new TreeMap<>();
|
||||
for (HistoryModel h : raw) {
|
||||
if (h.getTime() == null || h.getValue() == null)
|
||||
continue;
|
||||
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
|
||||
byMinute.put(minuteMs, h.getValue());
|
||||
}
|
||||
LinkedHashMap<Date, String> result = new LinkedHashMap<>();
|
||||
String last = null;
|
||||
for (Date slot : minuteSlots) {
|
||||
long slotMs = (slot.getTime() / 60_000L) * 60_000L;
|
||||
Map.Entry<Long, String> floor = byMinute.floorEntry(slotMs);
|
||||
if (floor != null && floor.getKey() == slotMs) {
|
||||
last = floor.getValue();
|
||||
}
|
||||
result.put(slot, last);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1089,6 +1089,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
SysUser sysUser = getLoginUser().getUser();
|
||||
// 添加设备
|
||||
device.setCreateTime(DateUtils.getNowDate());
|
||||
device.setUserId(sysUser.getUserId());
|
||||
device.setTenantId(sysUser.getDept().getDeptUserId());
|
||||
device.setTenantName(sysUser.getDept().getDeptUserName());
|
||||
device.setRssi(0);
|
||||
@ -1096,9 +1097,9 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
Product product = productService.selectProductByProductId(device.getProductId());
|
||||
device.setImgUrl(product.getImgUrl());
|
||||
// 随机经纬度和地址
|
||||
SysUser user = getLoginUser().getUser();
|
||||
device.setNetworkIp(user.getLoginIp());
|
||||
setLocation(user.getLoginIp(), device);
|
||||
//SysUser user = getLoginUser().getUser();
|
||||
device.setNetworkIp(sysUser.getLoginIp());
|
||||
setLocation(sysUser.getLoginIp(), device);
|
||||
deviceMapper.insertDevice(device);
|
||||
// 处理modbus产品
|
||||
if (FastBeeConstant.PROTOCOL.ModbusRtu.equals(product.getProtocolCode())
|
||||
|
||||
@ -54,3 +54,68 @@ export function exportDeviceHistory(data) {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
// 网关产品-可导入的子产品物模型列表
|
||||
export function getSubProductThingsModel(gwProductId) {
|
||||
return request({
|
||||
url: '/data/center/subProductThingsModel',
|
||||
method: 'get',
|
||||
params: { gwProductId },
|
||||
});
|
||||
}
|
||||
|
||||
// 网关产品-一键导入子产品物模型
|
||||
export function importSubProductThingsToGateway(gwProductId) {
|
||||
return request({
|
||||
url: '/data/center/importSubProductThingsToGateway',
|
||||
method: 'post',
|
||||
data: { gwProductId },
|
||||
});
|
||||
}
|
||||
|
||||
// 网关设备仅计算物模型列表
|
||||
export function getGatewayCalculatedModels(deviceId) {
|
||||
return request({
|
||||
url: '/data/center/gatewayCalculatedModels',
|
||||
method: 'get',
|
||||
params: { deviceId },
|
||||
});
|
||||
}
|
||||
|
||||
// 网关计算物模型历史(查询时计算,按分钟对齐)
|
||||
export function getDeviceHistoryCalculated(data) {
|
||||
return request({
|
||||
url: '/data/center/deviceHistoryCalculated',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 导出网关计算物模型历史Excel
|
||||
export function exportDeviceHistoryCalculated(data) {
|
||||
return request({
|
||||
url: '/data/center/deviceHistoryCalculated/export',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
// 多设备同一物理量对比历史
|
||||
export function getSubDeviceComparativeHistory(data) {
|
||||
return request({
|
||||
url: '/data/center/subDeviceComparativeHistory',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 导出多设备对比历史Excel
|
||||
export function exportSubDeviceComparativeHistory(data) {
|
||||
return request({
|
||||
url: '/data/center/subDeviceComparativeHistory/export',
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
@ -45,5 +45,8 @@
|
||||
"dataCenter.history.384934-21": "In the past seven days",
|
||||
"dataCenter.history.384934-22": "Last 30 days",
|
||||
"dataCenter.history.384934-23": "total",
|
||||
"dataCenter.history.384934-24": "Export Excel"
|
||||
"dataCenter.history.384934-24": "Export Excel",
|
||||
"dataCenter.history.gwCalculated": "Gateway Calculated",
|
||||
"dataCenter.history.subCompare": "Sub-Device Compare",
|
||||
"dataCenter.history.gateway": "Gateway Device"
|
||||
}
|
||||
|
||||
@ -45,5 +45,8 @@
|
||||
"dataCenter.history.384934-21": "最近七天",
|
||||
"dataCenter.history.384934-22": "最近三十天",
|
||||
"dataCenter.history.384934-23": "总量",
|
||||
"dataCenter.history.384934-24": "导出Excel"
|
||||
"dataCenter.history.384934-24": "导出Excel",
|
||||
"dataCenter.history.gwCalculated": "网关计算量",
|
||||
"dataCenter.history.subCompare": "多设备对比",
|
||||
"dataCenter.history.gateway": "网关设备"
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<el-form @submit.native.prevent :model="devQueryParams" ref="devQueryForm" size="mini" :inline="true" label-width="68px">
|
||||
<el-form-item :label="$t('dataCenter.history.384934-1')" prop="deviceId">
|
||||
<el-select style="width: 100%" v-model="devQueryParams.deviceId" :placeholder="$t('dataCenter.history.384934-2')" filterable @change="handleDevDeviceChange">
|
||||
<el-option v-for="(item, index) in devDeviceList" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
|
||||
<el-option v-for="(item, index) in devDeviceListNonGw" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="devSubDeviceList.length > 0" label="子设备" prop="subDeviceId">
|
||||
@ -70,6 +70,95 @@
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="网关设备" name="gateway">
|
||||
<div class="gateway-wrap">
|
||||
<!-- 统一 4 步:1 选网关 2 选物模型 3 选子设备(可选) 4 选时间 -->
|
||||
<el-form @submit.native.prevent :model="gwQueryParams" ref="gwQueryForm" size="mini" :inline="true" label-width="90px">
|
||||
<el-form-item label="网关设备" prop="gwDeviceId">
|
||||
<el-select style="width: 200px" v-model="gwQueryParams.gwDeviceId" placeholder="请选择网关设备" filterable @change="handleGwDeviceChange">
|
||||
<el-option v-for="item in gwDeviceList" :key="item.deviceId" :label="item.deviceName" :value="item.deviceId"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="物模型" prop="identifiers">
|
||||
<el-select style="width: 320px" v-model="gwQueryParams.identifiers" placeholder="请选择变量(可多选)" filterable multiple collapse-tags @change="handleGwIdentifierChange">
|
||||
<el-option label="全部勾选/反选" value="__selectAll__">
|
||||
<span style="font-weight: bold; color: #409EFF;">{{ gwGwIsAllSelected ? '全部反选' : '全部勾选' }}</span>
|
||||
</el-option>
|
||||
<el-option-group label="计算量(网关纯计算,无需选子设备)" v-if="gwVariableListCalcUnique.length">
|
||||
<el-option v-for="item in gwVariableListCalcUnique" :key="'calc-' + item.identifier" :label="item.modelName + ' (计算)'" :value="item.identifier"></el-option>
|
||||
</el-option-group>
|
||||
<el-option-group label="子设备变量" v-if="gwVariableListSubUnique.length">
|
||||
<el-option v-for="item in gwVariableListSubUnique" :key="'sub-' + item.identifier" :label="item.modelName" :value="item.identifier"></el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="子设备" prop="subDeviceIds" v-if="gwShowSubDeviceSelect">
|
||||
<el-select
|
||||
style="width: 280px"
|
||||
v-model="gwSubDeviceSelectValue"
|
||||
placeholder="不选则全部子设备对比,可多选指定子设备"
|
||||
filterable
|
||||
multiple
|
||||
collapse-tags
|
||||
clearable
|
||||
>
|
||||
<el-option label="全部子设备(多设备对比)" value="__all__"></el-option>
|
||||
<el-option v-for="item in gwSubDeviceList" :key="item.subDeviceId" :label="item.subDeviceName + ' (从机' + item.slaveId + ')'" :value="item.subDeviceId"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围" prop="dayDaterange">
|
||||
<el-date-picker
|
||||
style="width: 315px"
|
||||
v-model="gwQueryParams.dayDaterange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="datetimerange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:picker-options="pickerOptions"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleGwQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="handleGwReset">重置</el-button>
|
||||
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!gwHasResult" @click="handleGwExport" v-loading="exportLoading">导出Excel</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-card v-loading="loading" shadow="never">
|
||||
<div slot="header"><span>{{ gwResultTitle }}</span></div>
|
||||
<!-- 计算量结果 -->
|
||||
<template v-if="gwResultMode === 'calc'">
|
||||
<div v-show="gwCalcDatas.length !== 0" ref="gwCalcLineChart" style="width: 100%; height: 480px"></div>
|
||||
<el-empty v-if="gwCalcDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(计算量)和时间范围后查询"></el-empty>
|
||||
<el-table v-show="gwCalcTotal > 0" style="margin-top: 20px" :data="gwCalcTableList" border>
|
||||
<el-table-column label="更新时间" prop="time" width="180" />
|
||||
<el-table-column v-for="item in gwCalcTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
|
||||
</el-table>
|
||||
<pagination v-show="gwCalcTotal > 0" :total="gwCalcTotal" :page.sync="gwCalcPageNum" :limit.sync="gwCalcPageSize" @pagination="getGwCalcTableList" />
|
||||
</template>
|
||||
<!-- 多设备对比结果 -->
|
||||
<template v-else-if="gwResultMode === 'compare'">
|
||||
<div v-show="subCompareDatas.length !== 0" ref="subCompareLineChart" style="width: 100%; height: 480px"></div>
|
||||
<el-empty v-if="subCompareDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(子设备变量)和时间范围,子设备可不选(多设备对比)"></el-empty>
|
||||
<el-table v-show="subCompareTotal > 0" style="margin-top: 20px" :data="subCompareTableList" border>
|
||||
<el-table-column v-for="col in subCompareTableColumns" :key="col.prop" :label="col.label" :prop="col.prop" :min-width="col.prop === 'time' ? 180 : 100" />
|
||||
</el-table>
|
||||
<pagination v-show="subCompareTotal > 0" :total="subCompareTotal" :page.sync="subComparePageNum" :limit.sync="subComparePageSize" @pagination="getSubCompareTableList" />
|
||||
</template>
|
||||
<!-- 单子设备历史结果 -->
|
||||
<template v-else-if="gwResultMode === 'single'">
|
||||
<div v-show="gwSingleDatas.length !== 0" ref="gwSingleLineChart" style="width: 100%; height: 480px"></div>
|
||||
<el-empty v-if="gwSingleDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型、子设备和时间范围后查询"></el-empty>
|
||||
<el-table v-show="gwSingleTotal > 0" style="margin-top: 20px" :data="gwSingleTableList" border>
|
||||
<el-table-column label="更新时间" prop="time" width="180" />
|
||||
<el-table-column v-for="item in gwSingleTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
|
||||
</el-table>
|
||||
<pagination v-show="gwSingleTotal > 0" :total="gwSingleTotal" :page.sync="gwSinglePageNum" :limit.sync="gwSinglePageSize" @pagination="getGwSingleTableList" />
|
||||
</template>
|
||||
<el-empty v-else style="height: 480px" description="请按顺序选择:1.网关设备 2.物模型 3.子设备(可选) 4.时间范围,然后查询"></el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('dataCenter.history.384934-14')" name="scene">
|
||||
<div class="scene-wrap">
|
||||
<el-form @submit.native.prevent :model="sceneQueryParams" ref="sceneQueryForm" size="mini" :inline="true" label-width="68px">
|
||||
@ -155,7 +244,16 @@ import moment from 'moment';
|
||||
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
|
||||
import { listGateway } from '@/api/iot/gateway.js';
|
||||
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
|
||||
import { getDataCenterDeviceHistory, getDataCenterSceneHistory, exportDeviceHistory } from '@/api/data/center.js';
|
||||
import {
|
||||
getDataCenterDeviceHistory,
|
||||
getDataCenterSceneHistory,
|
||||
exportDeviceHistory,
|
||||
getGatewayCalculatedModels,
|
||||
getDeviceHistoryCalculated,
|
||||
exportDeviceHistoryCalculated,
|
||||
getSubDeviceComparativeHistory,
|
||||
exportSubDeviceComparativeHistory,
|
||||
} from '@/api/data/center.js';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { blobValidate } from '@/utils/ruoyi';
|
||||
|
||||
@ -246,8 +344,40 @@ export default {
|
||||
scenePageNum: 1,
|
||||
scenePageSize: 10,
|
||||
sceneTotal: 0,
|
||||
// 导出loading
|
||||
exportLoading: false,
|
||||
// 网关设备 tab 统一 4 步(物模型支持多选)
|
||||
gwQueryParams: {
|
||||
gwDeviceId: null,
|
||||
identifiers: [],
|
||||
/** 子设备:null=全部,[id]=单设备,[id1,id2,...]=多设备对比指定 */
|
||||
subDeviceIds: null,
|
||||
dayDaterange: [new Date(Date.now() - 3600 * 1000 * 2), new Date()],
|
||||
},
|
||||
gwSubDeviceList: [],
|
||||
gwCalcIdentifierList: [],
|
||||
subCompareIdentifierList: [],
|
||||
gwResultMode: '', // 'calc' | 'compare' | 'single'
|
||||
// 计算量结果(复用原结构)
|
||||
gwCalcDatas: [],
|
||||
gwCalcTableHeaderTemp: [],
|
||||
gwCalcTableComTemp: [],
|
||||
gwCalcTotal: 0,
|
||||
gwCalcPageNum: 1,
|
||||
gwCalcPageSize: 10,
|
||||
// 多设备对比结果
|
||||
subCompareDatas: [],
|
||||
subCompareTableColumns: [],
|
||||
subCompareTableComTemp: [],
|
||||
subCompareTotal: 0,
|
||||
subComparePageNum: 1,
|
||||
subComparePageSize: 10,
|
||||
// 单子设备历史结果
|
||||
gwSingleDatas: [],
|
||||
gwSingleTableHeaderTemp: [],
|
||||
gwSingleTableComTemp: [],
|
||||
gwSingleTotal: 0,
|
||||
gwSinglePageNum: 1,
|
||||
gwSinglePageSize: 10,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -301,6 +431,107 @@ export default {
|
||||
return allIdentifiers.length > 0 &&
|
||||
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
|
||||
},
|
||||
/** 设备 tab 仅展示非网关设备 */
|
||||
devDeviceListNonGw() {
|
||||
return (this.devDeviceList || []).filter((d) => d.deviceType !== 2 && d.deviceType !== 4);
|
||||
},
|
||||
/** 网关设备 tab 仅展示网关设备 */
|
||||
gwDeviceList() {
|
||||
return (this.devDeviceList || []).filter((d) => d.deviceType === 2);
|
||||
},
|
||||
gwCalcTableList() {
|
||||
const start = (this.gwCalcPageNum - 1) * this.gwCalcPageSize;
|
||||
return (this.gwCalcTableComTemp || []).slice(start, start + this.gwCalcPageSize);
|
||||
},
|
||||
subCompareTableList() {
|
||||
const start = (this.subComparePageNum - 1) * this.subComparePageSize;
|
||||
return (this.subCompareTableComTemp || []).slice(start, start + this.subComparePageSize);
|
||||
},
|
||||
// 网关统一:计算量变量列表(用于下拉分组)
|
||||
gwVariableListCalc() {
|
||||
return this.gwCalcIdentifierList || [];
|
||||
},
|
||||
// 计算量按 identifier 去重,避免重复 key
|
||||
gwVariableListCalcUnique() {
|
||||
const list = this.gwCalcIdentifierList || [];
|
||||
const seen = new Set();
|
||||
return list.filter((i) => {
|
||||
if (seen.has(i.identifier)) return false;
|
||||
seen.add(i.identifier);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
// 网关统一:子设备变量列表
|
||||
gwVariableListSub() {
|
||||
return this.subCompareIdentifierList || [];
|
||||
},
|
||||
// 子设备变量按 identifier 去重,且排除计算量(跨设备计算量不展示在子设备变量中)
|
||||
gwVariableListSubUnique() {
|
||||
const list = this.subCompareIdentifierList || [];
|
||||
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
|
||||
const seen = new Set();
|
||||
return list.filter((i) => {
|
||||
if (seen.has(i.identifier) || calcSet.has(i.identifier)) return false;
|
||||
seen.add(i.identifier);
|
||||
return true;
|
||||
});
|
||||
},
|
||||
// 当前选中的物模型是否全部为计算量(计算量时不展示子设备选择)
|
||||
gwSelectedIsCalculated() {
|
||||
const ids = this.gwQueryParams.identifiers || [];
|
||||
if (ids.length === 0) return false;
|
||||
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
|
||||
return ids.every((id) => calcSet.has(id));
|
||||
},
|
||||
// 选中的计算量 identifier 列表
|
||||
gwSelectedCalcIdentifiers() {
|
||||
const ids = this.gwQueryParams.identifiers || [];
|
||||
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
|
||||
return ids.filter((id) => calcSet.has(id));
|
||||
},
|
||||
// 选中的子设备变量 identifier 列表(排除同时属于计算量的,避免仅选计算量时报混合选择)
|
||||
gwSelectedSubIdentifiers() {
|
||||
const ids = this.gwQueryParams.identifiers || [];
|
||||
const subSet = new Set((this.subCompareIdentifierList || []).map((i) => i.identifier));
|
||||
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
|
||||
return ids.filter((id) => subSet.has(id) && !calcSet.has(id));
|
||||
},
|
||||
// 网关物模型“全部勾选”仅针对子设备变量(不包含计算量)
|
||||
gwGwIsAllSelected() {
|
||||
const ids = this.gwQueryParams.identifiers || [];
|
||||
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
|
||||
return allSub.length > 0 && allSub.every((id) => ids.includes(id));
|
||||
},
|
||||
gwShowSubDeviceSelect() {
|
||||
return (this.gwQueryParams.identifiers || []).length > 0 && !this.gwSelectedIsCalculated;
|
||||
},
|
||||
/** 子设备多选绑定:null 显示为“全部”,选具体设备时去掉“全部” */
|
||||
gwSubDeviceSelectValue: {
|
||||
get() {
|
||||
const ids = this.gwQueryParams.subDeviceIds;
|
||||
return ids === null || (Array.isArray(ids) && ids.length === 0) ? ['__all__'] : ids;
|
||||
},
|
||||
set(val) {
|
||||
const ids = (val || []).filter((v) => v !== '__all__');
|
||||
this.gwQueryParams.subDeviceIds = ids.length === 0 ? null : ids;
|
||||
},
|
||||
},
|
||||
gwResultTitle() {
|
||||
if (this.gwResultMode === 'calc') return '曲线趋势图(计算量)';
|
||||
if (this.gwResultMode === 'compare') return '多设备同一变量对比(仅显示有数据的时间点)';
|
||||
if (this.gwResultMode === 'single') return '单子设备历史';
|
||||
return '曲线趋势图';
|
||||
},
|
||||
gwHasResult() {
|
||||
if (this.gwResultMode === 'calc') return this.gwCalcDatas.length > 0;
|
||||
if (this.gwResultMode === 'compare') return this.subCompareDatas.length > 0;
|
||||
if (this.gwResultMode === 'single') return this.gwSingleDatas.length > 0;
|
||||
return false;
|
||||
},
|
||||
gwSingleTableList() {
|
||||
const start = (this.gwSinglePageNum - 1) * this.gwSinglePageSize;
|
||||
return (this.gwSingleTableComTemp || []).slice(start, start + this.gwSinglePageSize);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取设备列表
|
||||
@ -317,18 +548,13 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
// 选择设备后
|
||||
// 选择设备后(设备 tab 仅非网关,直接拉取物模型)
|
||||
handleDevDeviceChange(val) {
|
||||
this.devQueryParams.identifiers = [];
|
||||
this.devQueryParams.subDeviceId = null;
|
||||
this.devSubDeviceList = [];
|
||||
this.devIdentifierList = [];
|
||||
const device = this.devDeviceList.find((item) => item.deviceId === val);
|
||||
if (device && device.deviceType === 2) {
|
||||
this.getDevSubDeviceList(val);
|
||||
} else {
|
||||
this.getDevIdentifierList(val);
|
||||
}
|
||||
if (val) this.getDevIdentifierList(val);
|
||||
},
|
||||
// 选择子设备后
|
||||
handleDevSubDeviceChange(val) {
|
||||
@ -647,6 +873,434 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
// 网关统一:选网关后拉取计算量变量、子设备变量、子设备列表
|
||||
handleGwDeviceChange(val) {
|
||||
this.gwQueryParams.identifiers = [];
|
||||
this.gwQueryParams.subDeviceIds = null;
|
||||
this.gwCalcIdentifierList = [];
|
||||
this.subCompareIdentifierList = [];
|
||||
this.gwSubDeviceList = [];
|
||||
this.gwResultMode = '';
|
||||
if (!val) return;
|
||||
Promise.all([
|
||||
getGatewayCalculatedModels(val).then((res) => (res.code === 200 ? res.data || [] : [])),
|
||||
listThingsModel({ deviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => {
|
||||
if (res.code === 200) {
|
||||
const rows = res.rows || [];
|
||||
return rows.filter((r) => r.type === 1);
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
listGateway({ gwDeviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => (res.code === 200 ? res.rows || [] : [])),
|
||||
]).then(([calcList, subList, subDevices]) => {
|
||||
this.gwCalcIdentifierList = calcList;
|
||||
this.subCompareIdentifierList = subList;
|
||||
this.gwSubDeviceList = subDevices;
|
||||
});
|
||||
},
|
||||
handleGwIdentifierChange(val) {
|
||||
const raw = (val || []).filter((v) => v !== '__selectAll__');
|
||||
const index = (val || []).indexOf('__selectAll__');
|
||||
if (index > -1) {
|
||||
// 全部勾选/反选:仅作用于子设备变量,不勾选计算量
|
||||
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
|
||||
this.$nextTick(() => {
|
||||
this.gwQueryParams.identifiers = this.gwGwIsAllSelected ? [] : [...allSub];
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 计算量与子设备变量均支持多选,保留用户选择的全部项(混合选择在查询时校验)
|
||||
this.gwQueryParams.identifiers = raw;
|
||||
if (this.gwSelectedIsCalculated) this.gwQueryParams.subDeviceIds = null;
|
||||
},
|
||||
handleGwQuery() {
|
||||
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
|
||||
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange || !dayDaterange.length) {
|
||||
this.$message.warning('请选择网关设备、物模型和时间范围');
|
||||
return;
|
||||
}
|
||||
const calcIds = this.gwSelectedCalcIdentifiers;
|
||||
const subIds = this.gwSelectedSubIdentifiers;
|
||||
if (calcIds.length > 0 && subIds.length > 0) {
|
||||
this.$message.warning('不能混合选择计算量与子设备变量,请只选其中一类');
|
||||
return;
|
||||
}
|
||||
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
|
||||
if (!device) return;
|
||||
const isCalc = this.gwSelectedIsCalculated;
|
||||
const singleDeviceId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
|
||||
if (!isCalc && this.gwSubDeviceList.length > 0 && singleDeviceId) {
|
||||
// 单子设备历史(多变量)
|
||||
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleDeviceId);
|
||||
if (!sub) {
|
||||
this.$message.warning('请选择子设备');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.gwResultMode = 'single';
|
||||
const identifierList = subIds.map((id) => {
|
||||
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
|
||||
return { identifier: id, type: item.type || 1 };
|
||||
});
|
||||
const params = {
|
||||
deviceId: sub.subDeviceId,
|
||||
serialNumber: sub.subDeviceNo,
|
||||
identifierList,
|
||||
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
getDataCenterDeviceHistory(params).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.gwSingleDatas = res.data || [];
|
||||
this.formatGwSingleTableDatas();
|
||||
this.$nextTick(() => {
|
||||
if (this.gwSingleDatas.length) this.drawGwSingleLine();
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!isCalc) {
|
||||
// 多设备对比(支持多变量、指定子设备或全部)
|
||||
this.loading = true;
|
||||
this.gwResultMode = 'compare';
|
||||
const params = {
|
||||
gwDeviceId,
|
||||
identifierList: subIds,
|
||||
type: 1,
|
||||
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) {
|
||||
params.subDeviceIds = subDeviceIds;
|
||||
}
|
||||
getSubDeviceComparativeHistory(params).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.subCompareDatas = res.data || [];
|
||||
this.formatSubCompareTableDatas();
|
||||
this.$nextTick(() => {
|
||||
if (this.subCompareDatas.length) this.drawSubCompareLine();
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 计算量(多变量)
|
||||
this.loading = true;
|
||||
this.gwResultMode = 'calc';
|
||||
const identifierList = calcIds.map((id) => {
|
||||
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
|
||||
return { identifier: id, type: item.type || 1 };
|
||||
});
|
||||
const calcParams = {
|
||||
deviceId: device.deviceId,
|
||||
serialNumber: device.serialNumber,
|
||||
identifierList,
|
||||
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
getDeviceHistoryCalculated(calcParams).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.gwCalcDatas = res.data || [];
|
||||
this.gwCalcTableHeaderTemp = calcIds.map((id) => {
|
||||
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
|
||||
return { name: (m && m.modelName) || id, value: id };
|
||||
});
|
||||
this.formatGwCalcTableDatas();
|
||||
this.$nextTick(() => {
|
||||
if (this.gwCalcDatas.length) this.drawGwCalcLine();
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleGwReset() {
|
||||
this.gwQueryParams.gwDeviceId = null;
|
||||
this.gwQueryParams.identifiers = [];
|
||||
this.gwQueryParams.subDeviceIds = null;
|
||||
this.gwQueryParams.dayDaterange = [new Date(Date.now() - 3600 * 1000 * 2), new Date()];
|
||||
this.gwCalcIdentifierList = [];
|
||||
this.subCompareIdentifierList = [];
|
||||
this.gwSubDeviceList = [];
|
||||
this.gwResultMode = '';
|
||||
this.gwCalcDatas = [];
|
||||
this.gwCalcTableComTemp = [];
|
||||
this.gwCalcTotal = 0;
|
||||
this.subCompareDatas = [];
|
||||
this.subCompareTableComTemp = [];
|
||||
this.subCompareTotal = 0;
|
||||
this.gwSingleDatas = [];
|
||||
this.gwSingleTableComTemp = [];
|
||||
this.gwSingleTotal = 0;
|
||||
},
|
||||
async handleGwExport() {
|
||||
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
|
||||
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange?.length) {
|
||||
this.$message.warning('请先选择网关、物模型和时间范围');
|
||||
return;
|
||||
}
|
||||
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
|
||||
if (!device) return;
|
||||
const calcIds = this.gwSelectedCalcIdentifiers;
|
||||
const subIds = this.gwSelectedSubIdentifiers;
|
||||
if (calcIds.length > 0 && subIds.length > 0) {
|
||||
this.$message.warning('不能混合选择计算量与子设备变量');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.exportLoading = true;
|
||||
const beginTime = moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss');
|
||||
const endTime = moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss');
|
||||
const singleId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
|
||||
if (this.gwResultMode === 'single' && singleId) {
|
||||
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleId);
|
||||
if (!sub || this.gwSingleDatas.length === 0) {
|
||||
this.$message.warning('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
const identifierList = subIds.map((id) => {
|
||||
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
|
||||
return { identifier: id, type: item.type || 1 };
|
||||
});
|
||||
const params = { deviceId: sub.subDeviceId, serialNumber: sub.subDeviceNo, identifierList, beginTime, endTime };
|
||||
const res = await exportDeviceHistory(params);
|
||||
if (res instanceof Blob) {
|
||||
const clonedBlob = res.slice();
|
||||
const isValidBlob = await blobValidate(clonedBlob);
|
||||
if (!isValidBlob) {
|
||||
try {
|
||||
const text = await res.text();
|
||||
const err = JSON.parse(text);
|
||||
this.$message.error(err.msg || '导出失败');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.$message.error('导出失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
saveAs(res, `${sub.subDeviceNo}_${subIds.join('-')}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
|
||||
this.$message.success('导出成功');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.gwResultMode === 'compare' && this.subCompareDatas.length > 0) {
|
||||
const params = { gwDeviceId, identifierList: subIds, type: 1, beginTime, endTime };
|
||||
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) params.subDeviceIds = subDeviceIds;
|
||||
const res = await exportSubDeviceComparativeHistory(params);
|
||||
if (res instanceof Blob) {
|
||||
const clonedBlob = res.slice();
|
||||
const isValidBlob = await blobValidate(clonedBlob);
|
||||
if (!isValidBlob) {
|
||||
try {
|
||||
const text = await res.text();
|
||||
const err = JSON.parse(text);
|
||||
this.$message.error(err.msg || '导出失败');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.$message.error('导出失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const idPart = subIds.length ? subIds.join('-') : 'compare';
|
||||
saveAs(res, `sub_compare_${idPart}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
|
||||
this.$message.success('导出成功');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.gwResultMode === 'calc' && this.gwCalcDatas.length > 0) {
|
||||
const identifierList = calcIds.map((id) => {
|
||||
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
|
||||
return { identifier: id, type: item.type || 1 };
|
||||
});
|
||||
const params = { deviceId: device.deviceId, serialNumber: device.serialNumber, identifierList, beginTime, endTime };
|
||||
const res = await exportDeviceHistoryCalculated(params);
|
||||
if (res instanceof Blob) {
|
||||
const clonedBlob = res.slice();
|
||||
const isValidBlob = await blobValidate(clonedBlob);
|
||||
if (!isValidBlob) {
|
||||
try {
|
||||
const text = await res.text();
|
||||
const err = JSON.parse(text);
|
||||
this.$message.error(err.msg || '导出失败');
|
||||
return;
|
||||
} catch (e) {
|
||||
this.$message.error('导出失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
saveAs(res, `${device.serialNumber}_calculated_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
|
||||
this.$message.success('导出成功');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.$message.warning('没有数据可导出');
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || '导出失败');
|
||||
} finally {
|
||||
this.exportLoading = false;
|
||||
}
|
||||
},
|
||||
getGwCalcTableList(e) {
|
||||
if (e) {
|
||||
this.gwCalcPageNum = e.page;
|
||||
this.gwCalcPageSize = e.limit;
|
||||
}
|
||||
},
|
||||
formatGwCalcTableDatas() {
|
||||
this.gwCalcTableComTemp = (this.gwCalcDatas || []).map((item) => {
|
||||
const time = Object.keys(item)[0];
|
||||
const vals = Object.values(item)[0] || [];
|
||||
const obj = { time };
|
||||
vals.forEach((v) => {
|
||||
const k = Object.keys(v)[0];
|
||||
obj[k] = Object.values(v)[0];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
this.gwCalcTotal = this.gwCalcTableComTemp.length;
|
||||
const ids = this.gwSelectedCalcIdentifiers;
|
||||
this.gwCalcTableHeaderTemp = ids.map((id) => {
|
||||
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
|
||||
return { name: (m && m.modelName) || id, value: id };
|
||||
});
|
||||
},
|
||||
drawGwCalcLine() {
|
||||
if (!this.$refs.gwCalcLineChart) return;
|
||||
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
|
||||
chart.clear();
|
||||
const ids = this.gwSelectedCalcIdentifiers;
|
||||
const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]);
|
||||
const series = ids.map((id) => {
|
||||
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
|
||||
return {
|
||||
name: (m && m.modelName) || id,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.gwCalcDatas.map((d) => {
|
||||
const arr = Object.values(d)[0] || [];
|
||||
const o = arr.find((x) => Object.keys(x)[0] === id);
|
||||
return o ? Object.values(o)[0] : null;
|
||||
}),
|
||||
};
|
||||
});
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: series.map((s) => s.name) },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: times },
|
||||
yAxis: { type: 'value', scale: true },
|
||||
series,
|
||||
});
|
||||
},
|
||||
getGwSingleTableList(e) {
|
||||
if (e) {
|
||||
this.gwSinglePageNum = e.page;
|
||||
this.gwSinglePageSize = e.limit;
|
||||
}
|
||||
},
|
||||
formatGwSingleTableDatas() {
|
||||
const ids = this.gwSelectedSubIdentifiers;
|
||||
this.gwSingleTableComTemp = (this.gwSingleDatas || []).map((item) => {
|
||||
const time = Object.keys(item)[0];
|
||||
const vals = Object.values(item)[0] || [];
|
||||
const obj = { time };
|
||||
vals.forEach((v) => {
|
||||
const k = Object.keys(v)[0];
|
||||
obj[k] = Object.values(v)[0];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
this.gwSingleTotal = this.gwSingleTableComTemp.length;
|
||||
this.gwSingleTableHeaderTemp = ids.map((id) => {
|
||||
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
|
||||
return { name: (m && m.modelName) || id, value: id };
|
||||
});
|
||||
},
|
||||
drawGwSingleLine() {
|
||||
if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return;
|
||||
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
|
||||
chart.clear();
|
||||
const ids = this.gwSelectedSubIdentifiers;
|
||||
const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]);
|
||||
const series = ids.map((id) => {
|
||||
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
|
||||
return {
|
||||
name: (m && m.modelName) || id,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.gwSingleDatas.map((d) => {
|
||||
const arr = Object.values(d)[0] || [];
|
||||
const o = arr.find((x) => Object.keys(x)[0] === id);
|
||||
return o ? Object.values(o)[0] : null;
|
||||
}),
|
||||
};
|
||||
});
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: series.map((s) => s.name) },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: times },
|
||||
yAxis: { type: 'value', scale: true },
|
||||
series,
|
||||
});
|
||||
},
|
||||
getSubCompareTableList(e) {
|
||||
if (e) {
|
||||
this.subComparePageNum = e.page;
|
||||
this.subComparePageSize = e.limit;
|
||||
}
|
||||
},
|
||||
/** 多设备对比列名分隔符,与后端一致 */
|
||||
subCompareColSep() {
|
||||
return ':::';
|
||||
},
|
||||
/** 多设备对比表格/图例列展示名:后端返回 "设备标签:::identifier" 时解析为 "设备标签 - 变量名" */
|
||||
getSubCompareColumnLabel(key) {
|
||||
if (key === 'time') return '更新时间';
|
||||
const sep = this.subCompareColSep();
|
||||
if (key && key.includes(sep)) {
|
||||
const idx = key.indexOf(sep);
|
||||
const deviceLabel = key.substring(0, idx);
|
||||
const identifier = key.substring(idx + sep.length);
|
||||
const model = (this.subCompareIdentifierList || []).find((i) => i.identifier === identifier);
|
||||
return `${deviceLabel} - ${(model && model.modelName) || identifier}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
formatSubCompareTableDatas() {
|
||||
this.subCompareTableComTemp = this.subCompareDatas || [];
|
||||
this.subCompareTotal = this.subCompareTableComTemp.length;
|
||||
const cols = [];
|
||||
if (this.subCompareDatas.length) {
|
||||
Object.keys(this.subCompareDatas[0]).forEach((k) => {
|
||||
cols.push({ prop: k, label: this.getSubCompareColumnLabel(k) });
|
||||
});
|
||||
}
|
||||
this.subCompareTableColumns = cols;
|
||||
},
|
||||
drawSubCompareLine() {
|
||||
if (!this.$refs.subCompareLineChart || !this.subCompareDatas.length) return;
|
||||
const chart = this.$echarts.init(this.$refs.subCompareLineChart);
|
||||
chart.clear();
|
||||
const times = this.subCompareDatas.map((d) => d.time);
|
||||
const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time');
|
||||
const series = keys.map((k) => ({
|
||||
name: this.getSubCompareColumnLabel(k),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: this.subCompareDatas.map((d) => d[k]),
|
||||
}));
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: series.map((s) => s.name) },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: times },
|
||||
yAxis: { type: 'value', scale: true },
|
||||
series,
|
||||
});
|
||||
},
|
||||
// 格式化设备列表数据
|
||||
formatDevTableDatas() {
|
||||
this.devTableComTemp = this.devDatas.map((item) => {
|
||||
@ -826,5 +1480,8 @@ export default {
|
||||
.scene-wrap {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.gateway-wrap {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -156,10 +156,27 @@
|
||||
<el-button style="color: #f56c6c" size="mini" title="右括号" @click="insertOperator(')')">)</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 物模型变量区域 -->
|
||||
<!-- 物模型变量区域:网关设备按从机展示,便于插值计算 -->
|
||||
<div class="variable-section" v-loading="loadingModels">
|
||||
<span class="section-label">产品通用物模型变量:</span>
|
||||
<div class="variable-tags">
|
||||
<span class="section-label">{{ isGatewayDevice ? '从机物模型变量(点击插入,用于跨设备插值如 从机1-从机2):' : '产品通用物模型变量:' }}</span>
|
||||
<template v-if="isGatewayDevice && gatewayVariableGroups.length">
|
||||
<div v-for="grp in gatewayVariableGroups" :key="grp.slaveId" class="variable-group">
|
||||
<span class="group-label">从机{{ grp.slaveId }}:</span>
|
||||
<el-tag
|
||||
v-for="item in grp.items"
|
||||
:key="item.placeholder"
|
||||
size="small"
|
||||
type="primary"
|
||||
effect="plain"
|
||||
class="var-tag"
|
||||
@click="insertVariable(item.placeholder)"
|
||||
:title="item.placeholder"
|
||||
>
|
||||
{{ item.modelName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="variable-tags">
|
||||
<el-tag
|
||||
v-for="model in productModels"
|
||||
:key="model.identifier"
|
||||
@ -251,6 +268,21 @@
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
/* 网关按从机分组变量 */
|
||||
.variable-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.variable-group .group-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
min-width: 52px;
|
||||
}
|
||||
.variable-group .var-tag {
|
||||
margin: 2px 4px 2px 0;
|
||||
}
|
||||
/* 物模型变量区域 */
|
||||
.variable-section {
|
||||
margin-bottom: 12px;
|
||||
@ -371,6 +403,8 @@
|
||||
|
||||
<script>
|
||||
import { listModel, getModel, addModel, updateModel, delModel } from '@/api/iot/model';
|
||||
import { listGateway } from '@/api/iot/gateway';
|
||||
import { listThingsModel } from '@/api/iot/device';
|
||||
|
||||
export default {
|
||||
name: 'DeviceThingsModel',
|
||||
@ -439,8 +473,14 @@ export default {
|
||||
{ validator: this.validateFormula, trigger: 'none' },
|
||||
],
|
||||
},
|
||||
gatewayVariableGroups: [], // 网关设备时按从机分组的变量,用于公式占位 { slaveId, items: [{ modelName, placeholder }] }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isGatewayDevice() {
|
||||
return this.device && this.device.deviceType === 2;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
device: {
|
||||
handler(val) {
|
||||
@ -465,21 +505,22 @@ export default {
|
||||
// 去除空格后校验
|
||||
const formula = value.replace(/\s+/g, '');
|
||||
|
||||
// 1. 校验是否包含非法字符(只允许:数字、小数点、运算符、括号、大括号变量、sqrt)
|
||||
// 合法字符:0-9 . + - * / % ^ ( ) { } 字母(用于变量和sqrt)
|
||||
const validCharsPattern = /^[0-9a-zA-Z_+\-*/%^(){}.\s]+$/;
|
||||
// 1. 校验是否包含非法字符(含大括号变量、跨设备 slaveId:N.xxx 中的冒号)
|
||||
const validCharsPattern = /^[0-9a-zA-Z_+\-*/%^(){}.:\s]+$/;
|
||||
if (!validCharsPattern.test(formula)) {
|
||||
callback(new Error('公式包含非法字符'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 校验变量格式:{标识符},标识符以字母开头,只含字母、数字、下划线
|
||||
// 2. 校验变量格式:{标识符} 或 {slaveId:数字.标识符}(跨设备)
|
||||
const variablePattern = /\{([^}]*)\}/g;
|
||||
let match;
|
||||
const simpleVar = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||
const crossVar = /^slaveId:\d+\.[a-zA-Z][a-zA-Z0-9_]*$/;
|
||||
while ((match = variablePattern.exec(formula)) !== null) {
|
||||
const varName = match[1];
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(varName)) {
|
||||
callback(new Error(`变量格式错误: {${varName}},标识符需以字母开头,只含字母、数字、下划线`));
|
||||
if (!simpleVar.test(varName) && !crossVar.test(varName)) {
|
||||
callback(new Error(`变量格式错误: {${varName}}。支持:{标识符} 或 {slaveId:从机号.标识符}(如 {slaveId:1.WaterH})`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -535,8 +576,8 @@ export default {
|
||||
}
|
||||
|
||||
// 8. 校验公式必须包含至少一个变量
|
||||
if (!/\{[a-zA-Z][a-zA-Z0-9_]*\}/.test(formula)) {
|
||||
callback(new Error('计算公式必须包含至少一个物模型变量,如:{temperature}'));
|
||||
if (!/\{([a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*|[a-zA-Z]+:\d+\.[a-zA-Z][a-zA-Z0-9_]*)}/.test(formula)) {
|
||||
callback(new Error('计算公式必须包含至少一个物模型变量,如:{temperature} 或 {slaveId:1.temperature}'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -556,10 +597,45 @@ export default {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 加载产品物模型(用于公式变量选择) */
|
||||
/** 加载公式变量:网关设备按从机加载(用于跨设备插值),否则加载产品物模型 */
|
||||
loadProductModels() {
|
||||
if (!this.device.productId) return;
|
||||
if (!this.device || !this.device.deviceId) return;
|
||||
this.loadingModels = true;
|
||||
this.gatewayVariableGroups = [];
|
||||
if (this.isGatewayDevice) {
|
||||
listGateway({ gwDeviceId: this.device.deviceId, pageNum: 1, pageSize: 999 })
|
||||
.then((res) => {
|
||||
const subs = res.rows || res.data || [];
|
||||
const promises = subs.map((sub) =>
|
||||
listThingsModel({ deviceId: sub.subDeviceId, pageNum: 1, pageSize: 200 }).then((r) => ({
|
||||
slaveId: sub.slaveId,
|
||||
subName: sub.subDeviceName || '从机' + sub.slaveId,
|
||||
rows: (r.rows || []).filter((m) => m.isCalculated !== 1),
|
||||
}))
|
||||
);
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then((groups) => {
|
||||
this.gatewayVariableGroups = groups
|
||||
.filter((g) => g.rows && g.rows.length)
|
||||
.map((g) => ({
|
||||
slaveId: g.slaveId,
|
||||
items: g.rows.map((m) => ({
|
||||
modelName: m.modelName || m.identifier,
|
||||
placeholder: `{slaveId:${g.slaveId}.${m.identifier}}`,
|
||||
})),
|
||||
}));
|
||||
this.productModels = [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingModels = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.device.productId) {
|
||||
this.loadingModels = false;
|
||||
return;
|
||||
}
|
||||
listModel({
|
||||
productId: this.device.productId,
|
||||
deviceId: 0,
|
||||
@ -567,7 +643,6 @@ export default {
|
||||
pageSize: 200,
|
||||
})
|
||||
.then((response) => {
|
||||
// 过滤掉纯计算物模型(只保留设备上报的物模型)
|
||||
this.productModels = (response.rows || []).filter((m) => m.isCalculated !== 1);
|
||||
})
|
||||
.finally(() => {
|
||||
@ -648,9 +723,13 @@ export default {
|
||||
insertOperator(op) {
|
||||
this.insertAtCursor(op);
|
||||
},
|
||||
/** 插入变量(标识符使用大括号包裹) */
|
||||
insertVariable(identifier) {
|
||||
this.insertAtCursor(`{${identifier}}`);
|
||||
/** 插入变量:普通变量传 identifier 自动加大括号;网关跨设备变量传完整占位符如 {slaveId:1.WaterH} */
|
||||
insertVariable(identifierOrPlaceholder) {
|
||||
const text =
|
||||
identifierOrPlaceholder && (identifierOrPlaceholder.startsWith('{') || identifierOrPlaceholder.includes('slaveId:'))
|
||||
? identifierOrPlaceholder
|
||||
: `{${identifierOrPlaceholder}}`;
|
||||
this.insertAtCursor(text);
|
||||
},
|
||||
/** 在光标位置插入内容(支持替换选中文本) */
|
||||
insertAtCursor(text) {
|
||||
|
||||
@ -39,6 +39,11 @@
|
||||
{{ $t('product.product-things-model.142341-1') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-download" size="mini" @click="handleImportFromSubProduct" v-if="productInfo.status == 1 && productInfo.isOwner != 0" v-hasPermi="['iot:model:add']">
|
||||
从子产品导入
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList" v-hasPermi="['iot:model:list']">{{ $t('product.product-things-model.142341-2') }}</el-button>
|
||||
</el-col>
|
||||
@ -361,6 +366,21 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 从子产品一键导入物模型 -->
|
||||
<el-dialog title="从子产品导入物模型" :visible.sync="openSubProductImport" width="700px" append-to-body>
|
||||
<p style="margin-bottom: 10px; color: #909399;">将已绑定子产品的物模型按 identifier 去重后导入到当前网关产品,已存在的 identifier 会跳过。</p>
|
||||
<el-table :data="subProductModelList" max-height="400" border size="small">
|
||||
<el-table-column prop="modelName" label="物模型名称" min-width="120" />
|
||||
<el-table-column prop="identifier" label="标识符" width="140" />
|
||||
<el-table-column prop="datatype" label="数据类型" width="90" />
|
||||
</el-table>
|
||||
<div v-if="subProductModelList.length === 0" style="text-align: center; padding: 20px; color: #909399;">当前产品未绑定子产品或子产品下暂无物模型</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="confirmSubProductImport" :loading="subProductImportLoading" :disabled="subProductModelList.length === 0">确认导入</el-button>
|
||||
<el-button @click="openSubProductImport = false">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- {{ $t('product.product-things-model.142341-101') }}JSON -->
|
||||
<el-dialog :title="title" :visible.sync="openThingsModel" width="600px" append-to-body>
|
||||
<div style="border: 1px solid #ccc; margin-top: -15px; height: 600px; overflow: scroll">
|
||||
@ -390,6 +410,7 @@ import JsonViewer from 'vue-json-viewer';
|
||||
import 'vue-json-viewer/style.css';
|
||||
import thingsParameter from '../template/parameter';
|
||||
import { listModel, getModel, delModel, addModel, updateModel, importModel } from '@/api/iot/model';
|
||||
import { getSubProductThingsModel, importSubProductThingsToGateway } from '@/api/data/center';
|
||||
import importBatch from './components/batchImportThingsModel';
|
||||
|
||||
export default {
|
||||
@ -454,6 +475,9 @@ export default {
|
||||
open: false,
|
||||
openSelect: false,
|
||||
openThingsModel: false,
|
||||
openSubProductImport: false,
|
||||
subProductModelList: [],
|
||||
subProductImportLoading: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
productId: 0,
|
||||
@ -709,6 +733,36 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
/** 从子产品导入:打开弹窗并拉取可导入列表 */
|
||||
handleImportFromSubProduct() {
|
||||
if (!this.productInfo || !this.productInfo.productId) {
|
||||
this.$modal.msgWarning('请先选择产品');
|
||||
return;
|
||||
}
|
||||
this.openSubProductImport = true;
|
||||
this.subProductModelList = [];
|
||||
getSubProductThingsModel(this.productInfo.productId).then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
this.subProductModelList = res.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 从子产品导入:确认导入 */
|
||||
confirmSubProductImport() {
|
||||
if (!this.productInfo || !this.productInfo.productId) return;
|
||||
this.subProductImportLoading = true;
|
||||
importSubProductThingsToGateway(this.productInfo.productId)
|
||||
.then((res) => {
|
||||
this.subProductImportLoading = false;
|
||||
this.openSubProductImport = false;
|
||||
this.$modal.msgSuccess(res.msg || '导入成功');
|
||||
this.getList();
|
||||
this.$emit('updateModel');
|
||||
})
|
||||
.catch(() => {
|
||||
this.subProductImportLoading = false;
|
||||
});
|
||||
},
|
||||
containsUnderscore(value) {
|
||||
// 使用正则表达式检查值中是否包含下划线
|
||||
return /_/.test(value);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user