This commit is contained in:
高云鹏 2024-11-13 15:32:35 +08:00
commit a9c6329c44
47 changed files with 2575 additions and 705 deletions

View File

@ -31,6 +31,8 @@
<taosdata.verson>3.2.10</taosdata.verson>
<disruptor.version>3.4.4</disruptor.version>
<aviator.version>5.4.3</aviator.version>
<minio.version>8.4.3</minio.version>
<jfreechart.version>1.5.3</jfreechart.version>
</properties>
<dependencies>
@ -201,6 +203,17 @@
</exclusions>
</dependency>
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>${jfreechart.version}</version>
</dependency>
</dependencies>
<build>

View File

@ -1,10 +1,9 @@
package com.das.common.config;
import com.das.modules.node.handler.NodeHandshakeInterceptor;
import com.das.modules.node.handler.NodeMessageHandler;
import com.das.modules.node.handler.NodeWebsocketHandshakeInterceptor;
import com.das.modules.node.service.impl.NodeMessageServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@ -12,15 +11,13 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@EnableWebSocket
@Configuration
public class WebsocketConfig implements WebSocketConfigurer {
@Autowired
NodeHandshakeInterceptor nodeHandshakeInterceptor;
@Autowired
NodeMessageHandler nodeMessageHandler;
NodeMessageServiceImpl nodeMessageService;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(nodeMessageHandler, "/node/{nodeId}/{version}")
registry.addHandler(nodeMessageService, "/node/{nodeId}/{version}")
.setAllowedOrigins("*")
.addInterceptors(nodeHandshakeInterceptor);
.addInterceptors(new NodeWebsocketHandshakeInterceptor());
}
}

View File

@ -0,0 +1,27 @@
package com.das.common.constant;
/**
* @Author liuyuxia
* @ClassName FilesvrConstants
* @Date 2018/12/10 0010 11:02
* @Version 2.5
* @Description 文件服务基础常量
**/
public class FileConstants {
public static final String FILE_SEPARATOR = "/";
public static final String FILE_CHARSET = "UTF-8";
/**
* 递归
*/
public static final Integer YES_RECURSIVE=1;
public static final Integer NO_RECURSIVE=0;
public static final int META_R = 1001;
public static final int META_W = 1002;
public static final String META_R_NAME = "";
public static final String META_W_NAME = "";
}

View File

@ -137,4 +137,10 @@ public class SysEquipment extends BaseEntity {
*/
@TableField(value = "nominal_capacity")
private Double nominalCapacity;
/**
* 故障录波格式
*/
@TableField(value = "fdr_format")
private String fdrFormat;
}

View File

@ -469,7 +469,7 @@ public class SysIotModelServiceImpl implements SysIotModelService {
addModelFieldCache(highCreateList.get(i));
}
}
for (SysIotModelField item : calCreateList){
for (SysIotModelField item : calCreateList) {
createTdStableOrColumn(item);
addModelFieldCache(item);
}
@ -582,7 +582,7 @@ public class SysIotModelServiceImpl implements SysIotModelService {
if (sysIotModelField.getAttributeType() == 199) {
Long iotModelId = sysIotModelField.getIotModelId();
String modelCode = dataService.iotModelMap.get(iotModelId.toString());
tdEngineService.deleteStable("c_" + modelCode +"_"+ sysIotModelField.getAttributeCode());
tdEngineService.deleteStable("c_" + modelCode + "_" + sysIotModelField.getAttributeCode());
} else {
String stableName = null;
SysIotModel sysIotModel = sysIotModelMapper.selectById(sysIotModelField.getIotModelId());
@ -611,6 +611,15 @@ public class SysIotModelServiceImpl implements SysIotModelService {
private void addModelFieldCache(SysIotModelField sysIotModelField) {
//获取物模型编码
String modelCode = dataService.iotModelMap.get(sysIotModelField.getIotModelId().toString());
Map<String, String> fieldCodeNameMap = dataService.fieldCodeNameMap.get(modelCode);
if (fieldCodeNameMap == null) {
Map<String, String> fieldCodeName = new HashMap<>();
fieldCodeName.put(sysIotModelField.getAttributeCode(),sysIotModelField.getAttributeName());
dataService.fieldCodeNameMap.put(modelCode,fieldCodeName);
}
else {
fieldCodeNameMap.put(sysIotModelField.getAttributeCode(), sysIotModelField.getAttributeName());
}
if (sysIotModelField.getAttributeType() == 199) {
Map<String, String> map = dataService.calculateIotFieldMap.get(modelCode);
if (map == null) {
@ -657,10 +666,12 @@ public class SysIotModelServiceImpl implements SysIotModelService {
private void deleteModelFieldCache(SysIotModelField sysIotModelField) {
//获取物模型编码
String modelCode = dataService.iotModelMap.get(sysIotModelField.getIotModelId().toString());
if (sysIotModelField.getAttributeType() == 199){
Map<String, String> fieldCodeName = dataService.fieldCodeNameMap.get(modelCode);
fieldCodeName.remove(sysIotModelField.getAttributeCode());
if (sysIotModelField.getAttributeType() == 199) {
Map<String, String> map = dataService.calculateIotFieldMap.get(modelCode);
map.remove(sysIotModelField.getAttributeCode());
}else {
} else {
if (sysIotModelField.getHighSpeed() == 0) {
Map<String, Object> map = dataService.lowIotFieldMap.get(modelCode);
map.remove(sysIotModelField.getAttributeCode());

View File

@ -0,0 +1,71 @@
package com.das.modules.fdr.config;
import io.minio.*;
import io.minio.errors.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
@ConditionalOnClass(MinioClient.class)
public class MinioConfig {
@Resource
private MinioProperties minioAutoProperties;
@Bean
public MinioClient minioClient() {
log.info("开始初始化MinioClient, url为{}, accessKey为:{}", minioAutoProperties.getUrl(), minioAutoProperties.getAccessKey());
MinioClient minioClient = MinioClient
.builder()
.endpoint(minioAutoProperties.getUrl())
.credentials(minioAutoProperties.getAccessKey(), minioAutoProperties.getSecretKey())
.build();
minioClient.setTimeout(
minioAutoProperties.getConnectTimeout(),
minioAutoProperties.getWriteTimeout(),
minioAutoProperties.getReadTimeout()
);
// Start detection
if (minioAutoProperties.isCheckBucket()) {
log.info("checkBucket为{}, 开始检测桶是否存在", minioAutoProperties.isCheckBucket());
String bucketName = minioAutoProperties.getBucket();
if (!checkBucket(bucketName, minioClient)) {
log.info("文件桶[{}]不存在, 开始检查是否可以新建桶", bucketName);
if (minioAutoProperties.isCreateBucket()) {
log.info("createBucket为{},开始新建文件桶", minioAutoProperties.isCreateBucket());
createBucket(bucketName, minioClient);
}
}
log.info("文件桶[{}]已存在, minio客户端连接成功!", bucketName);
} else {
throw new RuntimeException("桶不存在, 请检查桶名称是否正确或者将checkBucket属性改为false");
}
return minioClient;
}
private boolean checkBucket(String bucketName, MinioClient minioClient) {
boolean isExists = false;
try {
isExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
} catch (Exception e) {
throw new RuntimeException("failed to check if the bucket exists", e);
}
return isExists;
}
private void createBucket(String bucketName, MinioClient minioClient) {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
log.info("文件桶[{}]新建成功, minio客户端已连接", bucketName);
} catch (Exception e) {
throw new RuntimeException("failed to create default bucket", e);
}
}
}

View File

@ -0,0 +1,70 @@
package com.das.modules.fdr.config;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Data
@Validated
@Component
public class MinioProperties {
/**
* 服务地址
*/
@NotEmpty(message = "minio服务地址不可为空")
@URL(message = "minio服务地址格式错误")
@Value("${minio.url}")
private String url;
/**
* 认证账户
*/
@NotEmpty(message = "minio认证账户不可为空")
@Value("${minio.accessKey}")
private String accessKey;
/**
* 认证密码
*/
@NotEmpty(message = "minio认证密码不可为空")
@Value("${minio.secretKey}")
private String secretKey;
/**
* 桶名称, 优先级最低
*/
@Value("${minio.bucket}")
private String bucket;
/**
* 桶不在的时候是否新建桶
*/
private boolean createBucket = true;
/**
* 启动的时候检查桶是否存在
*/
private boolean checkBucket = true;
/**
* 设置HTTP连接写入和读取超时值为0意味着没有超时
* HTTP连接超时以毫秒为单位
*/
private long connectTimeout;
/**
* 设置HTTP连接写入和读取超时值为0意味着没有超时
* HTTP写超时以毫秒为单位
*/
private long writeTimeout;
/**
* 设置HTTP连接写入和读取超时值为0意味着没有超时
* HTTP读取超时以毫秒为单位
*/
private long readTimeout;
}

View File

@ -0,0 +1,48 @@
package com.das.modules.fdr.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.das.common.result.R;
import com.das.modules.equipment.entity.SysEquipment;
import com.das.modules.fdr.domain.FileNode;
import com.das.modules.fdr.service.FaultRecorderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 故障录波controller
*/
@Slf4j
@RequestMapping("/api/fdr")
@RestController
public class FaultRecorderController {
@Autowired
private FaultRecorderService faultRecorderService;
@RequestMapping(value = "/files", method = RequestMethod.POST)
public R<List<FileNode>> findList(@RequestBody JSONObject jsonObject) {
String code = jsonObject.getString("deviceCode");
String startTime = jsonObject.getString("startTime");
String endTime = jsonObject.getString("endTime");
List<FileNode> result = faultRecorderService.getDirOrFileList(code,startTime,endTime);
return R.success(result);
}
@RequestMapping(value = "/parseData", method = RequestMethod.POST)
public R<Map<String, List<Object>>> parseData(@RequestBody JSONObject jsonObject) throws IOException {
Map<String, List<Object>> dataCurve = faultRecorderService.getDataCurve(jsonObject.getString("url"), jsonObject.getString("deviceCode"));
return R.success(dataCurve);
}
@RequestMapping(value = "/updateFdrConfig", method = RequestMethod.POST)
public void updateFdrConfig(@RequestBody SysEquipment sysEquipment){
faultRecorderService.updateFdrConfig(sysEquipment);
}
}

View File

@ -0,0 +1,22 @@
package com.das.modules.fdr.domain;
import lombok.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileNode {
//节点名称
private String name;
// 0代表文件夹1代表文件
private int type;
private String size;
private String lastModified;
private String path;
}

View File

@ -0,0 +1,17 @@
package com.das.modules.fdr.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FdrFormatVo {
private String timeFormat;
private String delimiter;
private Integer validStartLine;
}

View File

@ -0,0 +1,28 @@
package com.das.modules.fdr.service;
import com.das.modules.equipment.entity.SysEquipment;
import com.das.modules.fdr.domain.FileNode;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public interface FaultRecorderService {
List<FileNode> getDirOrFileList(String name,String startTime, String endTime);
Map<String, List<Object>> getDataCurve(String url, String deviceCode) throws IOException;
void updateFdrConfig(SysEquipment sysEquipment);
String upload(String parent, String folderName, MultipartFile file);
void readFileToSteam(String path, OutputStream stream);
void download(String path, Path tempDir);
}

View File

@ -0,0 +1,232 @@
package com.das.modules.fdr.service;
import cn.hutool.core.io.FileUtil;
import com.das.common.constant.FileConstants;
import com.das.modules.fdr.config.MinioProperties;
import com.das.modules.fdr.domain.FileNode;
import io.micrometer.common.util.StringUtils;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@Service
@Slf4j
public class MinioViewsServcie {
@Autowired
private MinioClient minioClient;
@Autowired
private MinioProperties minioProperties;
/**
* 删除文件
*
* @param bucketName 存储桶
* @param objectName 文件名称
*/
public void removeFile(String bucketName, String objectName, Boolean recursive) throws Exception {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(true).build());
List<Result<Item>> list = StreamSupport.stream(results.spliterator(), false)
.collect(Collectors.toList());
if (list.size() >= 2 && !recursive) {
throw new IOException("请清空文件后再删除目录");
}
for (Result<Item> result : results) {
Item item = result.get();
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(item.objectName())
.build());
}
}
public Boolean deleteFileViews(String path, Boolean recursive) throws IOException {
if (StringUtils.isBlank(path)) {
throw new IOException("请确认删除文件路径");
}
Boolean success = null;
try {
path = path.substring(path.indexOf("/") + 1);
removeFile(minioProperties.getBucket(), path, recursive);
} catch (Exception e) {
throw new RuntimeException(e);
}
return success;
}
public boolean deleteFile(File file) {
return file.delete();
}
public String upload(String path, String folderName,MultipartFile file) {
String targetFile = null;
try {
// 上传一个空对象来模拟文件夹
if (!StringUtils.isBlank(folderName)){
targetFile = path + folderName + FileConstants.FILE_SEPARATOR;
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioProperties.getBucket())
.object(targetFile)
.stream(bais, 0, -1)
.build());
}
else {
targetFile= path +"/" + file.getOriginalFilename();
uploadFile(minioProperties.getBucket(), file, targetFile, "application/octet-stream");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return targetFile;
}
/**
* 使用MultipartFile进行文件上传
*
* @param bucketName 存储桶
* @param file 文件名
* @param objectName 对象名
* @param contentType 类型
* @throws Exception
*/
public void uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) throws Exception {
InputStream inputStream = file.getInputStream();
try{
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.contentType(contentType)
.stream(inputStream, inputStream.available(), -1)
.build());
}catch (Exception e){
log.error("minio文件上传失败{}", e);
}
}
//获取路径下的文件夹文件列表
public List<FileNode> getFileTree(String directoryName) {
List<FileNode> fileNodes = new ArrayList<>();
ListObjectsArgs build;
try {
if (StringUtils.isBlank(directoryName)) {
build = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).recursive(false).build();
} else {
build = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).prefix(directoryName+"/").recursive(false).build();
}
Iterable<Result<Item>> results = minioClient.listObjects(build);
for (Result<Item> result : results) {
Item item = result.get();
String itemName = item.objectName();
boolean isDir = item.isDir();
String size = FileUtil.readableFileSize(item.size());
String relativePath = null;
String[] parts = null;
if (!StringUtils.isBlank(directoryName)){
relativePath = itemName.substring(directoryName.length());
parts = relativePath.split("/");
}
else {
parts = itemName.split("/");
}
String lastModifyTime = null;
DateTimeFormatter dateFormat =DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
if (!isDir){
ZonedDateTime zonedDateTime = item.lastModified();
lastModifyTime = zonedDateTime.format(dateFormat);
}
if (parts.length > 0) {
String nodeName = parts[0];
int type = isDir ? 0 : 1;
itemName= isDir ? itemName.substring(0,itemName.lastIndexOf("/")) : itemName;
FileNode node = new FileNode(nodeName, type,size,lastModifyTime,"/"+itemName);
if (!fileNodes.contains(node)) {
fileNodes.add(node);
}
}
}
} catch (Exception e) {
log.error("minio获取树列表失败", e);
}
return fileNodes;
}
public void readFileToStream(String path, OutputStream stream) {
try ( GetObjectResponse res = minioClient.getObject(
GetObjectArgs.builder().bucket(minioProperties.getBucket()).object(path).build())){
res.transferTo(stream);
} catch (Exception e) {
log.error("minio读取文件失败", e);
}
}
public void download(String path, Path tempDir) {
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(minioProperties.getBucket())
.object(path)
.build())) {
// 保存到临时文件夹
File tempFile = tempDir.resolve(tempDir+path).toFile();
FileUtil.writeFromStream(inputStream,tempFile);
}
catch (Exception ignored){
}
}
// 递归方式 计算文件的大小
public long getTotalSizeOfFilesInDir(File file) {
if (file.isFile()) {
return file.length();
}
File[] children = file.listFiles();
long total = 0;
if (children != null) {
for (final File child : children) {
total += getTotalSizeOfFilesInDir(child);
}
}
return total;
}
public InputStream getFileStream(String url){
InputStream inputStream = null;
try {
inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minioProperties.getBucket()).object(url).build());
} catch (Exception e) {
log.error("获取文件失败");
}
return inputStream;
}
}

View File

@ -0,0 +1,195 @@
package com.das.modules.fdr.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.das.common.constant.FileConstants;
import com.das.common.exceptions.ServiceException;
import com.das.modules.equipment.entity.SysEquipment;
import com.das.modules.equipment.mapper.SysEquipmentMapper;
import com.das.modules.fdr.config.MinioProperties;
import com.das.modules.fdr.domain.FileNode;
import com.das.modules.fdr.domain.vo.FdrFormatVo;
import com.das.modules.fdr.service.FaultRecorderService;
import com.das.modules.fdr.service.MinioViewsServcie;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.common.util.StringUtils;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Path;
import java.rmi.ServerException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class FaultRecorderServiceImpl implements FaultRecorderService {
@Autowired
private MinioViewsServcie minioViewsServcie;
@Autowired
MinioClient minioClient;
@Autowired
private MinioProperties minioProperties;
@Autowired
private SysEquipmentMapper sysEquipmentMapper;
@Override
public List<FileNode> getDirOrFileList(String name, String startTime, String endTime) {
List<FileNode> fileResult = new ArrayList<>();
List<String> monthsBetween = getMonthsBetween(startTime, endTime);
for (String item : monthsBetween) {
String directoryName = name + FileConstants.FILE_SEPARATOR + item.substring(0, item.indexOf("-")) + FileConstants.FILE_SEPARATOR + item.substring(item.indexOf("-") + 1);
List<FileNode> fileTree = minioViewsServcie.getFileTree(directoryName);
fileResult.addAll(fileTree);
}
Comparator<FileNode> fileNodeComparator = Comparator.comparing(FileNode::getLastModified)
.thenComparing(FileNode::getName);
fileResult.sort(fileNodeComparator);
return fileResult;
}
@Override
public String upload(String parent, String folderName, MultipartFile file) {
return minioViewsServcie.upload(parent, folderName, file);
}
@Override
public void readFileToSteam(String path, OutputStream stream) {
minioViewsServcie.readFileToStream(path, stream);
}
@Override
public void download(String path, Path tempDir) {
minioViewsServcie.download(path, tempDir);
}
private List<String> getMonthsBetween(String startTime, String endTime) {
List<String> months = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate start = LocalDate.parse(startTime + "-01", formatter);
LocalDate end = LocalDate.parse(endTime + "-01", formatter);
DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
while (!start.isAfter(end)) {
months.add(start.format(monthFormatter));
start = start.plusMonths(1);
}
return months;
}
@Override
public Map<String, List<Object>> getDataCurve(String url, String deviceCode) throws IOException {
Map<String, List<Object>> resultMap = null;
try (InputStream fileStream = minioViewsServcie.getFileStream(url)) {
//根据device Code查询故障录波格式
QueryWrapper<SysEquipment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("CODE", deviceCode);
SysEquipment sysEquipment = sysEquipmentMapper.selectOne(queryWrapper);
if (sysEquipment == null) {
throw new ServiceException("设备不存在,请选择正确设备");
}
if (StringUtils.isBlank(sysEquipment.getFdrFormat())){
throw new ServerException("请添加设备故障录波配置");
}
FdrFormatVo fdrFormatVo = JSON.parseObject(sysEquipment.getFdrFormat(), FdrFormatVo.class);
// 解析文件内容
resultMap = parseFile(fileStream, fdrFormatVo.getTimeFormat(), fdrFormatVo.getDelimiter(), fdrFormatVo.getValidStartLine());
} catch (Exception e) {
e.printStackTrace();
}
return resultMap;
}
@Override
public void updateFdrConfig(SysEquipment sysEquipment) {
sysEquipmentMapper.updateById(sysEquipment);
}
public Map<String, List<Object>> parseFile(InputStream inputStream, String timeFormat, String delimiter, int validStartLine) {
List<List<String>> result = new ArrayList<>();
Map<String, List<Object>> stringListMap = null;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// 忽略有效行之前的行
if (lineNumber < validStartLine) {
continue;
}
// 按照分隔符分割行数据
List<String> lineData = Arrays.stream(line.split(delimiter)).toList();
result.add(lineData);
}
stringListMap = parseDataCurve(result, timeFormat);
} catch (Exception e) {
log.error("文件解析失败{}", e);
}
return stringListMap;
}
public Map<String, List<Object>> parseDataCurve(List<List<String>> data, String timeFormat) throws ParseException {
List<String> listField = data.get(0);
Map<String, List<Object>> map = new HashMap<>();
data.remove(0);
for (List<String> item : data) {
for (int i = 0; i < item.size(); i++) {
if (map.get(listField.get(i)) == null) {
if (i == 0){
List<Object> timeList = new ArrayList<>();
long timestamp = convertToTimestamp(item.get(i), timeFormat);
timeList.add(timestamp);
map.put(listField.get(i),timeList);
}else {
List<Object> valueList = new ArrayList<>();
valueList.add(Double.valueOf(item.get(i)));
map.put(listField.get(i), valueList);
}
} else {
List<Object> valueList = map.get(listField.get(i));
if (i == 0){
valueList.add(convertToTimestamp(item.get(i),timeFormat));
}
else {
valueList.add(Double.valueOf(item.get(i)));
}
}
}
}
return map;
}
public long convertToTimestamp(String time, String pattern) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.parse(time).getTime();
}
}

