This commit is contained in:
GukSang.Jin
2026-05-05 19:13:53 +08:00
parent d11e1d4270
commit 33c8509df6
4 changed files with 120 additions and 24 deletions

View File

@@ -2,7 +2,7 @@ namespace ConeCalorimeter.Models;
public sealed class ReportInput public sealed class ReportInput
{ {
public string LaboratoryName { get; set; } = "广东安拓普检测中心"; public string LaboratoryName { get; set; } = "检测中心";
public string Operator { get; set; } = string.Empty; public string Operator { get; set; } = string.Empty;

View File

@@ -26,31 +26,54 @@ public sealed class NpoiReportExportService : IReportExportService
"热释放KW/m2", "热释放KW/m2",
"EHC", "EHC",
"损失质量", "损失质量",
"试样温度(℃)" "试样温度(℃)",
"Timestamp",
"点火计时(s)",
"火焰监测",
"孔板流量",
"辐射锥温度 (℃)",
"当前质量",
"初始质量",
"最大热释放",
"热释放速率180",
"热释放速率300",
"C-系数"
]; ];
public void Export(string outputPath, ReportInput input, IReadOnlyList<RealtimeDataRecord> records) public void Export(string outputPath, ReportInput input, IReadOnlyList<RealtimeDataRecord> records)
{ {
var exportRecords = records.Where(IsValidExperimentRecord).ToList();
if (exportRecords.Count == 0)
{
throw new InvalidOperationException("请先点击“测试开始”,产生有效实验数据后再导出报表。");
}
var templatePath = FindTemplatePath(); var templatePath = FindTemplatePath();
using var templateStream = File.OpenRead(templatePath); using var templateStream = File.OpenRead(templatePath);
var workbook = new HSSFWorkbook(templateStream); var workbook = new HSSFWorkbook(templateStream);
FillReportSheet(workbook.GetSheet("Result_ISO"), input, records); FillReportSheet(workbook.GetSheet("Result_ISO"), "Result_ISO", input, exportRecords);
FillReportSheet(workbook.GetSheet("Result_FTP"), input, records); FillReportSheet(workbook.GetSheet("Result_FTP"), "Result_FTP", input, exportRecords);
FillDataSheet(workbook.GetSheet("Data") ?? workbook.CreateSheet("Data"), records); FillDataSheet(workbook.GetSheet("Data") ?? workbook.CreateSheet("Data"), exportRecords);
using var outputStream = File.Create(outputPath); using var outputStream = File.Create(outputPath);
workbook.Write(outputStream); workbook.Write(outputStream);
} }
private static void FillReportSheet(ISheet? sheet, ReportInput input, IReadOnlyList<RealtimeDataRecord> records) private static void FillReportSheet(
ISheet? sheet,
string sheetName,
ReportInput input,
IReadOnlyList<RealtimeDataRecord> records)
{ {
if (sheet is null) if (sheet is null)
{ {
return; throw new InvalidOperationException($"报表模板缺少工作表:{sheetName}");
} }
var summary = BuildSummary(records); var summary = BuildSummary(records);
ValidateSummary(summary);
SetValueBesideLabel(sheet, "实验室名称", input.LaboratoryName); SetValueBesideLabel(sheet, "实验室名称", input.LaboratoryName);
SetValueBesideLabel(sheet, "实验员", input.Operator); SetValueBesideLabel(sheet, "实验员", input.Operator);
SetValueBesideLabel(sheet, "文件名", input.FileName); SetValueBesideLabel(sheet, "文件名", input.FileName);
@@ -59,7 +82,7 @@ public sealed class NpoiReportExportService : IReportExportService
SetValueBesideLabel(sheet, "材料", input.Material); SetValueBesideLabel(sheet, "材料", input.Material);
SetValueBesideLabel(sheet, "样品", input.SampleName); SetValueBesideLabel(sheet, "样品", input.SampleName);
SetValueBesideLabel(sheet, "厚度", input.Thickness); SetValueBesideLabel(sheet, "厚度", input.Thickness);
SetValueBesideLabel(sheet, "初始质量", FirstNonEmpty(input.InitialMass, summary.InitialMass)); SetRequiredValueBesideLabel(sheet, "初始质量", FirstNonEmpty(input.InitialMass, summary.InitialMass));
SetValueBesideLabel(sheet, "辐射面积", input.IrradiatedArea); SetValueBesideLabel(sheet, "辐射面积", input.IrradiatedArea);
SetValueBesideLabel(sheet, "热辐射值", input.Irradiance); SetValueBesideLabel(sheet, "热辐射值", input.Irradiance);
SetValueBesideLabel(sheet, "辐射距离", input.IrradianceDistance); SetValueBesideLabel(sheet, "辐射距离", input.IrradianceDistance);
@@ -87,7 +110,7 @@ public sealed class NpoiReportExportService : IReportExportService
SetValueBesideLabel(sheet, "结束标准", input.EndCriteria); SetValueBesideLabel(sheet, "结束标准", input.EndCriteria);
SetValueBesideLabel(sheet, "结束时间", FirstNonEmpty(input.EndTime, summary.EndTime)); SetValueBesideLabel(sheet, "结束时间", FirstNonEmpty(input.EndTime, summary.EndTime));
SetValueBesideLabel(sheet, "E等价热值", input.EquivalentHeatValue); SetValueBesideLabel(sheet, "E等价热值", input.EquivalentHeatValue);
SetValueBesideLabel(sheet, "C-系数", FirstNonEmpty(input.CFactor, summary.CFactor)); SetRequiredValueBesideLabel(sheet, "C-系数", FirstNonEmpty(input.CFactor, summary.CFactor));
SetValueBesideLabel(sheet, "光程", input.LightPath); SetValueBesideLabel(sheet, "光程", input.LightPath);
SetValueBesideLabel(sheet, "O2延迟时间", input.O2DelayTime); SetValueBesideLabel(sheet, "O2延迟时间", input.O2DelayTime);
SetValueBesideLabel(sheet, "CO2延迟时间", input.CO2DelayTime); SetValueBesideLabel(sheet, "CO2延迟时间", input.CO2DelayTime);
@@ -98,11 +121,11 @@ public sealed class NpoiReportExportService : IReportExportService
SetValueBesideLabel(sheet, "基线C2氧含量", input.BaselineC2Oxygen); SetValueBesideLabel(sheet, "基线C2氧含量", input.BaselineC2Oxygen);
SetValueBesideLabel(sheet, "基线CO2氧含量", input.BaselineCO2Oxygen); SetValueBesideLabel(sheet, "基线CO2氧含量", input.BaselineCO2Oxygen);
SetValueBesideLabel(sheet, "总热释放", summary.TotalHeatRelease); SetRequiredValueBesideLabel(sheet, "总热释放", summary.TotalHeatRelease);
SetValueBesideLabel(sheet, "总产烟量", summary.TotalSmoke); SetRequiredValueBesideLabel(sheet, "总产烟量", summary.TotalSmoke);
SetValueBesideLabel(sheet, "质量损失", summary.MassLoss); SetRequiredValueBesideLabel(sheet, "质量损失", summary.MassLoss);
SetValueBesideLabel(sheet, "热释放(30)最大", summary.PeakHeatReleaseRate); SetRequiredValueBesideLabel(sheet, "热释放(30)最大", summary.PeakHeatReleaseRate);
SetValueBesideLabel(sheet, "产烟率(30)最大", summary.PeakSmokeProduction); SetRequiredValueBesideLabel(sheet, "产烟率(30)最大", summary.PeakSmokeProduction);
} }
private static void FillDataSheet(ISheet sheet, IReadOnlyList<RealtimeDataRecord> records) private static void FillDataSheet(ISheet sheet, IReadOnlyList<RealtimeDataRecord> records)
@@ -119,6 +142,7 @@ public sealed class NpoiReportExportService : IReportExportService
{ {
var row = sheet.CreateRow(i + 1); var row = sheet.CreateRow(i + 1);
var record = records[i]; var record = records[i];
// Keep columns A-O fixed; the Excel template charts reference these positions.
SetNumeric(row, 0, record.TestSeconds >= 0 ? record.TestSeconds : double.NaN); SetNumeric(row, 0, record.TestSeconds >= 0 ? record.TestSeconds : double.NaN);
SetNumeric(row, 1, record.Oxygen); SetNumeric(row, 1, record.Oxygen);
SetNumeric(row, 2, record.CarbonDioxide); SetNumeric(row, 2, record.CarbonDioxide);
@@ -134,6 +158,17 @@ public sealed class NpoiReportExportService : IReportExportService
SetNumeric(row, 12, record.EffectiveHeatOfCombustion); SetNumeric(row, 12, record.EffectiveHeatOfCombustion);
SetNumeric(row, 13, record.MassLoss); SetNumeric(row, 13, record.MassLoss);
SetNumeric(row, 14, record.SampleTemperature); SetNumeric(row, 14, record.SampleTemperature);
row.CreateCell(15).SetCellValue(record.Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture));
SetNumeric(row, 16, record.IgnitionSeconds >= 0 ? record.IgnitionSeconds : double.NaN);
SetNumeric(row, 17, record.FlameDetected ? 1 : 0);
SetNumeric(row, 18, record.OrificeFlow);
SetNumeric(row, 19, record.ConeTemperature);
SetNumeric(row, 20, record.CurrentMass);
SetNumeric(row, 21, record.InitialMass);
SetNumeric(row, 22, record.PeakHeatReleaseRate);
SetNumeric(row, 23, record.Qa180);
SetNumeric(row, 24, record.Qa300);
SetNumeric(row, 25, record.CFactor);
} }
for (var i = 0; i < DataHeaders.Length; i++) for (var i = 0; i < DataHeaders.Length; i++)
@@ -142,11 +177,11 @@ public sealed class NpoiReportExportService : IReportExportService
} }
} }
private static void SetValueBesideLabel(ISheet sheet, string label, string value) private static bool SetValueBesideLabel(ISheet sheet, string label, string value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return; return false;
} }
for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++) for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++)
@@ -173,9 +208,24 @@ public sealed class NpoiReportExportService : IReportExportService
var targetColumn = FindTargetColumn(sheet, rowIndex, columnIndex); var targetColumn = FindTargetColumn(sheet, rowIndex, columnIndex);
var target = row.GetCell(targetColumn) ?? row.CreateCell(targetColumn); var target = row.GetCell(targetColumn) ?? row.CreateCell(targetColumn);
target.SetCellValue(value); target.SetCellValue(value);
return; return true;
} }
} }
return false;
}
private static void SetRequiredValueBesideLabel(ISheet sheet, string label, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"缺少关键导出数据:{label}");
}
if (!SetValueBesideLabel(sheet, label, value))
{
throw new InvalidOperationException($"报表模板缺少字段:{label}");
}
} }
private static int FindTargetColumn(ISheet sheet, int rowIndex, int columnIndex) private static int FindTargetColumn(ISheet sheet, int rowIndex, int columnIndex)
@@ -269,8 +319,28 @@ public sealed class NpoiReportExportService : IReportExportService
MassLoss: FormatWithUnit(LastFinite(records, record => record.MassLoss), "g"), MassLoss: FormatWithUnit(LastFinite(records, record => record.MassLoss), "g"),
InitialMass: FormatWithUnit(LastFinite(records, record => record.InitialMass), "g"), InitialMass: FormatWithUnit(LastFinite(records, record => record.InitialMass), "g"),
CFactor: FormatValue(LastFinite(records, record => record.CFactor)), CFactor: FormatValue(LastFinite(records, record => record.CFactor)),
IgnitionTime: ignition is null ? "" : $"{ignition.Value} s", IgnitionTime: FormatSeconds(ignition),
EndTime: $"{last.TestSeconds} s"); EndTime: FormatSeconds(last.TestSeconds));
}
private static void ValidateSummary(ReportSummary summary)
{
RequireSummaryValue("总热释放", summary.TotalHeatRelease);
RequireSummaryValue("总产烟量", summary.TotalSmoke);
RequireSummaryValue("质量损失", summary.MassLoss);
RequireSummaryValue("初始质量", summary.InitialMass);
RequireSummaryValue("C-系数", summary.CFactor);
RequireSummaryValue("热释放(30)最大", summary.PeakHeatReleaseRate);
RequireSummaryValue("产烟率(30)最大", summary.PeakSmokeProduction);
}
private static void RequireSummaryValue(string label, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException(
$"缺少关键导出数据:{label}。请确认已点击测试开始并采集到有效实验数据。");
}
} }
private static string FormatWithUnit(double? value, string unit) private static string FormatWithUnit(double? value, string unit)
@@ -283,6 +353,18 @@ public sealed class NpoiReportExportService : IReportExportService
return value is null || !double.IsFinite(value.Value) ? "" : $"{value.Value:0.00}"; return value is null || !double.IsFinite(value.Value) ? "" : $"{value.Value:0.00}";
} }
private static string FormatSeconds(int? value)
{
return value.HasValue && value.Value >= 0 ? $"{value.Value} s" : "";
}
private static bool IsValidExperimentRecord(RealtimeDataRecord record)
{
return record.TestSeconds >= 0
&& double.IsFinite(record.TotalHeatRelease)
&& double.IsFinite(record.TotalSmoke);
}
private static double? LastFinite(IReadOnlyList<RealtimeDataRecord> records, Func<RealtimeDataRecord, double> selector) private static double? LastFinite(IReadOnlyList<RealtimeDataRecord> records, Func<RealtimeDataRecord, double> selector)
{ {
for (var i = records.Count - 1; i >= 0; i--) for (var i = records.Count - 1; i >= 0; i--)

View File

@@ -59,7 +59,7 @@ public sealed class ReportPageViewModel : PageViewModel
[ [
new ReportSectionViewModel("报告信息", new ReportSectionViewModel("报告信息",
[ [
new ReportFieldViewModel("LaboratoryName", "实验室名称", "广东安拓普检测中心"), new ReportFieldViewModel("LaboratoryName", "实验室名称", "检测中心"),
new ReportFieldViewModel("Operator", "实验员"), new ReportFieldViewModel("Operator", "实验员"),
new ReportFieldViewModel("FileName", "文件名"), new ReportFieldViewModel("FileName", "文件名"),
new ReportFieldViewModel("ReportName", "报告名") new ReportFieldViewModel("ReportName", "报告名")
@@ -143,14 +143,21 @@ public sealed class ReportPageViewModel : PageViewModel
SummaryItems[2].Value = records.Last().Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture); SummaryItems[2].Value = records.Last().Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture);
var last = records.Last(); var last = records.Last();
SummaryItems[3].Value = FormatWithUnit(last.PeakHeatReleaseRate, "kW/㎡"); SummaryItems[3].Value = FormatWithUnit(last.PeakHeatReleaseRate, "kW/㎡");
SummaryItems[4].Value = FormatWithUnit(last.TotalHeatRelease, "MJ/㎡"); SummaryItems[4].Value = FormatWithUnit(LastFinite(records, record => record.TotalHeatRelease), "MJ/㎡");
SummaryItems[5].Value = FormatWithUnit(last.TotalSmoke, "m²"); SummaryItems[5].Value = FormatWithUnit(LastFinite(records, record => record.TotalSmoke), "m²");
SummaryItems[6].Value = FormatWithUnit(last.MassLoss, "g"); SummaryItems[6].Value = FormatWithUnit(LastFinite(records, record => record.MassLoss), "g");
FillCollectedReportFields(records); FillCollectedReportFields(records);
} }
private void ExportReport() private void ExportReport()
{ {
var records = _experimentDataService.Records.ToList();
if (!records.Any(IsValidExperimentRecord))
{
StatusText = "请先点击“测试开始”,产生有效实验数据后再导出报表";
return;
}
var input = BuildInput(); var input = BuildInput();
var defaultName = BuildDefaultFileName(input); var defaultName = BuildDefaultFileName(input);
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
@@ -171,7 +178,7 @@ public sealed class ReportPageViewModel : PageViewModel
try try
{ {
_reportExportService.Export(dialog.FileName, input, _experimentDataService.Records.ToList()); _reportExportService.Export(dialog.FileName, input, records);
StatusText = $"已导出:{dialog.FileName}"; StatusText = $"已导出:{dialog.FileName}";
} }
catch (Exception ex) catch (Exception ex)
@@ -262,4 +269,11 @@ public sealed class ReportPageViewModel : PageViewModel
{ {
return double.IsFinite(value) ? $"{value:0.00}" : string.Empty; return double.IsFinite(value) ? $"{value:0.00}" : string.Empty;
} }
private static bool IsValidExperimentRecord(RealtimeDataRecord record)
{
return record.TestSeconds >= 0
&& double.IsFinite(record.TotalHeatRelease)
&& double.IsFinite(record.TotalSmoke);
}
} }

Binary file not shown.