更新3/26

This commit is contained in:
GukSang.Jin
2026-03-26 15:11:31 +08:00
parent bd356a0082
commit 42634e1747
4 changed files with 490 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ using System.Windows;
using Cardiopulmonarybypasssystems.Services;
using Cardiopulmonarybypasssystems.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using OfficeOpenXml;
using QuestPDF.Infrastructure;
namespace Cardiopulmonarybypasssystems;
@@ -13,6 +14,7 @@ public partial class App : Application
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
QuestPDF.Settings.License = LicenseType.Community;
var services = new ServiceCollection();

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="EPPlus" Version="7.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="NModbus" Version="3.0.81" />
<PackageReference Include="QuestPDF" Version="2024.12.1" />

View File

@@ -0,0 +1,435 @@
using System.IO;
using System.Drawing;
using Cardiopulmonarybypasssystems.Models;
using OfficeOpenXml;
using OfficeOpenXml.Style;
namespace Cardiopulmonarybypasssystems.Services;
public sealed class ExcelReportDocument(
string pageTitle,
string batchNumber,
string currentStage,
string operatorName,
string reviewerName,
string approverName,
string complianceDisplay,
string deltaPressureDisplay,
string detectionSummary,
string configurationSummary,
DateTime exportTime,
IReadOnlyCollection<InspectionItem> inspectionItems,
IReadOnlyCollection<TraceEvent> traceEvents,
IReadOnlyCollection<KinkResistancePointEntry> kinkResistanceEntries,
string kinkResistanceFlowPointDisplay,
string kinkResistanceMandrelDiameterDisplay,
IReadOnlyCollection<PressureDropPointEntry> pressureDropEntries,
string pressureDropLimitDisplay,
string antiCollapseBaselineDisplay,
string antiCollapseComparisonDisplay,
string antiCollapseCurrentNegativePressure,
string antiCollapseCurrentFlowDisplay,
string antiCollapseAllowedIncreaseRateDisplay,
IReadOnlyCollection<RecirculationPointEntry> recirculationEntries,
string recirculationLimitDisplay)
{
private const int TotalColumns = 10;
private static readonly Color BorderColor = ColorTranslator.FromHtml("#A0AEC0");
private static readonly Color SectionFillColor = ColorTranslator.FromHtml("#DDEFEA");
private static readonly Color HeaderFillColor = ColorTranslator.FromHtml("#CFE8E3");
private static readonly Color SummaryFillColor = ColorTranslator.FromHtml("#D9F3EE");
private static readonly Color MetaLabelFillColor = ColorTranslator.FromHtml("#F7FAFC");
private static readonly Color MetaLabelFontColor = ColorTranslator.FromHtml("#4A5568");
public void GenerateExcel(string filePath)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("检查报告");
package.Workbook.Properties.Title = pageTitle;
ConfigureWorksheet(worksheet);
var row = 1;
row = ComposeTitle(worksheet, row);
row = ComposeMeta(worksheet, row + 2);
row = ComposeSummary(worksheet, row + 2);
row = ComposeKinkResistanceSection(worksheet, row + 2);
row = ComposePressureDropSection(worksheet, row + 2);
row = ComposeAntiCollapseSection(worksheet, row + 2);
row = ComposeRecirculationSection(worksheet, row + 2);
row = ComposeInspectionTable(worksheet, row + 2);
row = ComposeTraceEvents(worksheet, row + 2);
ComposeSignatures(worksheet, row + 2);
package.SaveAs(new FileInfo(filePath));
}
private static void ConfigureWorksheet(ExcelWorksheet worksheet)
{
worksheet.View.ShowGridLines = false;
worksheet.PrinterSettings.Orientation = eOrientation.Landscape;
worksheet.PrinterSettings.PaperSize = ePaperSize.A4;
worksheet.PrinterSettings.FitToPage = true;
worksheet.PrinterSettings.FitToWidth = 1;
worksheet.PrinterSettings.FitToHeight = 0;
worksheet.PrinterSettings.LeftMargin = 0.35m;
worksheet.PrinterSettings.RightMargin = 0.35m;
worksheet.PrinterSettings.TopMargin = 0.4m;
worksheet.PrinterSettings.BottomMargin = 0.4m;
worksheet.Cells.Style.Font.Name = "Microsoft YaHei";
worksheet.Cells.Style.Font.Size = 9;
worksheet.Cells.Style.WrapText = true;
worksheet.Cells.Style.VerticalAlignment = ExcelVerticalAlignment.Top;
var widths = new[] { 12d, 14d, 22d, 24d, 20d, 16d, 10d, 12d, 16d, 18d };
for (var index = 0; index < widths.Length; index++)
{
worksheet.Column(index + 1).Width = widths[index];
}
}
private int ComposeTitle(ExcelWorksheet worksheet, int row)
{
var range = worksheet.Cells[row, 1, row, TotalColumns];
range.Merge = true;
range.Value = ValueOrFallback(pageTitle);
range.Style.Font.Size = 18;
range.Style.Font.Bold = true;
range.Style.Font.Color.SetColor(ColorTranslator.FromHtml("#0F766E"));
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
worksheet.Row(row).Height = 26;
return row;
}
private int ComposeMeta(ExcelWorksheet worksheet, int startRow)
{
var items = new (string Label, string Value, int ColStart, int ColEnd)[]
{
("批次", batchNumber, 1, 2),
("阶段", currentStage, 3, 4),
("测试员", operatorName, 5, 6),
("复核人", reviewerName, 7, 8),
("批准人", approverName, 1, 2),
("导出时间", exportTime.ToString("yyyy-MM-dd HH:mm:ss"), 3, 4),
("合格率", complianceDisplay, 5, 6),
("压差", deltaPressureDisplay, 7, 8)
};
for (var index = 0; index < items.Length; index++)
{
var rowOffset = index < 4 ? 0 : 2;
WriteMetaCell(
worksheet,
startRow + rowOffset,
startRow + rowOffset + 1,
items[index].ColStart,
items[index].ColEnd,
items[index].Label,
items[index].Value);
}
return startRow + 3;
}
private int ComposeSummary(ExcelWorksheet worksheet, int startRow)
{
var titleRange = worksheet.Cells[startRow, 1, startRow, TotalColumns];
titleRange.Merge = true;
titleRange.Value = "检测结论";
titleRange.Style.Font.Bold = true;
titleRange.Style.Font.Size = 11;
titleRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
titleRange.Style.Fill.BackgroundColor.SetColor(SummaryFillColor);
ApplyBorder(titleRange);
var summaryRange = worksheet.Cells[startRow + 1, 1, startRow + 1, TotalColumns];
summaryRange.Merge = true;
summaryRange.Value = ValueOrFallback(detectionSummary);
ApplyBodyCell(summaryRange);
var configRange = worksheet.Cells[startRow + 2, 1, startRow + 2, TotalColumns];
configRange.Merge = true;
configRange.Value = $"产品配置:{ValueOrFallback(configurationSummary)}";
ApplyBodyCell(configRange);
return startRow + 2;
}
private int ComposeKinkResistanceSection(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "专项记录:抗扭结抗性");
row = WriteMergedBodyRow(worksheet, row + 1, kinkResistanceFlowPointDisplay);
row = WriteMergedBodyRow(worksheet, row + 1, kinkResistanceMandrelDiameterDisplay);
var headers = new[] { "流量点", "目标流量", "L0", "L1", "降幅", "L0时间", "L1时间" };
var rows = kinkResistanceEntries.Select(entry => new[]
{
entry.Label,
$"{entry.TargetFlow:F2} L/min",
entry.HasBaseline ? $"{entry.BaselineFlow:F2} L/min" : "-",
entry.HasKinked ? $"{entry.KinkedFlow:F2} L/min" : "-",
entry.FlowDropRateText,
entry.BaselineCapturedAtText,
entry.KinkedCapturedAtText
});
return WriteTable(worksheet, row + 1, headers, rows);
}
private int ComposePressureDropSection(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "专项记录:压力降");
row = WriteMergedBodyRow(worksheet, row + 1, $"声明限值:{pressureDropLimitDisplay}");
var headers = new[] { "流量点", "目标流量", "实际主泵", "近端压力", "远端压力", "ΔP" };
var rows = pressureDropEntries
.OrderBy(item => item.TargetFlow)
.Select(entry => new[]
{
entry.Label,
$"{entry.TargetFlow:F2} L/min",
entry.HasSample ? $"{entry.ActualPumpFlow:F2} L/min" : "-",
entry.HasSample ? $"{entry.ProximalPressure:F1} mmHg" : "-",
entry.HasSample ? $"{entry.DistalPressure:F1} mmHg" : "-",
entry.HasSample ? $"{entry.DeltaPressure:F1} mmHg" : "-"
});
return WriteTable(worksheet, row + 1, headers, rows);
}
private int ComposeAntiCollapseSection(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "专项记录:抗塌陷");
var entries = new (string Label, string Value)[]
{
("基线", antiCollapseBaselineDisplay),
("允许增幅", antiCollapseAllowedIncreaseRateDisplay),
("负压状态", antiCollapseCurrentNegativePressure),
("当前流量", antiCollapseCurrentFlowDisplay),
("比较结果", antiCollapseComparisonDisplay)
};
foreach (var entry in entries)
{
row++;
WriteKeyValueRow(worksheet, row, entry.Label, entry.Value);
}
return row;
}
private int ComposeRecirculationSection(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "专项记录:再循环");
row = WriteMergedBodyRow(worksheet, row + 1, recirculationLimitDisplay);
var headers = new[] { "流量点", "目标流量", "实际主泵", "C1(D)", "C2(C)", "R%", "在线参考", "时间" };
var rows = recirculationEntries
.OrderBy(item => item.TargetFlow)
.Select(entry => new[]
{
entry.Label,
$"{entry.TargetFlow:F2} L/min",
entry.HasSample ? $"{entry.ActualPumpFlow:F2} L/min" : "-",
entry.ConcentrationC1?.ToString("F0") ?? "-",
entry.ConcentrationC2?.ToString("F0") ?? "-",
entry.RecirculationResultText,
entry.HasSample ? $"{entry.OnlineEstimate:F1}%" : "-",
entry.SampledAtText
});
return WriteTable(worksheet, row + 1, headers, rows);
}
private int ComposeInspectionTable(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "项目记录");
var headers = new[] { "类别", "测试内容", "判定要求", "检测方法", "记录要点", "结果", "状态", "记录人", "记录时间", "备注" };
var rows = inspectionItems.Select(item => new[]
{
item.Category,
item.Item,
item.AcceptanceCriteria,
item.TestMethod,
item.RecordFocus,
item.Measured,
item.StatusText,
item.RecordedBy,
item.RecordedAtText,
item.Notes
});
return WriteTable(worksheet, row + 1, headers, rows);
}
private int ComposeTraceEvents(ExcelWorksheet worksheet, int startRow)
{
var row = WriteSectionTitle(worksheet, startRow, "追溯事件");
var orderedTraceEvents = traceEvents.OrderBy(item => item.Timestamp).ToList();
if (orderedTraceEvents.Count == 0)
{
return WriteMergedBodyRow(worksheet, row + 1, "-");
}
foreach (var trace in orderedTraceEvents)
{
row++;
var range = worksheet.Cells[row, 1, row, TotalColumns];
range.Merge = true;
range.Value = $"{trace.Timestamp:yyyy-MM-dd HH:mm:ss} {ValueOrFallback(trace.Stage)} {ValueOrFallback(trace.Detail)} ({ValueOrFallback(trace.Operator)})";
range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
range.Style.Border.Bottom.Color.SetColor(ColorTranslator.FromHtml("#CBD5E0"));
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
range.Style.VerticalAlignment = ExcelVerticalAlignment.Top;
}
return row;
}
private void ComposeSignatures(ExcelWorksheet worksheet, int startRow)
{
WriteSignatureCell(worksheet, startRow, 1, 3, "测试员", operatorName);
WriteSignatureCell(worksheet, startRow, 4, 6, "复核人", reviewerName);
WriteSignatureCell(worksheet, startRow, 7, 10, "批准人", approverName);
}
private static void WriteMetaCell(
ExcelWorksheet worksheet,
int labelRow,
int valueRow,
int startColumn,
int endColumn,
string label,
string value)
{
var labelRange = worksheet.Cells[labelRow, startColumn, labelRow, endColumn];
labelRange.Merge = true;
labelRange.Value = label;
labelRange.Style.Font.Size = 8;
labelRange.Style.Font.Color.SetColor(MetaLabelFontColor);
labelRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
labelRange.Style.Fill.BackgroundColor.SetColor(MetaLabelFillColor);
ApplyBorder(labelRange);
var valueRange = worksheet.Cells[valueRow, startColumn, valueRow, endColumn];
valueRange.Merge = true;
valueRange.Value = ValueOrFallback(value);
valueRange.Style.Font.Bold = true;
ApplyBodyCell(valueRange);
}
private static int WriteSectionTitle(ExcelWorksheet worksheet, int row, string title)
{
var range = worksheet.Cells[row, 1, row, TotalColumns];
range.Merge = true;
range.Value = title;
range.Style.Font.Bold = true;
range.Style.Font.Size = 11;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(SectionFillColor);
ApplyBorder(range);
return row;
}
private static int WriteMergedBodyRow(ExcelWorksheet worksheet, int row, string value)
{
var range = worksheet.Cells[row, 1, row, TotalColumns];
range.Merge = true;
range.Value = ValueOrFallback(value);
ApplyBodyCell(range);
return row;
}
private static void WriteKeyValueRow(ExcelWorksheet worksheet, int row, string label, string value)
{
var labelRange = worksheet.Cells[row, 1, row, 2];
labelRange.Merge = true;
labelRange.Value = label;
labelRange.Style.Font.Bold = true;
labelRange.Style.Fill.PatternType = ExcelFillStyle.Solid;
labelRange.Style.Fill.BackgroundColor.SetColor(HeaderFillColor);
ApplyBorder(labelRange);
var valueRange = worksheet.Cells[row, 3, row, TotalColumns];
valueRange.Merge = true;
valueRange.Value = ValueOrFallback(value);
ApplyBodyCell(valueRange);
}
private static int WriteTable(
ExcelWorksheet worksheet,
int startRow,
IReadOnlyList<string> headers,
IEnumerable<string[]> rows)
{
for (var index = 0; index < headers.Count; index++)
{
var cell = worksheet.Cells[startRow, index + 1];
cell.Value = headers[index];
cell.Style.Font.Bold = true;
cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
cell.Style.Fill.BackgroundColor.SetColor(HeaderFillColor);
cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
cell.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
ApplyBorder(cell);
}
var currentRow = startRow;
foreach (var row in rows)
{
currentRow++;
for (var index = 0; index < headers.Count; index++)
{
var cell = worksheet.Cells[currentRow, index + 1];
cell.Value = ValueOrFallback(row[index]);
ApplyBodyCell(cell);
}
}
return currentRow;
}
private static void WriteSignatureCell(
ExcelWorksheet worksheet,
int startRow,
int startColumn,
int endColumn,
string label,
string value)
{
var lineRange = worksheet.Cells[startRow, startColumn, startRow, endColumn];
lineRange.Merge = true;
lineRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
lineRange.Style.Border.Top.Color.SetColor(Color.Black);
var textRange = worksheet.Cells[startRow + 1, startColumn, startRow + 1, endColumn];
textRange.Merge = true;
textRange.Value = $"{label}{ValueOrFallback(value)}";
textRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
}
private static void ApplyBodyCell(ExcelRange range)
{
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
range.Style.VerticalAlignment = ExcelVerticalAlignment.Top;
ApplyBorder(range);
}
private static void ApplyBorder(ExcelRange range)
{
range.Style.Border.Top.Style = ExcelBorderStyle.Thin;
range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
range.Style.Border.Left.Style = ExcelBorderStyle.Thin;
range.Style.Border.Right.Style = ExcelBorderStyle.Thin;
range.Style.Border.Top.Color.SetColor(BorderColor);
range.Style.Border.Bottom.Color.SetColor(BorderColor);
range.Style.Border.Left.Color.SetColor(BorderColor);
range.Style.Border.Right.Color.SetColor(BorderColor);
}
private static string ValueOrFallback(string? value) =>
string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
}

