2352 lines
104 KiB
C#
2352 lines
104 KiB
C#
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、T360;T60/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/min,L0={entry.BaselineFlow:F2} L/min,L1={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);
|
||
|
||
}
|