diff --git a/Cardiopulmonarybypasssystems/App.xaml.cs b/Cardiopulmonarybypasssystems/App.xaml.cs index c12de06..783d899 100644 --- a/Cardiopulmonarybypasssystems/App.xaml.cs +++ b/Cardiopulmonarybypasssystems/App.xaml.cs @@ -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(); diff --git a/Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems.csproj b/Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems.csproj index a9b555c..6b3547d 100644 --- a/Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems.csproj +++ b/Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems.csproj @@ -12,6 +12,7 @@ + diff --git a/Cardiopulmonarybypasssystems/Services/ExcelReportDocument.cs b/Cardiopulmonarybypasssystems/Services/ExcelReportDocument.cs new file mode 100644 index 0000000..7b8a2e1 --- /dev/null +++ b/Cardiopulmonarybypasssystems/Services/ExcelReportDocument.cs @@ -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 inspectionItems, + IReadOnlyCollection traceEvents, + IReadOnlyCollection kinkResistanceEntries, + string kinkResistanceFlowPointDisplay, + string kinkResistanceMandrelDiameterDisplay, + IReadOnlyCollection pressureDropEntries, + string pressureDropLimitDisplay, + string antiCollapseBaselineDisplay, + string antiCollapseComparisonDisplay, + string antiCollapseCurrentNegativePressure, + string antiCollapseCurrentFlowDisplay, + string antiCollapseAllowedIncreaseRateDisplay, + IReadOnlyCollection 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 headers, + IEnumerable 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(); +} diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs index dd3fc4c..44d0ce0 100644 --- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs +++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs @@ -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");