20251213:数据中心-历史记录-设备-导出历史数据
This commit is contained in:
parent
1338393ca4
commit
d965eab5d6
@ -128,7 +128,7 @@ public class DeviceDebugLogAspect {
|
|||||||
Long productId = bo.getProductId();
|
Long productId = bo.getProductId();
|
||||||
|
|
||||||
saveErrorLog(serialNumber, sourceData, errorMsg, protocolCode, bo.getTopicName(),
|
saveErrorLog(serialNumber, sourceData, errorMsg, protocolCode, bo.getTopicName(),
|
||||||
DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "PARSE_ERROR", productId);
|
DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "DEBUG_ERROR", productId);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("记录数据解析错误日志失败: {}", ex.getMessage(), ex);
|
log.error("记录数据解析错误日志失败: {}", ex.getMessage(), ex);
|
||||||
@ -372,9 +372,13 @@ public class DeviceDebugLogAspect {
|
|||||||
PushMessageBo messageBo = new PushMessageBo();
|
PushMessageBo messageBo = new PushMessageBo();
|
||||||
messageBo.setTopic(topicsUtils.buildTopic(productId, serialNumber.toUpperCase(), TopicType.WS_SERVICE_INVOKE));
|
messageBo.setTopic(topicsUtils.buildTopic(productId, serialNumber.toUpperCase(), TopicType.WS_SERVICE_INVOKE));
|
||||||
|
|
||||||
|
// 将hex转换为ASCII
|
||||||
|
String sourceAscii = hexToAscii(sourceData);
|
||||||
|
|
||||||
JSONObject pushObj = new JSONObject();
|
JSONObject pushObj = new JSONObject();
|
||||||
pushObj.put("message", errorItems);
|
pushObj.put("message", errorItems);
|
||||||
pushObj.put("sources", StringUtils.isNotEmpty(sourceData) ? sourceData : "");
|
pushObj.put("sources", StringUtils.isNotEmpty(sourceData) ? sourceData : ""); // HEX格式
|
||||||
|
pushObj.put("sourcesAscii", sourceAscii); // ASCII格式
|
||||||
pushObj.put("errorType", identity); // 错误类型:PARSE_ERROR 或 SEND_ERROR
|
pushObj.put("errorType", identity); // 错误类型:PARSE_ERROR 或 SEND_ERROR
|
||||||
pushObj.put("isError", true); // 标记为错误消息
|
pushObj.put("isError", true); // 标记为错误消息
|
||||||
|
|
||||||
@ -382,10 +386,62 @@ public class DeviceDebugLogAspect {
|
|||||||
|
|
||||||
// 推送消息到MQTT
|
// 推送消息到MQTT
|
||||||
mqttRemoteManager.pushCommon(messageBo);
|
mqttRemoteManager.pushCommon(messageBo);
|
||||||
log.info("已推送错误信息到前端 - 设备: {}, 产品ID: {}, 错误类型: {}",
|
log.info("已推送错误信息到前端 - 设备: {}, 产品ID: {}, 错误类型: {}, HEX: {}, ASCII: {}",
|
||||||
serialNumber, productId, identity);
|
serialNumber, productId, identity,
|
||||||
|
StringUtils.isNotEmpty(sourceData) ? StringUtils.substring(sourceData, 0, 20) + "..." : "",
|
||||||
|
StringUtils.isNotEmpty(sourceAscii) ? StringUtils.substring(sourceAscii, 0, 20) + "..." : "");
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("推送错误信息到前端失败: {}", ex.getMessage(), ex);
|
log.error("推送错误信息到前端失败: {}", ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将16进制字符串转换为ASCII字符串
|
||||||
|
* @param hexStr 16进制字符串(可能包含空格)
|
||||||
|
* @return ASCII字符串,保留换行符和回车符,其他不可打印字符显示为点号
|
||||||
|
*/
|
||||||
|
private String hexToAscii(String hexStr) {
|
||||||
|
if (StringUtils.isEmpty(hexStr)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 移除空格
|
||||||
|
String cleanHex = hexStr.replaceAll("\\s+", "");
|
||||||
|
// 如果长度不是偶数,补0
|
||||||
|
if (cleanHex.length() % 2 != 0) {
|
||||||
|
cleanHex = "0" + cleanHex;
|
||||||
|
}
|
||||||
|
StringBuilder ascii = new StringBuilder();
|
||||||
|
for (int i = 0; i < cleanHex.length(); i += 2) {
|
||||||
|
String hexByte = cleanHex.substring(i, Math.min(i + 2, cleanHex.length()));
|
||||||
|
try {
|
||||||
|
int charCode = Integer.parseInt(hexByte, 16);
|
||||||
|
// 处理特殊字符:换行符(0x0A=10)和回车符(0x0D=13)
|
||||||
|
if (charCode == 10) {
|
||||||
|
// 换行符 \n
|
||||||
|
ascii.append("\n");
|
||||||
|
} else if (charCode == 13) {
|
||||||
|
// 回车符 \r
|
||||||
|
ascii.append("\r");
|
||||||
|
} else if (charCode == 9) {
|
||||||
|
// 制表符 \t
|
||||||
|
ascii.append("\t");
|
||||||
|
} else if (charCode >= 32 && charCode <= 126) {
|
||||||
|
// 可打印字符(32-126)
|
||||||
|
ascii.append((char) charCode);
|
||||||
|
} else {
|
||||||
|
// 其他不可打印字符显示为点号
|
||||||
|
ascii.append(".");
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
ascii.append(".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ascii.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("HEX转ASCII失败: {}, HEX字符串: {}", e.getMessage(),
|
||||||
|
StringUtils.isNotEmpty(hexStr) ? StringUtils.substring(hexStr, 0, 50) : "");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,26 +1,31 @@
|
|||||||
package com.fastbee.controller.datacenter;
|
package com.fastbee.controller.datacenter;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.ExcelWriter;
|
||||||
|
import com.alibaba.excel.write.metadata.WriteSheet;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.fastbee.common.annotation.Anonymous;
|
|
||||||
import com.fastbee.common.core.domain.AjaxResult;
|
import com.fastbee.common.core.domain.AjaxResult;
|
||||||
|
import com.fastbee.common.utils.DateUtils;
|
||||||
import com.fastbee.common.utils.StringUtils;
|
import com.fastbee.common.utils.StringUtils;
|
||||||
import com.fastbee.iot.domain.DeviceLog;
|
|
||||||
import com.fastbee.iot.model.AlertCountVO;
|
import com.fastbee.iot.model.AlertCountVO;
|
||||||
import com.fastbee.iot.model.DeviceHistoryParam;
|
import com.fastbee.iot.model.DeviceHistoryParam;
|
||||||
import com.fastbee.iot.model.HistoryModel;
|
|
||||||
import com.fastbee.iot.model.ThingsModelLogCountVO;
|
import com.fastbee.iot.model.ThingsModelLogCountVO;
|
||||||
|
import com.fastbee.iot.model.dto.ThingsModelDTO;
|
||||||
import com.fastbee.iot.model.param.DataCenterParam;
|
import com.fastbee.iot.model.param.DataCenterParam;
|
||||||
import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
|
import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
|
||||||
import com.fastbee.iot.service.DataCenterService;
|
import com.fastbee.iot.service.DataCenterService;
|
||||||
|
import com.fastbee.iot.service.IDeviceService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Date;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.List;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.net.URLEncoder;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
|
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
|
||||||
|
|
||||||
@ -38,6 +43,9 @@ public class DataCenterController {
|
|||||||
@Resource
|
@Resource
|
||||||
private DataCenterService dataCenterService;
|
private DataCenterService dataCenterService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDeviceService deviceService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询设备物模型的历史数据
|
* 查询设备物模型的历史数据
|
||||||
* @param deviceHistoryParam 传参
|
* @param deviceHistoryParam 传参
|
||||||
@ -122,4 +130,154 @@ public class DataCenterController {
|
|||||||
return AjaxResult.success(list);
|
return AjaxResult.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出设备历史数据Excel
|
||||||
|
* @param deviceHistoryParam 查询参数
|
||||||
|
* @param response HTTP响应
|
||||||
|
*/
|
||||||
|
@ApiOperation("导出设备历史数据Excel")
|
||||||
|
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
|
||||||
|
@PostMapping("/deviceHistory/export")
|
||||||
|
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);
|
||||||
|
response.getWriter().write("请选择设备");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
|
||||||
|
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取历史数据
|
||||||
|
List<JSONObject> dataList = dataCenterService.deviceHistory(deviceHistoryParam);
|
||||||
|
|
||||||
|
if (dataList == null || dataList.isEmpty()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||||
|
response.getWriter().write("没有数据可导出");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取变量名称映射(identifier -> modelName)
|
||||||
|
Map<String, String> identifierNameMap = new LinkedHashMap<>();
|
||||||
|
List<String> headerList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (deviceHistoryParam.getIdentifierList() != null && !deviceHistoryParam.getIdentifierList().isEmpty()) {
|
||||||
|
// 从参数中获取变量列表,查询物模型获取中文名称
|
||||||
|
List<String> identifierList = deviceHistoryParam.getIdentifierList().stream()
|
||||||
|
.map(DeviceHistoryParam.IdentifierVO::getIdentifier)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 查询物模型获取中文名称
|
||||||
|
if (deviceHistoryParam.getDeviceId() != null) {
|
||||||
|
List<ThingsModelDTO> thingsModelList = deviceService.listThingsModel(deviceHistoryParam.getDeviceId());
|
||||||
|
Map<String, ThingsModelDTO> thingsModelMap = thingsModelList.stream()
|
||||||
|
.collect(Collectors.toMap(ThingsModelDTO::getIdentifier, t -> t, (o, n) -> n));
|
||||||
|
|
||||||
|
for (String identifier : identifierList) {
|
||||||
|
ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
|
||||||
|
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
|
||||||
|
? thingsModel.getModelName() : identifier;
|
||||||
|
identifierNameMap.put(identifier, headerName);
|
||||||
|
headerList.add(headerName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有设备ID,直接使用identifier作为表头
|
||||||
|
headerList.addAll(identifierList);
|
||||||
|
for (String identifier : identifierList) {
|
||||||
|
identifierNameMap.put(identifier, identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!dataList.isEmpty()) {
|
||||||
|
// 从第一条数据中提取所有变量名
|
||||||
|
JSONObject firstData = dataList.get(0);
|
||||||
|
Set<String> identifierSet = new LinkedHashSet<>();
|
||||||
|
for (String key : firstData.keySet()) {
|
||||||
|
Object value = firstData.get(key);
|
||||||
|
if (value instanceof List) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<JSONObject> identifierList = (List<JSONObject>) value;
|
||||||
|
for (JSONObject identifierObj : identifierList) {
|
||||||
|
identifierSet.addAll(identifierObj.keySet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询物模型获取中文名称
|
||||||
|
if (deviceHistoryParam.getDeviceId() != null) {
|
||||||
|
List<ThingsModelDTO> thingsModelList = deviceService.listThingsModel(deviceHistoryParam.getDeviceId());
|
||||||
|
Map<String, ThingsModelDTO> thingsModelMap = thingsModelList.stream()
|
||||||
|
.collect(Collectors.toMap(ThingsModelDTO::getIdentifier, t -> t, (o, n) -> n));
|
||||||
|
|
||||||
|
for (String identifier : identifierSet) {
|
||||||
|
ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
|
||||||
|
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
|
||||||
|
? thingsModel.getModelName() : identifier;
|
||||||
|
identifierNameMap.put(identifier, headerName);
|
||||||
|
headerList.add(headerName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有设备ID,直接使用identifier作为表头
|
||||||
|
headerList.addAll(identifierSet);
|
||||||
|
for (String identifier : identifierSet) {
|
||||||
|
identifierNameMap.put(identifier, identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建导出数据
|
||||||
|
List<List<Object>> exportDataList = new ArrayList<>();
|
||||||
|
for (JSONObject jsonObject : dataList) {
|
||||||
|
List<Object> rowData = new ArrayList<>();
|
||||||
|
// 第一列是时间
|
||||||
|
String time = jsonObject.keySet().iterator().next();
|
||||||
|
rowData.add(time);
|
||||||
|
|
||||||
|
// 获取该时间点的数据
|
||||||
|
Object dataValue = jsonObject.get(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按照identifierNameMap的顺序添加数据
|
||||||
|
for (Map.Entry<String, String> entry : identifierNameMap.entrySet()) {
|
||||||
|
String identifier = entry.getKey();
|
||||||
|
Object value = valueMap.get(identifier);
|
||||||
|
rowData.add(value != null ? value : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exportDataList.add(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建表头 - EasyExcel格式:每个List<String>代表一列,List中的元素代表该列在不同层级的表头
|
||||||
|
List<List<String>> headList = new ArrayList<>();
|
||||||
|
// 第一列:时间
|
||||||
|
headList.add(Collections.singletonList("时间"));
|
||||||
|
// 其他列:每个变量一列
|
||||||
|
for (String header : headerList) {
|
||||||
|
headList.add(Collections.singletonList(header));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
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");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||||
|
|
||||||
|
// 使用EasyExcel写入数据
|
||||||
|
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
|
||||||
|
.head(headList)
|
||||||
|
.build();
|
||||||
|
WriteSheet writeSheet = EasyExcel.writerSheet(0, "设备历史数据").build();
|
||||||
|
excelWriter.write(exportDataList, writeSheet);
|
||||||
|
excelWriter.finish();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.fastbee.iot.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author fastb
|
||||||
|
* @version 1.0
|
||||||
|
* @description: 设备历史数据导出VO(用于EasyExcel动态表头导出)
|
||||||
|
* @date 2024-12-12
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DeviceHistoryExportVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间
|
||||||
|
*/
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态字段存储,key为变量名称,value为变量值
|
||||||
|
* 使用LinkedHashMap保持顺序
|
||||||
|
*/
|
||||||
|
private Map<String, Object> dynamicFields = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取动态字段值
|
||||||
|
*/
|
||||||
|
public Object getFieldValue(String fieldName) {
|
||||||
|
return dynamicFields.get(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置动态字段值
|
||||||
|
*/
|
||||||
|
public void setFieldValue(String fieldName, Object value) {
|
||||||
|
dynamicFields.put(fieldName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17843
vue/package-lock.json
generated
Normal file
17843
vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -44,3 +44,13 @@ export function getDataCenterCountThingsModelInvoke(query) {
|
|||||||
params: query,
|
params: query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出设备历史数据Excel
|
||||||
|
export function exportDeviceHistory(data) {
|
||||||
|
return request({
|
||||||
|
url: '/data/center/deviceHistory/export',
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 25 KiB |
@ -43,5 +43,6 @@
|
|||||||
"dataCenter.history.384934-20": "Last day",
|
"dataCenter.history.384934-20": "Last day",
|
||||||
"dataCenter.history.384934-21": "In the past seven days",
|
"dataCenter.history.384934-21": "In the past seven days",
|
||||||
"dataCenter.history.384934-22": "Last 30 days",
|
"dataCenter.history.384934-22": "Last 30 days",
|
||||||
"dataCenter.history.384934-23": "total"
|
"dataCenter.history.384934-23": "total",
|
||||||
|
"dataCenter.history.384934-24": "Export Excel"
|
||||||
}
|
}
|
||||||
@ -43,5 +43,6 @@
|
|||||||
"dataCenter.history.384934-20": "最近一天",
|
"dataCenter.history.384934-20": "最近一天",
|
||||||
"dataCenter.history.384934-21": "最近七天",
|
"dataCenter.history.384934-21": "最近七天",
|
||||||
"dataCenter.history.384934-22": "最近三十天",
|
"dataCenter.history.384934-22": "最近三十天",
|
||||||
"dataCenter.history.384934-23": "总量"
|
"dataCenter.history.384934-23": "总量",
|
||||||
|
"dataCenter.history.384934-24": "导出Excel"
|
||||||
}
|
}
|
||||||
@ -30,6 +30,7 @@
|
|||||||
<el-form-item style="float: right; margin-right: 0">
|
<el-form-item style="float: right; margin-right: 0">
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleDevQuery">{{ $t('dataCenter.history.384934-8') }}</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleDevQuery">{{ $t('dataCenter.history.384934-8') }}</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="handleDevResetQuery">{{ $t('dataCenter.history.384934-9') }}</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="handleDevResetQuery">{{ $t('dataCenter.history.384934-9') }}</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-download" size="mini" :disabled="devDatas.length === 0" @click="handleDevExport" v-loading="exportLoading">{{ $t('dataCenter.history.384934-24') }}</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@ -142,7 +143,9 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
|
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
|
||||||
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
|
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
|
||||||
import { getDataCenterDeviceHistory, getDataCenterSceneHistory } from '@/api/data/center.js';
|
import { getDataCenterDeviceHistory, getDataCenterSceneHistory, exportDeviceHistory } from '@/api/data/center.js';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { blobValidate } from '@/utils/ruoyi';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'dataCenterHistory',
|
name: 'dataCenterHistory',
|
||||||
@ -220,6 +223,8 @@ export default {
|
|||||||
scenePageNum: 1,
|
scenePageNum: 1,
|
||||||
scenePageSize: 10,
|
scenePageSize: 10,
|
||||||
sceneTotal: 0,
|
sceneTotal: 0,
|
||||||
|
// 导出loading
|
||||||
|
exportLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -534,6 +539,65 @@ export default {
|
|||||||
value: item,
|
value: item,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
// 导出设备历史数据
|
||||||
|
async handleDevExport() {
|
||||||
|
const isField = this.areAllFields(this.devQueryParams);
|
||||||
|
if (!isField) {
|
||||||
|
this.$message.warning('请先选择设备、变量和时间范围');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.devDatas.length === 0) {
|
||||||
|
this.$message.warning('没有数据可导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.exportLoading = true;
|
||||||
|
const devices = this.devDeviceList.find((item) => item.deviceId === this.devQueryParams.deviceId);
|
||||||
|
const identifierList = this.devQueryParams.identifiers.map((item) => {
|
||||||
|
const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item);
|
||||||
|
return { identifier: identifiers.identifier, type: identifiers.type };
|
||||||
|
});
|
||||||
|
const params = {
|
||||||
|
deviceId: devices.deviceId,
|
||||||
|
serialNumber: devices.serialNumber,
|
||||||
|
identifierList: identifierList,
|
||||||
|
beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
};
|
||||||
|
const res = await exportDeviceHistory(params);
|
||||||
|
|
||||||
|
// 检查是否为Blob对象
|
||||||
|
if (res instanceof Blob) {
|
||||||
|
// 验证是否为有效的blob(不是错误JSON)
|
||||||
|
const clonedBlob = res.slice();
|
||||||
|
const isValidBlob = await blobValidate(clonedBlob);
|
||||||
|
if (!isValidBlob) {
|
||||||
|
// 可能是错误响应,尝试解析
|
||||||
|
try {
|
||||||
|
const text = await res.text();
|
||||||
|
const errorObj = JSON.parse(text);
|
||||||
|
this.$message.error(errorObj.msg || '导出失败');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('导出失败,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 有效的Excel文件,直接下载
|
||||||
|
const fileName = `设备历史数据_${moment().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||||
|
saveAs(res, fileName);
|
||||||
|
this.$message.success('导出成功');
|
||||||
|
} else {
|
||||||
|
// 非Blob响应,可能是错误
|
||||||
|
this.$message.error('导出失败:服务器返回格式错误');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('导出失败:', err);
|
||||||
|
this.$message.error(err.message || '导出失败');
|
||||||
|
} finally {
|
||||||
|
this.exportLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
drawSceneLine() {
|
drawSceneLine() {
|
||||||
// 初始化折线图
|
// 初始化折线图
|
||||||
this.charts = this.$echarts.init(this.$refs.sceneLineChart);
|
this.charts = this.$echarts.init(this.$refs.sceneLineChart);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="instruction-parsing">
|
<div class="instruction-parsing">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="16" class="left">
|
<el-col :span="20" class="left">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<span style="color: #909399">{{ $t('device.instruction-parsing.830424-0') }}</span>
|
<span style="color: #909399">{{ $t('device.instruction-parsing.830424-0') }}</span>
|
||||||
<span>{{ device.serialNumber }}</span>
|
<span>{{ device.serialNumber }}</span>
|
||||||
@ -30,7 +30,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<div class="content-value">{{ item.value }}</div>
|
<div class="content-value">
|
||||||
|
<!-- 显示HEX格式 -->
|
||||||
|
<div v-if="item.value" class="data-format-item">
|
||||||
|
<span class="format-label">HEX:</span>
|
||||||
|
<span class="hex-value">{{ item.value }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 显示ASCII格式 -->
|
||||||
|
<div v-if="item.valueAscii" class="data-format-item ascii-item">
|
||||||
|
<span class="format-label">ASCII:</span>
|
||||||
|
<pre class="ascii-value">{{ item.valueAscii }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="analysis-data" v-if="item.type != 0 && item.analysis">
|
<div class="analysis-data" v-if="item.type != 0 && item.analysis">
|
||||||
<div class="data-col left-col">
|
<div class="data-col left-col">
|
||||||
@ -61,7 +72,7 @@
|
|||||||
<el-button round type="primary" class="send-btn" size="mini" @click.stop="sendMessage">{{ $t('device.instruction-parsing.830424-14') }}</el-button>
|
<el-button round type="primary" class="send-btn" size="mini" @click.stop="sendMessage">{{ $t('device.instruction-parsing.830424-14') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8" class="right">
|
<el-col :span="4" class="right">
|
||||||
<div class="head right-head">{{ $t('device.instruction-parsing.830424-15') }}</div>
|
<div class="head right-head">{{ $t('device.instruction-parsing.830424-15') }}</div>
|
||||||
<div class="content beautify-scroll-def">
|
<div class="content beautify-scroll-def">
|
||||||
<div v-for="(item, index) in quickParsing" :key="index" class="quick-item" @click.stop="quickClick(item)" @contextmenu.prevent="onContextmenu($event, item)">
|
<div v-for="(item, index) in quickParsing" :key="index" class="quick-item" @click.stop="quickClick(item)" @contextmenu.prevent="onContextmenu($event, item)">
|
||||||
@ -329,7 +340,7 @@
|
|||||||
<el-input v-model="editNameForm.name" :placeholder="$t('device.instruction-parsing.830424-30')" style="width: 60%"></el-input>
|
<el-input v-model="editNameForm.name" :placeholder="$t('device.instruction-parsing.830424-30')" style="width: 60%"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('device.instruction-parsing.830424-31')">
|
<el-form-item :label="$t('device.instruction-parsing.830424-31')">
|
||||||
<el-input v-model="editNameForm.command" :disabled="true" style="width: 60%"></el-input>
|
<el-input v-model="editNameForm.command" style="width: 60%"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@ -843,7 +854,8 @@ export default {
|
|||||||
//0是下发,1是回复,2是上报
|
//0是下发,1是回复,2是上报
|
||||||
type: 2,
|
type: 2,
|
||||||
time: formatDate(new Date()),
|
time: formatDate(new Date()),
|
||||||
value: data.sources,
|
value: data.sources, // HEX格式
|
||||||
|
valueAscii: data.sourcesAscii || '', // ASCII格式
|
||||||
loading: false,
|
loading: false,
|
||||||
analysis: false,
|
analysis: false,
|
||||||
//解析的数据数组
|
//解析的数据数组
|
||||||
@ -957,7 +969,7 @@ $right-btn-color: #1890ff;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: 600px;
|
height: 700px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@ -1031,11 +1043,55 @@ $right-btn-color: #1890ff;
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
|
||||||
.content-value {
|
.content-value {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.data-format-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ascii-item {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 60px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hex-value {
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-value {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #303133;
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user