Merge branch 'main' of https://git.jsspisoft.com/ry-das
This commit is contained in:
commit
a9c6329c44
13
das/pom.xml
13
das/pom.xml
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
27
das/src/main/java/com/das/common/constant/FileConstants.java
Normal file
27
das/src/main/java/com/das/common/constant/FileConstants.java
Normal 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 = "写";
|
||||
}
|
@ -137,4 +137,10 @@ public class SysEquipment extends BaseEntity {
|
||||
*/
|
||||
@TableField(value = "nominal_capacity")
|
||||
private Double nominalCapacity;
|
||||
|
||||
/**
|
||||
* 故障录波格式
|
||||
*/
|
||||
@TableField(value = "fdr_format")
|
||||
private String fdrFormat;
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
22
das/src/main/java/com/das/modules/fdr/domain/FileNode.java
Normal file
22
das/src/main/java/com/das/modules/fdr/domain/FileNode.java
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class TerminalMessage {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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[]
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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] = '-'
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 ?? '删除失败')
|
||||
|
Loading…
Reference in New Issue
Block a user