diff --git a/ConeCalorimeter/Models/ReportInput.cs b/ConeCalorimeter/Models/ReportInput.cs index fe54189..22642f8 100644 --- a/ConeCalorimeter/Models/ReportInput.cs +++ b/ConeCalorimeter/Models/ReportInput.cs @@ -2,7 +2,7 @@ namespace ConeCalorimeter.Models; public sealed class ReportInput { - public string LaboratoryName { get; set; } = "检测中心"; + public string LaboratoryName { get; set; } = string.Empty; public string Operator { get; set; } = string.Empty; @@ -16,19 +16,19 @@ public sealed class ReportInput public string SampleName { get; set; } = string.Empty; - public string Thickness { get; set; } = "3 mm"; + public string Thickness { get; set; } = string.Empty; public string InitialMass { get; set; } = string.Empty; - public string IrradiatedArea { get; set; } = "100 c㎡"; + public string IrradiatedArea { get; set; } = string.Empty; - public string Irradiance { get; set; } = "35 kW/㎡"; + public string Irradiance { get; set; } = string.Empty; - public string IrradianceDistance { get; set; } = "25 mm"; + public string IrradianceDistance { get; set; } = string.Empty; - public string SampleOrientation { get; set; } = "Vertical"; + public string SampleOrientation { get; set; } = string.Empty; - public string SampleCount { get; set; } = "1"; + public string SampleCount { get; set; } = string.Empty; public string RequiredExhaustFlow { get; set; } = string.Empty; @@ -46,7 +46,7 @@ public sealed class ReportInput public string TestCondition { get; set; } = string.Empty; - public string TestStandard { get; set; } = "ISO 5660-1"; + public string TestStandard { get; set; } = string.Empty; public DateTime TestDate { get; set; } = DateTime.Today; @@ -88,5 +88,5 @@ public sealed class ReportInput public string BaselineCO2Oxygen { get; set; } = string.Empty; - public string EquivalentHeatValue { get; set; } = "13.1 MJ/kg"; + public string EquivalentHeatValue { get; set; } = string.Empty; } diff --git a/ConeCalorimeter/Services/ExperimentDataService.cs b/ConeCalorimeter/Services/ExperimentDataService.cs index f08b20a..e915d6e 100644 --- a/ConeCalorimeter/Services/ExperimentDataService.cs +++ b/ConeCalorimeter/Services/ExperimentDataService.cs @@ -58,7 +58,6 @@ public sealed class ExperimentDataService : IExperimentDataService _initialMass = CurrentSnapshot.CurrentMass; } - RecordElapsedSnapshots(); QueueImmediateDevicePoll(); Log.Information( @@ -69,11 +68,6 @@ public sealed class ExperimentDataService : IExperimentDataService public void StopTest() { - if (_isTestRunning) - { - RecordElapsedSnapshots(); - } - _isTestRunning = false; _testClock.Stop(); PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot)); @@ -98,11 +92,6 @@ public sealed class ExperimentDataService : IExperimentDataService private async Task TickAsync() { - if (_isTestRunning) - { - RecordElapsedSnapshots(); - } - await RefreshDeviceSnapshotAsync(); } @@ -123,7 +112,14 @@ public sealed class ExperimentDataService : IExperimentDataService { var elapsed = _isTestRunning ? _testClock.Elapsed : TimeSpan.Zero; var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed)); - PublishSnapshot(BuildDisplaySnapshot(snapshot)); + if (_isTestRunning) + { + RecordDeviceSnapshot(snapshot); + } + else + { + PublishSnapshot(BuildIdleSnapshot(snapshot)); + } } catch (Exception ex) { @@ -154,29 +150,30 @@ public sealed class ExperimentDataService : IExperimentDataService return true; } - private void RecordElapsedSnapshots() + private void RecordDeviceSnapshot(RealtimeSnapshot snapshot) { if (!_isTestRunning) { return; } - var currentSecond = GetElapsedTestSeconds(); - var nextSecond = _lastRecordedSeconds.HasValue ? _lastRecordedSeconds.Value + 1 : 0; - - for (var second = nextSecond; second <= currentSecond; second++) + var currentSecond = Math.Max(0, snapshot.TestSeconds); + if (_lastRecordedSeconds.HasValue && currentSecond <= _lastRecordedSeconds.Value) { - var timedSnapshot = CurrentSnapshot with - { - TestSeconds = second - }; - var massSnapshot = ApplyMassLoss(timedSnapshot); - var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot); - - PublishSnapshot(accumulatedSnapshot); - Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot)); - _lastRecordedSeconds = second; + PublishSnapshot(BuildDisplaySnapshot(snapshot)); + return; } + + var timedSnapshot = snapshot with + { + TestSeconds = currentSecond + }; + var massSnapshot = ApplyMassLoss(timedSnapshot); + var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot); + + PublishSnapshot(accumulatedSnapshot); + Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot)); + _lastRecordedSeconds = currentSecond; } private RealtimeSnapshot BuildDisplaySnapshot(RealtimeSnapshot snapshot) diff --git a/ConeCalorimeter/Services/NpoiReportExportService.cs b/ConeCalorimeter/Services/NpoiReportExportService.cs index bcee2ff..7187794 100644 --- a/ConeCalorimeter/Services/NpoiReportExportService.cs +++ b/ConeCalorimeter/Services/NpoiReportExportService.cs @@ -56,6 +56,8 @@ public sealed class NpoiReportExportService : IReportExportService FillReportSheet(workbook.GetSheet("Result_ISO"), "Result_ISO", input, exportRecords); FillReportSheet(workbook.GetSheet("Result_FTP"), "Result_FTP", input, exportRecords); + FillGraphSheet(workbook.GetSheet("Graph"), input); + FillBaseSheet(workbook.GetSheet("BASE"), exportRecords); FillDataSheet(workbook.GetSheet("Data") ?? workbook.CreateSheet("Data"), exportRecords); ValidateWorkbookDataSheet(workbook, exportRecords.Count); @@ -198,6 +200,48 @@ public sealed class NpoiReportExportService : IReportExportService } } + private static void FillGraphSheet(ISheet? sheet, ReportInput input) + { + if (sheet is null) + { + return; + } + + SetGraphValuesBesideLabel(sheet, "实验室名称", input.LaboratoryName); + SetGraphValuesBesideLabel(sheet, "实验员", input.Operator); + SetGraphValuesBesideLabel(sheet, "文件名", input.FileName); + SetGraphValuesBesideLabel(sheet, "报告名", input.ReportName); + SetGraphValuesBesideLabel(sheet, "样品描述", input.SampleDescription); + SetGraphValuesBesideLabel(sheet, "材料", input.Material); + } + + private static void FillBaseSheet(ISheet? sheet, IReadOnlyList records) + { + if (sheet is null) + { + return; + } + + ClearRowsBelowHeader(sheet); + + for (var i = 0; i < records.Count; i++) + { + var row = sheet.GetRow(i + 1) ?? sheet.CreateRow(i + 1); + var record = records[i]; + + SetNumeric(row, 0, record.TestSeconds >= 0 ? record.TestSeconds : double.NaN); + SetNumeric(row, 1, record.HeatReleaseRate); + SetNumeric(row, 2, record.Oxygen); + SetNumeric(row, 3, record.CarbonMonoxide); + SetNumeric(row, 4, record.CarbonDioxide); + SetNumeric(row, 5, record.CurrentMass); + SetNumeric(row, 6, record.TotalHeatRelease); + SetNumeric(row, 7, record.SmokeProduction); + SetNumeric(row, 8, record.TotalSmoke); + SetNumeric(row, 13, record.OrificePressure); + } + } + private static double CalculateGrowthIndex(int testSeconds, double value, double multiplier) { if (testSeconds <= 0 || !double.IsFinite(value) || value < 0) @@ -281,11 +325,6 @@ public sealed class NpoiReportExportService : IReportExportService private static bool SetValueBesideLabel(ISheet sheet, string label, string value) { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++) { var row = sheet.GetRow(rowIndex); @@ -302,14 +341,14 @@ public sealed class NpoiReportExportService : IReportExportService } var cell = row.GetCell(columnIndex); - if (!CellText(cell).Contains(label, StringComparison.CurrentCultureIgnoreCase)) + if (!IsMatchingLabel(CellText(cell), label)) { continue; } var targetColumn = FindTargetColumn(sheet, rowIndex, columnIndex); var target = row.GetCell(targetColumn) ?? row.CreateCell(targetColumn); - target.SetCellValue(value); + SetTextOrBlank(target, value); return true; } } @@ -317,6 +356,59 @@ public sealed class NpoiReportExportService : IReportExportService return false; } + private static void SetGraphValuesBesideLabel(ISheet sheet, string label, string value) + { + for (var rowIndex = sheet.FirstRowNum; rowIndex <= sheet.LastRowNum; rowIndex++) + { + var row = sheet.GetRow(rowIndex); + if (row is null) + { + continue; + } + + for (var columnIndex = row.FirstCellNum; columnIndex < row.LastCellNum; columnIndex++) + { + if (columnIndex < 0) + { + continue; + } + + var cell = row.GetCell(columnIndex); + if (!IsMatchingLabel(CellText(cell), label)) + { + continue; + } + + var targetColumn = FindTargetColumn(sheet, rowIndex, columnIndex); + var target = row.GetCell(targetColumn) ?? row.CreateCell(targetColumn); + SetTextOrBlank(target, value); + } + } + } + + private static bool IsMatchingLabel(string cellText, string label) + { + var normalizedCell = NormalizeLabel(cellText); + var normalizedLabel = NormalizeLabel(label); + + return normalizedCell.Equals(normalizedLabel, StringComparison.CurrentCultureIgnoreCase) + || normalizedCell.StartsWith($"{normalizedLabel}(", StringComparison.CurrentCultureIgnoreCase) + || normalizedCell.StartsWith($"{normalizedLabel}(", StringComparison.CurrentCultureIgnoreCase); + } + + private static string NormalizeLabel(string value) + { + var chars = value + .Where(ch => !char.IsWhiteSpace(ch) && ch is not ':' and not ':' and not '?' and not '?') + .ToArray(); + return new string(chars); + } + + private static void SetTextOrBlank(ICell cell, string value) + { + cell.SetCellValue(string.IsNullOrWhiteSpace(value) ? string.Empty : value); + } + private static void SetRequiredValueBesideLabel(ISheet sheet, string label, string value) { if (string.IsNullOrWhiteSpace(value)) @@ -410,6 +502,27 @@ public sealed class NpoiReportExportService : IReportExportService } } + private static void ClearRowsBelowHeader(ISheet sheet) + { + for (var rowIndex = Math.Max(sheet.FirstRowNum + 1, 1); rowIndex <= sheet.LastRowNum; rowIndex++) + { + var row = sheet.GetRow(rowIndex); + if (row is null) + { + continue; + } + + for (var columnIndex = row.FirstCellNum; columnIndex < row.LastCellNum; columnIndex++) + { + var cell = columnIndex >= 0 ? row.GetCell(columnIndex) : null; + if (cell is not null) + { + row.RemoveCell(cell); + } + } + } + } + private static void SetNumeric(IRow row, int columnIndex, double value) { if (double.IsFinite(value)) diff --git a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs index f77d865..2a8db74 100644 --- a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs +++ b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs @@ -60,7 +60,7 @@ public sealed class ReportPageViewModel : PageViewModel [ new ReportSectionViewModel("报告信息", [ - new ReportFieldViewModel("LaboratoryName", "实验室名称", "检测中心"), + new ReportFieldViewModel("LaboratoryName", "实验室名称"), new ReportFieldViewModel("Operator", "实验员"), new ReportFieldViewModel("FileName", "文件名"), new ReportFieldViewModel("ReportName", "报告名") @@ -70,17 +70,17 @@ public sealed class ReportPageViewModel : PageViewModel new ReportFieldViewModel("SampleDescription", "样品描述"), new ReportFieldViewModel("Material", "材料"), new ReportFieldViewModel("SampleName", "样品"), - new ReportFieldViewModel("Thickness", "厚度", "3 mm"), + new ReportFieldViewModel("Thickness", "厚度"), new ReportFieldViewModel("InitialMass", "初始质量"), new ReportFieldViewModel("Manufacturer", "制造商") ]), new ReportSectionViewModel("测试条件", [ - new ReportFieldViewModel("IrradiatedArea", "辐射面积", "100 c㎡"), - new ReportFieldViewModel("Irradiance", "热辐射值", "35 kW/㎡"), - new ReportFieldViewModel("IrradianceDistance", "辐射距离", "25 mm"), - new ReportFieldViewModel("SampleOrientation", "试样方向", "Vertical"), - new ReportFieldViewModel("SampleCount", "试样数目", "1"), + new ReportFieldViewModel("IrradiatedArea", "辐射面积"), + new ReportFieldViewModel("Irradiance", "热辐射值"), + new ReportFieldViewModel("IrradianceDistance", "辐射距离"), + new ReportFieldViewModel("SampleOrientation", "试样方向"), + new ReportFieldViewModel("SampleCount", "试样数目"), new ReportFieldViewModel("RequiredExhaustFlow", "要求烟气流量"), new ReportFieldViewModel("FrameSelected", "边框选用?"), new ReportFieldViewModel("GridSelected", "栅格选用?"), @@ -88,7 +88,7 @@ public sealed class ReportPageViewModel : PageViewModel new ReportFieldViewModel("AmbientTemperature", "环境温度"), new ReportFieldViewModel("AmbientHumidity", "环境湿度"), new ReportFieldViewModel("TestCondition", "测试条件"), - new ReportFieldViewModel("TestStandard", "符合标准", "ISO 5660-1"), + new ReportFieldViewModel("TestStandard", "符合标准"), new ReportFieldViewModel("TestDate", "测试日期", DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.CurrentCulture)), new ReportFieldViewModel("ForecastCondition", "预测条件") ]), @@ -105,7 +105,7 @@ public sealed class ReportPageViewModel : PageViewModel ]), new ReportSectionViewModel("设备参数", [ - new ReportFieldViewModel("EquivalentHeatValue", "E等价热值", "13.1 MJ/kg"), + new ReportFieldViewModel("EquivalentHeatValue", "E等价热值"), new ReportFieldViewModel("CFactor", "C-系数"), new ReportFieldViewModel("LightPath", "光程"), new ReportFieldViewModel("O2DelayTime", "O2延迟时间"),