diff --git a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java index 6a75b38e..7ef51950 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/analyzer/AbstractDeviceAnalyzer.java @@ -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 格式化) * + *

兼容旧签名:默认按主指标口径解析单位。

+ * * @param data 该物模型的时序数据(可能为相对换算后的序列),用于多口径速率计算 * @param durationSeconds 统计窗口时长(秒) */ protected MetricPointData buildMetricPoint(Statistics stats, WarningAnalysis warning, ReportContext context, List 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 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(); } + /** + * 解析物模型展示单位 + * + *

{@link #backfillModelInfo} 已把空单位按报告主类别兜底写入了 {@code Statistics.unit}, + * 因此这里必须回到物模型原始配置判断,否则水温、设备电压、角度等辅助量会被错标成主类别单位 + * (如静力水准的 mm)。

+ * + * + */ + 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; + } + /** * 生成结论列表 */ diff --git a/fastbee-report/src/main/java/com/fastbee/report/chart/ChartGenerator.java b/fastbee-report/src/main/java/com/fastbee/report/chart/ChartGenerator.java index 615844a0..e55ea432 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/chart/ChartGenerator.java +++ b/fastbee-report/src/main/java/com/fastbee/report/chart/ChartGenerator.java @@ -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,禁止前端生成图表回传后端) * + *

中文字体策略:

+ *
    + *
  1. 优先加载 classpath 内置 {@code fonts/SimHei.ttf}(跨平台一致,避免 Linux headless 无中文字形导致乱码)
  2. + *
  3. 失败则按平台探测系统字体(macOS PingFang / Linux WenQuanYi·Noto / Windows SimSun)
  4. + *
  5. 最后回退 SansSerif(仍乱码但避免空指针)
  6. + *
+ * * @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 兜底 + * + *

JFreeChart 在 Linux headless 环境下默认使用 SansSerif 逻辑字体, + * 若系统未安装中文字体映射,标题/图例/坐标轴中文会渲染成方框(豆腐块)。 + * 故优先把项目内置的 {@code fonts/SimHei.ttf} 注册到 GraphicsEnvironment, + * 保证图表在任何部署环境下都能正常显示中文。

+ */ + 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 字体并注册到本地图形环境 + * + *

{@code Font.createFont(int, File)} 不接受 InputStream,需先落临时文件。

+ */ + 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 兜底"); + } + } + } } diff --git a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java index d3eb17d9..0c4768f0 100644 --- a/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java +++ b/fastbee-report/src/main/java/com/fastbee/report/renderer/WordRenderer.java @@ -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 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 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("断面")); + } + + /** + * 按内容权重给测点表分配列宽(固定布局) + * + *

poi-tl 的 {@code Tables.ofA4Width()} 会把 A4 宽度均分给所有列:20 列时每列仅约 414 twip + * (≈7.3mm),9pt 汉字表头被迫逐字竖排。此处按表头语义给列赋权重,宽度按权重比例分配, + * 并设置 {@code tblLayout=fixed} 让 Word 严格按指定宽度渲染。

+ */ + private void applyColumnWidths(XWPFTable table) { + List 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 charts = new HashMap<>(); @@ -290,11 +406,14 @@ public class WordRenderer implements ReportRenderer { *
    *
  • 断面:仅按设备分组(iot_group)导出时存在,否则不生成该列;
  • *
  • 主监测指标:初始值 / 本次值 / 变化量 / 变化速率(完整四列,主导报告结论);
  • - *
  • 其余物模型(如温度等影响量):本次值 / 变化量,保留物理量完整度;
  • + *
  • 其余物模型(如温度等影响量):本次值(变化量) 合并为一列,保留物理量完整度又不撑爆列宽;
  • *
  • 初始观测时间、监测结论:以主指标为准,结论取该测点所有物模型中的最严重等级。
  • *
+ * + *

数值精度与正文一致,按监测类别取 {@link ValueFormatter} 配置(静力水准 2 位), + * 避免 6 位小数把列撑宽导致内容折行。

*/ - private TableRenderData buildMetricTable(AnalysisResult analysis) { + private TableRenderData buildMetricTable(AnalysisResult analysis, MonitorCategory category) { List 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> 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 本次值(变化量)},变化量带正负号 + * + *

例:{@code 28.56(+0.25)}、{@code 11.80(0)};本次值缺失时返回 "-"。

+ */ + 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 位…)并去除尾零 + * + *

与正文({@link ValueFormatter})保持同一口径;无类别配置时保留 6 位。

+ */ + 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(); diff --git a/fastbee-report/src/main/resources/fonts/SimHei.ttf b/fastbee-report/src/main/resources/fonts/SimHei.ttf new file mode 100644 index 00000000..c5030aea Binary files /dev/null and b/fastbee-report/src/main/resources/fonts/SimHei.ttf differ diff --git a/yack-web/src/views/dataCenter/reportHistory.vue b/yack-web/src/views/dataCenter/reportHistory.vue index ffa7b429..9e155449 100644 --- a/yack-web/src/views/dataCenter/reportHistory.vue +++ b/yack-web/src/views/dataCenter/reportHistory.vue @@ -47,7 +47,7 @@ {{ statusText(scope.row.status) }} - +