View File

@ -1,11 +0,0 @@
package com.das.modules.node.disruptor;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.lmax.disruptor.EventFactory;
public class MessageEventFactory implements EventFactory<TerminalMessage> {
@Override
public TerminalMessage newInstance() {
return TerminalMessage.builder().build();
}
}

View File

@ -1,57 +0,0 @@
package com.das.modules.node.disruptor;
import com.das.common.utils.SpringUtils;
import com.das.modules.node.command.BaseCommand;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.das.modules.node.handler.NodeMessageHandler;
import com.lmax.disruptor.EventHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.*;
@Slf4j
@Component
public class TerminalMessageEventHandler implements EventHandler<TerminalMessage> {
@Autowired
NodeMessageHandler nodeMessageHandler;
private ConcurrentHashMap<String, CompletableFuture<TerminalMessage>> callbackMap = new ConcurrentHashMap<>(16);
@Override
public void onEvent(TerminalMessage terminalMessage, long sequence, boolean endOfBatch) throws Exception {
// log.info("收到消息: {}", terminalMessage.toJsonString());
if (callbackMap.containsKey(terminalMessage.getCmdId())){
//如果是回调函数推送到回调函数
callbackMap.get(terminalMessage.getCmdId()).complete(terminalMessage);
} else{
String cmd = terminalMessage.getCmd();
BaseCommand commander = null;
try {
commander = SpringUtils.getBean(cmd);
} catch (Exception e) {
log.debug("当前未找到执行command容器");
}
if (commander != null) {
try {
commander.doCommand(terminalMessage);
} catch (Exception ex) {
log.error(String.format("命令 - %s 处理失败", cmd), ex);
}
} else {
log.error("命令[{}]无效, 未发现实现适配器!", cmd);
}
}
}
public void sendTerminalMessageWithResult(Long nodeId, TerminalMessage message) throws ExecutionException, InterruptedException, TimeoutException {
nodeMessageHandler.sendActionMessage(nodeId, message);
CompletableFuture<TerminalMessage> future = new CompletableFuture<>();
callbackMap.put(message.getCmdId(), future);
TerminalMessage result = future.get(10, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,32 @@
package com.das.modules.node.disruptor;
import com.das.common.utils.SpringUtils;
import com.das.modules.node.command.BaseCommand;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.lmax.disruptor.WorkHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TerminalMessageWorkerHandler implements WorkHandler<TerminalMessage> {
@Override
public void onEvent(TerminalMessage event) throws Exception {
String cmd = event.getCmd();
BaseCommand commander = null;
try {
commander = SpringUtils.getBean(cmd);
} catch (Exception e) {
log.debug("当前未找到执行command容器");
}
if (commander != null) {
try {
commander.doCommand(event);
} catch (Exception ex) {
log.error(String.format("命令 - %s 处理失败", cmd), ex);
}
} else {
log.error("命令[{}]无效, 未发现实现适配器!", cmd);
}
}
}

View File

@ -6,8 +6,8 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TerminalMessage {

View File

@ -1,150 +0,0 @@
package com.das.modules.node.handler;
import com.das.common.utils.JsonUtils;
import com.das.common.utils.SpringUtil;
import com.das.modules.node.constant.NodeConstant;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.das.modules.node.service.NodeMessageService;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.concurrent.*;
@Component
@Slf4j
public class NodeMessageHandler extends TextWebSocketHandler {
public static final Long HEARTBEAT_TIMEOUT = 1000 * 60 * 1L;
private ConcurrentHashMap<Long, ConcurrentWebSocketSessionDecorator> onlineSessions = new ConcurrentHashMap<>(16);
private NodeMessageService nodeMessageService;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String remoteIp = (String) session.getAttributes().getOrDefault(NodeConstant.REMOTE_IP, "");
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
Long version = (Long)session.getAttributes().get(NodeConstant.VERSION);
long time = System.currentTimeMillis();
log.debug("IP: {} 请求连接. sessionId: {}", remoteIp, session.getId());
if (onlineSessions.containsKey(nodeId)){
//如果终端节点已在线则拒绝新的终端连接
try {
session.close(CloseStatus.NOT_ACCEPTABLE);
}
catch (Exception ignore){}
}
else {
log.info("IP: {} 准许连接, NodeId:{}, Version: {}, sessionId: {}", remoteIp, nodeId, version, session.getId());
onlineSessions.put(nodeId, new ConcurrentWebSocketSessionDecorator(session, 5 * 1000, 2 * 1024 * 1024));
}
// 如果version是0则需要调用一次configUpdate配置更新
if (version == 0){
if (nodeMessageService == null){
nodeMessageService = SpringUtil.getBean(NodeMessageService.class);
}
nodeMessageService.sendTerminalConfig(Long.valueOf(nodeId));
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
TerminalMessage msg = JsonUtils.parseObject(message.getPayload(), TerminalMessage.class);
String nodeId = session.getAttributes().get(NodeConstant.NODE_ID).toString();
String cmd = msg.getCmd();
JsonNode data = msg.getData();
log.info("收到 Node:{} 命令: {}", nodeId, cmd);
if (nodeMessageService == null){
nodeMessageService = SpringUtil.getBean(NodeMessageService.class);
}
if (nodeMessageService != null){
nodeMessageService.pushMessage(msg);
}
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
log.info("收到 Node:{} Pong Message", nodeId);
session.getAttributes().put(NodeConstant.LAST_PONG_TIME, System.currentTimeMillis());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error(String.format("通讯异常: NodeId: %s", session.getAttributes().get(NodeConstant.NODE_ID)), exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("IP: {} 已断开连接, NodeId:{}, Version: {}, sessionId:{}, 原因: {}",
session.getAttributes().get(NodeConstant.REMOTE_IP),
session.getAttributes().get(NodeConstant.NODE_ID),
session.getAttributes().get(NodeConstant.VERSION),
session.getId(),
status.toString());
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
onlineSessions.remove(nodeId);
}
/**
* 定时发送心跳报文并清理离线的终端
*/
@Scheduled(cron = "0/15 * * * * ?")
public void sendHeartbeat(){
for (ConcurrentWebSocketSessionDecorator session : onlineSessions.values()) {
//判断心跳是否超时超时则主动断开连接
Long lastPongTime = (Long) session.getAttributes().get(NodeConstant.LAST_PONG_TIME);
if (lastPongTime != null && ((System.currentTimeMillis() - lastPongTime) > HEARTBEAT_TIMEOUT)){
closeSession(session);
return;
}
SendPingMessage(session);
}
}
private void closeSession(WebSocketSession session){
try{
session.close(CloseStatus.NO_CLOSE_FRAME);
}
catch (Exception ignore){}
}
/**
* 发送ping消息
* @param session
*/
private void SendPingMessage(ConcurrentWebSocketSessionDecorator session){
try {
session.sendMessage(new PingMessage());
}
catch (Exception ignore){}
}
/**
* 发送无返回值消息
* @param nodeId
*/
public void sendActionMessage(Long nodeId, TerminalMessage message){
ConcurrentWebSocketSessionDecorator session = onlineSessions.get(nodeId);
if (session != null){
try {
session.sendMessage(new TextMessage(message.toJsonString()));
log.info("发送的消息为:{}", message.toJsonString());
}
catch (Exception exception){
log.error(String.format("发送消息失败: NodeId: %s", nodeId), exception);
closeSession(session);
}
}
}
}

View File

@ -7,10 +7,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
@ -22,8 +20,7 @@ import java.util.Map;
* Websocket握手拦截器
*/
@Slf4j
@Component
public class NodeHandshakeInterceptor implements HandshakeInterceptor {
public class NodeWebsocketHandshakeInterceptor implements HandshakeInterceptor {
public String getRealIp(ServerHttpRequest request) {

View File

@ -1,14 +1,16 @@
package com.das.modules.node.service;
import com.das.modules.node.domain.bo.CalculateRTData;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* 节点消息处理服务
*/
public interface NodeMessageService {
void pushMessage(TerminalMessage msg);
JsonNode sendTerminalConfig(Long nodeId);
@ -19,4 +21,26 @@ public interface NodeMessageService {
void handleLowSpeed(TerminalMessage data);
void handleDeviceEvent(TerminalMessage data);
/**
* 向指定采集节点发送指令(无返回值)
* @param nodeId 节点ID
* @param message 指令
*/
void sendActionMessage(Long nodeId, TerminalMessage message);
/**
* 向指定采集节点发送指令(有返回值)
* @param nodeId 节点ID
* @param message 指令
* @return 采集节点返回的指令
*/
TerminalMessage sendTerminalMessageWithResult(Long nodeId, TerminalMessage message) throws ExecutionException, InterruptedException, TimeoutException;
/**
* 向指定采集节点发送指令(有返回值)
* @param nodeId 节点ID
* @param message 指令
* @param timeout 超时时间
* @return 采集节点返回的指令
*/
TerminalMessage sendTerminalMessageWithResult(Long nodeId, TerminalMessage message, long timeout) throws ExecutionException, InterruptedException, TimeoutException;
}

View File

@ -1,64 +1,76 @@
package com.das.modules.node.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.das.common.constant.MeasType;
import com.das.common.utils.AdminRedisTemplate;
import com.das.common.utils.JsonUtils;
import com.das.common.utils.StringUtils;
import com.das.modules.cache.domain.DeviceInfoCache;
import com.das.modules.cache.service.CacheService;
import com.das.modules.data.domain.DeviceEventInfo;
import com.das.modules.data.service.TDEngineService;
import com.das.modules.data.service.impl.DataServiceImpl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import cn.hutool.core.util.IdUtil;
import com.das.common.constant.MeasType;
import com.das.common.utils.AdminRedisTemplate;
import com.das.modules.equipment.mapper.SysIotModelMapper;
import com.das.modules.node.disruptor.MessageEventFactory;
import com.das.modules.node.disruptor.TerminalMessageEventHandler;
import com.das.modules.node.constant.NodeConstant;
import com.das.modules.node.disruptor.TerminalMessageWorkerHandler;
import com.das.modules.node.domain.bo.RTData;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.das.modules.node.domain.vo.*;
import com.das.modules.node.handler.NodeMessageHandler;
import com.das.modules.node.mapper.SysCommunicationLinkMapper;
import com.das.modules.node.mapper.SysImpTabMappingMapper;
import com.das.modules.node.service.NodeMessageService;
import com.das.modules.data.service.TDEngineService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.YieldingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import com.lmax.disruptor.util.DaemonThreadFactory;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.*;
@Slf4j
@Service
public class NodeMessageServiceImpl implements NodeMessageService {
public class NodeMessageServiceImpl extends TextWebSocketHandler implements NodeMessageService {
public static final Long HEARTBEAT_TIMEOUT = 1000 * 60 * 1L;
/**
* JSON 转换器
*/
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
/**
* websocket 会话集合
*/
private final ConcurrentHashMap<Long, ConcurrentWebSocketSessionDecorator> onlineWSSessions = new ConcurrentHashMap<>(16);
private ConcurrentHashMap<String, CompletableFuture<TerminalMessage>> responseCallback = new ConcurrentHashMap<>(16);
private Disruptor<TerminalMessage> disruptor = null;
private RingBuffer<TerminalMessage> ringBuffer = null;
@Resource
TerminalMessageEventHandler terminalMessageEventHandler;
@Resource
SysCommunicationLinkMapper sysCommunicationLinkMapper;
@Resource
SysImpTabMappingMapper sysImptabmappingMapper;
@Resource
private NodeMessageHandler nodeMessageHandler;
@Autowired
AdminRedisTemplate adminRedisTemplate;
@ -79,27 +91,27 @@ public class NodeMessageServiceImpl implements NodeMessageService {
@PostConstruct
public void init() {
//初始化高性能队列
Executor executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
MessageEventFactory factory = new MessageEventFactory();
Disruptor<TerminalMessage> disruptor = new Disruptor<>(factory, 1024 * 256, executor);
disruptor.handleEventsWith(terminalMessageEventHandler);
int cpu = Runtime.getRuntime().availableProcessors();
int bufferSize = 1024 * 4;
disruptor = new Disruptor<>(TerminalMessage::new, bufferSize, DaemonThreadFactory.INSTANCE, ProducerType.MULTI, new YieldingWaitStrategy());
//
TerminalMessageWorkerHandler[] workerHandlers = new TerminalMessageWorkerHandler[cpu];
for (int i = 0; i < cpu; i++) {
workerHandlers[i] = new TerminalMessageWorkerHandler();
}
disruptor.handleEventsWithWorkerPool(workerHandlers);
disruptor.start();
ringBuffer = disruptor.getRingBuffer();
}
@PreDestroy
public void destroy() {
if (ringBuffer != null) {
ringBuffer = null;
}
if (disruptor != null) {
disruptor.shutdown();
}
}
@Override
public void pushMessage(TerminalMessage msg) {
RingBuffer<TerminalMessage> ringBuffer = disruptor.getRingBuffer();
if (ringBuffer == null) {
return;
}
@ -212,7 +224,7 @@ public class NodeMessageServiceImpl implements NodeMessageService {
.time(time)
.data(jsonNode)
.build();
nodeMessageHandler.sendActionMessage(nodeId, configUpdate);
sendActionMessage(nodeId, configUpdate);
return jsonNode;
}
@ -347,4 +359,177 @@ public class NodeMessageServiceImpl implements NodeMessageService {
default -> null;
};
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String remoteIp = (String) session.getAttributes().getOrDefault(NodeConstant.REMOTE_IP, "");
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
Long version = (Long)session.getAttributes().get(NodeConstant.VERSION);
if (nodeId == null || version == null) {
log.warn("检测到非法连接请求, IP: {}", remoteIp);
try {
session.close(CloseStatus.NOT_ACCEPTABLE);
} catch (IOException ignored) {
}
return;
}
if (onlineWSSessions.contains(nodeId)){
log.warn("检测到同一节点连接请求,已断开. NodeId: {}, IP:{}", nodeId, remoteIp);
try {
session.close(CloseStatus.NOT_ACCEPTABLE);
} catch (IOException ignored) {
}
return;
}
ConcurrentWebSocketSessionDecorator concurrentWebSocketSessionDecorator = new ConcurrentWebSocketSessionDecorator(session, 5 * 1000, 2 * 1024 * 1024);
onlineWSSessions.put(nodeId, concurrentWebSocketSessionDecorator);
//如果采集程序的版本是0则直接下发当前配置
if (version == 0){
sendTerminalConfig(nodeId);
}
}
/**
* 收到节点Websocket报文回调函数
* @param session websocket session
* @param message 报文内容
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
TerminalMessage msg = JsonUtils.parseObject(message.getPayload(), TerminalMessage.class);
if (msg == null) {
log.warn("收到非法报文:{}", message.getPayload());
return;
}
//如果是应答报文跳过队列直接异步返回
if (responseCallback.contains(msg.getCmdId())){
responseCallback.get(msg.getCmdId()).complete(msg);
}
else{
//如果是主动请求报文加入队列等待处理
String nodeId = session.getAttributes().get(NodeConstant.NODE_ID).toString();
String cmd = msg.getCmd();
JsonNode data = msg.getData();
log.debug("收到 Node:{} WS 报文: {}", nodeId, cmd);
pushMessage(msg);
}
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
log.info("收到 Node:{} Pong Message", nodeId);
session.getAttributes().put(NodeConstant.LAST_PONG_TIME, System.currentTimeMillis());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
String remoteIp = (String) session.getAttributes().getOrDefault(NodeConstant.REMOTE_IP, "");
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
Long version = (Long)session.getAttributes().get(NodeConstant.VERSION);
log.error(String.format("IP: %s 通讯异常, NodeId:%d, Version: %d", remoteIp, nodeId, version), exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String remoteIp = (String) session.getAttributes().getOrDefault(NodeConstant.REMOTE_IP, "");
Long nodeId = (Long)session.getAttributes().get(NodeConstant.NODE_ID);
Long version = (Long)session.getAttributes().get(NodeConstant.VERSION);
log.info("IP: {} 已断开连接, NodeId:{}, Version: {}, sessionId:{}, 原因: {}",
remoteIp,
nodeId,
version,
session.getId(),
status.toString());
onlineWSSessions.remove(nodeId);
}
/**
* 定时发送心跳报文并清理离线的终端
*/
@Scheduled(cron = "0/15 * * * * ?")
public void sendHeartbeat(){
for (ConcurrentWebSocketSessionDecorator session : onlineWSSessions.values()) {
//判断心跳是否超时超时则主动断开连接
Long lastPongTime = (Long) session.getAttributes().get(NodeConstant.LAST_PONG_TIME);
if (lastPongTime != null && ((System.currentTimeMillis() - lastPongTime) > HEARTBEAT_TIMEOUT)){
closeSession(session);
return;
}
SendPingMessage(session);
}
}
/**
* 发送ping消息
* @param session
*/
private void SendPingMessage(ConcurrentWebSocketSessionDecorator session){
try {
session.sendMessage(new PingMessage());
}
catch (Exception ignore){}
}
private void closeSession(WebSocketSession session){
try{
session.close(CloseStatus.NO_CLOSE_FRAME);
}
catch (Exception ignore){}
}
/**
* 向指定采集节点发送指令(无返回值)
* @param nodeId 节点ID
* @param message 指令
*/
public void sendActionMessage(Long nodeId, TerminalMessage message){
ConcurrentWebSocketSessionDecorator session = onlineWSSessions.get(nodeId);
if (session != null){
try {
session.sendMessage(new TextMessage(message.toJsonString()));
log.info("发送的消息为:{}", message.toJsonString());
}
catch (Exception exception){
log.error(String.format("发送消息失败: NodeId: %s", nodeId), exception);
closeSession(session);
}
}
}
/**
* 向指定采集节点发送指令(无返回值)
* @param nodeId 节点ID
* @param message 指令
* @return
* @throws ExecutionException
* @throws InterruptedException
* @throws TimeoutException
*/
@Override
public TerminalMessage sendTerminalMessageWithResult(Long nodeId, TerminalMessage message) throws ExecutionException, InterruptedException, TimeoutException {
return sendTerminalMessageWithResult(nodeId,message, 10);
}
/**
* 向指定采集节点发送指令(有返回值)
* @param nodeId 节点ID
* @param message 指令
* @param timeout 超时时间
* @return
* @throws ExecutionException
* @throws InterruptedException
* @throws TimeoutException
*/
@Override
public TerminalMessage sendTerminalMessageWithResult(Long nodeId, TerminalMessage message, long timeout) throws ExecutionException, InterruptedException, TimeoutException {
sendActionMessage(nodeId, message);
CompletableFuture<TerminalMessage> future = new CompletableFuture<>();
responseCallback.put(message.getCmdId(), future);
return future.get(timeout, TimeUnit.SECONDS);
}
}

View File

@ -2,11 +2,9 @@ package com.das.modules.node.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.das.common.config.SessionUtil;
import com.das.common.constant.MeasType;
import com.das.common.exceptions.ServiceException;
import com.das.common.utils.BeanCopyUtils;
import com.das.common.utils.PageDataInfo;
import com.das.common.utils.PageQuery;
@ -19,11 +17,14 @@ import com.das.modules.equipment.domain.vo.SysIotModelServiceVo;
import com.das.modules.equipment.mapper.SysEquipmentMapper;
import com.das.modules.equipment.mapper.SysIotModelFieldMapper;
import com.das.modules.equipment.mapper.SysIotModelServiceMapper;
import com.das.modules.node.constant.NodeConstant;
import com.das.modules.node.disruptor.TerminalMessageEventHandler;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.das.modules.node.domain.dto.*;
import com.das.modules.node.domain.vo.*;
import com.das.modules.node.domain.dto.BindEquipmentInfoDto;
import com.das.modules.node.domain.dto.QueryTabMappingParamDto;
import com.das.modules.node.domain.dto.SysCommunicationLinkDto;
import com.das.modules.node.domain.dto.SysNodeDto;
import com.das.modules.node.domain.vo.EquipmentVo;
import com.das.modules.node.domain.vo.SysCommunicationLinkVo;
import com.das.modules.node.domain.vo.SysNodeVo;
import com.das.modules.node.domain.vo.SysTabMappingVo;
import com.das.modules.node.entity.SysCommunicationLink;
import com.das.modules.node.entity.SysNode;
import com.das.modules.node.entity.SysTabMapping;
@ -32,7 +33,6 @@ import com.das.modules.node.mapper.SysImpTabMappingMapper;
import com.das.modules.node.mapper.SysNodeMapper;
import com.das.modules.node.service.SysNodeService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@ -42,7 +42,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -74,9 +77,6 @@ public class SysNodeServiceImpl implements SysNodeService {
@Autowired
SysIotModelServiceMapper iotModelServiceMapper;
@Autowired
TerminalMessageEventHandler terminalMessageEventHandler;
@Override
public List<SysNodeVo> querySysNodeList() {

View File

@ -8,10 +8,10 @@ import com.das.common.exceptions.ServiceException;
import com.das.common.utils.AdminRedisTemplate;
import com.das.modules.auth.domain.vo.SysUserVo;
import com.das.modules.node.constant.NodeConstant;
import com.das.modules.node.disruptor.TerminalMessageEventHandler;
import com.das.modules.node.domain.bo.TerminalMessage;
import com.das.modules.node.domain.vo.SysNodeVo;
import com.das.modules.node.mapper.SysNodeMapper;
import com.das.modules.node.service.NodeMessageService;
import com.das.modules.operation.domain.dto.CommandInfoDto;
import com.das.modules.operation.entity.SysManualStatus;
import com.das.modules.operation.entity.SysOperationLog;
@ -49,10 +49,10 @@ public class OperationService {
private SysOperationLogMapper sysOperationLogMapper;
@Autowired
TerminalMessageEventHandler terminalMessageEventHandler;
AdminRedisTemplate adminRedisTemplate;
@Autowired
AdminRedisTemplate adminRedisTemplate;
NodeMessageService nodeMessageService;
@ -127,7 +127,7 @@ public class OperationService {
.time(time)
.data(jsonNode)
.build();
terminalMessageEventHandler.sendTerminalMessageWithResult(activeNodeId, configUpdate);
nodeMessageService.sendTerminalMessageWithResult(activeNodeId, configUpdate);
} catch (Exception e) {
throw new ServiceException("设备控制失败 "+ e);
}

View File

@ -0,0 +1,55 @@
package com.das.modules.page.controller;
import com.das.modules.page.domian.dto.TrendAnalyseDto;
import com.das.modules.page.domian.dto.TrendContrastDto;
import com.das.modules.page.service.StatisticalAnalysisService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RequestMapping("/api/page/statistical")
@RestController
public class StatisticalAnalysisController {
@Autowired
private StatisticalAnalysisService statisticalAnalysisService;
/**
* 趋势分析Excel导出
* @param param 查询条件
*/
@PostMapping("/trendAnalyseExport")
public void trendAnalyseExport(@RequestBody List<TrendAnalyseDto> param ,HttpServletRequest request, HttpServletResponse response) {
statisticalAnalysisService.trendAnalyseExport(param, request, response);
}
/**
* 功率曲线Excel导出
* @param param 查询条件
*/
@PostMapping("/powerCurveExport")
public void powerCurveExport(@RequestBody TrendAnalyseDto param ,HttpServletRequest request, HttpServletResponse response) {
statisticalAnalysisService.powerCurveExport(param, request, response);
}
/**
* 趋势对比Excel导出
* @param param 查询条件
*/
@PostMapping("/trendContrastExport")
public void trendContrastExport(@RequestBody TrendContrastDto param , HttpServletRequest request, HttpServletResponse response) {
statisticalAnalysisService.trendContrastExport(param, request, response);
}
}

View File

@ -0,0 +1,55 @@
package com.das.modules.page.domian.dto;
import com.das.modules.data.domain.SnapshotValueQueryParam;
import lombok.Data;
import java.util.List;
/**
* 时序数据查询实体
*/
@Data
public class TrendAnalyseDto
{
/**
* 开始时间
*/
private String startTime;
/**
* 结束时间
*/
private String endTime;
/**
* 间隔
*/
private String interval;
/**
* 填充模式
*/
private String fill;
/**
* 时间条件名称
*/
private String timeName;
/**
* 设备属性列表
*/
private List<SnapshotValueQueryParam> devices;
/**
* 制造商
*/
private String madeinfactory;
/**
* 模型
*/
private String model;
}

View File

@ -0,0 +1,43 @@
package com.das.modules.page.domian.dto;
import com.das.modules.data.domain.SnapshotValueQueryParam;
import lombok.Data;
import java.util.List;
/**
* 时序数据查询实体
*/
@Data
public class TrendContrastDto
{
/**
* 开始时间
*/
private String startTime;
/**
* 结束时间
*/
private String endTime;
/**
* 间隔
*/
private String interval;
/**
* 填充模式
*/
private String fill;
/**
* 设备属性列表
*/
private List<SnapshotValueQueryParam> devices;
}

View File

@ -0,0 +1,30 @@
package com.das.modules.page.service;
import com.das.modules.page.domian.dto.TrendAnalyseDto;
import com.das.modules.page.domian.dto.TrendContrastDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
public interface StatisticalAnalysisService {
/**
* 趋势分析Excel导出
* @param param 查询条件
*/
void trendAnalyseExport(List<TrendAnalyseDto> param, HttpServletRequest request, HttpServletResponse response);
/**
* 功率曲线Excel导出
* @param param 查询条件
*/
void powerCurveExport(TrendAnalyseDto param, HttpServletRequest request, HttpServletResponse response);
/**
* 趋势对比Excel导出
* @param param 查询条件
*/
void trendContrastExport(TrendContrastDto param, HttpServletRequest request, HttpServletResponse response);
}

View File

@ -69,7 +69,8 @@ public class HomeServiceImpl implements HomeService {
attributesList.add("ikwhthisday");
//是否锁定
attributesList.add("Locked");
//叶轮转速
attributesList.add("iRotorSpeed");
for (SysEquipmentVo item : sysEquipmentVos) {
//构建查询属性参数
SnapshotValueQueryParam snapshotValueQueryParam = new SnapshotValueQueryParam();

View File

@ -0,0 +1,559 @@
package com.das.modules.page.service.impl;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.das.common.exceptions.ServiceException;
import com.das.common.utils.BeanCopyUtils;
import com.das.modules.curve.domain.entity.CurveItemEntity;
import com.das.modules.curve.service.TheoreticalPowerCurveService;
import com.das.modules.data.domain.SnapshotValueQueryParam;
import com.das.modules.data.domain.TSValueQueryParam;
import com.das.modules.data.service.DataService;
import com.das.modules.equipment.entity.SysIotModelField;
import com.das.modules.equipment.mapper.SysIotModelFieldMapper;
import com.das.modules.page.domian.dto.TrendAnalyseDto;
import com.das.modules.page.domian.dto.TrendContrastDto;
import com.das.modules.page.service.StatisticalAnalysisService;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.category.DefaultCategoryDataset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class StatisticalAnalysisServiceImpl implements StatisticalAnalysisService {
@Autowired
DataService dataService;
@Autowired
private TheoreticalPowerCurveService theoreticalPowerCurveService;
@Autowired
private SysIotModelFieldMapper sysIotModelFieldMapper;
private final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
/**
* 趋势分析Excel导出
*
* @param param 查询条件
*/
@Override
public void trendAnalyseExport(List<TrendAnalyseDto> param, HttpServletRequest request, HttpServletResponse response) {
//根据条件获取历史数据
List<Map<String, Map<String, Map<String, Object>>>> mapsList = new ArrayList<>();
for (TrendAnalyseDto trendAnalyseDto : param) {
TSValueQueryParam tsValueQueryParam = new TSValueQueryParam();
BeanCopyUtils.copy(trendAnalyseDto,tsValueQueryParam);
Map<String, Map<String, Map<String, Object>>> resultMap = dataService.queryTimeSeriesValues(tsValueQueryParam);
mapsList.add(resultMap);
}
//获取Excel的列
LinkedHashMap<String, String> map = getTrendColumnName(param);
List<Map<String, Object>> dataList = new ArrayList<>();
//图表数据集
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
// 遍历数据填充Excel和图表数据集
setTrendAnalyseExcelValue(mapsList, dataList, map, dataset);
ExcelWriter writer = ExcelUtil.getWriter(RandomUtil.randomInt(100, 1000) + "statistics" + ".xlsx");
//设置Excel样式
setExcelStyle(writer, map, dataList);
// 使用JFreeChart生成折线图
createChart(dataList, writer, dataset, "趋势分析");
//下载Excel
downloadExcel(response, writer,"趋势分析");
}
/**
* 功率曲线Excel导出
*
* @param param 查询条件
*/
@Override
public void powerCurveExport(TrendAnalyseDto param, HttpServletRequest request, HttpServletResponse response) {
//获取理论数据
List<CurveItemEntity> curveItemEntitieList = theoreticalPowerCurveService.
queryCurveItemByParent(param.getMadeinfactory(), param.getModel());
//根据条件获取历史数据
TSValueQueryParam tsValueQueryParam = new TSValueQueryParam();
BeanCopyUtils.copy(param,tsValueQueryParam);
Map<String, Map<String, Map<String, Object>>> resultMap = dataService.queryTimeSeriesValues(tsValueQueryParam);
List<Map<String, Object>> dataList = new ArrayList<>();
//填充功率曲线,Excel的数据集
setPowerCurveExcelValue(resultMap, dataList, curveItemEntitieList);
//获取功率曲线的列
LinkedHashMap<String, String> map = getPowerCurveColumnName();
//获取图表数据集
DefaultCategoryDataset dataset = getDefaultCategoryDataset(dataList);
ExcelWriter writer = ExcelUtil.getWriter(RandomUtil.randomInt(100, 1000) + "statistics" + ".xlsx");
//设置Excel样式
setExcelStyle(writer, map, dataList);
//使用JFreeChart生成图表
createChart(dataList, writer, dataset, "功率曲线分析");
//下载Excel
downloadExcel(response, writer,"功率曲线分析");
}
/**
* 趋势对比Excel导出
* @param param 查询条件
* @return TD数据库数据
*/
@Override
public void trendContrastExport(TrendContrastDto param, HttpServletRequest request, HttpServletResponse response) {
//根据条件获取历史数据
TSValueQueryParam tsValueQueryParam = new TSValueQueryParam();
BeanCopyUtils.copy(param,tsValueQueryParam);
Map<String, Map<String, Map<String, Object>>> maps = dataService.queryTimeSeriesValues(tsValueQueryParam);
//自定义别名 别名的key和实体类中的名称要对应上
LinkedHashMap<String, String> map = gettrendContrastColumnName(param);
List<Map<String, Object>> dataList = new ArrayList<>();
//图表数据集
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
// 遍历数据将数据添加到dataList中
setTrendContrastExcelValue(maps, dataList, map, dataset);
ExcelWriter writer = ExcelUtil.getWriter(RandomUtil.randomInt(100, 1000) + "statistics" + ".xlsx");
//设置Excel样式
setExcelStyle(writer, map, dataList);
// 使用JFreeChart生成折线图
createChart(dataList, writer, dataset, "趋势对比");
//下载Excel
downloadExcel(response, writer,"趋势对比");
}
/**
* 获取图表数据集
*
* @param dataList 数据集
* @return 图表数据集
*/
private DefaultCategoryDataset getDefaultCategoryDataset(List<Map<String, Object>> dataList) {
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
for (Map<String, Object> mapData : dataList) {
Object time = mapData.get("time");
Object speed = mapData.get("iWindSpeed");
Object power = mapData.get("iGenPower");
Object theorySpeed = mapData.get("theoryIWindSpeed");
Object theoryPower = mapData.get("theoryIGenPower");
if (speed != null && power != null) {
dataset.addValue((Float) speed, "风速实际值", (String) time);
dataset.addValue((Float) power, "功率实际值", (String) time);
}
if (theorySpeed != null && theoryPower != null) {
dataset.addValue((Float) theorySpeed, "风速理论值", (String) time);
dataset.addValue((Float) theoryPower, "功率理论值", (String) time);
}
}
return dataset;
}
/**
* 设置Excel样式
*
* @param writer ExcelWriter
* @param map 表格的列
* @param dataList Excel数据集
*/
private void setExcelStyle(ExcelWriter writer, LinkedHashMap<String, String> map, List<Map<String, Object>> dataList) {
//自定义别名 别名的key和实体类中的名称要对应上
writer.setHeaderAlias(map);
//水平居中对齐垂直中间对齐
writer.getStyleSet().setAlign(HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
//所有单元格宽25个字符
writer.setColumnWidth(-1, 25);
// 一次性写出内容使用默认样式强制输出标题
writer.write(dataList, true);
}
/**
* 趋势分析-遍历数据填充Excel和图表数据集
*
* @param mapsList 测点历史数据
* @param dataList Excel数据集
* @param map 表格的列
* @param dataset 图表数据集
*/
private void setTrendAnalyseExcelValue(List<Map<String, Map<String, Map<String, Object>>>> mapsList,
List<Map<String, Object>> dataList,
LinkedHashMap<String, String> map, DefaultCategoryDataset dataset) {
for (int i = 0; i < mapsList.size(); i++) {
List<String> timesListstr = new ArrayList<>();
List<Double> valuesList = new ArrayList<>();
int num = i + 1;
String pointName = null;
Map<String, Map<String, Map<String, Object>>> stringMapMap = mapsList.get(i);
for (Map.Entry<String, Map<String, Map<String, Object>>> stringMapEntry : stringMapMap.entrySet()) {
for (Map.Entry<String, Map<String, Object>> mapEntry : stringMapEntry.getValue().entrySet()) {
pointName = mapEntry.getKey();
for (Map.Entry<String, Object> stringObjectEntry : mapEntry.getValue().entrySet()) {
String key1 = stringObjectEntry.getKey();
if (key1.equals("times")) {
List<Long> times = (List<Long>) stringObjectEntry.getValue();
List<String> liststr = times.stream()
.map(timestamp -> new Date(timestamp))
.map(date -> sdf.format(date))
.toList();
timesListstr.addAll(liststr);
}
if (key1.equals("values")) {
List<Double> values = (List<Double>) stringObjectEntry.getValue();
valuesList.addAll(values);
}
}
}
}
String pointNameKey = pointName + num;
String timeKey = "time" + num;
//添加图表的数据集
for (int j = 0; j < timesListstr.size(); j++) {
dataset.addValue(valuesList.get(j), map.get(pointNameKey), timesListstr.get(j));
}
if (i == 0) {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put(timeKey, timesListstr.get(j));
dataMap.put(pointNameKey, valuesList.get(j));
dataList.add(dataMap);
}
} else {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> stringObjectMap = dataList.get(j);
stringObjectMap.put(timeKey, timesListstr.get(j));
stringObjectMap.put(pointNameKey, valuesList.get(j));
}
}
}
}
/**
* 趋势对比填充Excel数据集
*
* @param dataList Excel数据集
* @param map 表格的列
* @param dataset Excel数据集
*/
private void setTrendContrastExcelValue(Map<String, Map<String, Map<String, Object>>> maps,
List<Map<String, Object>> dataList,
LinkedHashMap<String, String> map, DefaultCategoryDataset dataset) {
for (Map.Entry<String, Map<String, Map<String, Object>>> stringMapEntry : maps.entrySet()) {
int flagNum = 0;
for (Map.Entry<String, Map<String, Object>> mapEntry : stringMapEntry.getValue().entrySet()) {
List<String> timesListstr = new ArrayList<>();
List<Double> valuesList = new ArrayList<>();
String key = mapEntry.getKey();
for (Map.Entry<String, Object> stringObjectEntry : mapEntry.getValue().entrySet()) {
String key1 = stringObjectEntry.getKey();
if (key1.equals("times")) {
List<Long> times = (List<Long>) stringObjectEntry.getValue();
List<String> liststr = times.stream()
.map(timestamp -> new Date(timestamp))
.map(date -> sdf.format(date))
.toList();
timesListstr.addAll(liststr);
}
if (key1.equals("values")) {
List<Double> values = (List<Double>) stringObjectEntry.getValue();
valuesList.addAll(values);
}
}
if (flagNum == 0) {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("time", timesListstr.get(j));
dataMap.put(key, valuesList.get(j));
dataList.add(dataMap);
}
} else {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> stringObjectMap = dataList.get(j);
stringObjectMap.put(key, valuesList.get(j));
}
}
flagNum++;
//生成图表的数据集
for (int j = 0; j < timesListstr.size(); j++) {
dataset.addValue(valuesList.get(j), map.get(key), timesListstr.get(j));
}
}
}
}
/**
* 趋势-获取表格的列
* *
*
* @param param 查询条件
* @return 表格的列
*/
private LinkedHashMap<String, String> getTrendColumnName(List<TrendAnalyseDto> param) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < param.size(); i++) {
TrendAnalyseDto trendAnalyseDto = param.get(i);
String timeName = trendAnalyseDto.getTimeName();
int num = i + 1;
map.put("time" + num, "时间" + num);
for (SnapshotValueQueryParam device : trendAnalyseDto.getDevices()) {
//获取属性名称
for (String attribute : device.getAttributes()) {
map.put(attribute + num, timeName);
}
}
}
return map;
}
/**
* 趋势对比-获取表格的列
*
* @param param 查询条件
* @return 表格的列
*/
private LinkedHashMap<String, String> gettrendContrastColumnName(TrendContrastDto param) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("time", "时间");
List<String> strList = new ArrayList<>();
for (SnapshotValueQueryParam device : param.getDevices()) {
strList.addAll(device.getAttributes());
}
QueryWrapper<SysIotModelField> queryWrapper = new QueryWrapper<>();
queryWrapper.in("attribute_code", strList);
List<SysIotModelField> sysIotModelFields = sysIotModelFieldMapper.selectVoList(queryWrapper);
for (SysIotModelField sysIotModelField : sysIotModelFields) {
map.put(sysIotModelField.getAttributeCode(), sysIotModelField.getAttributeName());
}
return map;
}
/**
* 获取功率曲线的列
*
* @return
*/
private LinkedHashMap<String, String> getPowerCurveColumnName() {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("time", "时间");
map.put("iWindSpeed", "风速");
map.put("iGenPower", "功率");
map.put("theoryIWindSpeed", "理论风速");
map.put("theoryIGenPower", "理论功率");
return map;
}
/**
* 功率曲线-遍历数据填充Excel数据集
*
* @param maps 测点历史数据
* @param dataList Excel数据集
* @param curveItemEntitieList 理论数据
*/
private void setPowerCurveExcelValue(Map<String, Map<String, Map<String, Object>>> maps,
List<Map<String, Object>> dataList, List<CurveItemEntity> curveItemEntitieList) {
for (Map.Entry<String, Map<String, Map<String, Object>>> stringMapEntry : maps.entrySet()) {
int flagNum = 0;
for (Map.Entry<String, Map<String, Object>> mapEntry : stringMapEntry.getValue().entrySet()) {
List<String> timesListstr = new ArrayList<>();
List<Double> valuesList = new ArrayList<>();
String key = mapEntry.getKey();
for (Map.Entry<String, Object> stringObjectEntry : mapEntry.getValue().entrySet()) {
String key1 = stringObjectEntry.getKey();
if (key1.equals("times")) {
List<Long> times = (List<Long>) stringObjectEntry.getValue();
List<String> liststr = times.stream()
.map(timestamp -> new Date(timestamp))
.map(date -> sdf.format(date))
.toList();
timesListstr.addAll(liststr);
}
if (key1.equals("values")) {
List<Double> values = (List<Double>) stringObjectEntry.getValue();
valuesList.addAll(values);
}
}
if (flagNum == 0) {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("time", timesListstr.get(j));
dataMap.put(key, valuesList.get(j));
dataList.add(dataMap);
}
} else {
for (int j = 0; j < timesListstr.size(); j++) {
Map<String, Object> stringObjectMap = dataList.get(j);
stringObjectMap.put(key, valuesList.get(j));
}
}
flagNum++;
}
}
//转换为map
List<Map<String, Object>> listMap = curveItemEntitieList.stream()
.map(entity -> {
Map<String, Object> map = new HashMap<>();
map.put("theoryIWindSpeed", entity.getPower());
map.put("theoryIGenPower", entity.getSpeed());
return map;
})
.collect(Collectors.toList());
if (listMap.size() > dataList.size()) {
for (int i = 0; i < listMap.size(); i++) {
Map<String, Object> stringObjectMap1 = listMap.get(i);
if (dataList.size() > i) {
Map<String, Object> stringObjectMap = dataList.get(i);
stringObjectMap.put("theoryIWindSpeed", stringObjectMap1.get("theoryIWindSpeed"));
stringObjectMap.put("theoryIGenPower", stringObjectMap1.get("theoryIGenPower"));
listMap.remove(i);
}
}
dataList.addAll(listMap);
} else {
for (int i = 0; i < dataList.size(); i++) {
Map<String, Object> dataMap = dataList.get(i);
if (listMap.size() > i) {
Map<String, Object> stringObjectMap1 = listMap.get(i);
dataMap.put("theoryIWindSpeed", stringObjectMap1.get("theoryIWindSpeed"));
dataMap.put("theoryIGenPower", stringObjectMap1.get("theoryIGenPower"));
}
}
}
}
/**
* 下载Excel
*
* @param response 响应对象
* @param writer Excel对象
*/
private void downloadExcel(HttpServletResponse response, ExcelWriter writer,String title) {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/vnd.ms-excel");
// 清除缓存
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
try {
// 设置请求头属性
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(title+".xls", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
// 写出到文件
writer.flush(out, true);
// 关闭writer释放内存
writer.close();
// 此处记得关闭输出Servlet流
IoUtil.close(out);
} catch (Exception e) {
throw new ServiceException("文件下载失败==" + e);
}
}
/**
* 使用JFreeChart生成折线图
*
* @param data excel数据集
* @param writer excel对象
* @param dataset 图表数据集
* @param titleStr 标题
*/
private void createChart(List<Map<String, Object>> data, ExcelWriter writer,
DefaultCategoryDataset dataset, String titleStr) {
// 获取Sheet对象
XSSFSheet xssfSheet = (XSSFSheet) writer.getSheet();
Workbook workbook = writer.getWorkbook();
JFreeChart chart = ChartFactory.createLineChart(
titleStr, // 图表标题
"", // 横轴标签
"", // 纵轴标签
dataset, // 数据集
PlotOrientation.VERTICAL, // 图表方向
true, // 是否显示图例
true, // 是否使用工具提示
false // 是否生成URL链接
);
// 设置图表标题的字体
TextTitle title = chart.getTitle();
title.setFont(new java.awt.Font("SimSun", java.awt.Font.BOLD, 16));
// 获取图表的绘图区域
CategoryPlot plot = chart.getCategoryPlot();
// 设置横轴标签
CategoryAxis domainAxis = plot.getDomainAxis();
domainAxis.setLabelFont(new java.awt.Font("SimSun", java.awt.Font.PLAIN, 12));
domainAxis.setMaximumCategoryLabelLines(1); // 可以控制标签行数
domainAxis.setCategoryMargin(3); // 控制类别之间的间距
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45); // 旋转横轴标签为45度,90度为:UP_90
domainAxis.setLabelFont(new Font("SansSerif", Font.PLAIN, 7));//调整字体大小
// if (data.size() > 50) {
// domainAxis.setVisible(false); // 隐藏横坐标
// }
// 设置图例的字体
LegendTitle legend = chart.getLegend();
if (legend != null) {
legend.setItemFont(new java.awt.Font("SimSun", java.awt.Font.PLAIN, 12));
}
// 设置绘图区域的背景颜色
plot.setBackgroundPaint(Color.WHITE);
// 设置绘图区域的边框
plot.setOutlinePaint(Color.LIGHT_GRAY);
plot.setOutlineVisible(true);
// 设置网格线的颜色
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
// 设置线条的宽度
CategoryItemRenderer renderer1 = plot.getRenderer();
renderer1.setSeriesStroke(0, new BasicStroke(2.0f)); // 设置线条宽度
// 将图表保存为 PNG 文件
String chartFilePath = "lineChart.png";
// 调整图表尺寸
int width = 750;
int height = 400;
try {
ChartUtils.saveChartAsPNG(new File(chartFilePath), chart, width, height);
byte[] bytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(chartFilePath));
int pictureIdx = workbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG);
// 计算数据的最后一行,创建图表插入位置的锚点
XSSFClientAnchor anchor = new XSSFClientAnchor(0, 1, 0, 1, 0, data.size() + 2, 15, data.size() + 30);
XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();
drawing.createPicture(anchor, pictureIdx);
} catch (IOException e) {
throw new ServiceException("图表保存失败==" + e);
}
}
}

View File

@ -98,4 +98,10 @@ logging:
tdengine:
password: taosdata
url: jdbc:TAOS-RS://192.168.109.160:6041/das
username: root
username: root
minio:
url: http://192.168.109.187:9000
bucket: das
accessKey: das
secretKey: zaq12WSX

View File

@ -36,49 +36,85 @@ export const runAirBlowerReq = (
})
}
export const getReportTemplateListReq = (data: { category: '单机报表' | '多机报表', pageNum: number, pageSize: number }) => {
return createAxios<never, Promise<{
code: number,
msg: string,
data: {
total: number,
rows: { id: string, category: '单机报表' | '多机报表', template: string }[]
code: number,
export const getReportTemplateListReq = (data: { category: '单机报表' | '多机报表'; pageNum: number; pageSize: number }) => {
return createAxios<
never,
Promise<{
code: number
msg: string
},
success: boolean
}>>({
data: {
total: number
rows: { id: string; category: '单机报表' | '多机报表'; template: string }[]
code: number
msg: string
}
success: boolean
}>
>({
url: '/api/page/report/template/getList',
method: 'post',
data
data,
})
}
export const addReportTemplateListReq = (data: { category: '单机报表' | '多机报表', template: string }) => {
return createAxios<never, Promise<{
code: number,
msg: string,
data: {
id: string,
category: '单机报表' | '多机报表',
template: string
}[],
success: boolean
}>>({
export const addReportTemplateListReq = (data: { category: '单机报表' | '多机报表'; template: string }) => {
return createAxios<
never,
Promise<{
code: number
msg: string
data: {
id: string
category: '单机报表' | '多机报表'
template: string
}[]
success: boolean
}>
>({
url: '/api/page/report/template/add',
method: 'post',
data
data,
})
}
export const delReportTemplateListReq = (data: { id: string }) => {
return createAxios<never, Promise<{
code: number,
msg: string,
success: boolean
}>>({
return createAxios<
never,
Promise<{
code: number
msg: string
success: boolean
}>
>({
url: '/api/page/report/template/del',
method: 'post',
data
data,
})
}
export function powerCurveExport(params: object = {}) {
return createAxios({
url: '/api/page/statistical/powerCurveExport',
method: 'POST',
data: params,
responseType: 'blob',
})
}
export function trendContrastExport(params: object = {}) {
return createAxios({
url: '/api/page/statistical/trendContrastExport',
method: 'POST',
data: params,
responseType: 'blob',
})
}
export function trendAnalyseExport(params: object = {}) {
return createAxios({
url: '/api/page/statistical/trendAnalyseExport',
method: 'POST',
data: params,
responseType: 'blob',
})
}

View File

@ -18,7 +18,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import Logo from '/@/layouts/backend/components/logo.vue'
import MenuVertical from '/@/layouts/backend/components/menus/menuVertical.vue'
import MenuVerticalChildren from '/@/layouts/backend/components/menus/menuVerticalChildren.vue'
@ -32,7 +32,7 @@ defineOptions({
const config = useConfig()
const navTabs = useNavTabs()
const menuWidth = computed(() => config.menuWidth())
const menuWidth = ref(config.menuWidth())
const onMenuCollapse = () => {
if (config.layout.menuCollapse) {
@ -40,6 +40,19 @@ const onMenuCollapse = () => {
} else {
config.setLayout('menuCollapse', true)
}
resizeHandler()
}
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
menuWidth.value = config.menuWidth()
}
</script>

View File

@ -23,9 +23,24 @@ import { closeShade } from '/@/utils/pageShade'
import { Session } from '/@/utils/storage'
import { BEFORE_RESIZE_LAYOUT } from '/@/stores/constant/cacheKey'
import { setNavTabsWidth } from '/@/utils/layout'
import { ref, onMounted, onUnmounted } from 'vue'
const config = useConfig()
// const siteConfig = useSiteConfig()
const headerHeight = ref(config.headerHeight())
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
headerHeight.value = config.headerHeight()
}
const onMenuCollapse = function () {
if (config.layout.shrink && !config.layout.menuCollapse) {
@ -49,7 +64,7 @@ const onMenuCollapse = function () {
<style scoped lang="scss">
.layout-logo {
width: 100%;
height: v-bind('config.headerHeight()');
height: v-bind(headerHeight);
display: flex;
align-items: center;
justify-content: center;

View File

@ -25,13 +25,27 @@ import type { RouteRecordRaw } from 'vue-router'
import { getFirstRoute, onClickMenu } from '/@/utils/router'
import { ElNotification } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const { t } = useI18n()
const config = useConfig()
const padding = computed(() => (window.screen.width < 1920 ? '10px' : '20px'))
const height = computed(() => (window.screen.width < 1920 ? '40px' : '56px'))
const padding = ref(window.screen.width < 1920 ? '10px' : '20px')
const height = ref(window.screen.width < 1920 ? '40px' : '56px')
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
padding.value = window.screen.width < 1920 ? '10px' : '20px'
height.value = window.screen.width < 1920 ? '40px' : '56px'
}
interface Props {
menus: RouteRecordRaw[]

View File

@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive } from 'vue'
import { ref, computed, nextTick, onMounted, reactive, onUnmounted } from 'vue'
import MenuTree from '/@/layouts/backend/components/menus/menuTree.vue'
import { useRoute, onBeforeRouteUpdate, type RouteLocationNormalizedLoaded } from 'vue-router'
import { layoutMenuRef, layoutMenuScrollbarRef } from '/@/stores/refs'
@ -29,10 +29,21 @@ const state = reactive({
defaultActive: '',
})
const width = computed(() => {
return config.layout.menuCollapse ? '5px' : window.screen.width < 1920 ? '10px' : '20px'
const width = ref(config.layout.menuCollapse ? '5px' : window.screen.width < 1920 ? '10px' : '20px')
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
width.value = config.layout.menuCollapse ? '5px' : window.screen.width < 1920 ? '10px' : '20px'
}
const verticalMenusScrollbarHeight = computed(() => {
let menuTopBarHeight = 0
if (config.layout.menuShowTopBar) {
@ -62,6 +73,7 @@ const verticalMenusScroll = () => {
onMounted(() => {
currentRouteActive(route)
verticalMenusScroll()
window.addEventListener('resize', resizeHandler)
})
onBeforeRouteUpdate((to) => {

View File

@ -15,11 +15,12 @@ import NavTabs from '/@/layouts/backend/components/navBar/tabs.vue'
import { layoutNavTabsRef } from '/@/stores/refs'
import NavMenus from '../navMenus.vue'
import { showShade } from '/@/utils/pageShade'
import { computed } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const config = useConfig()
const padding = computed(() => (window.screen.width < 1920 ? '10px' : '20px'))
const height = computed(() => (window.screen.width < 1920 ? '36px' : '48px'))
const padding = ref(window.screen.width < 1920 ? '10px' : '20px')
const height = ref(window.screen.width < 1920 ? '36px' : '48px')
const headerHeight = ref(config.headerHeight())
const onMenuCollapse = () => {
showShade('ba-aside-menu-shade', () => {
@ -27,12 +28,27 @@ const onMenuCollapse = () => {
})
config.setLayout('menuCollapse', false)
}
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
padding.value = window.screen.width < 1920 ? '10px' : '20px'
height.value = window.screen.width < 1920 ? '36px' : '48px'
headerHeight.value = config.headerHeight()
}
</script>
<style scoped lang="scss">
.nav-bar {
display: flex;
height: v-bind('config.headerHeight()');
height: v-bind(headerHeight);
width: 100%;
background-color: v-bind('config.getColorVal("headerBarBackground")');
:deep(.nav-tabs) {

View File

@ -24,14 +24,31 @@ import CloseFullScreen from '/@/layouts/backend/components/closeFullScreen.vue'
import { useNavTabs } from '/@/stores/navTabs'
import { useSiteConfig } from '/@/stores/siteConfig'
import { useConfig } from '/@/stores/config'
import { computed } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const siteConfig = useSiteConfig()
const navTabs = useNavTabs()
const config = useConfig()
const padding = computed(() => (window.screen.width < 1920 ? '0px' : '10px'))
const padding = ref(window.screen.width < 1920 ? '0px' : '10px')
const bodyPadding = computed(() => (window.screen.width < 1920 ? '10px' : '20px'))
const bodyPadding = ref(window.screen.width < 1920 ? '10px' : '20px')
const headerHeight = ref(config.headerHeight())
onMounted(() => {
//
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
//
window.removeEventListener('resize', resizeHandler)
})
const resizeHandler = () => {
padding.value = window.screen.width < 1920 ? '0px' : '10px'
bodyPadding.value = window.screen.width < 1920 ? '10px' : '20px'
headerHeight.value = config.headerHeight()
}
</script>
<style scoped>
@ -48,7 +65,7 @@ const bodyPadding = computed(() => (window.screen.width < 1920 ? '10px' : '20px'
.layout-logo {
position: absolute;
width: 100%;
height: v-bind('config.headerHeight()');
height: v-bind(headerHeight);
display: flex;
align-items: center;
box-sizing: border-box;

View File

@ -26,7 +26,7 @@
<div class="windBlower" ref="windBlower">
<el-row :gutter="10">
<el-col :span="6">
<el-col :md="24" :lg="6" :span="6">
<div class="cardContentLeft">
<!--实时预览-->
<div class="overview">
@ -92,7 +92,7 @@
</div>
</el-col>
<el-col :span="12">
<el-col :md="24" :lg="12" :span="12">
<div class="cardContentCenter">
<!--风机控制-->
<div class="controlBackgroundImg">
@ -178,7 +178,7 @@
</div>
</div>
</el-col>
<el-col :span="6">
<el-col :md="24" :lg="6" :span="6" style="background: #f5f5f5">
<div class="cardContentRight">
<!--发电量概况-->
<div class="summarize">

View File

@ -1,14 +1,14 @@
<template>
<div class="default-main">
<div class="default-main" ref="HomeHight">
<el-row :gutter="20">
<el-col :span="6">
<el-col :md="24" :lg="6" :span="6">
<div class="grid-content ep-bg-purple">
<!--风场概览-->
<div class="overview panelBg">
<el-text class="mx-1 homelabel">风场概览</el-text>
<el-row :gutter="10">
<el-col :span="12">
<div :sm="12" :lg="6" class="small-panel" style="margin-bottom: 10px">
<el-col :md="24" :lg="12" :span="12">
<div :sm="24" :lg="6" class="small-panel" style="margin-bottom: 10px">
<img class="small-panel-pic" src="~assets/dashboard/viewP.png" alt="" />
<div class="small-base">
<div>
@ -17,7 +17,7 @@
<div>功率</div>
</div>
</div>
<div :sm="12" :lg="6" class="small-panel">
<div :sm="24" :lg="6" class="small-panel">
<img class="small-panel-pic" src="~assets/dashboard/viewW.png" alt="" />
<div class="small-base">
<div>
@ -27,8 +27,8 @@
</div>
</div>
</el-col>
<el-col :span="12">
<div :sm="12" :lg="6" class="small-panel" style="margin-bottom: 10px">
<el-col :md="24" :lg="12" :span="12">
<div :sm="24" :lg="6" class="small-panel" style="margin-bottom: 10px">
<img class="small-panel-pic" src="~assets/dashboard/viewR.png" alt="" />
<div class="small-base">
<div>
@ -37,7 +37,7 @@
<div>日利用小时</div>
</div>
</div>
<div :sm="12" :lg="6" class="small-panel">
<div :sm="24" :lg="6" class="small-panel">
<img class="small-panel-pic" src="~assets/dashboard/viewY.png" alt="" />
<div class="small-base">
<div>
@ -55,7 +55,7 @@
<el-text class="mx-1 homelabel" style="margin-bottom: 0">今日运行状态</el-text>
<el-row :gutter="10" class="statusrow" style="margin-bottom: 0">
<el-col :span="12">
<div class="status-panel">
<div class="status-panel toal-panel">
<img class="status-panel-pic" src="~assets/dashboard/status1.png" alt="" />
<div class="status-base-main">
<div>
@ -66,7 +66,7 @@
</div>
</el-col>
<el-col :span="12">
<div class="status-panel">
<div class="status-panel toal-panel">
<img class="status-panel-pic" src="~assets/dashboard/status2.png" alt="" />
<div class="status-base-main">
<div>
@ -199,95 +199,80 @@
<!--功率趋势-->
<div class="power panelBg" style="margin-bottom: 0; padding-bottom: 10px">
<el-text class="mx-1 homelabel">功率趋势</el-text>
<el-row :gutter="10">
<el-col class="lg-mb-20" :span="24">
<div class="power-chart" ref="powerChartRef"></div>
</el-col>
</el-row>
<div class="cardLabel">功率趋势</div>
<div class="chartBox">
<div class="power-chart" ref="powerChartRef"></div>
</div>
</div>
</div>
</el-col>
<el-col :span="12" class="col-center">
<el-col :md="24" :lg="12" :span="12" class="col-center">
<div class="grid-content ep-bg-purple-light">
<!--风机矩阵-->
<div class="matrix panelBg">
<el-text class="mx-1 homelabel">风机矩阵</el-text>
<WindContent :parentData="FanList" @StatusListData="StatusListData"></WindContent>
<el-scrollbar>
<WindContent :parentData="FanList" @StatusListData="StatusListData"></WindContent>
</el-scrollbar>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content ep-bg-purple">
<el-col :md="24" :lg="6" :span="6">
<div class="grid-content ep-bg-purple cardContentRight">
<!--发电量概况-->
<div class="summarize panelBg">
<div class="summarize-title">
<el-text class="mx-1 homelabel">发电量概况</el-text>
<!-- <el-text class="mx-1" style="margin-bottom: 20px;">
当日发电量
<span class="content-number" style="color: #0277B3;">{{realData.attributeMap.windfarmdayprodenergy}}</span>
kWh
</el-text>-->
<div class="summarize">
<div class="cardLabel">发电量概况</div>
<div class="summarize-panel-list">
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl1.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmdayprodenergy }}</span>
</div>
<div>kWh</div>
<div>日发电量</div>
</div>
</div>
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl2.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmmonthprodenergy }}</span>
</div>
<div>kWh</div>
<div>月发电量</div>
</div>
</div>
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl3.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmyearprodenergy }}</span>
</div>
<div>kWh</div>
<div>年发电量</div>
</div>
</div>
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl4.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmtotalprodenergy }}</span>
</div>
<div>kWh</div>
<div>总发电量</div>
</div>
</div>
</div>
<el-row :gutter="5">
<el-col :span="6">
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl1.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmdayprodenergy }}</span>
</div>
<div><span>kWh</span></div>
<div><span>日发电量</span></div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl2.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmmonthprodenergy }}</span>
</div>
<div><span>kWh</span></div>
<div><span>月发电量</span></div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl3.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmyearprodenergy }}</span>
</div>
<div><span>kWh</span></div>
<div><span>年发电量</span></div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="summarize-panel">
<div class="summarize-panel-pic">
<img src="~assets/dashboard/fdl4.png" alt="" />
</div>
<div class="summarize-panel-base">
<div>
<span class="content-number">{{ realData.attributeMap.windfarmtotalprodenergy }}</span>
</div>
<div><span>kWh</span></div>
<div><span>总发电量</span></div>
</div>
</div>
</el-col>
</el-row>
</div>
<!--发电量趋势-->
@ -749,7 +734,7 @@ const inittrendChart = (type: 'day' | 'month') => {
grid: {
top: 30,
right: 10,
bottom: 20,
bottom: 30,
left: 25,
borderColor: '#dadada',
},
@ -1138,24 +1123,6 @@ const tabhandleClick = () => {
inittrendChart(trendChartType.value)
})
}
onMounted(() => {
getAllChartData()
createScroll()
overviewList()
StatusListData()
autoUpdate()
equipList({ objectType: 10002 }).then((res) => {
res.data.map((item: any) => {
deviceCode.value.push(item.name)
})
getTableData(deviceCode.value)
})
useEventListener(window, 'resize', echartsResize)
})
const activeName = ref('first')
//let autoUpdateTimer: any = null
let autoUpdateForSecondTimer: any = null
@ -1181,8 +1148,45 @@ const autoUpdate = () => {
}, 60000 * 30)
}
}
const HomeHight = ref()
const computedHeight = reactive({
powerHeight: '298px',
centerHeight: '1100px',
alarmHeight: '350px',
})
const sizeChange = () => {
const rect = HomeHight.value?.getBoundingClientRect()
if (!rect) return
computedHeight.powerHeight = rect.height - 636 + 'px'
computedHeight.alarmHeight = rect.height - 580 + 'px'
computedHeight.centerHeight = rect.height - 0 + 'px'
if (window.screen.width < 1360) {
computedHeight.alarmHeight = '200px'
computedHeight.powerHeight = '200px'
}
}
onMounted(() => {
window.addEventListener('resize', sizeChange)
sizeChange()
getAllChartData()
createScroll()
overviewList()
StatusListData()
autoUpdate()
equipList({ objectType: 10002 }).then((res) => {
res.data.map((item: any) => {
deviceCode.value.push(item.name)
})
getTableData(deviceCode.value)
})
useEventListener(window, 'resize', echartsResize)
})
onUnmounted(() => {
window.removeEventListener('resize', sizeChange)
clearInterval(timer)
autoUpdateForSecondTimer && clearInterval(autoUpdateForSecondTimer)
const chartKeys = Object.keys(state.charts) as Array<keyof typeof state.charts>
@ -1193,32 +1197,36 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@media screen and (max-width: 1910px) {
.default-main {
.content-number {
font-size: 16px !important;
}
.homelabel {
font-size: 16px !important;
}
.grid-content {
.panelBg {
padding: 10px !important;
margin-bottom: 10px !important;
}
}
.col-center {
padding: 0px !important;
}
$marginNum: 10px;
$labelHeight: 38px;
@mixin cardDefaultStyle {
margin-top: $marginNum;
margin-bottom: $marginNum;
padding: 10px;
border-radius: 10px;
background-color: #fff;
}
@mixin cardlabel {
.cardLabel {
width: 100%;
height: $labelHeight;
font-size: 18px;
line-height: 18px;
font-weight: 600;
color: #4e5969;
line-height: 38px;
padding-left: 10px;
}
}
.default-main {
height: 100%;
width: 100%;
// height: 100%;
// min-height: 920px;
padding: 0;
margin: 0;
color: #4e5969;
background-color: #f2f3f5;
overflow-x: hidden;
overflow: hidden;
.content-number {
color: #333333;
font-size: 20px;
@ -1234,6 +1242,7 @@ onUnmounted(() => {
display: block;
}
.grid-content {
/* overflow-x: hidden;*/
width: 100%;
height: 100%;
.panelBg {
@ -1243,6 +1252,9 @@ onUnmounted(() => {
margin-bottom: 20px;
}
.overview {
/* @include cardDefaultStyle;
@include cardlabel;*/
min-height: 216px;
.small-panel {
display: flex;
border: 1px solid #e1edf6;
@ -1253,12 +1265,16 @@ onUnmounted(() => {
height: 36px;
}
.small-base {
word-break: keep-all;
margin-left: 10px;
color: #2c3f5d;
}
}
}
.status {
/* @include cardDefaultStyle;
@include cardlabel;*/
min-height: 374px;
.statusrow {
margin-bottom: 10px;
}
@ -1276,6 +1292,7 @@ onUnmounted(() => {
height: 60px;
}
.status-base-main {
word-break: keep-all;
margin-left: 10px;
line-height: 23px;
padding-top: 6px;
@ -1295,46 +1312,79 @@ onUnmounted(() => {
}
}
.power {
.power-chart {
width: 100%;
height: 288px;
}
@media screen and (max-width: 1920px) {
@include cardDefaultStyle;
@include cardlabel;
width: 100%;
min-height: 298px;
height: v-bind('computedHeight.powerHeight');
.chartBox {
height: calc(100% - $labelHeight);
.power-chart {
height: 230px;
}
}
@media screen and (max-width: 1910px) {
.power-chart {
height: 230px;
width: 100%;
height: 100%;
}
}
}
.matrix {
/* @include cardDefaultStyle;
@include cardlabel;*/
background: url('/@/assets/dashboard/bg1.png') no-repeat #ffffff;
background-size: 100% 100%;
height: 100%;
min-height: 900px;
width: 100%;
height: v-bind('computedHeight.centerHeight');
margin-bottom: 0;
}
.summarize {
.summarize-title {
padding: 10px;
border-radius: 10px;
background-color: #fff;
margin-bottom: 20px;
word-break: keep-all;
/*@include cardDefaultStyle;*/
@include cardlabel;
min-height: 224px;
.summarize-panel-list {
width: 100%;
display: flex;
justify-content: space-between;
}
.summarize-panel {
background: #f0f6ff;
border-radius: 10px;
margin: 5px;
padding: 10px;
.summarize-panel-pic {
text-align: center;
}
width: 25%;
display: flex;
padding: 20px 0;
flex-direction: column;
align-items: center;
background-color: #f0f6ff;
border-radius: 10px;
.summarize-panel-base {
text-align: center;
width: 100%;
flex-direction: column;
align-items: center;
div {
display: flex;
justify-content: center;
font-size: 14px;
color: rgb(78, 89, 105);
.content-number {
color: #333333;
font-size: 20px;
}
}
}
}
}
.cardContentRight {
width: 100%;
height: 100%;
}
.trend {
height: 370px;
/* @include cardDefaultStyle;
@include cardlabel;*/
min-height: 300px;
height: 335px;
overflow: hidden;
.trend-tabs {
:deep(.el-tabs__item) {
@ -1375,40 +1425,100 @@ onUnmounted(() => {
}
}
.realPart {
height: 370px;
}
@media screen and (max-width: 1920px) {
/* .matrix {
height: 928px;
}*/
.realPart {
height: 349px;
}
.trend {
height: 315px;
}
}
@media screen and (max-width: 1910px) {
.matrix {
height: 875px;
}
.realPart {
height: 343px;
}
.trend {
height: 315px;
}
/* @include cardDefaultStyle;
@include cardlabel;*/
/*height: 370px;*/
min-height: 350px;
height: v-bind('computedHeight.alarmHeight');
}
}
}
@media screen and (max-width: 1910px) {
.default-main {
height: 878px;
}
}
@media screen and (max-width: 1920px) {
.default-main {
height: 928px;
.trend {
height: 315px !important;
}
}
}
@media screen and (max-width: 1280px) {
.default-main {
overflow: none;
.summarize {
.summarize-panel {
margin: 2px !important;
}
}
}
}
@media screen and (max-width: 1360px) {
.default-main {
.overview {
.small-panel {
.small-base {
margin-left: 0px !important;
}
}
}
}
}
@media screen and (max-width: 1480px) {
.default-main {
font-size: 12px !important;
.overview {
.small-panel {
padding: 13px 0px !important;
}
}
.status {
.status-panel {
.status-base-main {
margin-left: 5px !important;
}
}
}
}
}
@media screen and (max-width: 1680px) {
.default-main {
/*font-size: 12px !important;*/
.content-number {
font-size: 16px !important;
}
.homelabel {
font-size: 16px !important;
margin-bottom: 10px !important;
}
.grid-content {
.panelBg {
/* padding: 10px !important;*/
margin-bottom: 10px !important;
}
.summarize {
margin-bottom: 10px !important;
}
}
.toal-panel {
padding: 0 !important;
padding-bottom: 10px !important;
}
.col-center {
padding: 0 !important;
}
.matrix {
height: 908px !important;
}
:deep(.el-tabs__header) {
margin-top: -33px !important;
}
.overview {
.small-panel {
.small-base {
margin-left: 5px !important;
}
}
}
}
}
</style>

View File

@ -68,7 +68,7 @@
</template>
<script setup lang="ts">
import {reactive,defineProps, defineEmits} from 'vue'
import {ref,reactive,defineProps, defineEmits} from 'vue'
import { useRouter } from 'vue-router'
import { adminBaseRoutePath } from '/@/router/static/adminBase'
@ -148,152 +148,155 @@ const handleDoubleClick = (row) => {
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
height: 30px;
vertical-align: middle;
}
.FanList-panel{
border-radius: 8px;
cursor: pointer;
.fanlist-top{
display: flex;
width: 100%;
}
.fanlist-icon{
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
vertical-align: middle;
}
.fanlist-name{
width: 100%;
margin-top: 5px;
}
.tag-panel{
border-radius: 0 8px 0 0;
line-height: 20px;
}
.is-primary{
background: rgba(2,119,179,0.20);
border: 1px solid #0277B3;
color: #0277B3;
}
.is-success{
background: rgba(6,180,41,0.20);
border: 1px solid #06B429;
color: #06B429;
}
.is-info{
background: rgba(48,89,236,0.20);
border: 1px solid #3059EC;
color: #3059EC;
}
.is-warning{
background: rgba(255,126,0,0.20);
border: 1px solid #FF7E00;
color: #FF7E00;
}
.is-danger{
background: rgba(254,55,49,0.20);
border: 1px solid #FE3731;
color: #FE3731;
}
.is-offline{
background: rgba(153,153,153,0.20);
border: 1px solid #999999;
color: #999999;
}
.is-maintenance{
background: rgba(0,160,150,0.20);
border: 1px solid #00A096;
color: #00A096;
}
.fanlist-main{
width: 100%;
display: flex;
/*padding: 10px;*/
padding: 10px 10px 0 10px;
text-align: center;
.fanlist-pic{
.FanList-content{
overflow-x: hidden;
.FanList-panel{
border-radius: 8px;
cursor: pointer;
.fanlist-top{
display: flex;
width: 100%;
}
.fanlist-icon{
display: flex;
justify-content: center;
align-items: center;
margin-top: -10px;
.mask{
width: 43px;
height: 38px;
.heart {
position: absolute;
text-align: center;
top: 5px;
left: 17px;
z-index: 3;
}
.leafs {
z-index: 1;
position: absolute;
/* animation: leafRotate 1s infinite linear;*/
transform-origin: center center;
width: 46px;
height: 46px;
/*animation-duration: 1s;*/
animation-name: leafRotate;
animation-iteration-count: infinite;
animation-timing-function: linear;
.leaf_1 {
width: 9px;
height: 19px;
position: absolute;
left: 17px;
top: -1px;
/* transform-origin: left top;*/
}
.leaf_2 {
width: 9px;
height: 19px;
position: absolute;
left: 31px;
top: 20px;
/* transform-origin: left top;*/
transform: rotate(120deg);
}
.leaf_3 {
width: 9px;
height: 19px;
position: absolute;
left: 5px;
top: 22px;
/*transform-origin: left top;*/
transform: rotate(240deg);
}
}
}
width: 40px;
height: 30px;
vertical-align: middle;
}
.fanlist-text{
margin-top:20px;
display: flex;
flex-direction: column;
.content-number{
color: #333333;
font-size: 20px;
}
.fanlist-name{
width: 100%;
margin-top: 5px;
}
.fanlist-text span{
display: inline-block;
}
}
.fanlist-bottom{
display: flex;
justify-content: end;
height: 24px;
.tag-panel{
border-radius: 0 0 8px 0;
border-radius: 0 8px 0 0;
line-height: 20px;
}
.is-primary{
background: rgba(2,119,179,0.20);
border: 1px solid #0277B3;
color: #0277B3;
}
.is-success{
background: rgba(6,180,41,0.20);
border: 1px solid #06B429;
color: #06B429;
}
.is-info{
background: rgba(48,89,236,0.20);
border: 1px solid #3059EC;
color: #3059EC;
}
.is-warning{
background: rgba(255,126,0,0.20);
border: 1px solid #FF7E00;
color: #FF7E00;
}
.is-danger{
background: rgba(254,55,49,0.20);
border: 1px solid #FE3731;
color: #FE3731;
}
.is-offline{
background: rgba(153,153,153,0.20);
border: 1px solid #999999;
color: #999999;
}
.is-maintenance{
background: rgba(0,160,150,0.20);
border: 1px solid #00A096;
color: #00A096;
}
.fanlist-main{
width: 100%;
display: flex;
/*padding: 10px;*/
padding: 10px 10px 0 10px;
text-align: center;
.fanlist-pic{
display: flex;
justify-content: center;
align-items: center;
margin-top: -10px;
.mask{
width: 43px;
height: 38px;
.heart {
position: absolute;
text-align: center;
top: 5px;
left: 17px;
z-index: 3;
}
.leafs {
z-index: 1;
position: absolute;
/* animation: leafRotate 1s infinite linear;*/
transform-origin: center center;
width: 46px;
height: 46px;
/*animation-duration: 1s;*/
animation-name: leafRotate;
animation-iteration-count: infinite;
animation-timing-function: linear;
.leaf_1 {
width: 9px;
height: 19px;
position: absolute;
left: 17px;
top: -1px;
/* transform-origin: left top;*/
}
.leaf_2 {
width: 9px;
height: 19px;
position: absolute;
left: 31px;
top: 20px;
/* transform-origin: left top;*/
transform: rotate(120deg);
}
.leaf_3 {
width: 9px;
height: 19px;
position: absolute;
left: 5px;
top: 22px;
/*transform-origin: left top;*/
transform: rotate(240deg);
}
}
}
}
.fanlist-text{
margin-top:20px;
display: flex;
flex-direction: column;
.content-number{
color: #333333;
font-size: 20px;
}
}
.fanlist-text span{
display: inline-block;
}
}
.fanlist-bottom{
display: flex;
justify-content: end;
height: 24px;
.tag-panel{
border-radius: 0 0 8px 0;
line-height: 20px;
}
}
}
}
</style>

View File

@ -54,7 +54,7 @@
</div>
<div class="buttons">
<el-button class="button" :icon="Search" @click="queryHistoryData" type="primary">查询</el-button>
<el-button class="button" :icon="Upload" type="primary" plain>导出</el-button>
<el-button class="button" :icon="Upload" type="primary" plain @click="exportCsv">导出</el-button>
<el-button class="button" :icon="Notebook" type="primary" @click="addReportTemplate" plain>保存为模板</el-button>
</div>
</div>
@ -69,7 +69,7 @@
>
<el-table-column prop="deviceId" label="风机" fixed width="80px" align="center">
<template #default="scope">
{{ windBlowerList.find((val) => val.irn == scope.row.deviceId)!.name }}
{{ windBlowerList.find((val) => val.irn == scope.row.deviceId)?.name }}
</template>
</el-table-column>
<el-table-column prop="time" label="时间" fixed width="180px" align="center"> </el-table-column>
@ -143,7 +143,10 @@ import { useI18n } from 'vue-i18n'
import { shortUuid } from '/@/utils/random'
const { t } = useI18n()
import { useAdminInfo } from '/@/stores/adminInfo'
import { cloneDeep } from 'lodash-es'
const adminInfo = useAdminInfo()
import { useEnumStore } from '/@/stores/enums'
const enumStore = useEnumStore()
const shortcuts = [
{
text: '今天',
@ -233,6 +236,28 @@ const getReportTemplateList = () => {
}
})
}
const exportCsv = () => {
const exportColumnLabel = [{ prop: 'deviceId', label: '风机' }, { prop: 'time', label: '时间' }, ...reportTableColumn.value]
const itemsRest = cloneDeep(reportTableData.value).map((item: any) => {
if (item.deviceId) {
item.deviceId = windBlowerList.value.find((val: any) => val.irn == item.deviceId)?.name
}
const { id, ...rest } = item
return rest
})
const csvContent = [
exportColumnLabel.map((header: any) => header.label).join(', '),
...itemsRest.map((row: any) => exportColumnLabel.map((header) => row[header.prop] ?? '-').join(', ')),
].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = Date.now() + '.csv'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const addReportTemplate = () => {
ElMessageBox.prompt('请输入模板名称', '添加模板', {
confirmButtonText: '提交',
@ -336,7 +361,7 @@ const addColumn = () => {
const addRow = () => {}
const removeColumn = (val: any) => {
const columnName = val.column.property
reportTableColumn.value = reportTableColumn.value.filter((val:any) => val.prop !== columnName)
reportTableColumn.value = reportTableColumn.value.filter((val: any) => val.prop !== columnName)
}
const handleSelections = (value: any) => {
currentChooseRows.value = JSON.parse(JSON.stringify(value))
@ -424,7 +449,7 @@ const queryHistoryData = () => {
if (!windBlowerValue.value.length) return ElMessage.warning('请选择风机!')
if (!timeRange.value.length) return ElMessage.warning('请选择时间!')
if (!interval.value) return ElMessage.warning('请选择间隔!')
const attributeCodes = reportTableColumn.value.map((val:any) => val.prop).filter((item:any) => item != null && item !== '')
const attributeCodes = reportTableColumn.value.map((val: any) => val.prop).filter((item: any) => item != null && item !== '')
if (!attributeCodes.length) return ElMessage.warning('请添加测点!')
reportLoading.value = true
// idCounter.value = 0
@ -450,7 +475,7 @@ const queryHistoryData = () => {
if (realResult) {
let tableData = [] as any
attributeCodes.forEach((item:any) => {
attributeCodes.forEach((item: any) => {
if (Object.keys(realResult).includes(item)) {
tableData.push({
name: item,
@ -467,7 +492,8 @@ const queryHistoryData = () => {
if (!processedData.has(time)) {
processedData.set(time, { id: shortUuid(), time: timestampToTime(time), deviceId })
}
processedData.get(time)[name] = value[index]
const values = value[index]
processedData.get(time)[name] = enumStore.keys.includes(name) ? enumStore.data?.[name]?.[values] : values
})
})
}
@ -526,7 +552,7 @@ const generateMissingData = (data: any, deviceIds: any) => {
time: item.time,
deviceId: deviceId,
} as any
allKeys.forEach((key: any) => {
allKeys.forEach((key: string) => {
if (!['id', 'time', 'deviceId'].includes(key)) {
generatedItem[key] = '-'
}

View File

@ -16,7 +16,7 @@
<div style="width: 20px"></div>
<div class="buttons">
<el-button class="button" :icon="Search" @click="queryHistoryData" type="primary">查询</el-button>
<el-button class="button" :icon="Upload" type="primary" plain>导出</el-button>
<el-button class="button" :icon="Upload" type="primary" @click="exportCsv" plain>导出</el-button>
</div>
</el-space>
<div class="reportButtons">
@ -74,6 +74,7 @@ import { queryWindTurbinesPages, historyReq } from '/@/api/backend/statAnalysis/
import { shortUuid } from '/@/utils/random'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { cloneDeep } from 'lodash-es'
const selectedIndex = ref(0)
const buttons = ['日报', '月报']
const attributeCodes = ref(['iKWhThisDay', 'iOperationHoursDay', 'iWindSpeed'])
@ -186,6 +187,8 @@ const queryHistoryData = () => {
requestData.startTime = new Date(getFirstAndLastDate(timeValue.value)[0] + ' 00:00:00').getTime()
requestData.endTime = new Date(getFirstAndLastDate(timeValue.value)[1] + ' 23:59:59').getTime()
}
console.log(requestData, 7777)
reportLoading.value = true
historyReq(requestData).then((res) => {
@ -256,6 +259,26 @@ const queryHistoryData = () => {
})
}
const exportCsv = () => {
const exportColumnLabel = [{ prop: 'time', label: '时间' }, ...reportTableColumn.value]
const itemsRest = cloneDeep(reportTableData.value).map((item: any) => {
const { id, deviceId, prop, ...rest } = item
return rest
})
const csvContent = [
exportColumnLabel.map((header: any) => header.label).join(', '),
...itemsRest.map((row: any) => exportColumnLabel.map((header) => row[header.prop] ?? '-').join(', ')),
].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = Date.now() + '.csv'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
//
const timestampToTime = (timestamp: any) => {
let timestamps = timestamp ? timestamp : null

View File

@ -58,7 +58,7 @@
</div>
<div class="buttons">
<el-button class="button" :icon="Search" @click="queryHistoryData" type="primary">查询</el-button>
<el-button class="button" :icon="Upload" type="primary" plain>导出</el-button>
<el-button class="button" :icon="Upload" type="primary" plain @click="exportCsv">导出</el-button>
<el-button class="button" :icon="Notebook" type="primary" @click="addReportTemplate" plain>保存为模板</el-button>
</div>
</div>
@ -139,7 +139,10 @@ import Measurement from './measureList.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { useAdminInfo } from '/@/stores/adminInfo'
import { cloneDeep } from 'lodash-es'
const adminInfo = useAdminInfo()
import { useEnumStore } from '/@/stores/enums'
const enumStore = useEnumStore()
const shortcuts = [
{
text: '今天',
@ -229,6 +232,25 @@ const getReportTemplateList = () => {
}
})
}
const exportCsv = () => {
const exportColumnLabel = [{ prop: 'time', label: '时间' }, ...reportTableColumn.value]
const itemsRest = cloneDeep(reportTableData.value).map((item: any) => {
const { id, ...rest } = item
return rest
})
const csvContent = [
exportColumnLabel.map((header: any) => header.label).join(', '),
...itemsRest.map((row: any) => exportColumnLabel.map((header) => row[header.prop] ?? '-').join(', ')),
].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = Date.now() + '.csv'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const addReportTemplate = () => {
ElMessageBox.prompt('请输入模板名称', '添加模板', {
confirmButtonText: '提交',
@ -314,7 +336,7 @@ const chooseMeasurePoint = () => {
}
const removeColumn = (val: any) => {
const columnName = val.column.property
reportTableColumn.value = reportTableColumn.value.filter((val:any) => val.prop !== columnName)
reportTableColumn.value = reportTableColumn.value.filter((val: any) => val.prop !== columnName)
}
const handleSelections = (value: any) => {
@ -403,7 +425,7 @@ const queryHistoryData = () => {
if (!windBlowerValue.value) return ElMessage.warning('请选择风机!')
if (!timeRange.value.length) return ElMessage.warning('请选择时间!')
if (!interval.value) return ElMessage.warning('请选择间隔!')
const attributeCodes = reportTableColumn.value.map((val:any) => val.prop).filter((item:any) => item != null && item !== '')
const attributeCodes = reportTableColumn.value.map((val: any) => val.prop).filter((item: any) => item != null && item !== '')
if (!attributeCodes.length) return ElMessage.warning('请添加测点!')
reportLoading.value = true
const requestData = {
@ -423,7 +445,7 @@ const queryHistoryData = () => {
if (Object.keys(result)?.length) {
const realResult = result[windBlowerValue.value]
let tableData = [] as any
attributeCodes.forEach((item:any) => {
attributeCodes.forEach((item: any) => {
if (Object.keys(realResult).includes(item)) {
tableData.push({
name: item,
@ -440,7 +462,8 @@ const queryHistoryData = () => {
if (!processedData.has(time)) {
processedData.set(time, { id: idCounter.value++, time: timestampToTime(time) })
}
processedData.get(time)[name] = value[index]
const values = value[index]
processedData.get(time)[name] = enumStore.keys.includes(name) ? enumStore.data?.[name]?.[values] : values
})
})
}

View File

@ -11,7 +11,6 @@
</div>
<div class="topRight">
<el-button type="primary" @click="statAnalysisOperate()">{{ t('statAnalysis.search') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysiImport()">{{ t('statAnalysis.import') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysisExport()">{{ t('statAnalysis.export') }}</el-button>
</div>
</el-header>
@ -42,7 +41,7 @@
v-model="statAnalysisFatory"
:placeholder="'请选择' + t('statAnalysis.madeinfatory')"
class="statAnalysisSelect"
@change="factoryChange"
clearable
>
<el-option
v-for="v in statAnalysisFatoryList"
@ -61,11 +60,10 @@
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { queryWindTurbinesPages, historyReq } from '/@/api/backend/statAnalysis/request'
import { queryWindTurbinesPages, historyReq, powerCurveExport } from '/@/api/backend/statAnalysis/request'
import { theoreticalpowerCurveList, powerCurveQuery } from '/@/api/backend/theoreticalpowerCurve/request'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import { el } from 'element-plus/es/locales.mjs'
const { t } = useI18n()
const statAnalysisFatory = ref('')
@ -167,13 +165,6 @@ const queryfactoery = () => {
})
}
const factoryChange = (val: any) => {
if (!option.legend.data.includes('理论值')) {
querytheoretical(val)
} else {
ElMessage.info('理论曲线已存在,无需选择!!')
}
}
const querytheoretical = (val: any) => {
const madeinfactory = val.split(':')[0]
const model = val.split(':')[1]
@ -300,7 +291,8 @@ const statAnalysisOperate = () => {
startTime: new Date(statAnalysisTime.value[0]).getTime(),
endTime: new Date(statAnalysisTime.value[1]).getTime(),
}
querytheoretical(statAnalysisDeviceId.value)
const params = statAnalysisFatory.value ? statAnalysisFatory.value : statAnalysisDeviceId.value
querytheoretical(params)
historyDataReq(requestData)
}
const historyDataReq = (data: any) => {
@ -332,8 +324,33 @@ const historyDataReq = (data: any) => {
})
}
const statAnalysisExport = () => {}
const statAnalysiImport = () => {}
const statAnalysisExport = () => {
const params = statAnalysisFatory.value ? statAnalysisFatory.value : statAnalysisDeviceId.value
const requestData = {
devices: [
{
deviceId: statAnalysisDeviceId.value.split(':')[2],
attributes: ['iGenPower', 'iWindSpeed'],
},
],
interval: statAnalysisInterval.value || '5m',
startTime: new Date(statAnalysisTime.value[0]).getTime(),
endTime: new Date(statAnalysisTime.value[1]).getTime(),
madeinfactory: params.split(':')[0],
model: params.split(':')[1],
}
console.log(requestData)
powerCurveExport(requestData).then((res: any) => {
const downloadUrl = window.URL.createObjectURL(res)
const a = document.createElement('a')
a.href = downloadUrl
a.download = '功率曲线' + new Date().getTime()
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(downloadUrl)
document.body.removeChild(a)
})
}
</script>
<style scoped lang="scss">
.statAnalysis {

View File

@ -31,7 +31,6 @@
</div>
<div class="topRight">
<el-button type="primary" @click="statAnalysisOperate()">{{ t('statAnalysis.search') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysiImport()">{{ t('statAnalysis.import') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysisExport()">{{ t('statAnalysis.export') }}</el-button>
</div>
</el-header>
@ -101,7 +100,7 @@
<script setup lang="ts">
import { reactive, ref, onMounted, markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import { queryWindTurbinesPages, historyReq } from '/@/api/backend/statAnalysis/request'
import { queryWindTurbinesPages, historyReq, trendAnalyseExport } from '/@/api/backend/statAnalysis/request'
import { ElMessage } from 'element-plus'
import { DArrowRight, Plus, Delete } from '@element-plus/icons-vue'
import MeasurementPage from './analysisAttributes.vue'
@ -419,8 +418,37 @@ const historyDataReq = (data: any, index: number) => {
})
}
const statAnalysisExport = () => {}
const statAnalysiImport = () => {}
const statAnalysisExport = () => {
const requestData: any = []
times.forEach((time: any, index: number) => {
if (time[0] && time[1]) {
const devices = {
devices: [
{
deviceId: statAnalysisSelect.deviceId,
attributes: [statAnalysisSelect.attributeCode],
},
],
interval: statAnalysisSelect.interval || '5m',
startTime: new Date(time[0]).getTime(),
endTime: new Date(time[1]).getTime(),
timeName: customName[index],
}
requestData.push(devices)
}
})
console.log(requestData)
trendAnalyseExport(requestData).then((res: any) => {
const downloadUrl = window.URL.createObjectURL(res)
const a = document.createElement('a')
a.href = downloadUrl
a.download = '趋势分析' + new Date().getTime()
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(downloadUrl)
document.body.removeChild(a)
})
}
function calculateStats(numbers: any) {
const max = Math.max(...numbers)

View File

@ -23,7 +23,6 @@
</div>
<div class="topRight">
<el-button type="primary" @click="statAnalysisOperate()">{{ t('statAnalysis.search') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysiImport()">{{ t('statAnalysis.import') }}</el-button>
<el-button style="color: #0064aa" @click="statAnalysisExport()">{{ t('statAnalysis.export') }}</el-button>
</div>
</el-header>
@ -107,7 +106,7 @@
<script setup lang="ts">
import { markRaw, reactive, ref, watch, nextTick, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { queryWindTurbinesPages, historyReq } from '/@/api/backend/statAnalysis/request'
import { queryWindTurbinesPages, historyReq, trendContrastExport } from '/@/api/backend/statAnalysis/request'
import { ElMessage, ElMenu } from 'element-plus'
import { DArrowRight, Plus, Delete } from '@element-plus/icons-vue'
import MeasurementPage from './analysisAttributes.vue'
@ -325,26 +324,7 @@ const getDateRange = (type: 'week' | 'month') => {
}
const statAnalysisOperate = () => {
option.series = []
chart.value.setOption(option, { notMerge: true })
const attributes = statAnalysisAttributeCode
const devices = statAnalysisDeviceId.reduce((deviceId: any, curr, index) => {
const existing: any = deviceId.find((item: any) => item.deviceId === curr)
if (existing) {
existing.attributes.push(statAnalysisAttributeCode[index])
} else {
deviceId.push({ deviceId: curr, attributes: [statAnalysisAttributeCode[index]] })
}
return deviceId
}, [])
const requestData = {
devices: devices,
interval: statAnalysisInterval.value || '5m',
startTime: new Date(statAnalysisTime.value[0]).getTime(),
endTime: new Date(statAnalysisTime.value[1]).getTime(),
}
historyDataReq(requestData)
historyDataReq(getRequestData())
}
const calculate: any = reactive([{ max: '', min: '', average: '' }])
const historyDataReq = (data: any) => {
@ -399,8 +379,38 @@ const findAllOccurrences = (arr: any, target: any) => {
const getCommonElements = (arr1: any, arr2: any) => {
return arr1.filter((item: any) => arr2.some((x: any) => x === item))
}
const statAnalysisExport = () => {}
const statAnalysiImport = () => {}
const statAnalysisExport = () => {
console.log('🚀 ~ trendContrastExport ~ getRequestData():', getRequestData())
trendContrastExport(getRequestData()).then((res: any) => {
const downloadUrl = window.URL.createObjectURL(res)
const a = document.createElement('a')
a.href = downloadUrl
a.download = '趋势对比' + new Date().getTime()
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(downloadUrl)
document.body.removeChild(a)
})
}
const getRequestData = () => {
const devices = statAnalysisDeviceId.reduce((deviceId: any, curr, index) => {
const existing: any = deviceId.find((item: any) => item.deviceId === curr)
if (existing) {
existing.attributes.push(statAnalysisAttributeCode[index])
} else {
deviceId.push({ deviceId: curr, attributes: [statAnalysisAttributeCode[index]] })
}
return deviceId
}, [])
const requestData = {
devices: devices,
interval: statAnalysisInterval.value || '5m',
startTime: new Date(statAnalysisTime.value[0]).getTime(),
endTime: new Date(statAnalysisTime.value[1]).getTime(),
}
return requestData
}
function calculateStats(numbers: any) {
const max = Math.max(...numbers)
const min = Math.min(...numbers)

View File

@ -102,7 +102,7 @@ const update = (file: any) => {
formData.append('file', file.file)
const v = generateRandomNumber(16)
const id = encrypt_aes(currentRow.value.id, v)
formData.append('id', currentRow.value.id)
formData.append('id', id)
return importData(formData, v)
.then((res: any) => {
if (res.success) {
@ -266,6 +266,7 @@ const deleteDetails = (val: any) => {
.then((res: any) => {
if (res.code == 200) {
ElMessage.success(res.msg ?? '删除成功')
currentRow.value = null
getList()
} else {
ElMessage.error(res.msg ?? '删除失败')