View File

@@ -856,6 +856,58 @@ public partial class MainViewModel : ObservableObject, IDisposable
[RelayCommand]
private void ExportReport()
{
ExportReportExcel();
}
private void ExportReportExcel()
{
var outputDirectory = ResolveReportOutputDirectory();
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
var batchToken = string.IsNullOrWhiteSpace(BatchNumber) ? "未填写批号" : SanitizeFileNameSegment(BatchNumber.Trim());
var excelPath = Path.Combine(outputDirectory, $"检查报告-{batchToken}-{timestamp}.xlsx");
try
{
var document = new ExcelReportDocument(
pageTitle: PageTitle,
batchNumber: BatchNumber,
currentStage: CurrentStage,
operatorName: OperatorName,
reviewerName: ReviewerName,
approverName: ApproverName,
complianceDisplay: ComplianceDisplay,
deltaPressureDisplay: DeltaPressureDisplay,
detectionSummary: DetectionSummary,
configurationSummary: ConfigurationSummary,
exportTime: DateTime.Now,
inspectionItems: InspectionItems.ToList(),
traceEvents: TraceEvents.ToList(),
kinkResistanceEntries: KinkResistanceEntries.ToList(),
kinkResistanceFlowPointDisplay: KinkResistanceFlowPointDisplay,
kinkResistanceMandrelDiameterDisplay: KinkResistanceMandrelDiameterDisplay,
pressureDropEntries: PressureDropEntries.ToList(),
pressureDropLimitDisplay: PressureDropLimitDisplay,
antiCollapseBaselineDisplay: AntiCollapseBaselineDisplay,
antiCollapseComparisonDisplay: AntiCollapseComparisonDisplay,
antiCollapseCurrentNegativePressure: NegativeAssistPressureDisplay,
antiCollapseCurrentFlowDisplay: PumpFlowDisplay,
antiCollapseAllowedIncreaseRateDisplay: $"{AntiCollapseAllowedIncreaseRate:F1}%",
recirculationEntries: RecirculationEntries.ToList(),
recirculationLimitDisplay: RecirculationLimitDisplay);
document.GenerateExcel(excelPath);
LatestAction = $"已导出检查报告: {excelPath}";
TraceEvents.Insert(0, NewTrace("检查报告导出", Path.GetFileName(excelPath)));
}
catch (Exception ex)
{
LatestAction = $"检查报告导出失败: {ex.Message}";
TraceEvents.Insert(0, NewTrace("检查报告导出失败", ex.Message));
}
}
private void ExportReportPdf()
{
var outputDirectory = ResolveReportOutputDirectory();
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");