Files
Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
2026-03-26 09:28:12 +08:00

2352 lines
104 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Text.Json;
using System.Windows.Data;
using System.Windows.Threading;
using Cardiopulmonarybypasssystems.Models;
using Cardiopulmonarybypasssystems.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using QuestPDF.Fluent;
namespace Cardiopulmonarybypasssystems.ViewModels;
public partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IModbusTelemetryService _telemetryService;
private readonly DispatcherTimer _timer;
private const double AntiCollapseTargetNegativePressure = -6.67;
private const int TrendHistoryCapacity = 60;
private static readonly string LimitSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Cardiopulmonarybypasssystems",
"manufacturer-limits.json");
private double? _antiCollapseBaselinePressureDrop;
private double? _antiCollapseBaselineFlow;
private DateTime? _antiCollapseBaselineCapturedAt;
private string _lastAutoAntiCollapseResult = string.Empty;
private string _lastAutoAntiCollapseNote = string.Empty;
private string _lastAutoRecirculationResult = string.Empty;
private string _lastAutoRecirculationNote = string.Empty;
private string _lastAutoHemolysisResult = string.Empty;
private string _lastAutoHemolysisNote = string.Empty;
private bool _suppressLimitSettingsSave;
[ObservableProperty]
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
[ObservableProperty]
private string currentStage = "检测进行中";
[ObservableProperty]
private string operatorName = Environment.UserName;
[ObservableProperty]
private string reviewerName = "";
[ObservableProperty]
private string approverName = "";
[ObservableProperty]
private string batchNumber = "";
[ObservableProperty]
private string deviceStatus = "等待连接";
[ObservableProperty]
private bool acquisitionRunning = true;
[ObservableProperty]
private bool detectionCompleted;
[ObservableProperty]
private double complianceRate;
[ObservableProperty]
private int qualifiedCount;
[ObservableProperty]
private int warningCount;
[ObservableProperty]
private int pendingCount;
[ObservableProperty]
private double deltaPressure;
[ObservableProperty]
private string productModel = "24Fr/32Fr 双腔";
[ObservableProperty]
private string applicablePopulation = "成人";
[ObservableProperty]
private double ratedMaxFlow = 6.0;
[ObservableProperty]
private double kinkResistanceMinimumFlow = 1.0;
[ObservableProperty]
private double kinkResistanceOuterDiameter = 8.0;
[ObservableProperty]
private double pressureDropLimit50 = 20;
[ObservableProperty]
private double pressureDropLimit75 = 22;
[ObservableProperty]
private double pressureDropLimit100 = 24;
[ObservableProperty]
private double antiCollapseAllowedIncreaseRate = 50;
[ObservableProperty]
private double recirculationAllowedLimit = 8;
[ObservableProperty]
private string latestAction = "系统已载入标准项目,等待 PLC 实时数据。";
[ObservableProperty]
private InspectionItem? selectedItem;
[ObservableProperty]
private string resultValue = "";
[ObservableProperty]
private string resultNote = "";
[ObservableProperty]
private string resultOperator = Environment.UserName;
[ObservableProperty]
private string selectedResultStatusText = "合格";
[ObservableProperty]
private string detectionSummary = "";
[ObservableProperty]
private string itemSearchText = "";
private string activeFilter = "全部";
public MainViewModel(IStandardRepository repository, IModbusTelemetryService telemetryService)
{
_telemetryService = telemetryService;
InspectionItems = new ObservableCollection<InspectionItem>(repository.GetInspectionItems());
FilteredItemsView = CollectionViewSource.GetDefaultView(InspectionItems);
FilteredItemsView.Filter = MatchesFilteredItem;
Channels = new ObservableCollection<DeviceChannel>(telemetryService.GetChannels());
PumpControls = new ObservableCollection<PumpControlChannel>(telemetryService.GetPumpControls());
ValveControls = new ObservableCollection<ValveControlChannel>(telemetryService.GetValveControls());
TraceEvents = new ObservableCollection<TraceEvent>(repository.GetInitialTraceEvents());
AlarmMessages = new ObservableCollection<AlarmMessage>();
ResultStatusOptions = new ObservableCollection<string>(["待检", "合格", "预警", "不合格"]);
PressureDropEntries = new ObservableCollection<PressureDropPointEntry>(
[
new() { Label = "50%", TargetFlow = 3.0 },
new() { Label = "75%", TargetFlow = 4.5 },
new() { Label = "100%", TargetFlow = 6.0 }
]);
KinkResistanceEntries = new ObservableCollection<KinkResistancePointEntry>(
[
new() { Label = "最大流量", TargetFlow = RatedMaxFlow },
new() { Label = "最小流量", TargetFlow = KinkResistanceMinimumFlow }
]);
RecirculationEntries = new ObservableCollection<RecirculationPointEntry>(
[
new() { Label = "50%", TargetFlow = 3.0 },
new() { Label = "75%", TargetFlow = 4.5 },
new() { Label = "100%", TargetFlow = 6.0 }
]);
HemolysisTestParameters = new HemolysisTestParameters
{
BloodSource = "肝素化牛血",
Anticoagulant = "肝素",
AdjustedHematocrit = 0.30,
Glucose = 10,
TotalHemoglobin = 12,
SetFlow = RatedMaxFlow,
RunTimeMinutes = 360,
TargetTemperature = 37
};
HemolysisSamplingEntries = new ObservableCollection<HemolysisSamplingEntry>(
[
new() { Sequence = 1, TimePoint = "T0 (初始)", Flow = 0, Pressure = 0, Temperature = 37.0, Remarks = "背景值" },
new() { Sequence = 2, TimePoint = "T30" },
new() { Sequence = 3, TimePoint = "T60", Remarks = "过程观察" },
new() { Sequence = 4, TimePoint = "T120", Remarks = "过程观察" },
new() { Sequence = 5, TimePoint = "T180" },
new() { Sequence = 6, TimePoint = "T240", Remarks = "过程观察" },
new() { Sequence = 7, TimePoint = "T300", Remarks = "过程观察" },
new() { Sequence = 8, TimePoint = "T360 (结束)", Remarks = "试验完成" }
]);
foreach (var entry in PressureDropEntries)
{
entry.PropertyChanged += OnPressureDropEntryPropertyChanged;
}
foreach (var entry in KinkResistanceEntries)
{
entry.PropertyChanged += OnKinkResistanceEntryPropertyChanged;
}
foreach (var entry in RecirculationEntries)
{
entry.PropertyChanged += OnRecirculationEntryPropertyChanged;
}
foreach (var entry in HemolysisSamplingEntries)
{
entry.PropertyChanged += OnHemolysisSamplingEntryPropertyChanged;
}
HemolysisTestParameters.PropertyChanged += OnHemolysisTestParametersPropertyChanged;
LoadManufacturerLimitSettings();
SelectedItem = InspectionItems.FirstOrDefault();
if (SelectedItem is not null)
{
LoadSelectedItemDraft(SelectedItem);
}
RefreshTelemetryPanel();
RefreshDeviceStatus();
RefreshComputedState();
TraceEvents.Insert(0, NewTrace("任务初始化", $"已载入 {InspectionItems.Count} 项检测标准,实时端点 {_telemetryService.EndpointDescription}"));
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
_timer.Tick += OnTelemetryTimerTick;
_timer.Start();
}
public ObservableCollection<InspectionItem> InspectionItems { get; }
public ICollectionView FilteredItemsView { get; }
public ObservableCollection<DeviceChannel> Channels { get; }
public ObservableCollection<PumpControlChannel> PumpControls { get; }
public ObservableCollection<ValveControlChannel> ValveControls { get; }
public IEnumerable<PumpControlChannel> PressureDropPumpControls => PumpControlsFor("NegativeAssistPump", "PressureDropPump");
public IEnumerable<PumpControlChannel> RecirculationPumpControls => PumpControlsFor("RecirculationMainPump", "RecirculationReturnPump", "RecirculationDrainagePump");
public IEnumerable<PumpControlChannel> KinkResistancePumpControls => PumpControlsFor("KinkResistancePump");
public IEnumerable<PumpControlChannel> HemolysisPumpControls => PumpControlsFor("HemolysisDrainageSinglePump", "HemolysisReturnSinglePump", "HemolysisDualLumenPump");
public ObservableCollection<TraceEvent> TraceEvents { get; }
public ObservableCollection<AlarmMessage> AlarmMessages { get; }
public ObservableCollection<string> ResultStatusOptions { get; }
public ObservableCollection<PressureDropPointEntry> PressureDropEntries { get; }
public ObservableCollection<KinkResistancePointEntry> KinkResistanceEntries { get; }
public ObservableCollection<RecirculationPointEntry> RecirculationEntries { get; }
public HemolysisTestParameters HemolysisTestParameters { get; }
public ObservableCollection<HemolysisSamplingEntry> HemolysisSamplingEntries { get; }
public ObservableCollection<double> ProximalPressureTrendValues { get; } = [];
public ObservableCollection<double> DistalPressureTrendValues { get; } = [];
public ObservableCollection<double> DeltaPressureTrendValues { get; } = [];
public ObservableCollection<double> PressureDropPumpTrendValues { get; } = [];
public ObservableCollection<double> RecirculationMainPumpTrendValues { get; } = [];
public ObservableCollection<double> RecirculationReturnPumpTrendValues { get; } = [];
public ObservableCollection<double> RecirculationDrainagePumpTrendValues { get; } = [];
public ObservableCollection<double> KinkResistancePumpTrendValues { get; } = [];
public ObservableCollection<double> HemolysisDrainageSingleTrendValues { get; } = [];
public ObservableCollection<double> HemolysisReturnSingleTrendValues { get; } = [];
public ObservableCollection<double> HemolysisDualLumenTrendValues { get; } = [];
public ObservableCollection<string> ItemFilterOptions { get; } = new(["全部", "待填写", "已完成", "实时监控", "手动填写"]);
public ObservableCollection<string> HemolysisBloodSourceOptions { get; } = new(["肝素化牛血", "肝素化猪血", "肝素化羊血"]);
public ObservableCollection<string> HemolysisAnticoagulantOptions { get; } = new(["肝素", "枸橼酸钠", "其他"]);
public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
public bool HasSelectedItem => SelectedItem is not null;
public IEnumerable<DeviceChannel> FlowSensorChannels => Channels.Where(IsFlowSensorChannel);
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
public bool IsTelemetryOnline => _telemetryService.IsLiveConnected;
public string PlcEndpointDisplay => _telemetryService.EndpointDescription;
public string TelemetryLastUpdatedDisplay => _telemetryService.LastSuccessfulReadAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "未收到实时数据";
public string TelemetryStatusDetail => _telemetryService.LastErrorMessage;
public string TelemetryAvailabilityDisplay => $"{Channels.Count(channel => channel.IsAvailable)}/{Channels.Count} 路已接入";
public string AlarmSummaryDisplay => AlarmMessages.Count == 0 ? "无实时告警" : $"{AlarmMessages.Count} 条实时告警";
public string TelemetryCoverageDisplay => "已映射:泵状态、阀状态、流量寄存器、近端/远端压力;未映射:负压、温度、游离 Hb、白细胞减少率。";
public string ComplianceDisplay => $"{ComplianceRate:F0}%";
public string DeltaPressureDisplay => HasChannelTelemetry("近端压力", "远端压力") ? $"{DeltaPressure:F1} mmHg" : "--";
public string ExportStateText => DetectionCompleted ? "检测已完成,可导出检查报告" : "检测进行中,完成后可导出检查报告";
public string SelectedItemTitle => SelectedItem?.Item ?? "未选择项目";
public string SelectedItemStatusText => SelectedItem?.StatusText ?? "待检";
public string RealtimeRecirculationDisplay => ChannelDisplay("再循环率", "F1", "%");
public string PumpFlowDisplay => ChannelDisplay("主泵流量", "F2", "L/min");
public string DrainageFlowDisplay => ChannelDisplay("静脉引流流量", "F2", "L/min");
public string ReturnFlowDisplay => ChannelDisplay("动脉回输流量", "F2", "L/min");
public string PressureDropPumpFlowDisplay => ChannelDisplay("主泵流量", "F2", "L/min");
public string RecirculationPumpFlowDisplay => ChannelDisplay("再循环主泵流量", "F2", "L/min");
public string KinkResistancePumpFlowDisplay => ChannelDisplay("抗扭结主泵流量", "F2", "L/min");
public string HemolysisDrainageSingleFlowDisplay => ChannelDisplay("血细胞破坏-单腔引流/回输流量", "F2", "L/min");
public string HemolysisReturnSingleFlowDisplay => ChannelDisplay("双腔插管试验回路流量", "F2", "L/min");
public string HemolysisDualLumenFlowDisplay => ChannelDisplay("双腔插管试验回路流量(两个管腔)", "F2", "L/min");
public string FlowTrendTitle => SelectedItem?.Clause switch
{
"4.3.3" => "再循环流量趋势",
"4.2.3" => "抗扭结流量趋势",
"4.3.4" => "血细胞破坏流量趋势",
_ => "压力降/抗塌陷流量趋势"
};
public string FlowTrendPrimaryLabel => SelectedItem?.Clause switch
{
"4.3.3" => "主泵",
"4.2.3" => "抗扭结泵",
"4.3.4" => "单腔引流/回输",
_ => "主泵"
};
public string FlowTrendSecondaryLabel => SelectedItem?.Clause switch
{
"4.3.3" => "回流",
"4.3.4" => "双腔插管试验回路",
_ => string.Empty
};
public string FlowTrendTertiaryLabel => SelectedItem?.Clause switch
{
"4.3.3" => "引流",
"4.3.4" => "双腔插管试验回路(两个管腔)",
_ => string.Empty
};
public bool HasFlowTrendSecondary => !string.IsNullOrWhiteSpace(FlowTrendSecondaryLabel);
public bool HasFlowTrendTertiary => !string.IsNullOrWhiteSpace(FlowTrendTertiaryLabel);
public ObservableCollection<double> ActiveFlowTrendPrimaryValues => SelectedItem?.Clause switch
{
"4.3.3" => RecirculationMainPumpTrendValues,
"4.2.3" => KinkResistancePumpTrendValues,
"4.3.4" => HemolysisDrainageSingleTrendValues,
_ => PressureDropPumpTrendValues
};
public ObservableCollection<double> ActiveFlowTrendSecondaryValues => SelectedItem?.Clause switch
{
"4.3.3" => RecirculationReturnPumpTrendValues,
"4.3.4" => HemolysisReturnSingleTrendValues,
_ => []
};
public ObservableCollection<double> ActiveFlowTrendTertiaryValues => SelectedItem?.Clause switch
{
"4.3.3" => RecirculationDrainagePumpTrendValues,
"4.3.4" => HemolysisDualLumenTrendValues,
_ => []
};
public double PressureTrendMax => MaxTrendValue([ProximalPressureTrendValues, DistalPressureTrendValues, DeltaPressureTrendValues], 40d);
public double FlowTrendMax => MaxTrendValue([ActiveFlowTrendPrimaryValues, ActiveFlowTrendSecondaryValues, ActiveFlowTrendTertiaryValues], Math.Max(RatedMaxFlow, 1d));
public string ProximalPressureDisplay => ChannelDisplay("近端压力", "F1", "mmHg");
public string DistalPressureDisplay => ChannelDisplay("远端压力", "F1", "mmHg");
public string FlowImbalanceDisplay => HasChannelTelemetry("主泵流量", "动脉回输流量") ? $"{Math.Abs(PumpFlow - ReturnFlow):F2} L/min" : "--";
public string PumpFlowLoadDisplay => ChannelLoadDisplay("主泵流量");
public string DrainageFlowLoadDisplay => ChannelLoadDisplay("静脉引流流量");
public string ReturnFlowLoadDisplay => ChannelLoadDisplay("动脉回输流量");
public double PumpFlowNormalizedValue => ChannelNormalizedValue("主泵流量");
public double DrainageFlowNormalizedValue => ChannelNormalizedValue("静脉引流流量");
public double ReturnFlowNormalizedValue => ChannelNormalizedValue("动脉回输流量");
public string FilteredItemSummary => $"{FilteredItemsView.Cast<object>().Count()} / {InspectionItems.Count} 项";
public bool HasItemSearchText => !string.IsNullOrWhiteSpace(ItemSearchText);
public int RealtimeMonitorCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor);
public int RealtimeAssistCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.RealtimeAssist);
public int ManualEntryCount => InspectionItems.Count(item => item.CaptureMode == InspectionItemCaptureMode.ManualEntry);
public string SelectedItemCaptureModeText => SelectedItem?.CaptureModeText ?? "未选择";
public string SelectedItemMeasurementSource => SelectedItem?.MeasurementSource ?? "-";
public bool SelectedItemUsesRealtimeValue => SelectedItem?.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor;
public string RealtimeMeasurementHint => SelectedItemUsesRealtimeValue
? "当前项目使用实时数据自动判定,无需手动填写。"
: SelectedItem?.ManualEntryHint ?? "当前项目需要人工填写结果。";
public string SelectedItemLiveDisplay => BuildSelectedItemLiveDisplay();
public string SelectedItemLiveHint => SelectedItem?.LiveDisplayHint ?? "当前项目无实时映射。";
public string NegativeAssistPressureDisplay => ChannelDisplay("负压辅助引流", "F1", "kPa");
public string TemperatureDisplay => ChannelDisplay("模拟血液温度", "F1", "°C");
public string FreeHemoglobinDisplay => ChannelDisplay("游离血红蛋白", "F3", "g/L");
public string WhiteCellLossDisplay => ChannelDisplay("白细胞减少率", "F1", "%");
public string PressureDropConditionDisplay => $"模拟血液 3.2±0.2 mPa·s / 温度 {TemperatureDisplay} / 最终灭菌成品";
public string ConfigurationSummary => $"型号:{ProductModel} / 适用人群:{ApplicablePopulation} / 标称最大流量:{RatedMaxFlow:F2} L/min";
public string PressureDropFlowPointDisplay =>
$"50%={PressureDropFlowPoint(0.50):F2} L/min / 75%={PressureDropFlowPoint(0.75):F2} L/min / 100%={PressureDropFlowPoint(1.00):F2} L/min";
public string PressureDropLimitDisplay => $"50%≤{PressureDropLimit50:F1} / 75%≤{PressureDropLimit75:F1} / 100%≤{PressureDropLimit100:F1} mmHg";
public string KinkResistanceFlowPointDisplay => $"最大={RatedMaxFlow:F2} L/min / 最小={Math.Clamp(KinkResistanceMinimumFlow, 0d, Math.Max(RatedMaxFlow, 0d)):F2} L/min";
public string KinkResistanceMandrelDiameterDisplay => $"圆角模板直径:{Math.Max(KinkResistanceOuterDiameter, 0d) * 4d:F1} mm外径 {KinkResistanceOuterDiameter:F1} mm × 4";
public bool IsPressureDropSelected => SelectedItem?.Clause == "4.3.1";
public string PressureDropSamplingSummary => BuildPressureDropSamplingSummary();
public bool IsKinkResistanceSelected => SelectedItem?.Clause == "4.2.3";
public string KinkResistanceSamplingSummary => BuildKinkResistanceSamplingSummary();
public bool IsAntiCollapseSelected => SelectedItem?.Clause == "4.3.2";
public bool IsRecirculationSelected => SelectedItem?.Clause == "4.3.3";
public bool IsHemolysisSelected => SelectedItem?.Clause == "4.3.4";
public bool IsHemolysisPrimaryInputSelected => IsHemolysisSelected && string.Equals(SelectedItem?.Item, "血细胞破坏", StringComparison.Ordinal);
public bool IsHemolysisReductionSelected => IsHemolysisSelected && string.Equals(SelectedItem?.Item, "血小板/白细胞减少率", StringComparison.Ordinal);
public string HemolysisStandardSummary =>
IsHemolysisReductionSelected
? "共用记录要点:血小板/白细胞减少率与“血细胞破坏”共用同一套试样运行与取样记录,减少率以前后标准取样点的细胞计数变化计算。"
: "血细胞破坏记录要点:试验介质应采用肝素化牛血、猪血或羊血;装配两个通用且等同的回路;两个回路初始血液通道试验液容积差不应超过 1%;关键条件包括血流量为制造商临床使用规定的最大值、血中葡萄糖 10 mmol/L、血红蛋白 12 g/dL标准取样点为试验前、30 min、180 min、360 min。";
public string HemolysisTemplateGuidance =>
IsHemolysisReductionSelected
? "录入建议:本项复用“血细胞破坏”已维护的基础条件,仅需核对下方共用取样表和减少率计算结果,无需重复录入试样准备。"
: "录入建议:先填写试验血液准备和回路运行条件,再在下方表格录入取样数据;系统会自动汇总 ΔfHb、NIH、白细胞减少率和血小板减少率。";
public string HemolysisSharedRecordHint =>
IsHemolysisReductionSelected
? "当前为“血小板/白细胞减少率”项目。本区沿用“血细胞破坏”同一批试样运行与取样记录,基础条件不重复录入,只核对共用记录和减少率结果。"
: "当前录入的试样运行与取样记录会同时服务“血细胞破坏”和“血小板/白细胞减少率”两项,建议先完成基础条件,再统一录入标准取样点。";
public string HemolysisSamplingCompletionSummary => BuildHemolysisSamplingCompletionSummary();
public bool HemolysisHasMissingRequiredPoints => GetHemolysisMissingRequiredPoints().Count > 0;
public string HemolysisRequiredPointAlert => BuildHemolysisRequiredPointAlert();
public string HemolysisCalculationSummary => BuildHemolysisCalculationSummary();
public string HemolysisCalculationTitle => IsHemolysisReductionSelected ? "减少率计算摘要" : "自动计算摘要";
public string HemolysisCalculationDetail =>
IsHemolysisReductionSelected
? $"白细胞减少率 {FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")};血小板减少率 {FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}{BuildHemolysisSamplingCompletionSummary()}"
: BuildHemolysisCalculationSummary();
public string HemolysisSamplingSectionTitle => IsHemolysisReductionSelected ? "共用试样运行与取样记录" : "试样运行与取样记录";
public string PressureTrendCurrentSummary => $"近端 {ProximalPressureDisplay} / 远端 {DistalPressureDisplay} / ΔP {DeltaPressureDisplay}";
public string FlowTrendCurrentSummary => BuildFlowTrendCurrentSummary();
public string HemolysisRecordTemplate =>
"3.2 试验血液准备\n" +
$"- 依据 GB/T 16886.4 进行血液预处理。\n" +
$"- 血液来源:{HemolysisTestParameters.BloodSource}\n" +
$"- 采血日期:{FormatHemolysisDate(HemolysisTestParameters.CollectionDate)}\n" +
$"- 抗凝剂:{HemolysisTestParameters.Anticoagulant}\n" +
$"- 初始 Hct{FormatHemolysisValue(HemolysisTestParameters.InitialHematocrit, "F2")}\n" +
$"- 调整后 Hct{FormatHemolysisValue(HemolysisTestParameters.AdjustedHematocrit, "F2")}(目标 0.30 ± 0.02\n" +
$"- 血中葡萄糖:{FormatHemolysisValue(HemolysisTestParameters.Glucose, "F1")} mmol/L\n" +
$"- 血红蛋白:{FormatHemolysisValue(HemolysisTestParameters.TotalHemoglobin, "F1")} g/dL\n" +
$"- 初始游离血红蛋白 fHb{FormatHemolysisValue(GetHemolysisInitialFreeHemoglobin(), "F1")} mg/dL\n\n" +
"3.3 测试回路说明\n" +
$"- 回路总充盈量:{FormatHemolysisValue(HemolysisTestParameters.CircuitPrimingVolume, "F0")} mL\n" +
$"- 回路初始血液通道试验液容积差:{FormatHemolysisValue(HemolysisTestParameters.CircuitVolumeDifference, "F2")} %(应 ≤ 1%\n" +
$"- 设定流量:{FormatHemolysisValue(HemolysisTestParameters.SetFlow, "F2")} L/min\n" +
$"- 设定运行时间:{FormatHemolysisValue(HemolysisTestParameters.RunTimeMinutes, "F0")} min\n" +
$"- 温度控制:{FormatHemolysisValue(HemolysisTestParameters.TargetTemperature, "F1")} ℃ ± 2 ℃\n\n" +
"4. 试验运行与取样记录";
public string HemolysisRecordNoteTemplate => BuildHemolysisRecordNoteText();
public bool HasAntiCollapseBaseline => _antiCollapseBaselinePressureDrop.HasValue && _antiCollapseBaselineFlow.HasValue;
public string AntiCollapseBaselineDisplay => HasAntiCollapseBaseline
? $"基线 ΔP {_antiCollapseBaselinePressureDrop:F1} mmHg / 流量 {_antiCollapseBaselineFlow:F2} L/min / 时间 {_antiCollapseBaselineCapturedAt:HH:mm:ss}"
: "尚未采集基线";
public string AntiCollapseComparisonDisplay
{
get
{
var comparison = GetAntiCollapseComparison();
return comparison.HasBaseline
? $"当前 ΔP {DeltaPressure:F1} mmHg较基线增加 {comparison.Increase:F1} mmHg ({comparison.IncreaseRate:F1}%){comparison.StatusText}"
: "请先在无负压条件下采集基线,再进行负压比较";
}
}
public string RecirculationFlowPointDisplay =>
$"50%={PressureDropFlowPoint(0.50):F2} L/min / 75%={PressureDropFlowPoint(0.75):F2} L/min / 100%={PressureDropFlowPoint(1.00):F2} L/min";
public string RecirculationSamplingSummary => BuildRecirculationSamplingSummary();
public string RecirculationLimitDisplay => $"制造商声明限值R ≤ {RecirculationAllowedLimit:F1}%";
public double PressureDropPumpFlow => ChannelValue("主泵流量");
public double RecirculationPumpFlow => ChannelValue("再循环主泵流量");
public double KinkResistancePumpFlow => ChannelValue("抗扭结主泵流量");
public double HemolysisDrainageSingleFlow => ChannelValue("血细胞破坏-单腔引流/回输流量");
public double HemolysisReturnSingleFlow => ChannelValue("双腔插管试验回路流量");
public double HemolysisDualLumenFlow => ChannelValue("双腔插管试验回路流量(两个管腔)");
public double PumpFlow => ChannelValue("主泵流量");
public double DrainageFlow => ChannelValue("静脉引流流量");
public double ReturnFlow => ChannelValue("动脉回输流量");
public double RecirculationRate => ChannelValue("再循环率");
partial void OnComplianceRateChanged(double value) => OnPropertyChanged(nameof(ComplianceDisplay));
partial void OnDeltaPressureChanged(double value) => OnPropertyChanged(nameof(DeltaPressureDisplay));
partial void OnDetectionCompletedChanged(bool value) => OnPropertyChanged(nameof(ExportStateText));
partial void OnProductModelChanged(string value) => UpdateAndPersistLimitSettings();
partial void OnApplicablePopulationChanged(string value) => UpdateAndPersistLimitSettings();
partial void OnRatedMaxFlowChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnKinkResistanceMinimumFlowChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnKinkResistanceOuterDiameterChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnPressureDropLimit50Changed(double value) => UpdateAndPersistLimitSettings();
partial void OnPressureDropLimit75Changed(double value) => UpdateAndPersistLimitSettings();
partial void OnPressureDropLimit100Changed(double value) => UpdateAndPersistLimitSettings();
partial void OnAntiCollapseAllowedIncreaseRateChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnRecirculationAllowedLimitChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnItemSearchTextChanged(string value)
{
RefreshFilteredItemsView();
}
public string ActiveFilter
{
get => activeFilter;
set
{
if (SetProperty(ref activeFilter, value))
{
RefreshFilteredItemsView();
}
}
}
partial void OnSelectedItemChanged(InspectionItem? value)
{
OnPropertyChanged(nameof(HasSelectedItem));
OnPropertyChanged(nameof(SelectedItemTitle));
OnPropertyChanged(nameof(SelectedItemStatusText));
OnPropertyChanged(nameof(SelectedItemCaptureModeText));
OnPropertyChanged(nameof(SelectedItemMeasurementSource));
OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue));
OnPropertyChanged(nameof(IsPressureDropSelected));
OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(IsKinkResistanceSelected));
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(IsAntiCollapseSelected));
OnPropertyChanged(nameof(IsRecirculationSelected));
OnPropertyChanged(nameof(IsHemolysisSelected));
OnPropertyChanged(nameof(IsHemolysisPrimaryInputSelected));
OnPropertyChanged(nameof(IsHemolysisReductionSelected));
OnPropertyChanged(nameof(HemolysisStandardSummary));
OnPropertyChanged(nameof(HemolysisTemplateGuidance));
OnPropertyChanged(nameof(HemolysisSharedRecordHint));
OnPropertyChanged(nameof(HemolysisSamplingCompletionSummary));
OnPropertyChanged(nameof(HemolysisHasMissingRequiredPoints));
OnPropertyChanged(nameof(HemolysisRequiredPointAlert));
OnPropertyChanged(nameof(HemolysisCalculationTitle));
OnPropertyChanged(nameof(HemolysisCalculationDetail));
OnPropertyChanged(nameof(HemolysisCalculationSummary));
OnPropertyChanged(nameof(HemolysisSamplingSectionTitle));
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
OnPropertyChanged(nameof(RecirculationFlowPointDisplay));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
OnPropertyChanged(nameof(RealtimeMeasurementHint));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
OnPropertyChanged(nameof(SelectedItemLiveHint));
OnPropertyChanged(nameof(FlowTrendTitle));
OnPropertyChanged(nameof(FlowTrendPrimaryLabel));
OnPropertyChanged(nameof(FlowTrendSecondaryLabel));
OnPropertyChanged(nameof(FlowTrendTertiaryLabel));
OnPropertyChanged(nameof(HasFlowTrendSecondary));
OnPropertyChanged(nameof(HasFlowTrendTertiary));
OnPropertyChanged(nameof(ActiveFlowTrendPrimaryValues));
OnPropertyChanged(nameof(ActiveFlowTrendSecondaryValues));
OnPropertyChanged(nameof(ActiveFlowTrendTertiaryValues));
OnPropertyChanged(nameof(FlowTrendMax));
if (value is not null)
{
LoadSelectedItemDraft(value);
return;
}
ResultValue = string.Empty;
ResultNote = string.Empty;
ResultOperator = OperatorName;
SelectedResultStatusText = "待检";
}
[RelayCommand]
private void SelectItem(InspectionItem? item)
{
if (item is null)
{
return;
}
SelectedItem = item;
}
[RelayCommand]
private void ClearItemSearch()
{
ItemSearchText = string.Empty;
}
[RelayCommand]
private void ShowPendingItems()
{
ActiveFilter = "待填写";
LatestAction = $"已切换到待处理项目,共 {PendingCount} 项。";
}
[RelayCommand]
private void ShowAllItems()
{
ActiveFilter = "全部";
LatestAction = $"已切换到全部项目,共 {InspectionItems.Count} 项。";
}
[RelayCommand]
private void TogglePumpControl(PumpControlChannel? pump)
{
if (pump is null)
{
return;
}
var nextState = !pump.IsRunning;
_telemetryService.SetPumpRunning(pump.Key, nextState);
LatestAction = _telemetryService.IsLiveConnected
? $"{pump.Name} 已发送{(nextState ? "" : "")}指令。"
: $"{pump.Name} 指令未下发PLC 当前离线。";
TraceEvents.Insert(0, NewTrace("泵控", $"{pump.Name} => {(nextState ? "" : "")}"));
RefreshTelemetry();
}
[RelayCommand]
private void ToggleValveControl(ValveControlChannel? valve)
{
if (valve is null)
{
return;
}
var nextState = !valve.IsOpen;
_telemetryService.SetValveOpen(valve.Key, nextState);
LatestAction = _telemetryService.IsLiveConnected
? $"{valve.Name} 已发送{(nextState ? "" : "")}指令。"
: $"{valve.Name} 指令未下发PLC 当前离线。";
TraceEvents.Insert(0, NewTrace("阀控", $"{valve.Name} => {(nextState ? "" : "")}"));
RefreshTelemetry();
}
[RelayCommand]
private void ClearTrendData()
{
ClearTrendSeries(
ProximalPressureTrendValues,
DistalPressureTrendValues,
DeltaPressureTrendValues,
PressureDropPumpTrendValues,
RecirculationMainPumpTrendValues,
RecirculationReturnPumpTrendValues,
RecirculationDrainagePumpTrendValues,
KinkResistancePumpTrendValues,
HemolysisDrainageSingleTrendValues,
HemolysisReturnSingleTrendValues,
HemolysisDualLumenTrendValues);
LatestAction = "已清空实时趋势曲线。";
TraceEvents.Insert(0, NewTrace("趋势图", "已清空实时趋势曲线"));
RaiseTrendPropertyChanges();
}
[RelayCommand]
private void CapturePressureDrop50() => CapturePressureDropSample("50%");
[RelayCommand]
private void CapturePressureDrop75() => CapturePressureDropSample("75%");
[RelayCommand]
private void CapturePressureDrop100() => CapturePressureDropSample("100%");
[RelayCommand]
private void CaptureKinkResistanceMaxBaseline() => CaptureKinkResistanceBaseline("最大流量");
[RelayCommand]
private void CaptureKinkResistanceMaxKinked() => CaptureKinkResistanceKinked("最大流量");
[RelayCommand]
private void CaptureKinkResistanceMinBaseline() => CaptureKinkResistanceBaseline("最小流量");
[RelayCommand]
private void CaptureKinkResistanceMinKinked() => CaptureKinkResistanceKinked("最小流量");
[RelayCommand]
private void CaptureAntiCollapseBaseline()
{
if (!IsAntiCollapseSelected)
{
LatestAction = "当前选择的不是抗塌陷项目。";
return;
}
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "抗塌陷基线采集失败:主泵或压力实时数据未接入。";
return;
}
_antiCollapseBaselinePressureDrop = DeltaPressure;
_antiCollapseBaselineFlow = PumpFlow;
_antiCollapseBaselineCapturedAt = DateTime.Now;
_lastAutoAntiCollapseResult = string.Empty;
_lastAutoAntiCollapseNote = string.Empty;
var baselineText = BuildAntiCollapseMeasuredText();
var noteText = BuildAntiCollapseRecordNote();
ResultValue = baselineText;
ResultNote = noteText;
LatestAction = $"已采集抗塌陷基线ΔP {DeltaPressure:F1} mmHg流量 {PumpFlow:F2} L/min。";
TraceEvents.Insert(0, NewTrace("抗塌陷基线", $"ΔP {DeltaPressure:F1} mmHg / 流量 {PumpFlow:F2} L/min"));
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
[RelayCommand]
private void CaptureAntiCollapseComparison()
{
if (!IsAntiCollapseSelected)
{
LatestAction = "当前选择的不是抗塌陷项目。";
return;
}
if (!HasAntiCollapseBaseline)
{
LatestAction = "请先采集抗塌陷基线。";
return;
}
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "抗塌陷比较失败:主泵或压力实时数据未接入。";
return;
}
var resultText = BuildAntiCollapseMeasuredText();
var noteText = BuildAntiCollapseRecordNote();
var comparison = GetAntiCollapseComparison();
ResultValue = resultText;
ResultNote = noteText;
SelectedResultStatusText = comparison.StatusText.StartsWith("合格", StringComparison.Ordinal) ? "合格"
: comparison.StatusText.StartsWith("预警", StringComparison.Ordinal) ? "预警"
: "不合格";
_lastAutoAntiCollapseResult = resultText;
_lastAutoAntiCollapseNote = noteText;
LatestAction = $"已采集抗塌陷负压比较:增幅 {comparison.IncreaseRate:F1}% {comparison.StatusText}。";
TraceEvents.Insert(0, NewTrace("抗塌陷比较", $"增量 {comparison.Increase:F1} mmHg / 增幅 {comparison.IncreaseRate:F1}%"));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
[RelayCommand]
private void CaptureRecirculation50() => CaptureRecirculationSample("50%");
[RelayCommand]
private void CaptureRecirculation75() => CaptureRecirculationSample("75%");
[RelayCommand]
private void CaptureRecirculation100() => CaptureRecirculationSample("100%");
[RelayCommand]
private void ToggleAcquisition()
{
AcquisitionRunning = !AcquisitionRunning;
RefreshDeviceStatus();
LatestAction = AcquisitionRunning ? "继续采集实时数据,供检测参考。" : "已暂停实时采集。";
if (AcquisitionRunning)
{
_timer.Start();
TraceEvents.Insert(0, NewTrace("采集控制", "恢复实时采集"));
}
else
{
_timer.Stop();
TraceEvents.Insert(0, NewTrace("采集控制", "暂停实时采集"));
}
}
[RelayCommand]
private void SelectPreviousItem()
{
var scope = ActiveItemScope();
if (SelectedItem is null)
{
SelectedItem = scope.FirstOrDefault();
return;
}
var index = scope.IndexOf(SelectedItem);
if (index > 0)
{
SelectedItem = scope[index - 1];
}
}
[RelayCommand]
private void SelectNextItem()
{
var scope = ActiveItemScope();
if (SelectedItem is null)
{
SelectedItem = scope.FirstOrDefault();
return;
}
var index = scope.IndexOf(SelectedItem);
if (index >= 0 && index < scope.Count - 1)
{
SelectedItem = scope[index + 1];
}
}
[RelayCommand]
private void ApplyResult()
{
if (SelectedItem is null)
{
LatestAction = "请先选择项目。";
return;
}
if (SelectedItemUsesRealtimeValue)
{
LatestAction = "当前项目使用实时数据自动判定,无需手动填写。";
return;
}
if (IsHemolysisSelected)
{
ResultValue = BuildHemolysisResultText();
ResultNote = BuildHemolysisRecordNoteText();
_lastAutoHemolysisResult = ResultValue;
_lastAutoHemolysisNote = ResultNote;
}
if (string.IsNullOrWhiteSpace(ResultValue))
{
LatestAction = "请先填写检测结果或判定结论。";
return;
}
SelectedItem.Measured = ResultValue.Trim();
SelectedItem.Notes = ResultNote.Trim();
SelectedItem.RecordedBy = string.IsNullOrWhiteSpace(ResultOperator) ? OperatorName : ResultOperator.Trim();
SelectedItem.RecordedAt = DateTime.Now;
SelectedItem.Status = SelectedResultStatusText switch
{
"待检" => InspectionItemStatus.Pending,
"合格" => InspectionItemStatus.Qualified,
"预警" => InspectionItemStatus.Warning,
"不合格" => InspectionItemStatus.Critical,
_ => InspectionItemStatus.Pending
};
LatestAction = $"已填写 {SelectedItem.Item} 的检测结果。";
TraceEvents.Insert(0, new TraceEvent
{
Timestamp = DateTime.Now,
Stage = "结果填写",
Detail = $"{SelectedItem.Item}: {SelectedItem.Measured}",
Operator = SelectedItem.RecordedBy
});
RefreshComputedState();
RefreshFilteredItemsView();
}
[RelayCommand]
private void CompleteDetection()
{
DetectionCompleted = true;
CurrentStage = "检测完成";
AcquisitionRunning = false;
_timer.Stop();
DeviceStatus = "采集停止";
LatestAction = PendingCount == 0 ? "检测完成,全部项目已填写,可导出检查报告。" : $"检测完成,但仍有 {PendingCount} 项待处理,请确认后导出检查报告。";
TraceEvents.Insert(0, NewTrace("检测完成", "检测员结束本次检测任务"));
}
[RelayCommand]
private void AcknowledgeAlarm()
{
if (AlarmMessages.Count == 0)
{
LatestAction = "当前没有需要确认的告警。";
return;
}
var count = AlarmMessages.Count;
AlarmMessages.Clear();
OnPropertyChanged(nameof(AlarmSummaryDisplay));
LatestAction = $"已确认并清空 {count} 条实时告警。";
TraceEvents.Insert(0, NewTrace("告警确认", $"确认 {count} 条实时告警"));
}
[RelayCommand]
private void ExportReport()
{
var outputDirectory = ResolveReportOutputDirectory();
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
var batchToken = string.IsNullOrWhiteSpace(BatchNumber) ? "未填写批号" : SanitizeFileNameSegment(BatchNumber.Trim());
var pdfPath = Path.Combine(outputDirectory, $"检查报告-{batchToken}-{timestamp}.pdf");
try
{
var document = new PdfReportDocument(
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.GeneratePdf(pdfPath);
LatestAction = $"已导出检查报告: {pdfPath}";
TraceEvents.Insert(0, NewTrace("检查报告导出", Path.GetFileName(pdfPath)));
}
catch (Exception ex)
{
LatestAction = $"检查报告导出失败: {ex.Message}";
TraceEvents.Insert(0, NewTrace("检查报告导出失败", ex.Message));
}
}
private void RefreshTelemetry()
{
var alarms = _telemetryService.UpdateChannels();
AlarmMessages.Clear();
foreach (var alarm in alarms.OrderByDescending(a => a.Timestamp))
{
AlarmMessages.Add(alarm);
}
RefreshTelemetryPanel();
RefreshDeviceStatus();
RefreshComputedState();
RefreshFilteredItemsView();
}
private void RefreshTelemetryPanel()
{
DeltaPressure = HasChannelTelemetry("近端压力", "远端压力")
? ChannelValue("近端压力") - ChannelValue("远端压力")
: 0d;
OnPropertyChanged(nameof(PumpFlow));
OnPropertyChanged(nameof(DrainageFlow));
OnPropertyChanged(nameof(ReturnFlow));
OnPropertyChanged(nameof(RecirculationRate));
OnPropertyChanged(nameof(PumpFlowDisplay));
OnPropertyChanged(nameof(DrainageFlowDisplay));
OnPropertyChanged(nameof(ReturnFlowDisplay));
OnPropertyChanged(nameof(PressureDropPumpFlowDisplay));
OnPropertyChanged(nameof(RecirculationPumpFlowDisplay));
OnPropertyChanged(nameof(KinkResistancePumpFlowDisplay));
OnPropertyChanged(nameof(HemolysisDrainageSingleFlowDisplay));
OnPropertyChanged(nameof(HemolysisReturnSingleFlowDisplay));
OnPropertyChanged(nameof(HemolysisDualLumenFlowDisplay));
OnPropertyChanged(nameof(ProximalPressureDisplay));
OnPropertyChanged(nameof(DistalPressureDisplay));
OnPropertyChanged(nameof(RealtimeRecirculationDisplay));
OnPropertyChanged(nameof(FlowImbalanceDisplay));
OnPropertyChanged(nameof(NegativeAssistPressureDisplay));
OnPropertyChanged(nameof(TemperatureDisplay));
OnPropertyChanged(nameof(FreeHemoglobinDisplay));
OnPropertyChanged(nameof(WhiteCellLossDisplay));
OnPropertyChanged(nameof(PressureDropConditionDisplay));
OnPropertyChanged(nameof(ConfigurationSummary));
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(PressureDropFlowPointDisplay));
OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
OnPropertyChanged(nameof(RecirculationFlowPointDisplay));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
OnPropertyChanged(nameof(PumpFlowLoadDisplay));
OnPropertyChanged(nameof(DrainageFlowLoadDisplay));
OnPropertyChanged(nameof(ReturnFlowLoadDisplay));
OnPropertyChanged(nameof(PumpFlowNormalizedValue));
OnPropertyChanged(nameof(DrainageFlowNormalizedValue));
OnPropertyChanged(nameof(ReturnFlowNormalizedValue));
OnPropertyChanged(nameof(FlowSensorChannels));
OnPropertyChanged(nameof(OtherChannels));
OnPropertyChanged(nameof(PumpControls));
OnPropertyChanged(nameof(IsTelemetryOnline));
OnPropertyChanged(nameof(PlcEndpointDisplay));
OnPropertyChanged(nameof(TelemetryLastUpdatedDisplay));
OnPropertyChanged(nameof(TelemetryStatusDetail));
OnPropertyChanged(nameof(TelemetryAvailabilityDisplay));
OnPropertyChanged(nameof(AlarmSummaryDisplay));
OnPropertyChanged(nameof(TelemetryCoverageDisplay));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
OnPropertyChanged(nameof(SelectedItemLiveHint));
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
CaptureTrendSamples();
SyncRealtimeItems();
}
private void RefreshDeviceStatus()
{
if (!AcquisitionRunning)
{
DeviceStatus = DetectionCompleted ? "采集停止" : "采集暂停";
return;
}
DeviceStatus = _telemetryService.IsLiveConnected
? "PLC在线"
: _telemetryService.LastSuccessfulReadAt.HasValue
? "PLC断连"
: "等待连接";
}
private void RefreshComputedState()
{
QualifiedCount = InspectionItems.Count(r => r.Status == InspectionItemStatus.Qualified);
WarningCount = InspectionItems.Count(r => r.Status == InspectionItemStatus.Warning || r.Status == InspectionItemStatus.Critical);
PendingCount = InspectionItems.Count(r => r.Status == InspectionItemStatus.Pending);
ComplianceRate = InspectionItems.Count == 0 ? 0 : QualifiedCount * 100d / InspectionItems.Count;
}
private void CaptureTrendSamples()
{
AppendTrendValueIfAvailable(ProximalPressureTrendValues, "近端压力");
AppendTrendValueIfAvailable(DistalPressureTrendValues, "远端压力");
if (HasChannelTelemetry("近端压力", "远端压力"))
{
AppendTrendValue(DeltaPressureTrendValues, DeltaPressure);
}
AppendTrendValueIfAvailable(PressureDropPumpTrendValues, "主泵流量");
AppendTrendValueIfAvailable(RecirculationMainPumpTrendValues, "再循环主泵流量");
AppendTrendValueIfAvailable(RecirculationReturnPumpTrendValues, "动脉回输流量");
AppendTrendValueIfAvailable(RecirculationDrainagePumpTrendValues, "静脉引流流量");
AppendTrendValueIfAvailable(KinkResistancePumpTrendValues, "抗扭结主泵流量");
AppendTrendValueIfAvailable(HemolysisDrainageSingleTrendValues, "血细胞破坏-单腔引流/回输流量");
AppendTrendValueIfAvailable(HemolysisReturnSingleTrendValues, "双腔插管试验回路流量");
AppendTrendValueIfAvailable(HemolysisDualLumenTrendValues, "双腔插管试验回路流量(两个管腔)");
RaiseTrendPropertyChanges();
}
private void AppendTrendValueIfAvailable(ObservableCollection<double> series, string channelName)
{
if (TryGetChannel(channelName, out var channel) && channel.IsAvailable)
{
AppendTrendValue(series, channel.Value);
}
}
private static void AppendTrendValue(ObservableCollection<double> series, double value)
{
series.Add(value);
while (series.Count > TrendHistoryCapacity)
{
series.RemoveAt(0);
}
}
private static double MaxTrendValue(IEnumerable<IEnumerable<double>> seriesGroup, double fallback)
{
var max = seriesGroup
.SelectMany(series => series.DefaultIfEmpty(0d))
.DefaultIfEmpty(fallback)
.Max();
return Math.Max(fallback, max) * 1.1d;
}
private void ClearTrendSeries(params ObservableCollection<double>[] seriesGroup)
{
foreach (var series in seriesGroup)
{
series.Clear();
}
}
private void RaiseTrendPropertyChanges()
{
OnPropertyChanged(nameof(ProximalPressureTrendValues));
OnPropertyChanged(nameof(DistalPressureTrendValues));
OnPropertyChanged(nameof(DeltaPressureTrendValues));
OnPropertyChanged(nameof(PressureDropPumpTrendValues));
OnPropertyChanged(nameof(RecirculationMainPumpTrendValues));
OnPropertyChanged(nameof(RecirculationReturnPumpTrendValues));
OnPropertyChanged(nameof(RecirculationDrainagePumpTrendValues));
OnPropertyChanged(nameof(KinkResistancePumpTrendValues));
OnPropertyChanged(nameof(HemolysisDrainageSingleTrendValues));
OnPropertyChanged(nameof(HemolysisReturnSingleTrendValues));
OnPropertyChanged(nameof(HemolysisDualLumenTrendValues));
OnPropertyChanged(nameof(ActiveFlowTrendPrimaryValues));
OnPropertyChanged(nameof(ActiveFlowTrendSecondaryValues));
OnPropertyChanged(nameof(ActiveFlowTrendTertiaryValues));
OnPropertyChanged(nameof(PressureTrendMax));
OnPropertyChanged(nameof(FlowTrendMax));
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
}
private void RefreshFilteredItemsView()
{
FilteredItemsView.Refresh();
OnPropertyChanged(nameof(FilteredItemSummary));
OnPropertyChanged(nameof(HasFilteredItems));
OnPropertyChanged(nameof(HasItemSearchText));
var filtered = FilteredItemsView.Cast<InspectionItem>().ToList();
if (filtered.Count == 0)
{
SelectedItem = null;
return;
}
if (SelectedItem is null || !filtered.Contains(SelectedItem))
{
SelectedItem = filtered[0];
}
}
private void LoadSelectedItemDraft(InspectionItem item)
{
ResultValue = SelectedItemUsesRealtimeValue
? item.Measured
: item.Measured == "待检测" ? string.Empty : item.Measured;
ResultNote = item.Notes;
if (item.Clause == "4.3.4")
{
if (string.IsNullOrWhiteSpace(ResultValue))
{
ResultValue = BuildHemolysisResultText();
}
if (string.IsNullOrWhiteSpace(ResultNote))
{
ResultNote = BuildHemolysisRecordNoteText();
}
_lastAutoHemolysisResult = BuildHemolysisResultText();
_lastAutoHemolysisNote = BuildHemolysisRecordNoteText();
}
ResultOperator = string.IsNullOrWhiteSpace(item.RecordedBy) ? OperatorName : item.RecordedBy;
SelectedResultStatusText = item.Status switch
{
InspectionItemStatus.Warning => "预警",
InspectionItemStatus.Critical => "不合格",
InspectionItemStatus.Pending => "待检",
_ => "合格"
};
}
private TraceEvent NewTrace(string stage, string detail) => new()
{
Timestamp = DateTime.Now,
Stage = stage,
Detail = detail,
Operator = string.IsNullOrWhiteSpace(ResultOperator) ? OperatorName : ResultOperator
};
private void SyncRealtimeItems()
{
var pressureItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.1");
if (pressureItem is not null)
{
if (HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
var pressureStatus = GetPressureDropOverallStatus();
pressureItem.Measured = BuildPressureDropMeasuredText();
pressureItem.Notes = BuildPressureDropRecordNote();
pressureItem.RecordedBy = "实时数据";
pressureItem.RecordedAt = DateTime.Now;
pressureItem.Status = pressureStatus;
if (SelectedItem == pressureItem)
{
ResultValue = pressureItem.Measured;
ResultNote = pressureItem.Notes;
SelectedResultStatusText = pressureItem.Status switch
{
InspectionItemStatus.Pending => "待检",
InspectionItemStatus.Warning => "预警",
InspectionItemStatus.Critical => "不合格",
_ => "合格"
};
}
}
else
{
pressureItem.Measured = "等待 PLC 实时压力与流量数据";
pressureItem.Notes = $"当前实时链路不可用:{TelemetryStatusDetail}";
pressureItem.RecordedBy = string.Empty;
pressureItem.RecordedAt = null;
pressureItem.Status = InspectionItemStatus.Pending;
}
}
var kinkResistanceItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.2.3");
if (kinkResistanceItem is not null && SelectedItem == kinkResistanceItem)
{
var suggestedResult = BuildKinkResistanceMeasuredText();
var suggestedNote = BuildKinkResistanceRecordNote();
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == kinkResistanceItem.Measured)
{
ResultValue = suggestedResult;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == kinkResistanceItem.Notes)
{
ResultNote = suggestedNote;
}
}
var antiCollapseItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.2");
if (antiCollapseItem is not null)
{
if (HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
UpdateAntiCollapseBaseline();
}
if (SelectedItem == antiCollapseItem)
{
var suggestedResult = BuildAntiCollapseMeasuredText();
var suggestedNote = BuildAntiCollapseRecordNote();
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoAntiCollapseResult)
{
ResultValue = suggestedResult;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoAntiCollapseNote)
{
ResultNote = suggestedNote;
}
_lastAutoAntiCollapseResult = suggestedResult;
_lastAutoAntiCollapseNote = suggestedNote;
}
}
var recirculationItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.3");
if (recirculationItem is not null && SelectedItem == recirculationItem && HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
{
var suggestedResult = BuildRecirculationMeasuredText();
var suggestedNote = BuildRecirculationRecordNote();
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoRecirculationResult)
{
ResultValue = suggestedResult;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoRecirculationNote)
{
ResultNote = suggestedNote;
}
_lastAutoRecirculationResult = suggestedResult;
_lastAutoRecirculationNote = suggestedNote;
}
}
private string BuildHemolysisResultText() =>
HemolysisRecordTemplate + "\n\n" + BuildHemolysisSamplingTableText() + "\n\n" + BuildHemolysisCalculationSection();
private string BuildHemolysisSamplingTableText()
{
var lines = new List<string>
{
"标准取样节点T0、T30、T180、T360T60/T120/T240/T300 可作为过程观察补充。",
"| 取样序号 | 时间点 (min) | 挂钟时间 | 游离Hb (mg/dL) | Hct | 白细胞 (×10^9/L) | 血小板 (×10^9/L) | Hb (g/dL) | 流量 (L/min) | 压力 (mmHg) | 温度 (°C) | 备注 |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
};
foreach (var entry in HemolysisSamplingEntries)
{
lines.Add($"| {entry.Sequence} | {entry.TimePoint} | {entry.ClockTime} | {FormatHemolysisValue(entry.FreeHemoglobin, "F1")} | {FormatHemolysisValue(entry.Hematocrit, "F2")} | {FormatHemolysisValue(entry.WhiteCellCount, "F1")} | {FormatHemolysisValue(entry.PlateletCount, "F1")} | {FormatHemolysisValue(entry.Hemoglobin, "F1")} | {FormatHemolysisValue(entry.Flow, "F2")} | {FormatHemolysisValue(entry.Pressure, "F0")} | {FormatHemolysisValue(entry.Temperature, "F1")} | {entry.Remarks} |");
}
return string.Join("\n", lines);
}
private string BuildHemolysisCalculationSection()
{
var calculationHematocrit = GetHemolysisCalculationHematocrit();
return
"5. 结果计算\n" +
$"- 标准取样点完成情况:{BuildHemolysisSamplingCompletionSummary()}\n" +
$"- ΔfHb (T360 - T0){FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")}\n" +
"- NIH = ΔfHb × V(L) × (1-Hct) / (Q×T)\n" +
$"- NIH 计算参数V={FormatHemolysisDisplay(HemolysisTestParameters.CircuitPrimingVolume, "F0", "mL")}, Hct={FormatHemolysisDisplay(calculationHematocrit, "F2", string.Empty)}, Q={FormatHemolysisDisplay(HemolysisTestParameters.SetFlow, "F2", "L/min")}, T={FormatHemolysisDisplay(HemolysisTestParameters.RunTimeMinutes, "F0", "min")}\n" +
$"- 计算所得 NIH{FormatHemolysisDisplay(GetHemolysisNih(), "F3", "g/100L")}\n" +
$"- 白细胞减少率:{FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")}\n" +
$"- 血小板减少率:{FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}\n" +
$"- 判定结论:{SelectedResultStatusText}";
}
private string BuildHemolysisRecordNoteText()
{
var calculationHematocrit = GetHemolysisCalculationHematocrit();
return
"标准核对:\n" +
"1. 试验介质应为肝素化牛血、猪血或羊血。\n" +
"2. 两个通用等同回路的初始血液通道试验液容积差不应超过 1%。\n" +
"3. 应记录最大血流量、血中葡萄糖 10 mmol/L、血红蛋白 12 g/dL。\n" +
$"4. 标准取样点完成情况:{BuildHemolysisSamplingCompletionSummary()}。\n" +
$"5. NIH 计算采用 Hct={FormatHemolysisDisplay(calculationHematocrit, "F2", string.Empty)}、V={FormatHemolysisDisplay(HemolysisTestParameters.CircuitPrimingVolume, "F0", "mL")}、Q={FormatHemolysisDisplay(HemolysisTestParameters.SetFlow, "F2", "L/min")}、T={FormatHemolysisDisplay(HemolysisTestParameters.RunTimeMinutes, "F0", "min")}。\n" +
"6. T60、T120、T240、T300 为过程观察补充,不替代标准取样点。";
}
private string BuildHemolysisSamplingCompletionSummary()
{
var requiredPoints = new[] { "T0", "T30", "T180", "T360" };
var completedPoints = requiredPoints
.Where(point => HasHemolysisRequiredSample(GetHemolysisEntry(point)))
.ToList();
var missingPoints = requiredPoints.Except(completedPoints).ToList();
return missingPoints.Count == 0
? "标准取样点已完成 4/4"
: $"标准取样点已完成 {completedPoints.Count}/4缺少 {string.Join("", missingPoints)}";
}
private List<string> GetHemolysisMissingRequiredPoints()
{
var requiredPoints = new[] { "T0", "T30", "T180", "T360" };
return requiredPoints
.Where(point => !HasHemolysisRequiredSample(GetHemolysisEntry(point)))
.ToList();
}
private string BuildHemolysisRequiredPointAlert()
{
var missingPoints = GetHemolysisMissingRequiredPoints();
return missingPoints.Count == 0
? "标准取样点已全部完成。"
: $"请优先补齐标准取样点:{string.Join("", missingPoints)}。";
}
private string BuildHemolysisCalculationSummary() =>
$"{BuildHemolysisSamplingCompletionSummary()}ΔfHb {FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")}NIH {FormatHemolysisDisplay(GetHemolysisNih(), "F3", "g/100L")};白细胞减少率 {FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")};血小板减少率 {FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}";
private HemolysisSamplingEntry? GetHemolysisEntry(string timePointPrefix) =>
HemolysisSamplingEntries.FirstOrDefault(entry => entry.TimePoint.StartsWith(timePointPrefix, StringComparison.OrdinalIgnoreCase));
private static bool HasHemolysisRequiredSample(HemolysisSamplingEntry? entry) =>
entry is not null
&& !string.IsNullOrWhiteSpace(entry.ClockTime)
&& entry.FreeHemoglobin.HasValue;
private double? GetHemolysisInitialFreeHemoglobin() =>
HemolysisTestParameters.InitialFreeHemoglobin ?? GetHemolysisEntry("T0")?.FreeHemoglobin;
private double? GetHemolysisCalculationHematocrit() =>
HemolysisTestParameters.AdjustedHematocrit ?? GetHemolysisEntry("T0")?.Hematocrit;
private double? GetHemolysisDeltaFreeHemoglobin()
{
var start = GetHemolysisEntry("T0")?.FreeHemoglobin;
var end = GetHemolysisEntry("T360")?.FreeHemoglobin;
return start.HasValue && end.HasValue ? end.Value - start.Value : null;
}
private double? GetHemolysisNih()
{
var delta = GetHemolysisDeltaFreeHemoglobin();
var volumeMilliliter = HemolysisTestParameters.CircuitPrimingVolume;
var hematocrit = GetHemolysisCalculationHematocrit();
var flow = HemolysisTestParameters.SetFlow;
var time = HemolysisTestParameters.RunTimeMinutes;
if (!delta.HasValue || !volumeMilliliter.HasValue || !hematocrit.HasValue || !flow.HasValue || !time.HasValue
|| flow.Value <= 0 || time.Value <= 0)
{
return null;
}
var volumeLiter = volumeMilliliter.Value / 1000d;
return delta.Value * volumeLiter * (1d - hematocrit.Value) / (flow.Value * time.Value);
}
private double? GetHemolysisWhiteCellReduction() => CalculateHemolysisReduction(
GetHemolysisEntry("T0")?.WhiteCellCount,
GetHemolysisEntry("T360")?.WhiteCellCount);
private double? GetHemolysisPlateletReduction() => CalculateHemolysisReduction(
GetHemolysisEntry("T0")?.PlateletCount,
GetHemolysisEntry("T360")?.PlateletCount);
private static double? CalculateHemolysisReduction(double? initialValue, double? finalValue)
{
if (!initialValue.HasValue || !finalValue.HasValue || initialValue.Value <= 0)
{
return null;
}
return (initialValue.Value - finalValue.Value) / initialValue.Value * 100d;
}
private static string FormatHemolysisDisplay(double? value, string format, string unit)
{
if (!value.HasValue)
{
return "待计算";
}
return string.IsNullOrWhiteSpace(unit)
? value.Value.ToString(format)
: $"{value.Value.ToString(format)} {unit}";
}
private string BuildFlowTrendCurrentSummary() => SelectedItem?.Clause switch
{
"4.3.3" => $"主泵 {RecirculationPumpFlowDisplay} / 回流 {ReturnFlowDisplay} / 引流 {DrainageFlowDisplay}",
"4.2.3" => $"抗扭结泵 {KinkResistancePumpFlowDisplay}",
"4.3.4" => $"单腔引流/回输 {HemolysisDrainageSingleFlowDisplay} / 双腔插管试验回路 {HemolysisReturnSingleFlowDisplay} / 双腔插管试验回路(两个管腔) {HemolysisDualLumenFlowDisplay}",
_ => $"主泵 {PressureDropPumpFlowDisplay} / 流量偏差 {FlowImbalanceDisplay}"
};
private static string FormatHemolysisDate(DateTime? value) => value?.ToString("yyyy-MM-dd") ?? string.Empty;
private static string FormatHemolysisValue(double? value, string format) => value.HasValue ? value.Value.ToString(format) : string.Empty;
private double ChannelValue(string name) => Channels.First(channel => channel.Name == name).Value;
private double ChannelNormalizedValue(string name) => Channels.FirstOrDefault(channel => channel.Name == name)?.NormalizedValue ?? 0d;
private double ChannelValueOrDefault(string name) => TryGetChannel(name, out var channel) && channel.IsAvailable ? channel.Value : 0d;
private bool HasChannelTelemetry(params string[] names) => names.All(name => TryGetChannel(name, out var channel) && channel.IsAvailable);
private bool TryGetChannel(string name, out DeviceChannel channel)
{
channel = Channels.FirstOrDefault(item => item.Name == name)!;
return channel is not null;
}
private string ChannelDisplay(string name, string format, string unit)
{
if (!TryGetChannel(name, out var channel) || !channel.IsAvailable)
{
return "--";
}
return string.IsNullOrWhiteSpace(unit)
? channel.Value.ToString(format)
: $"{channel.Value.ToString(format)} {unit}";
}
private string ChannelLoadDisplay(string name)
{
if (!TryGetChannel(name, out var channel) || !channel.IsAvailable)
{
return "--";
}
return $"{channel.NormalizedValue:P0} 量程";
}
private IEnumerable<PumpControlChannel> PumpControlsFor(params string[] pumpKeys)
{
var orderLookup = pumpKeys
.Select((key, index) => new { key, index })
.ToDictionary(item => item.key, item => item.index);
return PumpControls
.Where(pump => orderLookup.ContainsKey(pump.Key))
.OrderBy(pump => orderLookup[pump.Key]);
}
private List<InspectionItem> ActiveItemScope() => FilteredItemsView.Cast<InspectionItem>().ToList();
private bool MatchesFilteredItem(object item) =>
item is InspectionItem inspectionItem
&& MatchesActiveFilter(inspectionItem)
&& MatchesItemSearch(inspectionItem);
private bool MatchesItemSearch(InspectionItem item)
{
if (string.IsNullOrWhiteSpace(ItemSearchText))
{
return true;
}
var keyword = ItemSearchText.Trim();
return MatchesKeyword(item.Clause, keyword)
|| MatchesKeyword(item.Item, keyword)
|| MatchesKeyword(item.Category, keyword)
|| MatchesKeyword(item.AcceptanceCriteria, keyword)
|| MatchesKeyword(item.TestMethod, keyword);
}
private bool MatchesActiveFilter(InspectionItem item) => ActiveFilter switch
{
"待填写" => item.Status == InspectionItemStatus.Pending,
"已完成" => item.Status != InspectionItemStatus.Pending,
"实时监控" => item.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor,
"手动填写" => item.CaptureMode != InspectionItemCaptureMode.RealtimeMonitor,
_ => true
};
private static bool MatchesKeyword(string source, string keyword)
{
if (source.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var sourceIndex = 0;
var keywordIndex = 0;
while (sourceIndex < source.Length && keywordIndex < keyword.Length)
{
if (char.ToUpperInvariant(source[sourceIndex]) == char.ToUpperInvariant(keyword[keywordIndex]))
{
keywordIndex++;
}
sourceIndex++;
}
return keywordIndex == keyword.Length;
}
private static bool IsFlowSensorChannel(DeviceChannel channel) => channel.Unit == "L/min";
private string BuildSelectedItemLiveDisplay()
{
if (SelectedItem is null)
{
return "未选择项目";
}
return (SelectedItem.Clause, SelectedItem.Item) switch
{
("4.2.3", _) => BuildKinkResistanceLiveDisplay(),
("4.3.1", _) => BuildPressureDropLiveDisplay(),
("4.3.2", _) => BuildAntiCollapseLiveDisplay(),
("4.3.3", _) => BuildRecirculationLiveDisplay(),
("4.3.4", "血细胞破坏") => $"ΔfHb {FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")} / NIH {FormatHemolysisDisplay(GetHemolysisNih(), "F3", "g/100L")} / {BuildHemolysisSamplingCompletionSummary()}",
("4.3.4", "血小板/白细胞减少率") => $"白细胞减少率 {FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")} / 血小板减少率 {FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")} / {BuildHemolysisSamplingCompletionSummary()}",
_ => "当前项目无实时信号,按检测原始记录手动填写。"
};
}
private string BuildKinkResistanceLiveDisplay()
{
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
return "抗扭结主泵实时流量未接入,当前仅可查看已保存采样记录。";
}
return
$"条件:血液/模拟血液 2.0~3.5 mPa·s / 当前温度 {TemperatureDisplay} / 40±1 °C\n" +
$"流量点:{KinkResistanceFlowPointDisplay}\n" +
$"{KinkResistanceMandrelDiameterDisplay}\n" +
$"当前主泵:{KinkResistancePumpFlow:F2} L/min\n" +
$"{BuildKinkResistanceSamplingSummary()}";
}
private string BuildKinkResistanceMeasuredText()
{
var lines = new List<string>
{
"试验条件:试验液体为血液或模拟血液,黏度 2.0~3.5 mPa·s试验温度 40 ℃ ± 1 ℃。",
$"流量点:{KinkResistanceFlowPointDisplay}",
KinkResistanceMandrelDiameterDisplay,
"试验方法:先在插管笔直状态采集基线流量 L0再围绕圆角模板缠绕至少 180°在保持离心泵固定转速条件下采集弯曲流量 L1。"
};
foreach (var entry in KinkResistanceEntries)
{
lines.Add(entry.HasCompleteSample
? $"{entry.Label}:目标 {entry.TargetFlow:F2} L/minL0={entry.BaselineFlow:F2} L/minL1={entry.KinkedFlow:F2} L/min流量降低率={entry.FlowDropRate:F1}%"
: $"{entry.Label}:目标 {entry.TargetFlow:F2} L/min待完成 L0/L1 采样");
}
return string.Join("\n", lines);
}
private string BuildKinkResistanceRecordNote()
{
return
"录入建议:最大流量和制造商规定的最小流量都应完成直管 L0 与扭结后 L1 采样。\n" +
"判定规则:(L0-L1)/L0 不得大于 50%。泵型应为离心泵,且采样时保持固定转速,不应使用滚压式血泵。\n" +
$"{BuildKinkResistanceSamplingSummary()}";
}
private string BuildKinkResistanceSamplingSummary()
{
var completedEntries = KinkResistanceEntries.Where(entry => entry.HasCompleteSample).ToList();
if (completedEntries.Count == 0)
{
return "尚未完成最大/最小流量点的扭结抗性采样。";
}
return string.Join(
"",
completedEntries.Select(entry =>
$"{entry.Label} 降幅 {entry.FlowDropRate:F1}% / {ResolveKinkResistanceStatusText(entry)}"));
}
private void CaptureKinkResistanceBaseline(string label)
{
if (!IsKinkResistanceSelected)
{
LatestAction = "当前选择的不是抗扭结抗性项目。";
return;
}
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
LatestAction = "抗扭结基线采集失败:抗扭结主泵实时流量未接入。";
return;
}
var entry = KinkResistanceEntries.First(item => item.Label == label);
entry.BaselineFlow = KinkResistancePumpFlow;
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
entry.BaselineCapturedAt = DateTime.Now;
ResultValue = BuildKinkResistanceMeasuredText();
ResultNote = BuildKinkResistanceRecordNote();
LatestAction = $"已采集{label} L0{entry.BaselineFlow:F2} L/min。";
TraceEvents.Insert(0, NewTrace("抗扭结基线", $"{label} / L0 {entry.BaselineFlow:F2} L/min"));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
private void CaptureKinkResistanceKinked(string label)
{
if (!IsKinkResistanceSelected)
{
LatestAction = "当前选择的不是抗扭结抗性项目。";
return;
}
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
LatestAction = "抗扭结比较失败:抗扭结主泵实时流量未接入。";
return;
}
var entry = KinkResistanceEntries.First(item => item.Label == label);
if (!entry.HasBaseline)
{
LatestAction = $"请先采集{label}的直管基线 L0。";
return;
}
entry.KinkedFlow = KinkResistancePumpFlow;
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
entry.KinkedCapturedAt = DateTime.Now;
var resultText = BuildKinkResistanceMeasuredText();
var noteText = BuildKinkResistanceRecordNote();
ResultValue = resultText;
ResultNote = noteText;
SelectedResultStatusText = GetKinkResistanceOverallStatus() switch
{
InspectionItemStatus.Qualified => "合格",
InspectionItemStatus.Warning => "预警",
InspectionItemStatus.Critical => "不合格",
_ => "待检"
};
LatestAction = $"已采集{label} L1{entry.KinkedFlow:F2} L/min降幅 {entry.FlowDropRate:F1}%。";
TraceEvents.Insert(0, NewTrace("抗扭结比较", $"{label} / L1 {entry.KinkedFlow:F2} L/min / 降幅 {entry.FlowDropRate:F1}%"));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
private InspectionItemStatus GetKinkResistanceOverallStatus()
{
var completedEntries = KinkResistanceEntries.Where(entry => entry.HasCompleteSample).ToList();
if (completedEntries.Count == 0)
{
return InspectionItemStatus.Pending;
}
if (completedEntries.Any(entry => entry.FlowDropRate > 50d))
{
return InspectionItemStatus.Critical;
}
return completedEntries.Count == KinkResistanceEntries.Count
? InspectionItemStatus.Qualified
: InspectionItemStatus.Warning;
}
private static string ResolveKinkResistanceStatusText(KinkResistancePointEntry entry) =>
entry.FlowDropRate switch
{
<= 50d => "合格",
<= 55d => "预警",
_ => "不合格"
};
private string BuildPressureDropMeasuredText()
{
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"实时压力降数据未接入,无法自动生成记录。当前状态:{TelemetryStatusDetail}";
}
var lines = new List<string>
{
$"试验条件:{PressureDropConditionDisplay}",
$"流量点:{PressureDropFlowPointDisplay}"
};
foreach (var entry in PressureDropEntries.OrderBy(item => item.TargetFlow))
{
lines.Add(entry.HasSample
? $"{entry.Label}:目标 {entry.TargetFlow:F2} L/min实际 {entry.ActualPumpFlow:F2} L/min近端 {entry.ProximalPressure:F1} mmHg远端 {entry.DistalPressure:F1} mmHgΔP {entry.DeltaPressure:F1} mmHg"
: $"{entry.Label}:尚未采样");
}
lines.Add($"当前实时:近端 {ProximalPressureDisplay};远端 {DistalPressureDisplay}ΔP {DeltaPressureDisplay}");
return string.Join("\n", lines);
}
private string BuildPressureDropRecordNote()
{
var pressureStatusText = DeltaPressure switch
{
<= 20 => "实时判定合格",
<= 24 => "实时判定预警,建议补充中间流量点复测",
_ => "实时判定超限,建议复核回路、样品及声明范围"
};
return
$"标准记录建议:在 50% / 75% / 100% 最大标称流量点分别保存压力数据。\n" +
$"制造商声明限值:{PressureDropLimitDisplay}。\n" +
$"已采样流量点:{BuildPressureDropSamplingSummary()}。\n" +
$"当前记录:主泵流量 {PumpFlowDisplay},温度 {TemperatureDisplay}{pressureStatusText}。\n" +
$"制造商声明范围需在正式报告中复核;若压降-流量关系呈非线性,应补充更多流量点。";
}
private string BuildPressureDropLiveDisplay()
{
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"压力降实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
return
$"条件:{PressureDropConditionDisplay}\n" +
$"流量点:{PressureDropFlowPointDisplay}\n" +
$"限值:{PressureDropLimitDisplay}\n" +
$"当前:主泵 {PumpFlowDisplay} / 近端 {ProximalPressureDisplay} / 远端 {DistalPressureDisplay} / ΔP {DeltaPressureDisplay}\n" +
$"{BuildPressureDropSamplingSummary()}";
}
private string BuildPressureDropSamplingSummary()
{
var capturedEntries = PressureDropEntries.Where(entry => entry.HasSample).OrderBy(entry => entry.TargetFlow).ToList();
if (capturedEntries.Count == 0)
{
return "尚未采集 50% / 75% / 100% 压力降流量点。";
}
return string.Join(
"",
capturedEntries.Select(entry => $"{entry.Label} ΔP {entry.DeltaPressure:F1} mmHg @ {entry.ActualPumpFlow:F2} L/min"));
}
private void CapturePressureDropSample(string label)
{
if (!IsPressureDropSelected)
{
LatestAction = "当前选择的不是压力降项目。";
return;
}
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "压力降采样失败:主泵或压力实时数据未接入。";
return;
}
var entry = PressureDropEntries.First(item => item.Label == label);
entry.ActualPumpFlow = PumpFlow;
entry.ProximalPressure = ChannelValue("近端压力");
entry.DistalPressure = ChannelValue("远端压力");
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
entry.SampledAt = DateTime.Now;
LatestAction = $"已采集压力降 {entry.Label} 流量点ΔP {entry.DeltaPressure:F1} mmHg。";
TraceEvents.Insert(0, NewTrace("压力降采样", $"{entry.Label} / ΔP {entry.DeltaPressure:F1} mmHg / 主泵 {entry.ActualPumpFlow:F2} L/min"));
OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
private void UpdateAntiCollapseBaseline()
{
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return;
}
var negativePressure = ChannelValueOrDefault("负压辅助引流");
if (_antiCollapseBaselinePressureDrop is null || negativePressure >= -1.0)
{
_antiCollapseBaselinePressureDrop = DeltaPressure;
_antiCollapseBaselineFlow = PumpFlow;
_antiCollapseBaselineCapturedAt = DateTime.Now;
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
}
private string BuildAntiCollapseLiveDisplay()
{
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"抗塌陷比较所需的主泵/压力实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
var comparison = GetAntiCollapseComparison();
var baselineText = comparison.HasBaseline
? $"基线 ΔP {comparison.BaselinePressureDrop:F1} mmHg @ {comparison.BaselineFlow:F2} L/min"
: "基线待采集(请先在无负压条件下运行)";
var increaseText = comparison.HasBaseline
? $"增量 {comparison.Increase:F1} mmHg / 增幅 {comparison.IncreaseRate:F1}% / 判定 {comparison.StatusText}"
: "尚无法比较增幅";
return
$"条件:模拟血液 2.0~3.5 mPa·s / {TemperatureDisplay} / 目标负压 {AntiCollapseTargetNegativePressure:F2} kPa\n" +
$"限值:压力降增幅 ≤ {AntiCollapseAllowedIncreaseRate:F1}%\n" +
$"当前:负压 {NegativeAssistPressureDisplay} / 主泵 {PumpFlow:F2} L/min / ΔP {DeltaPressure:F1} mmHg\n" +
$"{baselineText}\n" +
$"{increaseText}";
}
private string BuildAntiCollapseMeasuredText()
{
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"抗塌陷实时数据未接入,无法自动生成比较结果。当前状态:{TelemetryStatusDetail}";
}
var comparison = GetAntiCollapseComparison();
var baselineText = comparison.HasBaseline
? $"{comparison.BaselinePressureDrop:F1} mmHg"
: "未采集";
var increaseText = comparison.HasBaseline
? $"{comparison.Increase:F1} mmHg ({comparison.IncreaseRate:F1}%)"
: "无法比较";
return
$"试验条件:模拟血液 2.0~3.5 mPa·s{TemperatureDisplay},有效长度处于 37±2 ℃ 条件\n" +
$"基线测量:最大血流量 {comparison.BaselineFlow:F2} L/min压力降 {baselineText}\n" +
$"负压加载:远端施加 {NegativeAssistPressureDisplay}(目标 {AntiCollapseTargetNegativePressure:F2} kPa\n" +
$"比较结果:当前压力降 {DeltaPressure:F1} mmHg较基线增加 {increaseText}\n" +
$"建议判定:{comparison.StatusText}";
}
private string BuildAntiCollapseRecordNote()
{
var comparison = GetAntiCollapseComparison();
var baselineCapturedText = _antiCollapseBaselineCapturedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "未采集";
return
$"录入建议:先在无负压条件下采集基线压力降,再在远端施加 -6.67 kPa 负压后记录比较值。\n" +
$"当前基线时间:{baselineCapturedText};基线流量 {comparison.BaselineFlow:F2} L/min当前流量 {PumpFlow:F2} L/min。\n" +
$"判定规则:负压后压力降增幅不超过基线 {AntiCollapseAllowedIncreaseRate:F1}% 为合格;当前比较结果为 {comparison.StatusText}。";
}
private AntiCollapseComparison GetAntiCollapseComparison()
{
if (_antiCollapseBaselinePressureDrop is null || _antiCollapseBaselineFlow is null)
{
return new AntiCollapseComparison(false, 0, 0, 0, 0, "等待基线");
}
var baseline = _antiCollapseBaselinePressureDrop.Value;
var increase = DeltaPressure - baseline;
var increaseRate = baseline <= 0 ? 0 : increase / baseline * 100d;
var statusText = increaseRate switch
{
var rate when rate <= AntiCollapseAllowedIncreaseRate => "合格",
var rate when rate <= AntiCollapseAllowedIncreaseRate + 10 => "预警,建议复测",
_ => "不合格,压降增幅超限"
};
return new AntiCollapseComparison(true, baseline, _antiCollapseBaselineFlow.Value, increase, increaseRate, statusText);
}
private InspectionItemStatus GetPressureDropOverallStatus()
{
var sampledEntries = PressureDropEntries.Where(entry => entry.HasSample).ToList();
if (sampledEntries.Count > 0)
{
if (sampledEntries.Any(entry => entry.DeltaPressure > PressureDropLimit(entry.Label)))
{
return InspectionItemStatus.Critical;
}
return sampledEntries.Count == PressureDropEntries.Count
? InspectionItemStatus.Qualified
: InspectionItemStatus.Warning;
}
var nearestLabel = ResolveNearestFlowPointLabel();
var limit = PressureDropLimit(nearestLabel);
return DeltaPressure switch
{
var value when value <= limit => InspectionItemStatus.Qualified,
var value when value <= limit * 1.1 => InspectionItemStatus.Warning,
_ => InspectionItemStatus.Critical
};
}
private double PressureDropLimit(string label) => label switch
{
"50%" => PressureDropLimit50,
"75%" => PressureDropLimit75,
"100%" => PressureDropLimit100,
_ => PressureDropLimit100
};
private string ResolveNearestFlowPointLabel()
{
return new[]
{
("50%", PressureDropFlowPoint(0.50)),
("75%", PressureDropFlowPoint(0.75)),
("100%", PressureDropFlowPoint(1.00))
}
.OrderBy(item => Math.Abs(item.Item2 - PumpFlow))
.First().Item1;
}
private void RefreshSpecializedJudgements()
{
SyncSpecializedTargetFlows();
OnPropertyChanged(nameof(ConfigurationSummary));
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(PressureDropFlowPointDisplay));
OnPropertyChanged(nameof(RecirculationFlowPointDisplay));
OnPropertyChanged(nameof(PressureDropLimitDisplay));
OnPropertyChanged(nameof(RecirculationLimitDisplay));
OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
private void UpdateAndPersistLimitSettings()
{
RefreshSpecializedJudgements();
if (_suppressLimitSettingsSave)
{
return;
}
SaveManufacturerLimitSettings();
}
private void LoadManufacturerLimitSettings()
{
try
{
if (!File.Exists(LimitSettingsPath))
{
return;
}
var json = File.ReadAllText(LimitSettingsPath);
var settings = JsonSerializer.Deserialize<ManufacturerLimitSettings>(json);
if (settings is null)
{
return;
}
_suppressLimitSettingsSave = true;
ProductModel = settings.ProductModel;
ApplicablePopulation = settings.ApplicablePopulation;
RatedMaxFlow = settings.RatedMaxFlow;
KinkResistanceMinimumFlow = settings.KinkResistanceMinimumFlow;
KinkResistanceOuterDiameter = settings.KinkResistanceOuterDiameter;
PressureDropLimit50 = settings.PressureDropLimit50;
PressureDropLimit75 = settings.PressureDropLimit75;
PressureDropLimit100 = settings.PressureDropLimit100;
AntiCollapseAllowedIncreaseRate = settings.AntiCollapseAllowedIncreaseRate;
RecirculationAllowedLimit = settings.RecirculationAllowedLimit;
}
catch
{
}
finally
{
_suppressLimitSettingsSave = false;
RefreshSpecializedJudgements();
}
}
private void SaveManufacturerLimitSettings()
{
try
{
var directory = Path.GetDirectoryName(LimitSettingsPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var settings = new ManufacturerLimitSettings
{
ProductModel = ProductModel,
ApplicablePopulation = ApplicablePopulation,
RatedMaxFlow = RatedMaxFlow,
KinkResistanceMinimumFlow = KinkResistanceMinimumFlow,
KinkResistanceOuterDiameter = KinkResistanceOuterDiameter,
PressureDropLimit50 = PressureDropLimit50,
PressureDropLimit75 = PressureDropLimit75,
PressureDropLimit100 = PressureDropLimit100,
AntiCollapseAllowedIncreaseRate = AntiCollapseAllowedIncreaseRate,
RecirculationAllowedLimit = RecirculationAllowedLimit
};
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(LimitSettingsPath, json);
}
catch
{
}
}
private string BuildRecirculationLiveDisplay()
{
if (!HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
{
return $"再循环所需的主泵/引流/回输实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
return
$"条件:试验液体为水 + 示踪粒子 / 当前温度 {TemperatureDisplay}\n" +
$"目标流量点:{RecirculationFlowPointDisplay}\n" +
$"{RecirculationLimitDisplay}\n" +
$"当前:主泵 {RecirculationPumpFlow:F2} L/min / 引流 {DrainageFlow:F2} L/min / 回输 {ReturnFlow:F2} L/min / 在线参考 {RecirculationRate:F1}%\n" +
$"{BuildRecirculationSamplingSummary()}";
}
private string BuildRecirculationMeasuredText()
{
var lines = new List<string>
{
"试验条件:血液通道试验液体为水,加入示踪粒子;按双腔插管模拟回路进行。",
$"流量点:{RecirculationFlowPointDisplay}",
RecirculationLimitDisplay,
"计算公式R = C1 / C2 × 100其中 C1 为贮血器 D 浓度C2 为贮血器 C 浓度。"
};
foreach (var entry in RecirculationEntries.OrderBy(item => item.TargetFlow))
{
if (entry.HasSample)
{
lines.Add(
$"{entry.Label}:目标 {entry.TargetFlow:F2} L/min当前主泵 {entry.ActualPumpFlow:F2} L/min引流 {entry.DrainageFlow:F2} L/min回输 {entry.ReturnFlow:F2} L/min" +
$"在线参考 {entry.OnlineEstimate:F1}%C1={(entry.ConcentrationC1?.ToString("F0") ?? "____")}C2={(entry.ConcentrationC2?.ToString("F0") ?? "____")}R={entry.RecirculationResultText}");
}
else
{
lines.Add($"{entry.Label}尚未采样C1=____C2=____R=____%");
}
}
return string.Join("\n", lines);
}
private string BuildRecirculationRecordNote()
{
var capturedEntries = RecirculationEntries.Where(entry => entry.HasSample).OrderBy(entry => entry.TargetFlow).ToList();
var capturedLabels = capturedEntries.Count == 0
? "未采样"
: string.Join("、", capturedEntries.Select(entry => entry.Label));
return
"录入建议:在 50% / 75% / 100% 最大标称流量分别采集贮血器 C、D 的示踪浓度,并按标准公式计算。\n" +
$"已采样流量点:{capturedLabels}。\n" +
$"在线参考值仅用于判断回路稳定性,正式判定应以 C1/C2 浓度计算结果与制造商限值比较({RecirculationLimitDisplay})。";
}
private string BuildRecirculationSamplingSummary()
{
var capturedEntries = RecirculationEntries.Where(entry => entry.HasSample).OrderBy(entry => entry.TargetFlow).ToList();
if (capturedEntries.Count == 0)
{
return "尚未采集 50% / 75% / 100% 流量点。";
}
return string.Join(
"",
capturedEntries.Select(entry =>
{
var statusText = entry.RecirculationResult switch
{
null => "待录入 C1/C2",
var value when value <= RecirculationAllowedLimit => "合格",
var value when value <= RecirculationAllowedLimit + 2 => "预警",
_ => "超限"
};
return $"{entry.Label} 在线参考 {entry.OnlineEstimate:F1}% / R {entry.RecirculationResultText} / {statusText}";
}));
}
private void CaptureRecirculationSample(string label)
{
if (!IsRecirculationSelected)
{
LatestAction = "当前选择的不是再循环项目。";
return;
}
if (!HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
{
LatestAction = "再循环采样失败:主泵/引流/回输实时数据未接入。";
return;
}
var entry = RecirculationEntries.First(item => item.Label == label);
entry.ActualPumpFlow = RecirculationPumpFlow;
entry.DrainageFlow = DrainageFlow;
entry.ReturnFlow = ReturnFlow;
entry.OnlineEstimate = RecirculationRate;
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
entry.SampledAt = DateTime.Now;
var resultText = BuildRecirculationMeasuredText();
var noteText = BuildRecirculationRecordNote();
ResultValue = resultText;
ResultNote = noteText;
_lastAutoRecirculationResult = resultText;
_lastAutoRecirculationNote = noteText;
LatestAction = $"已采集再循环 {entry.Label} 流量点快照,当前在线参考 {RecirculationRate:F1}%。";
TraceEvents.Insert(0, NewTrace("再循环采样", $"{entry.Label} / 主泵 {RecirculationPumpFlow:F2} L/min / 在线参考 {RecirculationRate:F1}%"));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
private void OnRecirculationEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(RecirculationPointEntry.ConcentrationC1)
or nameof(RecirculationPointEntry.ConcentrationC2)
or nameof(RecirculationPointEntry.SampledAt)
or nameof(RecirculationPointEntry.ActualPumpFlow))
{
OnPropertyChanged(nameof(RecirculationSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
if (IsRecirculationSelected)
{
var resultText = BuildRecirculationMeasuredText();
var noteText = BuildRecirculationRecordNote();
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoRecirculationResult)
{
ResultValue = resultText;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoRecirculationNote)
{
ResultNote = noteText;
}
var filledResults = RecirculationEntries
.Where(entry => entry.RecirculationResult.HasValue)
.Select(entry => entry.RecirculationResult!.Value)
.ToList();
if (filledResults.Count > 0)
{
var maxResult = filledResults.Max();
SelectedResultStatusText = maxResult <= RecirculationAllowedLimit ? "合格"
: maxResult <= RecirculationAllowedLimit + 2 ? "预警"
: "不合格";
}
_lastAutoRecirculationResult = resultText;
_lastAutoRecirculationNote = noteText;
}
}
}
private void OnHemolysisSamplingEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
RefreshHemolysisDraftIfNeeded();
}
private void OnHemolysisTestParametersPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
RefreshHemolysisDraftIfNeeded();
}
private void RefreshHemolysisDraftIfNeeded()
{
OnPropertyChanged(nameof(HemolysisSamplingCompletionSummary));
OnPropertyChanged(nameof(HemolysisHasMissingRequiredPoints));
OnPropertyChanged(nameof(HemolysisRequiredPointAlert));
OnPropertyChanged(nameof(HemolysisCalculationDetail));
OnPropertyChanged(nameof(HemolysisCalculationSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
var resultText = BuildHemolysisResultText();
var noteText = BuildHemolysisRecordNoteText();
if (IsHemolysisSelected)
{
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoHemolysisResult)
{
ResultValue = resultText;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoHemolysisNote)
{
ResultNote = noteText;
}
}
_lastAutoHemolysisResult = resultText;
_lastAutoHemolysisNote = noteText;
}
private void OnPressureDropEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(PressureDropPointEntry.SampledAt)
or nameof(PressureDropPointEntry.ActualPumpFlow)
or nameof(PressureDropPointEntry.ProximalPressure)
or nameof(PressureDropPointEntry.DistalPressure))
{
OnPropertyChanged(nameof(PressureDropSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
}
private void OnKinkResistanceEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(KinkResistancePointEntry.BaselineCapturedAt)
or nameof(KinkResistancePointEntry.KinkedCapturedAt)
or nameof(KinkResistancePointEntry.BaselineFlow)
or nameof(KinkResistancePointEntry.KinkedFlow))
{
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
}
}
private void SyncSpecializedTargetFlows()
{
foreach (var entry in PressureDropEntries)
{
entry.TargetFlow = PressureDropFlowPoint(LabelToRatio(entry.Label));
}
foreach (var entry in KinkResistanceEntries)
{
entry.TargetFlow = entry.Label == "最小流量"
? Math.Clamp(KinkResistanceMinimumFlow, 0d, Math.Max(RatedMaxFlow, 0d))
: RatedMaxFlow;
}
foreach (var entry in RecirculationEntries)
{
entry.TargetFlow = PressureDropFlowPoint(LabelToRatio(entry.Label));
}
}
private static double LabelToRatio(string label) => label switch
{
"50%" => 0.50,
"75%" => 0.75,
"100%" => 1.00,
_ => 1.00
};
private double PressureDropFlowPoint(double ratio) =>
Math.Max(RatedMaxFlow, 0) * ratio;
public void Dispose()
{
_timer.Stop();
_timer.Tick -= OnTelemetryTimerTick;
foreach (var entry in PressureDropEntries)
{
entry.PropertyChanged -= OnPressureDropEntryPropertyChanged;
}
foreach (var entry in KinkResistanceEntries)
{
entry.PropertyChanged -= OnKinkResistanceEntryPropertyChanged;
}
foreach (var entry in RecirculationEntries)
{
entry.PropertyChanged -= OnRecirculationEntryPropertyChanged;
}
foreach (var entry in HemolysisSamplingEntries)
{
entry.PropertyChanged -= OnHemolysisSamplingEntryPropertyChanged;
}
HemolysisTestParameters.PropertyChanged -= OnHemolysisTestParametersPropertyChanged;
if (_telemetryService is IDisposable disposableTelemetryService)
{
disposableTelemetryService.Dispose();
}
}
private void OnTelemetryTimerTick(object? sender, EventArgs e) => RefreshTelemetry();
private static string ResolveReportOutputDirectory()
{
foreach (var folder in new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Cardiopulmonarybypasssystems",
"Reports")
})
{
if (string.IsNullOrWhiteSpace(folder))
{
continue;
}
try
{
Directory.CreateDirectory(folder);
return folder;
}
catch
{
}
}
return AppContext.BaseDirectory;
}
private static string SanitizeFileNameSegment(string value)
{
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = new string(value.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()).Trim();
return string.IsNullOrWhiteSpace(sanitized) ? "未填写批号" : sanitized;
}
private readonly record struct AntiCollapseComparison(
bool HasBaseline,
double BaselinePressureDrop,
double BaselineFlow,
double Increase,
double IncreaseRate,
string StatusText);
}