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,
|
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)
|
// 主监测指标标记(温度等辅助量为影响量,primary=false)
|
||||||
point.setPrimary(identity.equals(primary));
|
point.setPrimary(identity.equals(primary));
|
||||||
if (identity.equals(primary)) {
|
if (identity.equals(primary)) {
|
||||||
@ -477,12 +478,26 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
|
|||||||
/**
|
/**
|
||||||
* 构建单个测点数据(使用 ValueFormatter 格式化)
|
* 构建单个测点数据(使用 ValueFormatter 格式化)
|
||||||
*
|
*
|
||||||
|
* <p>兼容旧签名:默认按主指标口径解析单位。</p>
|
||||||
|
*
|
||||||
* @param data 该物模型的时序数据(可能为相对换算后的序列),用于多口径速率计算
|
* @param data 该物模型的时序数据(可能为相对换算后的序列),用于多口径速率计算
|
||||||
* @param durationSeconds 统计窗口时长(秒)
|
* @param durationSeconds 统计窗口时长(秒)
|
||||||
*/
|
*/
|
||||||
protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context,
|
protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context,
|
||||||
List<MetricData> data, long durationSeconds) {
|
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);
|
String rateUnit = valueFormatter.rateUnit(resolvedUnit);
|
||||||
|
|
||||||
// 主口径:回归斜率;参考口径:本期平均速率、最大分段速率
|
// 主口径:回归斜率;参考口径:本期平均速率、最大分段速率
|
||||||
@ -512,6 +527,46 @@ public abstract class AbstractDeviceAnalyzer implements ReportAnalyzer {
|
|||||||
.build();
|
.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;
|
package com.fastbee.report.chart;
|
||||||
|
|
||||||
import com.fastbee.report.data.MetricData;
|
import com.fastbee.report.data.MetricData;
|
||||||
|
import lombok.extern.log4j.Log4j;
|
||||||
import org.jfree.chart.ChartFactory;
|
import org.jfree.chart.ChartFactory;
|
||||||
import org.jfree.chart.ChartUtils;
|
import org.jfree.chart.ChartUtils;
|
||||||
import org.jfree.chart.JFreeChart;
|
import org.jfree.chart.JFreeChart;
|
||||||
@ -14,8 +15,12 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.awt.BasicStroke;
|
import java.awt.BasicStroke;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -23,9 +28,17 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Java 图表生成器(JFreeChart → PNG,禁止前端生成图表回传后端)
|
* 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
|
* @author FastBee
|
||||||
* @date 2026-08-16
|
* @date 2026-08-16
|
||||||
*/
|
*/
|
||||||
|
@Log4j
|
||||||
@Component
|
@Component
|
||||||
public class ChartGenerator {
|
public class ChartGenerator {
|
||||||
|
|
||||||
@ -34,8 +47,22 @@ public class ChartGenerator {
|
|||||||
System.setProperty("java.awt.headless", "true");
|
System.setProperty("java.awt.headless", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Font CN_FONT = new Font("SansSerif", Font.PLAIN, 13);
|
/** classpath 内置中文字体路径 */
|
||||||
private static final Font CN_TITLE_FONT = new Font("SansSerif", Font.BOLD, 16);
|
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
|
* 生成时序曲线图 PNG
|
||||||
@ -65,9 +92,9 @@ public class ChartGenerator {
|
|||||||
((XYLineAndShapeRenderer) plot.getRenderer()).setDefaultStroke(new BasicStroke(1.5f));
|
((XYLineAndShapeRenderer) plot.getRenderer()).setDefaultStroke(new BasicStroke(1.5f));
|
||||||
}
|
}
|
||||||
plot.getDomainAxis().setLabelFont(CN_FONT);
|
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().setLabelFont(CN_FONT);
|
||||||
plot.getRangeAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
plot.getRangeAxis().setTickLabelFont(CN_TICK_FONT);
|
||||||
chart.getLegend().setItemFont(CN_FONT);
|
chart.getLegend().setItemFont(CN_FONT);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
@ -112,9 +139,9 @@ public class ChartGenerator {
|
|||||||
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||||
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||||
plot.getDomainAxis().setLabelFont(CN_FONT);
|
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().setLabelFont(CN_FONT);
|
||||||
plot.getRangeAxis().setTickLabelFont(new Font("SansSerif", Font.PLAIN, 11));
|
plot.getRangeAxis().setTickLabelFont(CN_TICK_FONT);
|
||||||
chart.getLegend().setItemFont(CN_FONT);
|
chart.getLegend().setItemFont(CN_FONT);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
@ -132,4 +159,67 @@ public class ChartGenerator {
|
|||||||
public byte[] blankChart(String title) {
|
public byte[] blankChart(String title) {
|
||||||
return multiSeriesChart(title, null, new java.util.LinkedHashMap<>());
|
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.data.style.PictureStyle;
|
||||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||||
import com.deepoove.poi.policy.TableRenderPolicy;
|
import com.deepoove.poi.policy.TableRenderPolicy;
|
||||||
|
import com.fastbee.common.enums.MonitorCategory;
|
||||||
import com.fastbee.common.utils.StringUtils;
|
import com.fastbee.common.utils.StringUtils;
|
||||||
|
import com.fastbee.report.analysis.ValueFormatter;
|
||||||
import com.fastbee.report.exception.ReportException;
|
import com.fastbee.report.exception.ReportException;
|
||||||
import com.fastbee.report.model.AnalysisResult;
|
import com.fastbee.report.model.AnalysisResult;
|
||||||
import com.fastbee.report.model.Conclusion;
|
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.XWPFTable;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@ -32,6 +41,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -40,7 +50,6 @@ import java.util.HashMap;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Word 渲染器(poi-tl 模板渲染 + JFreeChart PNG 嵌入,禁止查库)
|
* Word 渲染器(poi-tl 模板渲染 + JFreeChart PNG 嵌入,禁止查库)
|
||||||
@ -54,6 +63,9 @@ public class WordRenderer implements ReportRenderer {
|
|||||||
@Resource
|
@Resource
|
||||||
private WordTemplateResolver templateResolver;
|
private WordTemplateResolver templateResolver;
|
||||||
|
|
||||||
|
/** 类别精度解析(静力水准/深部位移 2 位、温度 1 位…),表格数值与正文口径保持一致 */
|
||||||
|
private static final ValueFormatter VALUE_FORMATTER = new ValueFormatter();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] render(ReportModel model) {
|
public byte[] render(ReportModel model) {
|
||||||
byte[] template = templateResolver.resolve(model.getTemplateCode());
|
byte[] template = templateResolver.resolve(model.getTemplateCode());
|
||||||
@ -68,8 +80,8 @@ public class WordRenderer implements ReportRenderer {
|
|||||||
try {
|
try {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
tpl.write(out);
|
tpl.write(out);
|
||||||
// 后处理:清除循环表格残留的空行,避免排版出现空白行
|
// 后处理:清除循环表格残留空行 + 测点表列宽分配(避免均分导致内容竖排)
|
||||||
return cleanupEmptyTableRows(out.toByteArray());
|
return postProcessDocument(out.toByteArray());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ReportException("Word 渲染失败: " + e.getMessage(), e);
|
throw new ReportException("Word 渲染失败: " + e.getMessage(), e);
|
||||||
} finally {
|
} 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))) {
|
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docx))) {
|
||||||
for (XWPFTable table : document.getTables()) {
|
for (XWPFTable table : document.getTables()) {
|
||||||
|
// 1. 清除 poi-tl 循环表格的标记行渲染后残留的空行
|
||||||
List<XWPFTableRow> rows = table.getRows();
|
List<XWPFTableRow> rows = table.getRows();
|
||||||
for (int i = rows.size() - 1; i >= 0; i--) {
|
for (int i = rows.size() - 1; i >= 0; i--) {
|
||||||
if (isRowBlank(rows.get(i))) {
|
if (isRowBlank(rows.get(i))) {
|
||||||
table.removeRow(i);
|
table.removeRow(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 2. 测点表列数多,按内容权重分配列宽,避免 poi-tl 默认均分导致每列过窄、表头竖排
|
||||||
|
if (isMetricPointTable(table)) {
|
||||||
|
applyColumnWidths(table);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
document.write(buffer);
|
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);
|
data.put("metricPoints", metricPointsList);
|
||||||
// 动态列测点表:列由导出设备的物模型(物理量)决定,一行一个测点(iot_device)
|
// 动态列测点表:列由导出设备的物模型(物理量)决定,一行一个测点(iot_device)
|
||||||
data.put("metricTable", buildMetricTable(analysis));
|
data.put("metricTable", buildMetricTable(analysis, model.getCategory()));
|
||||||
|
|
||||||
// 图表:兼容 timeSeriesChart(主指标)/ chart2 / chart3(分组指标)/ crackTrend(旧模板)
|
// 图表:兼容 timeSeriesChart(主指标)/ chart2 / chart3(分组指标)/ crackTrend(旧模板)
|
||||||
Map<String, Object> charts = new HashMap<>();
|
Map<String, Object> charts = new HashMap<>();
|
||||||
@ -290,11 +406,14 @@ public class WordRenderer implements ReportRenderer {
|
|||||||
* <ul>
|
* <ul>
|
||||||
* <li>断面:仅按设备分组(iot_group)导出时存在,否则不生成该列;</li>
|
* <li>断面:仅按设备分组(iot_group)导出时存在,否则不生成该列;</li>
|
||||||
* <li>主监测指标:初始值 / 本次值 / 变化量 / 变化速率(完整四列,主导报告结论);</li>
|
* <li>主监测指标:初始值 / 本次值 / 变化量 / 变化速率(完整四列,主导报告结论);</li>
|
||||||
* <li>其余物模型(如温度等影响量):本次值 / 变化量,保留物理量完整度;</li>
|
* <li>其余物模型(如温度等影响量):本次值(变化量) 合并为一列,保留物理量完整度又不撑爆列宽;</li>
|
||||||
* <li>初始观测时间、监测结论:以主指标为准,结论取该测点所有物模型中的最严重等级。</li>
|
* <li>初始观测时间、监测结论:以主指标为准,结论取该测点所有物模型中的最严重等级。</li>
|
||||||
* </ul>
|
* </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
|
List<MetricPointData> points = analysis.getMetricPoints() == null
|
||||||
? new ArrayList<>() : analysis.getMetricPoints();
|
? new ArrayList<>() : analysis.getMetricPoints();
|
||||||
if (points.isEmpty()) {
|
if (points.isEmpty()) {
|
||||||
@ -344,16 +463,17 @@ public class WordRenderer implements ReportRenderer {
|
|||||||
header.add(name + "·变化量" + unitSuffix(unit));
|
header.add(name + "·变化量" + unitSuffix(unit));
|
||||||
header.add(name + "·变化速率" + unitSuffix(sample == null ? "" : nvl(sample.getRateUnit())));
|
header.add(name + "·变化速率" + unitSuffix(sample == null ? "" : nvl(sample.getRateUnit())));
|
||||||
} else {
|
} else {
|
||||||
header.add(name + "·本次值" + unitSuffix(unit));
|
// 辅助量(水温/电压/角度等)本次值与变化量合并为一列,避免列数膨胀后每列过窄
|
||||||
header.add(name + "·变化量" + unitSuffix(unit));
|
header.add(name + "·本次值(变化量)" + unitSuffix(unit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
header.add("初始观测时间");
|
header.add("初始观测时间");
|
||||||
header.add("监测结论");
|
header.add("监测结论");
|
||||||
|
|
||||||
Tables.TableBuilder builder = Tables.ofA4Width().center();
|
Tables.TableBuilder builder = Tables.ofA4Width().center();
|
||||||
|
// 表头字号小于数据行,减少长表头(如"设备电压·本次值(变化量)(V)")的折行行数
|
||||||
builder.addRow(Rows.of(header.toArray(new String[0]))
|
builder.addRow(Rows.of(header.toArray(new String[0]))
|
||||||
.bgColor("DCE6F1").textBold().center().textFontSize(9).create());
|
.bgColor("DCE6F1").textBold().center().textFontSize(8).create());
|
||||||
|
|
||||||
// 4. 数据行
|
// 4. 数据行
|
||||||
for (Map.Entry<String, List<MetricPointData>> entry : byPoint.entrySet()) {
|
for (Map.Entry<String, List<MetricPointData>> entry : byPoint.entrySet()) {
|
||||||
@ -378,22 +498,20 @@ public class WordRenderer implements ReportRenderer {
|
|||||||
row.add("-");
|
row.add("-");
|
||||||
row.add("-");
|
row.add("-");
|
||||||
row.add("-");
|
row.add("-");
|
||||||
} else {
|
|
||||||
row.add("-");
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (identifier.equals(primaryIdentifier)) {
|
if (identifier.equals(primaryIdentifier)) {
|
||||||
row.add(str(point.getInitialValue()));
|
row.add(strForTable(point.getInitialValue(), category));
|
||||||
row.add(str(point.getCurrentValue()));
|
row.add(strForTable(point.getCurrentValue(), category));
|
||||||
row.add(str(point.getChangeValue()));
|
row.add(strForTable(point.getChangeValue(), category));
|
||||||
row.add(str(point.getChangeRate()));
|
row.add(strForTable(point.getChangeRate(), category));
|
||||||
} else {
|
} 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));
|
row.add(worstStatusText(rowPoints, primaryIdentifier));
|
||||||
builder.addRow(Rows.of(row.toArray(new String[0])).center().textFontSize(9).create());
|
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);
|
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) {
|
private String withUnit(String value, String unit) {
|
||||||
return (unit == null || unit.trim().isEmpty()) ? value : value + " " + unit.trim();
|
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>
|
<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="190" align="center" fixed="right">
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.status === 'SUCCESS'"
|
v-if="scope.row.status === 'SUCCESS'"
|
||||||
@ -58,23 +58,23 @@
|
|||||||
>下载</el-button>
|
>下载</el-button>
|
||||||
<template v-if="scope.row.status === 'FAILED'">
|
<template v-if="scope.row.status === 'FAILED'">
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="text"
|
||||||
plain
|
|
||||||
size="mini"
|
size="mini"
|
||||||
|
class="btn-error-detail"
|
||||||
@click="handleShowError(scope.row)"
|
@click="handleShowError(scope.row)"
|
||||||
>失败原因</el-button>
|
>失败原因</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPermi="['iot:report:generate']"
|
v-hasPermi="['iot:report:generate']"
|
||||||
type="primary"
|
type="text"
|
||||||
plain
|
|
||||||
size="mini"
|
size="mini"
|
||||||
|
icon="el-icon-refresh"
|
||||||
:loading="regeneratingNo === scope.row.reportNo"
|
:loading="regeneratingNo === scope.row.reportNo"
|
||||||
@click="handleRegenerate(scope.row)"
|
@click="handleRegenerate(scope.row)"
|
||||||
>重新生成</el-button>
|
>重新生成</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.status === 'CREATING'"
|
v-if="scope.row.status === 'CREATING'"
|
||||||
plain
|
type="text"
|
||||||
icon="el-icon-refresh"
|
icon="el-icon-refresh"
|
||||||
size="mini"
|
size="mini"
|
||||||
@click="getList()"
|
@click="getList()"
|
||||||
@ -300,6 +300,20 @@ export default {
|
|||||||
.data-center-report-history .el-card {
|
.data-center-report-history .el-card {
|
||||||
min-height: calc(100vh - 110px);
|
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 {
|
.report-error-detail .detail-item {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user