reportEngine:报告内容 字符、表格样式等调整
This commit is contained in:
parent
e81c128e1c
commit
426cdb2567
@ -438,7 +438,8 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
|
||||
}
|
||||
|
||||
MetricPointData point = buildMetricPoint(stats, result.getWarnings().get(identity), context,
|
||||
dataByIdentity == null ? null : dataByIdentity.get(identity), durationSeconds);
|
||||
dataByIdentity == null ? null : dataByIdentity.get(identity), durationSeconds,
|
||||
identity.equals(primary));
|
||||
// 主监测指标标记(温度等辅助量为影响量,primary=false)
|
||||
point.setPrimary(identity.equals(primary));
|
||||
if (identity.equals(primary)) {
|
||||
@ -477,12 +478,26 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
|
||||
/**
|
||||
* 构建单个测点数据(使用 ValueFormatter 格式化)
|
||||
*
|
||||
* <p>兼容旧签名:默认按主指标口径解析单位。</p>
|
||||
*
|
||||
* @param data 该物模型的时序数据(可能为相对换算后的序列),用于多口径速率计算
|
||||
* @param durationSeconds 统计窗口时长(秒)
|
||||
*/
|
||||
protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context,
|
||||
List<MetricData> data, long durationSeconds) {
|
||||
String resolvedUnit = valueFormatter.resolveUnit(stats.getUnit(), context.getCategory());
|
||||
return buildMetricPoint(stats, warning, context, data, durationSeconds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单个测点数据(使用 ValueFormatter 格式化)
|
||||
*
|
||||
* @param data 该物模型的时序数据(可能为相对换算后的序列),用于多口径速率计算
|
||||
* @param durationSeconds 统计窗口时长(秒)
|
||||
* @param primaryMetric 是否为主监测指标(决定单位缺失时是否按监测类别兜底)
|
||||
*/
|
||||
protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context,
|
||||
List<MetricData> data, long durationSeconds, boolean primaryMetric) {
|
||||
String resolvedUnit = resolveMetricUnit(stats, context, primaryMetric);
|
||||
String rateUnit = valueFormatter.rateUnit(resolvedUnit);
|
||||
|
||||
// 主口径:回归斜率;参考口径:本期平均速率、最大分段速率
|
||||
@ -512,6 +527,46 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析物模型展示单位
|
||||
*
|
||||
* <p>{@link #backfillModelInfo} 已把空单位按<b>报告主类别</b>兜底写入了 {@code Statistics.unit},
|
||||
* 因此这里必须回到物模型原始配置判断,否则水温、设备电压、角度等辅助量会被错标成主类别单位
|
||||
* (如静力水准的 mm)。</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>物模型配置了单位:直接使用;</li>
|
||||
* <li>未配置且为主指标:按监测类别兜底(静力水准 → mm);</li>
|
||||
* <li>未配置且为辅助量:留空,宁可不显示也不能显示错误单位。</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String resolveMetricUnit(Statistics stats, ReportContext context, boolean primaryMetric) {
|
||||
ThingsModel model = findModelByIdentifier(context, stats.getIdentifier());
|
||||
String modelUnit = model == null ? null : model.getUnit();
|
||||
if (modelUnit != null && !modelUnit.trim().isEmpty()) {
|
||||
return modelUnit.trim();
|
||||
}
|
||||
if (!primaryMetric) {
|
||||
return "";
|
||||
}
|
||||
return valueFormatter.resolveUnit(null, context.getCategory());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按物模型标识符查找物模型定义
|
||||
*/
|
||||
private ThingsModel findModelByIdentifier(ReportContext context, String identifier) {
|
||||
if (context.getThingsModels() == null || identifier == null) {
|
||||
return null;
|
||||
}
|
||||
for (ThingsModel model : context.getThingsModels()) {
|
||||
if (identifier.equals(model.getIdentifier())) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成结论列表
|
||||
*/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.fastbee.report.chart;
|
||||
|
||||
import com.fastbee.report.data.MetricData;
|
||||
import lombok.extern.log4j.Log4j;
|
||||
import org.jfree.chart.ChartFactory;
|
||||
import org.jfree.chart.ChartUtils;
|
||||
import org.jfree.chart.JFreeChart;
|
||||
@ -14,8 +15,12 @@ import org.springframework.stereotype.Component;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -23,9 +28,17 @@ import java.util.Map;
|
||||
/**
|
||||
* Java 图表生成器(JFreeChart → PNG,禁止前端生成图表回传后端)
|
||||
*
|
||||
* <p>中文字体策略:</p>
|
||||
* <ol>
|
||||
* <li>优先加载 classpath 内置 {@code fonts/SimHei.ttf}(跨平台一致,避免 Linux headless 无中文字形导致乱码)</li>
|
||||
* <li>失败则按平台探测系统字体(macOS PingFang / Linux WenQuanYi·Noto / Windows SimSun)</li>
|
||||
* <li>最后回退 SansSerif(仍乱码但避免空指针)</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author FastBee
|
||||
* @date 2026-08-16
|
||||
*/
|
||||
@Log4j
|
||||
@Component
|
||||
public class ChartGenerator {
|
||||
|
||||
@ -34,8 +47,22 @@ public class ChartGenerator {
|
||||
System.setProperty("java.awt.headless", "true");
|
||||
}
|
||||
|
||||
private static final Font CN_FONT = new Font("SansSerif", Font.PLAIN, 13);
|
||||
private static final Font CN_TITLE_FONT = new Font("SansSerif", Font.BOLD, 16);
|
||||
/** classpath 内置中文字体路径 */
|
||||
private static final String BUNDLED_FONT_PATH = "/fonts/SimHei.ttf";
|
||||
|
||||
/** 系统字体探测候选(按平台/通用度排序) */
|
||||
private static final String[] SYSTEM_FONT_CANDIDATES = {
|
||||
"PingFang SC", "STHeiti", "Heiti SC", // macOS
|
||||
"WenQuanYi Micro Hei", "WenQuanYi Zen Hei", // Linux (fontconfig)
|
||||
"Noto Sans CJK SC", "Noto Sans SC",
|
||||
"Source Han Sans SC", "Source Han Sans CN",
|
||||
"Microsoft YaHei", "SimSun", "SimHei" // Windows
|
||||
};
|
||||
|
||||
private static final Font BASE_FONT = loadCnFont();
|
||||
private static final Font CN_FONT = BASE_FONT.deriveFont(Font.PLAIN, 13f);
|
||||
private static final Font CN_TITLE_FONT = BASE_FONT.deriveFont(Font.BOLD, 16f);
|
||||
private static final Font CN_TICK_FONT = BASE_FONT.deriveFont(Font.PLAIN, 11f);
|
||||
|
||||
/**
|
||||
* 生成时序曲线图 PNG
|
||||
@ -65,9 +92,9 @@ public class ChartGenerator {
|
||||
((XYLineAndShapeRenderer) plot.getRenderer()).setDefaultStroke(new BasicStroke(1.5f));
|
||||
}
|
||||
plot.getDomainAxis().setLabelFont(CN_FONT);
|
||||
plot.getDomainAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
plot.getDomainAxis().setTickLabelFont(CN_TICK_FONT);
|
||||
plot.getRangeAxis().setLabelFont(CN_FONT);
|
||||
plot.getRangeAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
plot.getRangeAxis().setTickLabelFont(CN_TICK_FONT);
|
||||
chart.getLegend().setItemFont(CN_FONT);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@ -112,9 +139,9 @@ public class ChartGenerator {
|
||||
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||
plot.getDomainAxis().setLabelFont(CN_FONT);
|
||||
plot.getDomainAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
plot.getDomainAxis().setTickLabelFont(CN_TICK_FONT);
|
||||
plot.getRangeAxis().setLabelFont(CN_FONT);
|
||||
plot.getRangeAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
||||
plot.getRangeAxis().setTickLabelFont(CN_TICK_FONT);
|
||||
chart.getLegend().setItemFont(CN_FONT);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@ -132,4 +159,67 @@ public class ChartGenerator {
|
||||
public byte[] blankChart(String title) {
|
||||
return multiSeriesChart(title, null, new java.util.LinkedHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载中文字体:classpath 内置优先 → 系统字体探测 → SansSerif 兜底
|
||||
*
|
||||
* <p>JFreeChart 在 Linux headless 环境下默认使用 SansSerif 逻辑字体,
|
||||
* 若系统未安装中文字体映射,标题/图例/坐标轴中文会渲染成方框(豆腐块)。
|
||||
* 故优先把项目内置的 {@code fonts/SimHei.ttf} 注册到 GraphicsEnvironment,
|
||||
* 保证图表在任何部署环境下都能正常显示中文。</p>
|
||||
*/
|
||||
private static Font loadCnFont() {
|
||||
// 1. classpath 内置字体(跨环境一致)
|
||||
Font bundled = loadBundledFont();
|
||||
if (bundled != null) {
|
||||
return bundled;
|
||||
}
|
||||
// 2. 系统字体探测
|
||||
for (String name : SYSTEM_FONT_CANDIDATES) {
|
||||
Font f = new Font(name, Font.PLAIN, 13);
|
||||
if (f.canDisplay('中') && f.canDisplay('静')) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
// 3. 回退(仍可能乱码,但避免空指针)
|
||||
return new Font("SansSerif", Font.PLAIN, 13);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 classpath 加载内置 TTF 字体并注册到本地图形环境
|
||||
*
|
||||
* <p>{@code Font.createFont(int, File)} 不接受 InputStream,需先落临时文件。</p>
|
||||
*/
|
||||
private static Font loadBundledFont() {
|
||||
java.io.File tmp = null;
|
||||
try (InputStream in = ChartGenerator.class.getResourceAsStream(BUNDLED_FONT_PATH)) {
|
||||
if (in == null) {
|
||||
return null;
|
||||
}
|
||||
tmp = java.io.File.createTempFile("report-cn-font-", ".ttf");
|
||||
tmp.deleteOnExit();
|
||||
try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
}
|
||||
Font f = Font.createFont(Font.TRUETYPE_FONT, tmp);
|
||||
try {
|
||||
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(f);
|
||||
} catch (Exception ignored) {
|
||||
// 注册失败不影响 createFont 已返回的字体使用
|
||||
}
|
||||
return f;
|
||||
} catch (Exception e) {
|
||||
// 加载失败回退系统探测,不影响图表生成主流程
|
||||
return null;
|
||||
} finally {
|
||||
if (tmp != null && tmp.exists()) {
|
||||
// JVM 持有临时文件句柄,createFont 后可删除;保留 deleteOnExit 兜底
|
||||
log.error("JVM 持有临时文件句柄,createFont 后可删除;保留 deleteOnExit 兜底");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,9 @@ import com.deepoove.poi.data.Rows;
|
||||
import com.deepoove.poi.data.style.PictureStyle;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
import com.deepoove.poi.policy.TableRenderPolicy;
|
||||
import com.fastbee.common.enums.MonitorCategory;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.report.analysis.ValueFormatter;
|
||||
import com.fastbee.report.exception.ReportException;
|
||||
import com.fastbee.report.model.AnalysisResult;
|
||||
import com.fastbee.report.model.Conclusion;
|
||||
@ -25,6 +27,13 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblLayoutType;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblLayoutType;
|
||||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@ -32,6 +41,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
@ -40,7 +50,6 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Word 渲染器(poi-tl 模板渲染 + JFreeChart PNG 嵌入,禁止查库)
|
||||
@ -54,6 +63,9 @@ public class WordRenderer implements ReportRenderer {
|
||||
@Resource
|
||||
private WordTemplateResolver templateResolver;
|
||||
|
||||
/** 类别精度解析(静力水准/深部位移 2 位、温度 1 位…),表格数值与正文口径保持一致 */
|
||||
private static final ValueFormatter VALUE_FORMATTER = new ValueFormatter();
|
||||
|
||||
@Override
|
||||
public byte[] render(ReportModel model) {
|
||||
byte[] template = templateResolver.resolve(model.getTemplateCode());
|
||||
@ -68,8 +80,8 @@ public class WordRenderer implements ReportRenderer {
|
||||
try {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
tpl.write(out);
|
||||
// 后处理:清除循环表格残留的空行,避免排版出现空白行
|
||||
return cleanupEmptyTableRows(out.toByteArray());
|
||||
// 后处理:清除循环表格残留空行 + 测点表列宽分配(避免均分导致内容竖排)
|
||||
return postProcessDocument(out.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new ReportException("Word 渲染失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
@ -81,18 +93,26 @@ public class WordRenderer implements ReportRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/** A4 纵向页面正文可用宽度(twip),与模板中固定表格宽度保持一致 */
|
||||
private static final int A4_CONTENT_WIDTH_TWIP = 9026;
|
||||
|
||||
/**
|
||||
* 清除表格中的空白行(poi-tl 循环表格的标记行渲染后会残留为空行)
|
||||
* 渲染后处理:清空行 + 测点表列宽分配
|
||||
*/
|
||||
private byte[] cleanupEmptyTableRows(byte[] docx) {
|
||||
private byte[] postProcessDocument(byte[] docx) {
|
||||
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docx))) {
|
||||
for (XWPFTable table : document.getTables()) {
|
||||
// 1. 清除 poi-tl 循环表格的标记行渲染后残留的空行
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
for (int i = rows.size() - 1; i >= 0; i--) {
|
||||
if (isRowBlank(rows.get(i))) {
|
||||
table.removeRow(i);
|
||||
}
|
||||
}
|
||||
// 2. 测点表列数多,按内容权重分配列宽,避免 poi-tl 默认均分导致每列过窄、表头竖排
|
||||
if (isMetricPointTable(table)) {
|
||||
applyColumnWidths(table);
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
document.write(buffer);
|
||||
@ -103,6 +123,102 @@ public class WordRenderer implements ReportRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别测点表:表头首列为「测点编号」(带断面时为「断面」)
|
||||
*/
|
||||
private boolean isMetricPointTable(XWPFTable table) {
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
if (rows.isEmpty() || rows.get(0).getTableCells().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String firstHeader = rows.get(0).getCell(0).getText();
|
||||
return firstHeader != null && (firstHeader.contains("测点编号") || firstHeader.contains("断面"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按内容权重给测点表分配列宽(固定布局)
|
||||
*
|
||||
* <p>poi-tl 的 {@code Tables.ofA4Width()} 会把 A4 宽度均分给所有列:20 列时每列仅约 414 twip
|
||||
* (≈7.3mm),9pt 汉字表头被迫逐字竖排。此处按表头语义给列赋权重,宽度按权重比例分配,
|
||||
* 并设置 {@code tblLayout=fixed} 让 Word 严格按指定宽度渲染。</p>
|
||||
*/
|
||||
private void applyColumnWidths(XWPFTable table) {
|
||||
List<XWPFTableRow> rows = table.getRows();
|
||||
int columnCount = rows.get(0).getTableCells().size();
|
||||
if (columnCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 表格整体:固定布局 + A4 可用宽度
|
||||
CTTblPr tblPr = table.getCTTbl().getTblPr();
|
||||
if (tblPr == null) {
|
||||
tblPr = table.getCTTbl().addNewTblPr();
|
||||
}
|
||||
CTTblLayoutType layout = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
|
||||
layout.setType(STTblLayoutType.FIXED);
|
||||
CTTblWidth tableWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
|
||||
tableWidth.setType(STTblWidth.DXA);
|
||||
tableWidth.setW(BigInteger.valueOf(A4_CONTENT_WIDTH_TWIP));
|
||||
|
||||
// 计算各列权重
|
||||
int[] weights = new int[columnCount];
|
||||
int totalWeight = 0;
|
||||
for (int i = 0; i < columnCount; i++) {
|
||||
weights[i] = columnWeight(rows.get(0).getCell(i).getText());
|
||||
totalWeight += weights[i];
|
||||
}
|
||||
if (totalWeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CTTblGrid grid = table.getCTTbl().getTblGrid();
|
||||
for (int i = 0; i < columnCount; i++) {
|
||||
int width = Math.round((float) A4_CONTENT_WIDTH_TWIP * weights[i] / totalWeight);
|
||||
if (grid != null && i < grid.sizeOfGridColArray()) {
|
||||
grid.getGridColArray(i).setW(BigInteger.valueOf(width));
|
||||
}
|
||||
for (XWPFTableRow row : rows) {
|
||||
if (i >= row.getTableCells().size()) {
|
||||
continue;
|
||||
}
|
||||
CTTcPr tcPr = row.getCell(i).getCTTc().isSetTcPr()
|
||||
? row.getCell(i).getCTTc().getTcPr()
|
||||
: row.getCell(i).getCTTc().addNewTcPr();
|
||||
CTTblWidth cellWidth = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
|
||||
cellWidth.setType(STTblWidth.DXA);
|
||||
cellWidth.setW(BigInteger.valueOf(width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列宽权重:表头文字越长、内容越宽,权重越高
|
||||
*/
|
||||
private int columnWeight(String headerText) {
|
||||
String text = headerText == null ? "" : headerText.trim();
|
||||
if (text.startsWith("断面")) {
|
||||
return 12;
|
||||
}
|
||||
if (text.startsWith("测点编号")) {
|
||||
return 12;
|
||||
}
|
||||
if (text.startsWith("测点名称")) {
|
||||
return 14;
|
||||
}
|
||||
if (text.startsWith("初始观测时间")) {
|
||||
return 16;
|
||||
}
|
||||
if (text.startsWith("监测结论")) {
|
||||
return 10;
|
||||
}
|
||||
// 辅助量合并列:本次值(变化量),内容较长
|
||||
if (text.contains("(变化量)")) {
|
||||
return 13;
|
||||
}
|
||||
// 主指标:初始值 / 本次值 / 变化量 / 变化速率(数值最长,权重最高)
|
||||
return 11;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断表格行是否完全空白
|
||||
*/
|
||||
@ -236,7 +352,7 @@ public class WordRenderer implements ReportRenderer {
|
||||
}
|
||||
data.put("metricPoints", metricPointsList);
|
||||
// 动态列测点表:列由导出设备的物模型(物理量)决定,一行一个测点(iot_device)
|
||||
data.put("metricTable", buildMetricTable(analysis));
|
||||
data.put("metricTable", buildMetricTable(analysis, model.getCategory()));
|
||||
|
||||
// 图表:兼容 timeSeriesChart(主指标)/ chart2 / chart3(分组指标)/ crackTrend(旧模板)
|
||||
Map<String, Object> charts = new HashMap<>();
|
||||
@ -290,11 +406,14 @@ public class WordRenderer implements ReportRenderer {
|
||||
* <ul>
|
||||
* <li>断面:仅按设备分组(iot_group)导出时存在,否则不生成该列;</li>
|
||||
* <li>主监测指标:初始值 / 本次值 / 变化量 / 变化速率(完整四列,主导报告结论);</li>
|
||||
* <li>其余物模型(如温度等影响量):本次值 / 变化量,保留物理量完整度;</li>
|
||||
* <li>其余物模型(如温度等影响量):本次值(变化量) 合并为一列,保留物理量完整度又不撑爆列宽;</li>
|
||||
* <li>初始观测时间、监测结论:以主指标为准,结论取该测点所有物模型中的最严重等级。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>数值精度与正文一致,按监测类别取 {@link ValueFormatter} 配置(静力水准 2 位),
|
||||
* 避免 6 位小数把列撑宽导致内容折行。</p>
|
||||
*/
|
||||
private TableRenderData buildMetricTable(AnalysisResult analysis) {
|
||||
private TableRenderData buildMetricTable(AnalysisResult analysis, MonitorCategory category) {
|
||||
List<MetricPointData> points = analysis.getMetricPoints() == null
|
||||
? new ArrayList<>() : analysis.getMetricPoints();
|
||||
if (points.isEmpty()) {
|
||||
@ -344,16 +463,17 @@ public class WordRenderer implements ReportRenderer {
|
||||
header.add(name + "·变化量" + unitSuffix(unit));
|
||||
header.add(name + "·变化速率" + unitSuffix(sample == null ? "" : nvl(sample.getRateUnit())));
|
||||
} else {
|
||||
header.add(name + "·本次值" + unitSuffix(unit));
|
||||
header.add(name + "·变化量" + unitSuffix(unit));
|
||||
// 辅助量(水温/电压/角度等)本次值与变化量合并为一列,避免列数膨胀后每列过窄
|
||||
header.add(name + "·本次值(变化量)" + unitSuffix(unit));
|
||||
}
|
||||
}
|
||||
header.add("初始观测时间");
|
||||
header.add("监测结论");
|
||||
|
||||
Tables.TableBuilder builder = Tables.ofA4Width().center();
|
||||
// 表头字号小于数据行,减少长表头(如"设备电压·本次值(变化量)(V)")的折行行数
|
||||
builder.addRow(Rows.of(header.toArray(new String[0]))
|
||||
.bgColor("DCE6F1").textBold().center().textFontSize(9).create());
|
||||
.bgColor("DCE6F1").textBold().center().textFontSize(8).create());
|
||||
|
||||
// 4. 数据行
|
||||
for (Map.Entry<String, List<MetricPointData>> entry : byPoint.entrySet()) {
|
||||
@ -378,22 +498,20 @@ public class WordRenderer implements ReportRenderer {
|
||||
row.add("-");
|
||||
row.add("-");
|
||||
row.add("-");
|
||||
} else {
|
||||
row.add("-");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (identifier.equals(primaryIdentifier)) {
|
||||
row.add(str(point.getInitialValue()));
|
||||
row.add(str(point.getCurrentValue()));
|
||||
row.add(str(point.getChangeValue()));
|
||||
row.add(str(point.getChangeRate()));
|
||||
row.add(strForTable(point.getInitialValue(), category));
|
||||
row.add(strForTable(point.getCurrentValue(), category));
|
||||
row.add(strForTable(point.getChangeValue(), category));
|
||||
row.add(strForTable(point.getChangeRate(), category));
|
||||
} else {
|
||||
row.add(str(point.getCurrentValue()));
|
||||
row.add(str(point.getChangeValue()));
|
||||
// 辅助量合并列:本次值(变化量)
|
||||
row.add(mergedValue(point.getCurrentValue(), point.getChangeValue(), category));
|
||||
}
|
||||
}
|
||||
row.add(fmt(primaryTime(rowPoints, primaryIdentifier)));
|
||||
row.add(fmtShort(primaryTime(rowPoints, primaryIdentifier)));
|
||||
row.add(worstStatusText(rowPoints, primaryIdentifier));
|
||||
builder.addRow(Rows.of(row.toArray(new String[0])).center().textFontSize(9).create());
|
||||
}
|
||||
@ -460,6 +578,50 @@ public class WordRenderer implements ReportRenderer {
|
||||
return date == null ? "-" : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
|
||||
}
|
||||
|
||||
/** 时间格式化:yyyy-MM-dd HH:mm,空值返回 "-" */
|
||||
private String fmtShort(Date date) {
|
||||
return date == null ? "-" : new SimpleDateFormat("yyyy-MM-dd HH:mm").format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助量合并单元格:{@code 本次值(变化量)},变化量带正负号
|
||||
*
|
||||
* <p>例:{@code 28.56(+0.25)}、{@code 11.80(0)};本次值缺失时返回 "-"。</p>
|
||||
*/
|
||||
private String mergedValue(BigDecimal current, BigDecimal change, MonitorCategory category) {
|
||||
if (current == null) {
|
||||
return "-";
|
||||
}
|
||||
return strForTable(current, category) + "(" + signed(change, category) + ")";
|
||||
}
|
||||
|
||||
/** 变化量带符号展示(正数显式补 +,便于一眼看出升降) */
|
||||
private String signed(BigDecimal value, MonitorCategory category) {
|
||||
if (value == null) {
|
||||
return "-";
|
||||
}
|
||||
if (value.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return "0";
|
||||
}
|
||||
String text = strForTable(value, category);
|
||||
return text.startsWith("-") ? text : "+" + text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格数值格式化:按监测类别精度(静力水准/深部位移 2 位、温度 1 位…)并去除尾零
|
||||
*
|
||||
* <p>与正文({@link ValueFormatter})保持同一口径;无类别配置时保留 6 位。</p>
|
||||
*/
|
||||
private String strForTable(BigDecimal value, MonitorCategory category) {
|
||||
if (value == null) {
|
||||
return "-";
|
||||
}
|
||||
ValueFormatter.CategoryFormat format = VALUE_FORMATTER.getCategoryFormat(category);
|
||||
int decimals = format == null ? 6 : format.decimalPlaces;
|
||||
BigDecimal formatted = value.setScale(decimals, RoundingMode.HALF_UP).stripTrailingZeros();
|
||||
return formatted.compareTo(BigDecimal.ZERO) == 0 ? "0" : formatted.toPlainString();
|
||||
}
|
||||
|
||||
/** 拼接数值与单位(单位为空时只返回数值,避免尾部空格) */
|
||||
private String withUnit(String value, String unit) {
|
||||
return (unit == null || unit.trim().isEmpty()) ? value : value + " " + unit.trim();
|
||||
|
||||
BIN
fastbee-report/src/main/resources/fonts/SimHei.ttf
Normal file
BIN
fastbee-report/src/main/resources/fonts/SimHei.ttf
Normal file
Binary file not shown.
@ -47,7 +47,7 @@
|
||||
<el-tag :type="statusType(scope.row.status)" size="mini">{{ statusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'SUCCESS'"
|
||||
@ -58,23 +58,23 @@
|
||||
>下载</el-button>
|
||||
<template v-if="scope.row.status === 'FAILED'">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
type="text"
|
||||
size="mini"
|
||||
class="btn-error-detail"
|
||||
@click="handleShowError(scope.row)"
|
||||
>失败原因</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['iot:report:generate']"
|
||||
type="primary"
|
||||
plain
|
||||
type="text"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
:loading="regeneratingNo === scope.row.reportNo"
|
||||
@click="handleRegenerate(scope.row)"
|
||||
>重新生成</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'CREATING'"
|
||||
plain
|
||||
type="text"
|
||||
icon="el-icon-refresh"
|
||||
size="mini"
|
||||
@click="getList()"
|
||||
@ -300,6 +300,20 @@ export default {
|
||||
.data-center-report-history .el-card {
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
/* 操作列文字按钮:去掉左右内边距,保证「失败原因 + 重新生成」同行不换行 */
|
||||
.data-center-report-history .el-table .el-button--text {
|
||||
padding: 0;
|
||||
}
|
||||
.data-center-report-history .el-table .el-button--text + .el-button--text {
|
||||
margin-left: 12px;
|
||||
}
|
||||
/* 「失败原因」只是查看,用弱化的红色文字,避免和「重新生成」抢视觉焦点 */
|
||||
.data-center-report-history .el-table .btn-error-detail {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.data-center-report-history .el-table .btn-error-detail:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
.report-error-detail .detail-item {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user