reportEngine:完善报告导出
This commit is contained in:
parent
86b85f2327
commit
ae37f21d6d
@ -7,13 +7,13 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:latest
|
image: quay.io/minio/minio:latest
|
||||||
container_name: minio
|
container_name: minio
|
||||||
restart: always
|
restart: always
|
||||||
# privileged: true 它几乎拥有与宿主机 root 相同的所有权限;MinIO 是纯用户空间的对象存储服务;无需用到特权模式,故而安全性与最小权限原则
|
# privileged: true 它几乎拥有与宿主机 root 相同的所有权限;MinIO 是纯用户空间的对象存储服务;无需用到特权模式,故而安全性与最小权限原则
|
||||||
networks:
|
networks:
|
||||||
network:
|
network:
|
||||||
ipv4_address: 177.7.0.6
|
ipv4_address: 176.6.0.6
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
>
|
>
|
||||||
> **本 Skill 目标**:基于已落库的 IoT 物模型监测数据,为单个监测设备按「设备 + 时间范围」生成专业分析报告(Word `.docx`),支持历史报表下载,**保留期 1 个月**。
|
> **本 Skill 目标**:基于已落库的 IoT 物模型监测数据,为单个监测设备按「设备 + 时间范围」生成专业分析报告(Word `.docx`),支持历史报表下载,**保留期 1 个月**。
|
||||||
>
|
>
|
||||||
> **前端入口(2 处)**:① 大屏分析模式;② 数据中心分析页。两处均通过「选设备 + 选时间」即可生成并导出报表;另有历史报表列表可下载。
|
> **前端入口(2 处)**:① 大屏分析模式;② 数据中心分析页。两处均通过「选设备 + 选时间」即可生成并导出分析报告;另有历史报表列表可下载。
|
||||||
>
|
>
|
||||||
> **存储决策**:**已确认未购买云 OSS 服务 → 统一使用自建 MinIO**(S3 协议兼容,复用现有 `fastbee-oss` 插件,不改上传链路)。
|
> **存储决策**:**已确认未购买云 OSS 服务 → 统一使用自建 MinIO**(S3 协议兼容,复用现有 `fastbee-oss` 插件,不改上传链路)。
|
||||||
>
|
>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_things_threshold`。
|
4. 结构化报告阈值:告警阈值藏在 LiteFlow `iot_scene` 脚本里,报告不可直接读 → 新增 `iot_things_threshold`。
|
||||||
5. Java 端图表(JFreeChart)与 Word 模板引擎(poi-tl)。
|
5. Java 端图表(JFreeChart)与 Word 模板引擎(poi-tl)。
|
||||||
6. MinIO 默认存储配置。
|
6. MinIO 默认存储配置。
|
||||||
7. 前端两处「生成并导出报表」按钮 + 历史报表页(列表/下载/过期)。
|
7. 前端两处「生成并导出分析报告」按钮 + 历史报表页(列表/下载/过期)。
|
||||||
|
|
||||||
## 1.4 前端现状
|
## 1.4 前端现状
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ UploadResult r = OssFactory.instance(redis)
|
|||||||
|
|
||||||
## 5.1 入口一:大屏分析模式(`bigScreen/home.vue`)
|
## 5.1 入口一:大屏分析模式(`bigScreen/home.vue`)
|
||||||
|
|
||||||
- 在「分析条件」卡片“查询数据和曲线”旁新增按钮 **「生成并导出报表」**。
|
- 在「分析条件」卡片“查询数据和曲线”旁新增按钮 **「生成并导出分析报告」**。
|
||||||
- 参数直接取已有 `analysisQuery.deviceId` + `analysisQuery.timeRange`(`[startTime, endTime]`)。
|
- 参数直接取已有 `analysisQuery.deviceId` + `analysisQuery.timeRange`(`[startTime, endTime]`)。
|
||||||
- 交互:点击 → `POST /iot/report/generate`(loading 防重复)→ 成功拿 `reportNo/fileUrl` 直接触发下载,并提示可在历史报表查看。
|
- 交互:点击 → `POST /iot/report/generate`(loading 防重复)→ 成功拿 `reportNo/fileUrl` 直接触发下载,并提示可在历史报表查看。
|
||||||
- 未选设备或时间时按钮禁用。
|
- 未选设备或时间时按钮禁用。
|
||||||
@ -288,7 +288,7 @@ async handleGenerateReport() {
|
|||||||
|
|
||||||
## 5.2 入口二:数据中心(`dataCenter/analysis.vue`)
|
## 5.2 入口二:数据中心(`dataCenter/analysis.vue`)
|
||||||
|
|
||||||
- 在筛选栏“查询/重置”旁新增 **「生成并导出报表」**。
|
- 在筛选栏“查询/重置”旁新增 **「生成并导出分析报告」**。
|
||||||
- 参数取已有 `queryParams.deviceId` + `queryParams.dayDaterange`。
|
- 参数取已有 `queryParams.deviceId` + `queryParams.dayDaterange`。
|
||||||
- 交互与 5.1 一致,复用同一 API 封装。
|
- 交互与 5.1 一致,复用同一 API 封装。
|
||||||
|
|
||||||
@ -402,7 +402,7 @@ CREATE TABLE report_record (
|
|||||||
6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ThingsThresholdProvider`。
|
6. **Provider**:`ReportDataProvider`(委托 `DeviceLogMapper`)、`ThingsThresholdProvider`。
|
||||||
7. **渲染**:`ChartGenerator`(JFreeChart)+ `WordRenderer`(poi-tl)+ 模板 `DEVICE_CRACK_METER_V1.docx`。
|
7. **渲染**:`ChartGenerator`(JFreeChart)+ `WordRenderer`(poi-tl)+ 模板 `DEVICE_CRACK_METER_V1.docx`。
|
||||||
8. **落库与下载**:`report_record` + `expire_time`;MinIO 生命周期规则;下载接口(校验过期)。
|
8. **落库与下载**:`report_record` + `expire_time`;MinIO 生命周期规则;下载接口(校验过期)。
|
||||||
9. **前端**:大屏分析模式 + 数据中心加「生成并导出报表」;历史报表页 + 下载。
|
9. **前端**:大屏分析模式 + 数据中心加「生成并导出分析报告」;历史报表页 + 下载。
|
||||||
10. **闭环验证**:真实设备 + 时间范围 → 生成 → 下载 → 列表可见 → 过期处理验证。
|
10. **闭环验证**:真实设备 + 时间范围 → 生成 → 下载 → 列表可见 → 过期处理验证。
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -420,7 +420,7 @@ CREATE TABLE report_record (
|
|||||||
[ ] report_record 落库,expire_time = +30 天
|
[ ] report_record 落库,expire_time = +30 天
|
||||||
[ ] 列表接口只返回未过期记录
|
[ ] 列表接口只返回未过期记录
|
||||||
[ ] 下载接口可下载;过期返回明确提示
|
[ ] 下载接口可下载;过期返回明确提示
|
||||||
[ ] 大屏分析模式与数据中心均可「生成并导出报表」
|
[ ] 大屏分析模式与数据中心均可「生成并导出分析报告」
|
||||||
[ ] 历史报表页面可查看/下载
|
[ ] 历史报表页面可查看/下载
|
||||||
[ ] 设备 304259363251 完整闭环验证通过
|
[ ] 设备 304259363251 完整闭环验证通过
|
||||||
```
|
```
|
||||||
|
|||||||
@ -47,7 +47,7 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void insertFill(MetaObject metaObject) {
|
public void insertFill(MetaObject metaObject) {
|
||||||
log.debug("mybatisPlus插入填充--------------");
|
//log.debug("mybatisPlus插入填充--------------");
|
||||||
Date date = new Date();
|
Date date = new Date();
|
||||||
|
|
||||||
fillValIfNullByName("createTime", date, metaObject, false);
|
fillValIfNullByName("createTime", date, metaObject, false);
|
||||||
@ -59,7 +59,7 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFill(MetaObject metaObject) {
|
public void updateFill(MetaObject metaObject) {
|
||||||
log.debug("mybatisPlus更新填充--------------");
|
//log.debug("mybatisPlus更新填充--------------");
|
||||||
fillValIfNullByName("updateTime", new Date(), metaObject, true);
|
fillValIfNullByName("updateTime", new Date(), metaObject, true);
|
||||||
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
|
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -285,6 +285,17 @@ public class DeviceController extends BaseController {
|
|||||||
return deviceService.updateDevice(device);
|
return deviceService.updateDevice(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新设备基准点(仅写入 baseline_device_id)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('iot:device:edit')")
|
||||||
|
@Log(title = "批量更新设备基准点", businessType = BusinessType.UPDATE)
|
||||||
|
@PostMapping("/baseline/batch")
|
||||||
|
@ApiOperation("批量更新设备基准点")
|
||||||
|
public AjaxResult updateBaselineBatch(@RequestBody List<Device> devices) {
|
||||||
|
return AjaxResult.success(deviceService.updateDeviceBaselineBatch(devices));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置设备状态
|
* 重置设备状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -79,25 +79,31 @@ public class ThingsThresholdController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增阈值
|
* 批量新增阈值
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
||||||
@Log(title = "监测阈值配置", businessType = BusinessType.INSERT)
|
@Log(title = "监测阈值配置", businessType = BusinessType.INSERT)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ApiOperation("新增阈值")
|
@ApiOperation("批量新增阈值")
|
||||||
public AjaxResult add(@RequestBody ThingsThreshold threshold) {
|
public AjaxResult add(@RequestBody List<ThingsThreshold> thresholds) {
|
||||||
return toAjax(thingsThresholdService.insertThreshold(threshold));
|
if (thresholds == null || thresholds.isEmpty()) {
|
||||||
|
return AjaxResult.error("新增列表不能为空");
|
||||||
|
}
|
||||||
|
return toAjax(thingsThresholdService.insertThresholdBatch(thresholds));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改阈值
|
* 批量修改阈值
|
||||||
*/
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
||||||
@Log(title = "监测阈值配置", businessType = BusinessType.UPDATE)
|
@Log(title = "监测阈值配置", businessType = BusinessType.UPDATE)
|
||||||
@PutMapping
|
@PutMapping
|
||||||
@ApiOperation("修改阈值")
|
@ApiOperation("批量修改阈值")
|
||||||
public AjaxResult edit(@RequestBody ThingsThreshold threshold) {
|
public AjaxResult edit(@RequestBody List<ThingsThreshold> thresholds) {
|
||||||
return toAjax(thingsThresholdService.updateThreshold(threshold));
|
if (thresholds == null || thresholds.isEmpty()) {
|
||||||
|
return AjaxResult.error("修改列表不能为空");
|
||||||
|
}
|
||||||
|
return toAjax(thingsThresholdService.updateThresholdBatch(thresholds));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -19,12 +19,16 @@ import com.fastbee.report.period.ReportPeriodResolver;
|
|||||||
import com.fastbee.report.period.TimeWindow;
|
import com.fastbee.report.period.TimeWindow;
|
||||||
import com.fastbee.report.record.ReportRecord;
|
import com.fastbee.report.record.ReportRecord;
|
||||||
import com.fastbee.report.record.ReportRecordService;
|
import com.fastbee.report.record.ReportRecordService;
|
||||||
|
import com.fastbee.report.data.MetricData;
|
||||||
|
import com.fastbee.report.model.Statistics;
|
||||||
import com.fastbee.report.renderer.ReportRenderer;
|
import com.fastbee.report.renderer.ReportRenderer;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
@ -111,6 +115,15 @@ public class ReportAsyncExecutor {
|
|||||||
|
|
||||||
ReportAnalyzer analyzer = analyzerFactory.get(context.getCategory());
|
ReportAnalyzer analyzer = analyzerFactory.get(context.getCategory());
|
||||||
AnalysisResult analysis = analyzer.analyze(context, request);
|
AnalysisResult analysis = analyzer.analyze(context, request);
|
||||||
|
|
||||||
|
// 无有效监测数据时终止生成,避免导出空白文档
|
||||||
|
if (isNoEffectiveData(context, analysis)) {
|
||||||
|
log.warn("报告生成中止:时间范围内无有效监测数据 reportNo={} deviceId={}",
|
||||||
|
reportNo, request.getDeviceId());
|
||||||
|
reportRecordService.markFailed(record, "所选时间范围内无有效监测数据,无法生成报告");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ReportModel model = reportAssembler.assemble(context, analysis);
|
ReportModel model = reportAssembler.assemble(context, analysis);
|
||||||
byte[] docBytes = reportRenderer.render(model);
|
byte[] docBytes = reportRenderer.render(model);
|
||||||
|
|
||||||
@ -129,6 +142,24 @@ public class ReportAsyncExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断本次报告是否无任何有效监测数据,避免导出空白文档。
|
||||||
|
*
|
||||||
|
* <p>判定口径:① 时序数据列表为空(设备完全未上报);② 所有物模型统计 count 均为 0
|
||||||
|
* (上报数据均无效或未落在统计窗口内);③ 未配置任何监测物模型(无统计数据)。</p>
|
||||||
|
*/
|
||||||
|
private boolean isNoEffectiveData(ReportContext context, AnalysisResult analysis) {
|
||||||
|
List<MetricData> metricDataList = context.getMetricDataList();
|
||||||
|
if (metricDataList == null || metricDataList.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Map<String, Statistics> statisticsMap = analysis.getStatistics();
|
||||||
|
if (statisticsMap == null || statisticsMap.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return statisticsMap.values().stream().noneMatch(s -> s.getCount() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void validate(ReportRequest request) {
|
private void validate(ReportRequest request) {
|
||||||
if (request == null || request.getDeviceId() == null) {
|
if (request == null || request.getDeviceId() == null) {
|
||||||
throw new ReportException("请选择设备");
|
throw new ReportException("请选择设备");
|
||||||
|
|||||||
@ -53,4 +53,20 @@ public interface IThingsThresholdService extends IService<ThingsThreshold> {
|
|||||||
* @return 影响行数
|
* @return 影响行数
|
||||||
*/
|
*/
|
||||||
int deleteThresholdByIds(Long[] ids);
|
int deleteThresholdByIds(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量新增阈值
|
||||||
|
*
|
||||||
|
* @param thresholds 阈值列表
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int insertThresholdBatch(List<ThingsThreshold> thresholds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量修改阈值
|
||||||
|
*
|
||||||
|
* @param thresholds 阈值列表
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int updateThresholdBatch(List<ThingsThreshold> thresholds);
|
||||||
}
|
}
|
||||||
@ -8,6 +8,7 @@ import com.fastbee.report.mapper.ThingsThresholdMapper;
|
|||||||
import com.fastbee.report.service.IThingsThresholdService;
|
import com.fastbee.report.service.IThingsThresholdService;
|
||||||
import com.fastbee.report.threshold.ThingsThreshold;
|
import com.fastbee.report.threshold.ThingsThreshold;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -63,4 +64,24 @@ public class ThingsThresholdServiceImpl extends ServiceImpl<ThingsThresholdMappe
|
|||||||
public int deleteThresholdByIds(Long[] ids) {
|
public int deleteThresholdByIds(Long[] ids) {
|
||||||
return baseMapper.deleteBatchIds(Arrays.asList(ids));
|
return baseMapper.deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int insertThresholdBatch(List<ThingsThreshold> thresholds) {
|
||||||
|
if (thresholds == null || thresholds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
boolean ok = saveBatch(thresholds);
|
||||||
|
return ok ? thresholds.size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int updateThresholdBatch(List<ThingsThreshold> thresholds) {
|
||||||
|
if (thresholds == null || thresholds.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
boolean ok = updateBatchById(thresholds);
|
||||||
|
return ok ? thresholds.size() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.fastbee.report.threshold;
|
package com.fastbee.report.threshold;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -48,13 +50,17 @@ public class ThingsThreshold {
|
|||||||
|
|
||||||
private String unit;
|
private String unit;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private String createBy;
|
private String createBy;
|
||||||
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private String updateBy;
|
private String updateBy;
|
||||||
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,4 +57,10 @@ public class SubDeviceListVO {
|
|||||||
|
|
||||||
@ApiModelProperty("在线状态")
|
@ApiModelProperty("在线状态")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基准点设备id(iot_device.baseline_device_id):同一子产品下作为相对测量基准的设备;0/null 表示绝对量测量
|
||||||
|
*/
|
||||||
|
@ApiModelProperty("基准点设备id")
|
||||||
|
private Long baselineDeviceId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,6 +202,15 @@ public interface IDeviceService {
|
|||||||
*/
|
*/
|
||||||
public AjaxResult updateDevice(Device device);
|
public AjaxResult updateDevice(Device device);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新设备基准点(仅写入 baseline_device_id)
|
||||||
|
* <p>同一子产品下通过单选确定一个基准点设备,组内其余设备指向该基准点;基准点设备自身写 0。</p>
|
||||||
|
*
|
||||||
|
* @param devices 仅包含 deviceId 与 baselineDeviceId 的设备列表
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
public int updateDeviceBaselineBatch(List<Device> devices);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新设备状态和定位
|
* 更新设备状态和定位
|
||||||
*
|
*
|
||||||
|
|||||||
@ -325,6 +325,26 @@ public class DeviceServiceImpl implements IDeviceService {
|
|||||||
return AjaxResult.success("修改成功", 1);
|
return AjaxResult.success("修改成功", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新设备基准点(仅写入 baseline_device_id)
|
||||||
|
* <p>组内其余设备指向基准点设备,基准点设备自身写 0。循环复用 updateDevice 的 baselineDeviceId 分支。</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int updateDeviceBaselineBatch(List<Device> devices) {
|
||||||
|
if (devices == null || devices.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int rows = 0;
|
||||||
|
for (Device device : devices) {
|
||||||
|
if (device.getDeviceId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
device.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
rows += deviceMapper.updateDevice(device);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
@Caching(evict = {
|
@Caching(evict = {
|
||||||
@CacheEvict(cacheNames = "Device", key = "'selectDeviceBySerialNumber:' + #device.getSerialNumber()"),
|
@CacheEvict(cacheNames = "Device", key = "'selectDeviceBySerialNumber:' + #device.getSerialNumber()"),
|
||||||
// @CacheEvict(cacheNames = "Device", key = "'selectDeviceByDeviceId:' +
|
// @CacheEvict(cacheNames = "Device", key = "'selectDeviceByDeviceId:' +
|
||||||
@ -1024,7 +1044,10 @@ public class DeviceServiceImpl implements IDeviceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillShortListLatestReportTime(list, device.getEntrance());
|
fillShortListLatestReportTime(list, device.getEntrance());
|
||||||
log.warn("当前selectDeviceShortList查询接口耗时: {}", System.currentTimeMillis() - startTime);
|
long took = System.currentTimeMillis() - startTime;
|
||||||
|
if (took > 1000L) {
|
||||||
|
log.warn("selectDeviceShortList took: {}", took);
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="subProductId" column="sub_product_id"/>
|
<result property="subProductId" column="sub_product_id"/>
|
||||||
<result property="subProductName" column="sub_product_name"/>
|
<result property="subProductName" column="sub_product_name"/>
|
||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
|
<result property="baselineDeviceId" column="baseline_device_id"/>
|
||||||
<result property="id" column="id" />
|
<result property="id" column="id" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<select id="selectGatewayList" parameterType="com.fastbee.iot.domain.SubGateway" resultMap="GatewaySubDeviceListResult">
|
<select id="selectGatewayList" parameterType="com.fastbee.iot.domain.SubGateway" resultMap="GatewaySubDeviceListResult">
|
||||||
select ga.id, ga.gw_device_id, ga.sub_device_id, ga.slave_id, ga.create_time,
|
select ga.id, ga.gw_device_id, ga.sub_device_id, ga.slave_id, ga.create_time,
|
||||||
d.device_name, d.serial_number, d.product_id sub_product_id, d.status,
|
d.device_name, d.serial_number, d.product_id sub_product_id, d.status,
|
||||||
|
d.baseline_device_id,
|
||||||
p.product_name as sub_product_name
|
p.product_name as sub_product_name
|
||||||
from iot_sub_gateway ga
|
from iot_sub_gateway ga
|
||||||
inner join iot_device d on ga.sub_device_id = d.device_id
|
inner join iot_device d on ga.sub_device_id = d.device_id
|
||||||
|
|||||||
@ -167,7 +167,7 @@ INSERT IGNORE INTO `sys_menu`
|
|||||||
(menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
(menu_id, menu_name, parent_id, order_num, `path`, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
|
||||||
VALUES
|
VALUES
|
||||||
(3610, '历史报表', 3343, 4, 'reportHistory', 'dataCenter/reportHistory', NULL, 1, 0, 'C', '0', '0', 'iot:report:list', 'documentation', 'admin', NOW(), 'admin', NOW(), '设备监测报告历史报表'),
|
(3610, '历史报表', 3343, 4, 'reportHistory', 'dataCenter/reportHistory', NULL, 1, 0, 'C', '0', '0', 'iot:report:list', 'documentation', 'admin', NOW(), 'admin', NOW(), '设备监测报告历史报表'),
|
||||||
(3611, '报表生成', 3610, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'iot:report:generate', '#', 'admin', NOW(), 'admin', NOW(), '生成并导出报表');
|
(3611, '报表生成', 3610, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'iot:report:generate', '#', 'admin', NOW(), 'admin', NOW(), '生成并导出分析报告');
|
||||||
|
|
||||||
INSERT IGNORE INTO `sys_menu_translate` (id, zh_cn, en_us) VALUES
|
INSERT IGNORE INTO `sys_menu_translate` (id, zh_cn, en_us) VALUES
|
||||||
(3610, '历史报表', 'Report history'),
|
(3610, '历史报表', 'Report history'),
|
||||||
|
|||||||
@ -168,6 +168,15 @@ export function updateDevice(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量更新设备基准点(仅写入 baselineDeviceId)
|
||||||
|
export function updateDeviceBaselineBatch(data) {
|
||||||
|
return request({
|
||||||
|
url: '/iot/device/baseline/batch',
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 删除设备
|
// 删除设备
|
||||||
export function delDevice(deviceId) {
|
export function delDevice(deviceId) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
|
||||||
// 生成并导出报表(设备 + 时间范围)
|
// 生成并导出分析报告(设备 + 时间范围)
|
||||||
export function generateDeviceReport(data) {
|
export function generateDeviceReport(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/iot/report/generate',
|
url: '/iot/report/generate',
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export function getThreshold(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增阈值
|
// 新增阈值(批量,data 为 ThingsThreshold 数组)
|
||||||
export function addThreshold(data) {
|
export function addThreshold(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/iot/threshold',
|
url: '/iot/threshold',
|
||||||
@ -26,7 +26,7 @@ export function addThreshold(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改阈值
|
// 修改阈值(批量,data 为 ThingsThreshold 数组)
|
||||||
export function updateThreshold(data) {
|
export function updateThreshold(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/iot/threshold',
|
url: '/iot/threshold',
|
||||||
|
|||||||
@ -321,7 +321,7 @@
|
|||||||
>
|
>
|
||||||
<i v-if="!reportLoading" class="el-icon-document"></i>
|
<i v-if="!reportLoading" class="el-icon-document"></i>
|
||||||
<i v-else class="el-icon-loading"></i>
|
<i v-else class="el-icon-loading"></i>
|
||||||
<span>生成并导出报表</span>
|
<span>生成并导出分析报告</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -1553,7 +1553,7 @@ export default {
|
|||||||
format: 'WORD',
|
format: 'WORD',
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
// 使用弹框提示用户
|
// 使用弹框提示用户
|
||||||
this.$alert('报表正在后台生成中,生成完成后可在历史报表中下载', '提交成功', {
|
this.$alert('报表正在后台生成中,生成完成后可在历史报表中下载', '提交成功', {
|
||||||
confirmButtonText: '查看历史报表',
|
confirmButtonText: '查看历史报表',
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
size="mini"
|
size="mini"
|
||||||
:loading="reportLoading"
|
:loading="reportLoading"
|
||||||
@click="handleGenerateReport"
|
@click="handleGenerateReport"
|
||||||
>生成并导出报表</el-button>
|
>生成并导出分析报告</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
@ -333,7 +333,7 @@ export default {
|
|||||||
format: 'WORD',
|
format: 'WORD',
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
// 使用弹框提示用户
|
// 使用弹框提示用户
|
||||||
this.$alert('报表正在后台生成中,生成完成后可在历史报表中下载', '提交成功', {
|
this.$alert('报表正在后台生成中,生成完成后可在历史报表中下载', '提交成功', {
|
||||||
confirmButtonText: '查看历史报表',
|
confirmButtonText: '查看历史报表',
|
||||||
|
|||||||
@ -29,6 +29,9 @@
|
|||||||
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!canExportNormal" @click="handleExportNormal" v-loading="exportLoading">
|
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!canExportNormal" @click="handleExportNormal" v-loading="exportLoading">
|
||||||
{{ $t('dataCenter.history.384934-24') }}
|
{{ $t('dataCenter.history.384934-24') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="warning" icon="el-icon-document" size="mini" :disabled="!canExportNormal" @click="handleExportReport" v-loading="reportLoading">
|
||||||
|
导出分析报告
|
||||||
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@ -78,6 +81,7 @@
|
|||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleGwQuery">查询</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleGwQuery">查询</el-button>
|
||||||
<el-button icon="el-icon-refresh" size="mini" @click="handleReset">重置</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="handleReset">重置</el-button>
|
||||||
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!gwHasExportableData" @click="handleGwExport" v-loading="exportLoading">导出Excel</el-button>
|
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!gwHasExportableData" @click="handleGwExport" v-loading="exportLoading">导出Excel</el-button>
|
||||||
|
<el-button type="warning" icon="el-icon-document" size="mini" :disabled="!gwHasExportableData" @click="handleExportReport" v-loading="reportLoading">导出分析报告</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@ -127,6 +131,7 @@ import {
|
|||||||
getSubDeviceComparativeHistory,
|
getSubDeviceComparativeHistory,
|
||||||
exportSubDeviceComparativeHistory,
|
exportSubDeviceComparativeHistory,
|
||||||
} from '@/api/data/center.js';
|
} from '@/api/data/center.js';
|
||||||
|
import { generateDeviceReport } from '@/api/iot/report';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { blobValidate } from '@/utils/ruoyi';
|
import { blobValidate } from '@/utils/ruoyi';
|
||||||
|
|
||||||
@ -289,6 +294,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
exportLoading: false,
|
exportLoading: false,
|
||||||
|
reportLoading: false,
|
||||||
hasQueried: false,
|
hasQueried: false,
|
||||||
resultMode: '',
|
resultMode: '',
|
||||||
independentYAxis: true,
|
independentYAxis: true,
|
||||||
@ -459,6 +465,52 @@ export default {
|
|||||||
this.disposeChart();
|
this.disposeChart();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleExportReport() {
|
||||||
|
let reportDeviceId = this.device ? this.device.deviceId : null;
|
||||||
|
if (!reportDeviceId) {
|
||||||
|
this.$message.warning('请先选择设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const range = this.isGatewayLike ? this.gwQueryParams.dayDaterange : this.queryParams.dayDaterange;
|
||||||
|
if (!range || range.length < 2) {
|
||||||
|
this.$message.warning('请选择时间范围');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 网关设备需选定单个子设备后才能生成报告(网关本身不可生成)
|
||||||
|
if (this.isGatewayLike) {
|
||||||
|
const subIds = this.gwQueryParams.subDeviceIds;
|
||||||
|
if (Array.isArray(subIds) && subIds.length === 1) {
|
||||||
|
reportDeviceId = subIds[0];
|
||||||
|
} else {
|
||||||
|
this.$message.warning('网关设备请选择单个子设备后生成报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.reportLoading = true;
|
||||||
|
const payload = {
|
||||||
|
deviceId: reportDeviceId,
|
||||||
|
startTime: moment(range[0]).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
endTime: moment(range[1]).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
format: 'WORD',
|
||||||
|
};
|
||||||
|
generateDeviceReport(payload)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$alert('报表正在后台生成中,生成完成后可在历史报表中下载', '提交成功', {
|
||||||
|
confirmButtonText: '查看历史报表',
|
||||||
|
type: 'success',
|
||||||
|
callback: action => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
this.$router.push({ path: '/dataCenter/reportHistory' }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.reportLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
disposeChart() {
|
disposeChart() {
|
||||||
if (this.chartInstance) {
|
if (this.chartInstance) {
|
||||||
this.chartInstance.dispose();
|
this.chartInstance.dispose();
|
||||||
|
|||||||
@ -47,15 +47,30 @@
|
|||||||
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
|
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="scope.row.status === 'SUCCESS'"
|
||||||
type="success"
|
type="success"
|
||||||
icon="el-icon-download"
|
icon="el-icon-download"
|
||||||
size="mini"
|
size="mini"
|
||||||
:disabled="scope.row.status !== 'SUCCESS'"
|
|
||||||
@click="handleDownload(scope.row)"
|
@click="handleDownload(scope.row)"
|
||||||
>下载</el-button>
|
>下载</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.status === 'FAILED'"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-warning-outline"
|
||||||
|
size="mini"
|
||||||
|
@click="handleShowError(scope.row)"
|
||||||
|
>失败原因</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.status === 'CREATING'"
|
||||||
|
plain
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
size="mini"
|
||||||
|
@click="getList()"
|
||||||
|
>刷新</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -68,14 +83,40 @@
|
|||||||
@pagination="getList"
|
@pagination="getList"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 失败原因 -->
|
||||||
|
<el-dialog title="报告生成失败原因" :visible.sync="errorVisible" width="520px" append-to-body>
|
||||||
|
<div class="report-error-detail">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">报告编号:</span>{{ errorRow.reportNo || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">设备名称:</span>{{ errorRow.deviceName || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">时间范围:</span>{{ errorRow.startTime || '-' }} ~ {{ errorRow.endTime || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">失败原因:</span>
|
||||||
|
</div>
|
||||||
|
<pre class="error-message">{{ errorRow.errorMessage || '未记录失败原因,请联系管理员查看服务端日志' }}</pre>
|
||||||
|
</div>
|
||||||
|
<span slot="footer">
|
||||||
|
<el-button size="mini" type="primary" @click="errorVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
import moment from 'moment';
|
||||||
import { listReportRecords, downloadReportRecord } from '@/api/iot/report';
|
import { listReportRecords, downloadReportRecord } from '@/api/iot/report';
|
||||||
import { blobValidate } from '@/utils/ruoyi';
|
import { blobValidate } from '@/utils/ruoyi';
|
||||||
|
|
||||||
|
/** 报告保留天数,与后端 LogCleanTask#cleanLogs 默认保留天数一致 */
|
||||||
|
const REPORT_RETENTION_DAYS = 15;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReportHistory',
|
name: 'ReportHistory',
|
||||||
data() {
|
data() {
|
||||||
@ -83,6 +124,10 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
reportList: [],
|
reportList: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
|
/** 生成中记录轮询定时器 */
|
||||||
|
pollTimer: null,
|
||||||
|
errorVisible: false,
|
||||||
|
errorRow: {},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -107,18 +152,44 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopPolling();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getList() {
|
/** silent=true 时不展示 loading,用于轮询刷新 */
|
||||||
this.loading = true;
|
getList(silent) {
|
||||||
|
if (!silent) {
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
listReportRecords(this.queryParams)
|
listReportRecords(this.queryParams)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.reportList = res.rows || [];
|
this.reportList = res.rows || [];
|
||||||
this.total = res.total || 0;
|
this.total = res.total || 0;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.loading = false;
|
if (!silent) {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
this.schedulePolling();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/** 存在生成中记录时每 10s 轮询,全部转为终态后自动停止 */
|
||||||
|
schedulePolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
const hasCreating = this.reportList.some((item) => item.status === 'CREATING');
|
||||||
|
if (!hasCreating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pollTimer = setTimeout(() => {
|
||||||
|
this.getList(true);
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearTimeout(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
handleQuery() {
|
handleQuery() {
|
||||||
this.queryParams.pageNum = 1;
|
this.queryParams.pageNum = 1;
|
||||||
this.getList();
|
this.getList();
|
||||||
@ -127,6 +198,17 @@ export default {
|
|||||||
this.resetForm('queryForm');
|
this.resetForm('queryForm');
|
||||||
this.handleQuery();
|
this.handleQuery();
|
||||||
},
|
},
|
||||||
|
/** 报告无 expire_time 字段,按 create_time + 保留天数判断是否已过期 */
|
||||||
|
isExpired(row) {
|
||||||
|
if (!row || !row.createTime) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const created = moment(row.createTime, 'YYYY-MM-DD HH:mm:ss', true);
|
||||||
|
if (!created.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return moment().diff(created, 'days') >= REPORT_RETENTION_DAYS;
|
||||||
|
},
|
||||||
handleDownload(row) {
|
handleDownload(row) {
|
||||||
if (this.isExpired(row)) {
|
if (this.isExpired(row)) {
|
||||||
this.$message.warning('报告已过期');
|
this.$message.warning('报告已过期');
|
||||||
@ -148,6 +230,10 @@ export default {
|
|||||||
this.$message.error('下载失败,请稍后重试');
|
this.$message.error('下载失败,请稍后重试');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleShowError(row) {
|
||||||
|
this.errorRow = row || {};
|
||||||
|
this.errorVisible = true;
|
||||||
|
},
|
||||||
categoryText(code) {
|
categoryText(code) {
|
||||||
const map = {
|
const map = {
|
||||||
CRACK_METER: '裂缝计',
|
CRACK_METER: '裂缝计',
|
||||||
@ -179,4 +265,24 @@ export default {
|
|||||||
.data-center-report-history .el-card {
|
.data-center-report-history .el-card {
|
||||||
min-height: calc(100vh - 110px);
|
min-height: calc(100vh - 110px);
|
||||||
}
|
}
|
||||||
|
.report-error-detail .detail-item {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.report-error-detail .label {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.report-error-detail .error-message {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -257,7 +257,9 @@ export default {
|
|||||||
this.$modal.msgError('请先保存产品信息');
|
this.$modal.msgError('请先保存产品信息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tasks = [];
|
const toAdd = [];
|
||||||
|
const toUpdate = [];
|
||||||
|
const toDelete = [];
|
||||||
this.rows.forEach((row) => {
|
this.rows.forEach((row) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
productId: this.toNum(this.productId),
|
productId: this.toNum(this.productId),
|
||||||
@ -274,14 +276,18 @@ export default {
|
|||||||
};
|
};
|
||||||
if (this.hasAnyValue(row)) {
|
if (this.hasAnyValue(row)) {
|
||||||
if (row.id) {
|
if (row.id) {
|
||||||
tasks.push(updateThreshold(Object.assign({ id: row.id }, payload)));
|
toUpdate.push(Object.assign({ id: row.id }, payload));
|
||||||
} else {
|
} else {
|
||||||
tasks.push(addThreshold(payload));
|
toAdd.push(payload);
|
||||||
}
|
}
|
||||||
} else if (row.id) {
|
} else if (row.id) {
|
||||||
tasks.push(delThreshold(row.id));
|
toDelete.push(row.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const tasks = [];
|
||||||
|
if (toAdd.length) tasks.push(addThreshold(toAdd));
|
||||||
|
if (toUpdate.length) tasks.push(updateThreshold(toUpdate));
|
||||||
|
if (toDelete.length) tasks.push(delThreshold(toDelete.join(',')));
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
this.$modal.msgWarning('没有需要保存的阈值');
|
this.$modal.msgWarning('没有需要保存的阈值');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -54,8 +54,8 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- 基准点设备:相对测量类设备(静力水准等)需指定基准点,留空表示绝对量测量 -->
|
<!-- 基准点设备:相对测量类设备(静力水准等)需指定基准点,留空表示绝对量测量。不做 deviceType 限制,任意子设备均可配置 -->
|
||||||
<el-form-item v-if="form.deviceType != 2" prop="baselineDeviceId">
|
<el-form-item prop="baselineDeviceId">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<el-tooltip placement="top">
|
<el-tooltip placement="top">
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
@ -255,7 +255,7 @@
|
|||||||
|
|
||||||
<!-- 设备阈值(优先级高于产品阈值,留空则继承产品阈值) -->
|
<!-- 设备阈值(优先级高于产品阈值,留空则继承产品阈值) -->
|
||||||
<el-tab-pane name="deviceThreshold" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
|
<el-tab-pane name="deviceThreshold" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
|
||||||
<span slot="label">{{ $t('device.device-edit.148398-85') }}</span>
|
<span slot="label"><span style="color: red">* </span>{{ $t('device.device-edit.148398-85') }}</span>
|
||||||
<threshold-config
|
<threshold-config
|
||||||
ref="deviceThreshold"
|
ref="deviceThreshold"
|
||||||
:product-id="form.productId"
|
:product-id="form.productId"
|
||||||
|
|||||||
@ -39,6 +39,13 @@
|
|||||||
<el-input style="width: 120px; text-align: center" :disabled="!isSet" v-model="scope.row.slaveId" size="small" :placeholder="$t('device.sub.083943-2')"></el-input>
|
<el-input style="width: 120px; text-align: center" :disabled="!isSet" v-model="scope.row.slaveId" size="small" :placeholder="$t('device.sub.083943-2')"></el-input>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="基准点" align="center" width="90px" :render-header="renderBaselineHeader">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-radio v-model="baselineMap[scope.row.subProductId]" :label="scope.row.subDeviceId" :disabled="!isSet">
|
||||||
|
<span></span>
|
||||||
|
</el-radio>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column :label="$t('device.sub.onlineStatus')" align="center" prop="status">
|
<el-table-column :label="$t('device.sub.onlineStatus')" align="center" prop="status">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />
|
<dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />
|
||||||
@ -69,6 +76,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listGateway, delGateway, editGatewayBatch } from '@/api/iot/gateway';
|
import { listGateway, delGateway, editGatewayBatch } from '@/api/iot/gateway';
|
||||||
|
import { updateDeviceBaselineBatch } from '@/api/iot/device';
|
||||||
|
|
||||||
import subDeviceList from './sub-device-list';
|
import subDeviceList from './sub-device-list';
|
||||||
import subDeviceCreate from './sub-device-create';
|
import subDeviceCreate from './sub-device-create';
|
||||||
@ -119,6 +127,8 @@ export default {
|
|||||||
isSet: false,
|
isSet: false,
|
||||||
// 子产品行合并信息
|
// 子产品行合并信息
|
||||||
productSpans: [],
|
productSpans: [],
|
||||||
|
// 各子产品的基准点设备id(同子产品单选),key=subProductId,value=subDeviceId
|
||||||
|
baselineMap: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -143,8 +153,54 @@ export default {
|
|||||||
this.total = response.total;
|
this.total = response.total;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.computeProductSpans();
|
this.computeProductSpans();
|
||||||
|
this.computeBaselineMap();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 由后端回查数据推导各子产品的当前基准点。
|
||||||
|
* 约定:基准点设备 baselineDeviceId=0;同组其余设备 baselineDeviceId=基准点设备id。
|
||||||
|
* 因此某子产品下被多数设备引用的 deviceId 即为基准点。
|
||||||
|
*/
|
||||||
|
computeBaselineMap() {
|
||||||
|
const map = {};
|
||||||
|
const list = this.deviceList || [];
|
||||||
|
const groups = {};
|
||||||
|
list.forEach((d) => {
|
||||||
|
const pid = d.subProductId;
|
||||||
|
if (pid == null) return;
|
||||||
|
(groups[pid] = groups[pid] || []).push(d);
|
||||||
|
});
|
||||||
|
Object.keys(groups).forEach((pid) => {
|
||||||
|
const refCount = {};
|
||||||
|
groups[pid].forEach((d) => {
|
||||||
|
const base = d.baselineDeviceId;
|
||||||
|
if (base && base !== 0) {
|
||||||
|
refCount[base] = (refCount[base] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let baseline = null;
|
||||||
|
let max = 0;
|
||||||
|
Object.keys(refCount).forEach((devId) => {
|
||||||
|
if (refCount[devId] > max) {
|
||||||
|
max = refCount[devId];
|
||||||
|
baseline = Number(devId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map[pid] = baseline;
|
||||||
|
});
|
||||||
|
this.baselineMap = map;
|
||||||
|
},
|
||||||
|
/** 基准点列头带说明 */
|
||||||
|
renderBaselineHeader(h) {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
'基准点 ',
|
||||||
|
h('i', { class: 'el-icon-question', style: 'cursor:help;color:#909399;' }),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
computeProductSpans() {
|
computeProductSpans() {
|
||||||
const spans = [];
|
const spans = [];
|
||||||
const list = this.deviceList || [];
|
const list = this.deviceList || [];
|
||||||
@ -238,11 +294,39 @@ export default {
|
|||||||
/**保存子设备地址设置*/
|
/**保存子设备地址设置*/
|
||||||
saveSetting() {
|
saveSetting() {
|
||||||
this.isSet = !this.isSet;
|
this.isSet = !this.isSet;
|
||||||
editGatewayBatch(this.deviceList).then((response) => {
|
const baselinePayload = this.buildBaselinePayload();
|
||||||
|
const tasks = [editGatewayBatch(this.deviceList)];
|
||||||
|
if (baselinePayload.length > 0) {
|
||||||
|
tasks.push(updateDeviceBaselineBatch(baselinePayload));
|
||||||
|
}
|
||||||
|
Promise.all(tasks).then(() => {
|
||||||
this.getList();
|
this.getList();
|
||||||
this.$modal.msgSuccess(this.$t('saveSuccess'));
|
this.$modal.msgSuccess(this.$t('saveSuccess'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 依据 baselineMap 组装基准点回写数据。
|
||||||
|
* 同一子产品下:基准点设备 baselineDeviceId=0;其余设备 baselineDeviceId=基准点设备id。
|
||||||
|
*/
|
||||||
|
buildBaselinePayload() {
|
||||||
|
const groups = {};
|
||||||
|
(this.deviceList || []).forEach((d) => {
|
||||||
|
const pid = d.subProductId;
|
||||||
|
if (pid == null) return;
|
||||||
|
(groups[pid] = groups[pid] || []).push(d);
|
||||||
|
});
|
||||||
|
const payload = [];
|
||||||
|
Object.keys(groups).forEach((pid) => {
|
||||||
|
const baseline = this.baselineMap[pid];
|
||||||
|
groups[pid].forEach((d) => {
|
||||||
|
payload.push({
|
||||||
|
deviceId: d.subDeviceId,
|
||||||
|
baselineDeviceId: d.subDeviceId === baseline ? 0 : (baseline || 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
cancelSetting() {
|
cancelSetting() {
|
||||||
this.isSet = !this.isSet;
|
this.isSet = !this.isSet;
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user