Files
Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs

2352 lines
104 KiB
C#
Raw Normal View History

2026-03-14 15:21:24 +08:00
using System.Collections.ObjectModel;
2026-03-10 16:36:40 +08:00
using System.ComponentModel;
2026-03-09 17:03:13 +08:00
using System.IO;
2026-03-10 16:36:40 +08:00
using System.Text.Json;
using System.Windows.Data;
2026-03-09 17:03:13 +08:00
using System.Windows.Threading;
using Cardiopulmonarybypasssystems.Models;
using Cardiopulmonarybypasssystems.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
2026-03-10 14:17:43 +08:00
using QuestPDF.Fluent;
2026-03-09 17:03:13 +08:00
namespace Cardiopulmonarybypasssystems.ViewModels;
2026-03-26 09:28:12 +08:00
public partial class MainViewModel : ObservableObject, IDisposable
2026-03-09 17:03:13 +08:00
{
private readonly IModbusTelemetryService _telemetryService;
private readonly DispatcherTimer _timer;
2026-03-10 15:26:28 +08:00
private const double AntiCollapseTargetNegativePressure = -6.67;
2026-03-11 16:27:00 +08:00
private const int TrendHistoryCapacity = 60;
2026-03-10 16:36:40 +08:00
private static readonly string LimitSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Cardiopulmonarybypasssystems",
"manufacturer-limits.json");
2026-03-10 15:26:28 +08:00
private double? _antiCollapseBaselinePressureDrop;
private double? _antiCollapseBaselineFlow;
private DateTime? _antiCollapseBaselineCapturedAt;
private string _lastAutoAntiCollapseResult = string.Empty;
private string _lastAutoAntiCollapseNote = string.Empty;
2026-03-10 16:36:40 +08:00
private string _lastAutoRecirculationResult = string.Empty;
private string _lastAutoRecirculationNote = string.Empty;
2026-03-14 14:10:52 +08:00
private string _lastAutoHemolysisResult = string.Empty;
private string _lastAutoHemolysisNote = string.Empty;
2026-03-10 16:36:40 +08:00
private bool _suppressLimitSettingsSave;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string currentStage = "检测进行中";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string operatorName = Environment.UserName;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string reviewerName = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string approverName = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string batchNumber = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string deviceStatus = "等待连接";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
private bool acquisitionRunning = true;
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private bool detectionCompleted;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
private double complianceRate;
[ObservableProperty]
private int qualifiedCount;
[ObservableProperty]
private int warningCount;
[ObservableProperty]
private int pendingCount;
[ObservableProperty]
private double deltaPressure;
2026-03-10 16:36:40 +08:00
[ObservableProperty]
private string productModel = "24Fr/32Fr 双腔";
[ObservableProperty]
private string applicablePopulation = "成人";
[ObservableProperty]
private double ratedMaxFlow = 6.0;
2026-03-11 15:06:03 +08:00
[ObservableProperty]
private double kinkResistanceMinimumFlow = 1.0;
[ObservableProperty]
private double kinkResistanceOuterDiameter = 8.0;
2026-03-10 16:36:40 +08:00
[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;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string latestAction = "系统已载入标准项目,等待 PLC 实时数据。";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private InspectionItem? selectedItem;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string resultValue = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string resultNote = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-20 20:03:26 +08:00
private string resultOperator = Environment.UserName;
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string selectedResultStatusText = "合格";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string detectionSummary = "";
2026-03-09 17:03:13 +08:00
[ObservableProperty]
2026-03-09 19:06:55 +08:00
private string itemSearchText = "";
2026-03-09 17:03:13 +08:00
2026-03-09 19:21:25 +08:00
private string activeFilter = "全部";
2026-03-09 17:03:13 +08:00
public MainViewModel(IStandardRepository repository, IModbusTelemetryService telemetryService)
{
_telemetryService = telemetryService;
2026-03-09 19:06:55 +08:00
InspectionItems = new ObservableCollection<InspectionItem>(repository.GetInspectionItems());
2026-03-10 16:36:40 +08:00
FilteredItemsView = CollectionViewSource.GetDefaultView(InspectionItems);
FilteredItemsView.Filter = MatchesFilteredItem;
2026-03-09 17:03:13 +08:00
Channels = new ObservableCollection<DeviceChannel>(telemetryService.GetChannels());
2026-03-11 16:27:00 +08:00
PumpControls = new ObservableCollection<PumpControlChannel>(telemetryService.GetPumpControls());
2026-03-11 17:36:07 +08:00
ValveControls = new ObservableCollection<ValveControlChannel>(telemetryService.GetValveControls());
2026-03-20 20:03:26 +08:00
TraceEvents = new ObservableCollection<TraceEvent>(repository.GetInitialTraceEvents());
2026-03-09 17:03:13 +08:00
AlarmMessages = new ObservableCollection<AlarmMessage>();
2026-03-09 19:06:55 +08:00
ResultStatusOptions = new ObservableCollection<string>(["待检", "合格", "预警", "不合格"]);
2026-03-10 16:36:40 +08:00
PressureDropEntries = new ObservableCollection<PressureDropPointEntry>(
[
new() { Label = "50%", TargetFlow = 3.0 },
new() { Label = "75%", TargetFlow = 4.5 },
new() { Label = "100%", TargetFlow = 6.0 }
]);
2026-03-11 15:06:03 +08:00
KinkResistanceEntries = new ObservableCollection<KinkResistancePointEntry>(
[
new() { Label = "最大流量", TargetFlow = RatedMaxFlow },
new() { Label = "最小流量", TargetFlow = KinkResistanceMinimumFlow }
]);
2026-03-10 16:36:40 +08:00
RecirculationEntries = new ObservableCollection<RecirculationPointEntry>(
[
new() { Label = "50%", TargetFlow = 3.0 },
new() { Label = "75%", TargetFlow = 4.5 },
new() { Label = "100%", TargetFlow = 6.0 }
]);
2026-03-14 14:10:52 +08:00
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 = "试验完成" }
]);
2026-03-10 16:36:40 +08:00
foreach (var entry in PressureDropEntries)
{
entry.PropertyChanged += OnPressureDropEntryPropertyChanged;
}
2026-03-11 15:06:03 +08:00
foreach (var entry in KinkResistanceEntries)
{
entry.PropertyChanged += OnKinkResistanceEntryPropertyChanged;
}
2026-03-10 16:36:40 +08:00
foreach (var entry in RecirculationEntries)
{
entry.PropertyChanged += OnRecirculationEntryPropertyChanged;
}
2026-03-14 14:10:52 +08:00
foreach (var entry in HemolysisSamplingEntries)
{
entry.PropertyChanged += OnHemolysisSamplingEntryPropertyChanged;
}
HemolysisTestParameters.PropertyChanged += OnHemolysisTestParametersPropertyChanged;
2026-03-10 16:36:40 +08:00
LoadManufacturerLimitSettings();
2026-03-09 17:03:13 +08:00
2026-03-09 19:06:55 +08:00
SelectedItem = InspectionItems.FirstOrDefault();
if (SelectedItem is not null)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
LoadSelectedItemDraft(SelectedItem);
2026-03-09 17:03:13 +08:00
}
RefreshTelemetryPanel();
2026-03-14 15:21:24 +08:00
RefreshDeviceStatus();
2026-03-09 17:03:13 +08:00
RefreshComputedState();
2026-03-20 20:03:26 +08:00
TraceEvents.Insert(0, NewTrace("任务初始化", $"已载入 {InspectionItems.Count} 项检测标准,实时端点 {_telemetryService.EndpointDescription}"));
2026-03-09 17:03:13 +08:00
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
2026-03-26 09:28:12 +08:00
_timer.Tick += OnTelemetryTimerTick;
2026-03-09 17:03:13 +08:00
_timer.Start();
}
2026-03-09 19:06:55 +08:00
public ObservableCollection<InspectionItem> InspectionItems { get; }
2026-03-10 16:36:40 +08:00
public ICollectionView FilteredItemsView { get; }
2026-03-09 17:03:13 +08:00
public ObservableCollection<DeviceChannel> Channels { get; }
2026-03-11 16:27:00 +08:00
public ObservableCollection<PumpControlChannel> PumpControls { get; }
2026-03-11 17:36:07 +08:00
public ObservableCollection<ValveControlChannel> ValveControls { get; }
2026-03-11 16:27:00 +08:00
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");
2026-03-09 17:03:13 +08:00
public ObservableCollection<TraceEvent> TraceEvents { get; }
public ObservableCollection<AlarmMessage> AlarmMessages { get; }
2026-03-09 19:06:55 +08:00
public ObservableCollection<string> ResultStatusOptions { get; }
2026-03-10 16:36:40 +08:00
public ObservableCollection<PressureDropPointEntry> PressureDropEntries { get; }
2026-03-11 15:06:03 +08:00
public ObservableCollection<KinkResistancePointEntry> KinkResistanceEntries { get; }
2026-03-10 16:36:40 +08:00
public ObservableCollection<RecirculationPointEntry> RecirculationEntries { get; }
2026-03-14 14:10:52 +08:00
public HemolysisTestParameters HemolysisTestParameters { get; }
public ObservableCollection<HemolysisSamplingEntry> HemolysisSamplingEntries { get; }
2026-03-11 16:27:00 +08:00
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; } = [];
2026-03-09 19:21:25 +08:00
public ObservableCollection<string> ItemFilterOptions { get; } = new(["全部", "待填写", "已完成", "实时监控", "手动填写"]);
2026-03-14 14:10:52 +08:00
public ObservableCollection<string> HemolysisBloodSourceOptions { get; } = new(["肝素化牛血", "肝素化猪血", "肝素化羊血"]);
public ObservableCollection<string> HemolysisAnticoagulantOptions { get; } = new(["肝素", "枸橼酸钠", "其他"]);
2026-03-10 16:36:40 +08:00
public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
2026-03-26 09:28:12 +08:00
public bool HasSelectedItem => SelectedItem is not null;
2026-03-09 17:03:13 +08:00
public IEnumerable<DeviceChannel> FlowSensorChannels => Channels.Where(IsFlowSensorChannel);
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
2026-03-20 20:03:26 +08:00
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、白细胞减少率。";
2026-03-09 17:03:13 +08:00
public string ComplianceDisplay => $"{ComplianceRate:F0}%";
2026-03-20 20:03:26 +08:00
public string DeltaPressureDisplay => HasChannelTelemetry("近端压力", "远端压力") ? $"{DeltaPressure:F1} mmHg" : "--";
2026-03-21 10:24:26 +08:00
public string ExportStateText => DetectionCompleted ? "检测已完成,可导出检查报告" : "检测进行中,完成后可导出检查报告";
2026-03-09 19:06:55 +08:00
public string SelectedItemTitle => SelectedItem?.Item ?? "未选择项目";
public string SelectedItemStatusText => SelectedItem?.StatusText ?? "待检";
2026-03-20 20:03:26 +08:00
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");
2026-03-11 16:27:00 +08:00
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" => "抗扭结泵",
2026-03-14 14:10:52 +08:00
"4.3.4" => "单腔引流/回输",
2026-03-11 16:27:00 +08:00
_ => "主泵"
};
public string FlowTrendSecondaryLabel => SelectedItem?.Clause switch
{
"4.3.3" => "回流",
2026-03-14 14:10:52 +08:00
"4.3.4" => "双腔插管试验回路",
2026-03-11 16:27:00 +08:00
_ => string.Empty
};
public string FlowTrendTertiaryLabel => SelectedItem?.Clause switch
{
"4.3.3" => "引流",
2026-03-14 14:10:52 +08:00
"4.3.4" => "双腔插管试验回路(两个管腔)",
2026-03-11 16:27:00 +08:00
_ => 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));
2026-03-20 20:03:26 +08:00
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("动脉回输流量");
2026-03-09 17:03:13 +08:00
public double PumpFlowNormalizedValue => ChannelNormalizedValue("主泵流量");
public double DrainageFlowNormalizedValue => ChannelNormalizedValue("静脉引流流量");
public double ReturnFlowNormalizedValue => ChannelNormalizedValue("动脉回输流量");
2026-03-10 16:36:40 +08:00
public string FilteredItemSummary => $"{FilteredItemsView.Cast<object>().Count()} / {InspectionItems.Count} 项";
2026-03-09 19:06:55 +08:00
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 ?? "当前项目无实时映射。";
2026-03-20 20:03:26 +08:00
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} / 最终灭菌成品";
2026-03-10 16:36:40 +08:00
public string ConfigurationSummary => $"型号:{ProductModel} / 适用人群:{ApplicablePopulation} / 标称最大流量:{RatedMaxFlow:F2} L/min";
2026-03-10 14:35:50 +08:00
public string PressureDropFlowPointDisplay =>
$"50%={PressureDropFlowPoint(0.50):F2} L/min / 75%={PressureDropFlowPoint(0.75):F2} L/min / 100%={PressureDropFlowPoint(1.00):F2} L/min";
2026-03-10 16:36:40 +08:00
public string PressureDropLimitDisplay => $"50%≤{PressureDropLimit50:F1} / 75%≤{PressureDropLimit75:F1} / 100%≤{PressureDropLimit100:F1} mmHg";
2026-03-11 15:06:03 +08:00
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";
2026-03-10 16:36:40 +08:00
public bool IsPressureDropSelected => SelectedItem?.Clause == "4.3.1";
public string PressureDropSamplingSummary => BuildPressureDropSamplingSummary();
2026-03-11 15:06:03 +08:00
public bool IsKinkResistanceSelected => SelectedItem?.Clause == "4.2.3";
public string KinkResistanceSamplingSummary => BuildKinkResistanceSamplingSummary();
2026-03-10 15:26:28 +08:00
public bool IsAntiCollapseSelected => SelectedItem?.Clause == "4.3.2";
2026-03-10 16:36:40 +08:00
public bool IsRecirculationSelected => SelectedItem?.Clause == "4.3.3";
2026-03-14 14:10:52 +08:00
public bool IsHemolysisSelected => SelectedItem?.Clause == "4.3.4";
2026-03-26 09:28:12 +08:00
public bool IsHemolysisPrimaryInputSelected => IsHemolysisSelected && string.Equals(SelectedItem?.Item, "血细胞破坏", StringComparison.Ordinal);
public bool IsHemolysisReductionSelected => IsHemolysisSelected && string.Equals(SelectedItem?.Item, "血小板/白细胞减少率", StringComparison.Ordinal);
2026-03-14 14:10:52 +08:00
public string HemolysisStandardSummary =>
2026-03-26 09:28:12 +08:00
IsHemolysisReductionSelected
? "共用记录要点:血小板/白细胞减少率与“血细胞破坏”共用同一套试样运行与取样记录,减少率以前后标准取样点的细胞计数变化计算。"
: "血细胞破坏记录要点:试验介质应采用肝素化牛血、猪血或羊血;装配两个通用且等同的回路;两个回路初始血液通道试验液容积差不应超过 1%;关键条件包括血流量为制造商临床使用规定的最大值、血中葡萄糖 10 mmol/L、血红蛋白 12 g/dL标准取样点为试验前、30 min、180 min、360 min。";
2026-03-14 14:10:52 +08:00
public string HemolysisTemplateGuidance =>
2026-03-26 09:28:12 +08:00
IsHemolysisReductionSelected
? "录入建议:本项复用“血细胞破坏”已维护的基础条件,仅需核对下方共用取样表和减少率计算结果,无需重复录入试样准备。"
: "录入建议:先填写试验血液准备和回路运行条件,再在下方表格录入取样数据;系统会自动汇总 ΔfHb、NIH、白细胞减少率和血小板减少率。";
public string HemolysisSharedRecordHint =>
IsHemolysisReductionSelected
? "当前为“血小板/白细胞减少率”项目。本区沿用“血细胞破坏”同一批试样运行与取样记录,基础条件不重复录入,只核对共用记录和减少率结果。"
: "当前录入的试样运行与取样记录会同时服务“血细胞破坏”和“血小板/白细胞减少率”两项,建议先完成基础条件,再统一录入标准取样点。";
2026-03-14 14:10:52 +08:00
public string HemolysisSamplingCompletionSummary => BuildHemolysisSamplingCompletionSummary();
public bool HemolysisHasMissingRequiredPoints => GetHemolysisMissingRequiredPoints().Count > 0;
public string HemolysisRequiredPointAlert => BuildHemolysisRequiredPointAlert();
public string HemolysisCalculationSummary => BuildHemolysisCalculationSummary();
2026-03-26 09:28:12 +08:00
public string HemolysisCalculationTitle => IsHemolysisReductionSelected ? "减少率计算摘要" : "自动计算摘要";
public string HemolysisCalculationDetail =>
IsHemolysisReductionSelected
? $"白细胞减少率 {FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")};血小板减少率 {FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}{BuildHemolysisSamplingCompletionSummary()}"
: BuildHemolysisCalculationSummary();
public string HemolysisSamplingSectionTitle => IsHemolysisReductionSelected ? "共用试样运行与取样记录" : "试样运行与取样记录";
2026-03-14 14:10:52 +08:00
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();
2026-03-10 15:26:28 +08:00
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}"
: "请先在无负压条件下采集基线,再进行负压比较";
}
}
2026-03-10 16:36:40 +08:00
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}%";
2026-03-09 17:03:13 +08:00
2026-03-11 16:27:00 +08:00
public double PressureDropPumpFlow => ChannelValue("主泵流量");
public double RecirculationPumpFlow => ChannelValue("再循环主泵流量");
public double KinkResistancePumpFlow => ChannelValue("抗扭结主泵流量");
2026-03-14 14:10:52 +08:00
public double HemolysisDrainageSingleFlow => ChannelValue("血细胞破坏-单腔引流/回输流量");
public double HemolysisReturnSingleFlow => ChannelValue("双腔插管试验回路流量");
public double HemolysisDualLumenFlow => ChannelValue("双腔插管试验回路流量(两个管腔)");
2026-03-09 17:03:13 +08:00
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));
2026-03-09 19:06:55 +08:00
partial void OnDetectionCompletedChanged(bool value) => OnPropertyChanged(nameof(ExportStateText));
2026-03-10 16:36:40 +08:00
partial void OnProductModelChanged(string value) => UpdateAndPersistLimitSettings();
partial void OnApplicablePopulationChanged(string value) => UpdateAndPersistLimitSettings();
partial void OnRatedMaxFlowChanged(double value) => UpdateAndPersistLimitSettings();
2026-03-11 15:06:03 +08:00
partial void OnKinkResistanceMinimumFlowChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnKinkResistanceOuterDiameterChanged(double value) => UpdateAndPersistLimitSettings();
2026-03-10 16:36:40 +08:00
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();
2026-03-09 19:06:55 +08:00
partial void OnItemSearchTextChanged(string value)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:21:25 +08:00
RefreshFilteredItemsView();
}
2026-03-09 17:03:13 +08:00
2026-03-09 19:21:25 +08:00
public string ActiveFilter
{
get => activeFilter;
set
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:21:25 +08:00
if (SetProperty(ref activeFilter, value))
{
RefreshFilteredItemsView();
}
2026-03-09 17:03:13 +08:00
}
}
2026-03-09 19:06:55 +08:00
partial void OnSelectedItemChanged(InspectionItem? value)
2026-03-09 17:03:13 +08:00
{
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(HasSelectedItem));
2026-03-09 19:06:55 +08:00
OnPropertyChanged(nameof(SelectedItemTitle));
OnPropertyChanged(nameof(SelectedItemStatusText));
OnPropertyChanged(nameof(SelectedItemCaptureModeText));
OnPropertyChanged(nameof(SelectedItemMeasurementSource));
OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(IsPressureDropSelected));
OnPropertyChanged(nameof(PressureDropSamplingSummary));
2026-03-11 15:06:03 +08:00
OnPropertyChanged(nameof(IsKinkResistanceSelected));
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
2026-03-10 15:26:28 +08:00
OnPropertyChanged(nameof(IsAntiCollapseSelected));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(IsRecirculationSelected));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(IsHemolysisSelected));
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(IsHemolysisPrimaryInputSelected));
OnPropertyChanged(nameof(IsHemolysisReductionSelected));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(HemolysisStandardSummary));
OnPropertyChanged(nameof(HemolysisTemplateGuidance));
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(HemolysisSharedRecordHint));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(HemolysisSamplingCompletionSummary));
OnPropertyChanged(nameof(HemolysisHasMissingRequiredPoints));
OnPropertyChanged(nameof(HemolysisRequiredPointAlert));
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(HemolysisCalculationTitle));
OnPropertyChanged(nameof(HemolysisCalculationDetail));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(HemolysisCalculationSummary));
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(HemolysisSamplingSectionTitle));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
2026-03-10 15:26:28 +08:00
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(RecirculationFlowPointDisplay));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
2026-03-09 17:03:13 +08:00
OnPropertyChanged(nameof(RealtimeMeasurementHint));
2026-03-09 19:06:55 +08:00
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
OnPropertyChanged(nameof(SelectedItemLiveHint));
2026-03-11 16:27:00 +08:00
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));
2026-03-09 17:03:13 +08:00
if (value is not null)
{
2026-03-09 19:06:55 +08:00
LoadSelectedItemDraft(value);
2026-03-26 09:28:12 +08:00
return;
2026-03-09 17:03:13 +08:00
}
2026-03-26 09:28:12 +08:00
ResultValue = string.Empty;
ResultNote = string.Empty;
ResultOperator = OperatorName;
SelectedResultStatusText = "待检";
2026-03-09 17:03:13 +08:00
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void SelectItem(InspectionItem? item)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
if (item is null)
2026-03-09 17:03:13 +08:00
{
return;
}
2026-03-09 19:06:55 +08:00
SelectedItem = item;
2026-03-09 17:03:13 +08:00
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void ClearItemSearch()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
ItemSearchText = string.Empty;
2026-03-09 17:03:13 +08:00
}
2026-03-14 14:10:52 +08:00
[RelayCommand]
private void ShowPendingItems()
{
ActiveFilter = "待填写";
LatestAction = $"已切换到待处理项目,共 {PendingCount} 项。";
}
[RelayCommand]
private void ShowAllItems()
{
ActiveFilter = "全部";
LatestAction = $"已切换到全部项目,共 {InspectionItems.Count} 项。";
}
2026-03-11 16:27:00 +08:00
[RelayCommand]
private void TogglePumpControl(PumpControlChannel? pump)
{
if (pump is null)
{
return;
}
var nextState = !pump.IsRunning;
_telemetryService.SetPumpRunning(pump.Key, nextState);
2026-03-20 20:03:26 +08:00
LatestAction = _telemetryService.IsLiveConnected
? $"{pump.Name} 已发送{(nextState ? "" : "")}指令。"
: $"{pump.Name} 指令未下发PLC 当前离线。";
2026-03-11 16:27:00 +08:00
TraceEvents.Insert(0, NewTrace("泵控", $"{pump.Name} => {(nextState ? "" : "")}"));
RefreshTelemetry();
}
2026-03-11 17:36:07 +08:00
[RelayCommand]
private void ToggleValveControl(ValveControlChannel? valve)
{
if (valve is null)
{
return;
}
var nextState = !valve.IsOpen;
_telemetryService.SetValveOpen(valve.Key, nextState);
2026-03-20 20:03:26 +08:00
LatestAction = _telemetryService.IsLiveConnected
? $"{valve.Name} 已发送{(nextState ? "" : "")}指令。"
: $"{valve.Name} 指令未下发PLC 当前离线。";
2026-03-11 17:36:07 +08:00
TraceEvents.Insert(0, NewTrace("阀控", $"{valve.Name} => {(nextState ? "" : "")}"));
2026-03-20 20:03:26 +08:00
RefreshTelemetry();
2026-03-11 17:36:07 +08:00
}
2026-03-11 16:27:00 +08:00
[RelayCommand]
private void ClearTrendData()
{
ClearTrendSeries(
ProximalPressureTrendValues,
DistalPressureTrendValues,
DeltaPressureTrendValues,
PressureDropPumpTrendValues,
RecirculationMainPumpTrendValues,
RecirculationReturnPumpTrendValues,
RecirculationDrainagePumpTrendValues,
KinkResistancePumpTrendValues,
HemolysisDrainageSingleTrendValues,
HemolysisReturnSingleTrendValues,
HemolysisDualLumenTrendValues);
LatestAction = "已清空实时趋势曲线。";
TraceEvents.Insert(0, NewTrace("趋势图", "已清空实时趋势曲线"));
RaiseTrendPropertyChanges();
}
2026-03-10 16:36:40 +08:00
[RelayCommand]
private void CapturePressureDrop50() => CapturePressureDropSample("50%");
[RelayCommand]
private void CapturePressureDrop75() => CapturePressureDropSample("75%");
[RelayCommand]
private void CapturePressureDrop100() => CapturePressureDropSample("100%");
2026-03-11 15:06:03 +08:00
[RelayCommand]
private void CaptureKinkResistanceMaxBaseline() => CaptureKinkResistanceBaseline("最大流量");
[RelayCommand]
private void CaptureKinkResistanceMaxKinked() => CaptureKinkResistanceKinked("最大流量");
[RelayCommand]
private void CaptureKinkResistanceMinBaseline() => CaptureKinkResistanceBaseline("最小流量");
[RelayCommand]
private void CaptureKinkResistanceMinKinked() => CaptureKinkResistanceKinked("最小流量");
2026-03-10 15:26:28 +08:00
[RelayCommand]
private void CaptureAntiCollapseBaseline()
{
if (!IsAntiCollapseSelected)
{
LatestAction = "当前选择的不是抗塌陷项目。";
return;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "抗塌陷基线采集失败:主泵或压力实时数据未接入。";
return;
}
2026-03-10 15:26:28 +08:00
_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;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "抗塌陷比较失败:主泵或压力实时数据未接入。";
return;
}
2026-03-10 15:26:28 +08:00
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));
}
2026-03-10 16:36:40 +08:00
[RelayCommand]
private void CaptureRecirculation50() => CaptureRecirculationSample("50%");
[RelayCommand]
private void CaptureRecirculation75() => CaptureRecirculationSample("75%");
[RelayCommand]
private void CaptureRecirculation100() => CaptureRecirculationSample("100%");
2026-03-09 17:03:13 +08:00
[RelayCommand]
private void ToggleAcquisition()
{
AcquisitionRunning = !AcquisitionRunning;
2026-03-14 15:21:24 +08:00
RefreshDeviceStatus();
2026-03-09 19:06:55 +08:00
LatestAction = AcquisitionRunning ? "继续采集实时数据,供检测参考。" : "已暂停实时采集。";
2026-03-09 17:03:13 +08:00
if (AcquisitionRunning)
{
_timer.Start();
TraceEvents.Insert(0, NewTrace("采集控制", "恢复实时采集"));
}
else
{
_timer.Stop();
TraceEvents.Insert(0, NewTrace("采集控制", "暂停实时采集"));
}
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void SelectPreviousItem()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
var scope = ActiveItemScope();
if (SelectedItem is null)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
SelectedItem = scope.FirstOrDefault();
2026-03-09 17:03:13 +08:00
return;
}
2026-03-09 19:06:55 +08:00
var index = scope.IndexOf(SelectedItem);
2026-03-09 17:03:13 +08:00
if (index > 0)
{
2026-03-09 19:06:55 +08:00
SelectedItem = scope[index - 1];
2026-03-09 17:03:13 +08:00
}
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void SelectNextItem()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
var scope = ActiveItemScope();
if (SelectedItem is null)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
SelectedItem = scope.FirstOrDefault();
2026-03-09 17:03:13 +08:00
return;
}
2026-03-09 19:06:55 +08:00
var index = scope.IndexOf(SelectedItem);
2026-03-09 17:03:13 +08:00
if (index >= 0 && index < scope.Count - 1)
{
2026-03-09 19:06:55 +08:00
SelectedItem = scope[index + 1];
2026-03-09 17:03:13 +08:00
}
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void ApplyResult()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
if (SelectedItem is null)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
LatestAction = "请先选择项目。";
2026-03-09 17:03:13 +08:00
return;
}
2026-03-09 19:06:55 +08:00
if (SelectedItemUsesRealtimeValue)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
LatestAction = "当前项目使用实时数据自动判定,无需手动填写。";
2026-03-09 17:03:13 +08:00
return;
}
2026-03-14 14:10:52 +08:00
if (IsHemolysisSelected)
{
ResultValue = BuildHemolysisResultText();
ResultNote = BuildHemolysisRecordNoteText();
_lastAutoHemolysisResult = ResultValue;
_lastAutoHemolysisNote = ResultNote;
}
2026-03-09 19:06:55 +08:00
if (string.IsNullOrWhiteSpace(ResultValue))
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
LatestAction = "请先填写检测结果或判定结论。";
2026-03-09 17:03:13 +08:00
return;
}
2026-03-09 19:06:55 +08:00
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
2026-03-09 17:03:13 +08:00
};
2026-03-09 19:06:55 +08:00
LatestAction = $"已填写 {SelectedItem.Item} 的检测结果。";
2026-03-09 17:03:13 +08:00
TraceEvents.Insert(0, new TraceEvent
{
Timestamp = DateTime.Now,
2026-03-09 19:06:55 +08:00
Stage = "结果填写",
Detail = $"{SelectedItem.Item}: {SelectedItem.Measured}",
Operator = SelectedItem.RecordedBy
2026-03-09 17:03:13 +08:00
});
RefreshComputedState();
2026-03-09 19:21:25 +08:00
RefreshFilteredItemsView();
2026-03-09 17:03:13 +08:00
}
[RelayCommand]
2026-03-09 19:06:55 +08:00
private void CompleteDetection()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
DetectionCompleted = true;
CurrentStage = "检测完成";
2026-03-09 17:03:13 +08:00
AcquisitionRunning = false;
_timer.Stop();
DeviceStatus = "采集停止";
2026-03-21 10:24:26 +08:00
LatestAction = PendingCount == 0 ? "检测完成,全部项目已填写,可导出检查报告。" : $"检测完成,但仍有 {PendingCount} 项待处理,请确认后导出检查报告。";
2026-03-09 19:06:55 +08:00
TraceEvents.Insert(0, NewTrace("检测完成", "检测员结束本次检测任务"));
2026-03-09 17:03:13 +08:00
}
[RelayCommand]
private void AcknowledgeAlarm()
{
if (AlarmMessages.Count == 0)
{
LatestAction = "当前没有需要确认的告警。";
return;
}
var count = AlarmMessages.Count;
AlarmMessages.Clear();
2026-03-20 20:03:26 +08:00
OnPropertyChanged(nameof(AlarmSummaryDisplay));
2026-03-09 17:03:13 +08:00
LatestAction = $"已确认并清空 {count} 条实时告警。";
TraceEvents.Insert(0, NewTrace("告警确认", $"确认 {count} 条实时告警"));
}
[RelayCommand]
private void ExportReport()
{
2026-03-26 09:28:12 +08:00
var outputDirectory = ResolveReportOutputDirectory();
2026-03-09 17:03:13 +08:00
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
2026-03-26 09:28:12 +08:00
var batchToken = string.IsNullOrWhiteSpace(BatchNumber) ? "未填写批号" : SanitizeFileNameSegment(BatchNumber.Trim());
2026-03-21 10:24:26 +08:00
var pdfPath = Path.Combine(outputDirectory, $"检查报告-{batchToken}-{timestamp}.pdf");
2026-03-09 17:03:13 +08:00
2026-03-10 14:17:43 +08:00
try
2026-03-09 17:03:13 +08:00
{
2026-03-10 14:17:43 +08:00
var document = new PdfReportDocument(
pageTitle: PageTitle,
batchNumber: BatchNumber,
currentStage: CurrentStage,
operatorName: OperatorName,
reviewerName: ReviewerName,
approverName: ApproverName,
complianceDisplay: ComplianceDisplay,
deltaPressureDisplay: DeltaPressureDisplay,
detectionSummary: DetectionSummary,
2026-03-10 16:36:40 +08:00
configurationSummary: ConfigurationSummary,
2026-03-10 14:17:43 +08:00
exportTime: DateTime.Now,
inspectionItems: InspectionItems.ToList(),
2026-03-10 16:36:40 +08:00
traceEvents: TraceEvents.ToList(),
2026-03-11 15:06:03 +08:00
kinkResistanceEntries: KinkResistanceEntries.ToList(),
kinkResistanceFlowPointDisplay: KinkResistanceFlowPointDisplay,
kinkResistanceMandrelDiameterDisplay: KinkResistanceMandrelDiameterDisplay,
2026-03-10 16:36:40 +08:00
pressureDropEntries: PressureDropEntries.ToList(),
pressureDropLimitDisplay: PressureDropLimitDisplay,
antiCollapseBaselineDisplay: AntiCollapseBaselineDisplay,
antiCollapseComparisonDisplay: AntiCollapseComparisonDisplay,
antiCollapseCurrentNegativePressure: NegativeAssistPressureDisplay,
antiCollapseCurrentFlowDisplay: PumpFlowDisplay,
antiCollapseAllowedIncreaseRateDisplay: $"{AntiCollapseAllowedIncreaseRate:F1}%",
recirculationEntries: RecirculationEntries.ToList(),
recirculationLimitDisplay: RecirculationLimitDisplay);
2026-03-10 14:17:43 +08:00
document.GeneratePdf(pdfPath);
2026-03-21 10:24:26 +08:00
LatestAction = $"已导出检查报告: {pdfPath}";
TraceEvents.Insert(0, NewTrace("检查报告导出", Path.GetFileName(pdfPath)));
2026-03-09 17:03:13 +08:00
}
2026-03-10 14:17:43 +08:00
catch (Exception ex)
2026-03-09 17:03:13 +08:00
{
2026-03-21 10:24:26 +08:00
LatestAction = $"检查报告导出失败: {ex.Message}";
TraceEvents.Insert(0, NewTrace("检查报告导出失败", ex.Message));
2026-03-09 17:03:13 +08:00
}
}
private void RefreshTelemetry()
{
var alarms = _telemetryService.UpdateChannels();
2026-03-10 17:35:25 +08:00
AlarmMessages.Clear();
2026-03-09 17:03:13 +08:00
foreach (var alarm in alarms.OrderByDescending(a => a.Timestamp))
{
2026-03-10 17:35:25 +08:00
AlarmMessages.Add(alarm);
2026-03-09 17:03:13 +08:00
}
RefreshTelemetryPanel();
2026-03-14 15:21:24 +08:00
RefreshDeviceStatus();
2026-03-09 17:03:13 +08:00
RefreshComputedState();
2026-03-09 19:21:25 +08:00
RefreshFilteredItemsView();
2026-03-09 17:03:13 +08:00
}
private void RefreshTelemetryPanel()
{
2026-03-20 20:03:26 +08:00
DeltaPressure = HasChannelTelemetry("近端压力", "远端压力")
? ChannelValue("近端压力") - ChannelValue("远端压力")
: 0d;
2026-03-09 17:03:13 +08:00
OnPropertyChanged(nameof(PumpFlow));
OnPropertyChanged(nameof(DrainageFlow));
OnPropertyChanged(nameof(ReturnFlow));
OnPropertyChanged(nameof(RecirculationRate));
OnPropertyChanged(nameof(PumpFlowDisplay));
OnPropertyChanged(nameof(DrainageFlowDisplay));
OnPropertyChanged(nameof(ReturnFlowDisplay));
2026-03-11 16:27:00 +08:00
OnPropertyChanged(nameof(PressureDropPumpFlowDisplay));
OnPropertyChanged(nameof(RecirculationPumpFlowDisplay));
OnPropertyChanged(nameof(KinkResistancePumpFlowDisplay));
OnPropertyChanged(nameof(HemolysisDrainageSingleFlowDisplay));
OnPropertyChanged(nameof(HemolysisReturnSingleFlowDisplay));
OnPropertyChanged(nameof(HemolysisDualLumenFlowDisplay));
2026-03-10 17:35:25 +08:00
OnPropertyChanged(nameof(ProximalPressureDisplay));
OnPropertyChanged(nameof(DistalPressureDisplay));
2026-03-09 17:03:13 +08:00
OnPropertyChanged(nameof(RealtimeRecirculationDisplay));
OnPropertyChanged(nameof(FlowImbalanceDisplay));
OnPropertyChanged(nameof(NegativeAssistPressureDisplay));
OnPropertyChanged(nameof(TemperatureDisplay));
OnPropertyChanged(nameof(FreeHemoglobinDisplay));
OnPropertyChanged(nameof(WhiteCellLossDisplay));
2026-03-10 14:35:50 +08:00
OnPropertyChanged(nameof(PressureDropConditionDisplay));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(ConfigurationSummary));
2026-03-11 15:06:03 +08:00
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
2026-03-10 14:35:50 +08:00
OnPropertyChanged(nameof(PressureDropFlowPointDisplay));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(PressureDropSamplingSummary));
2026-03-10 15:26:28 +08:00
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
2026-03-10 16:36:40 +08:00
OnPropertyChanged(nameof(RecirculationFlowPointDisplay));
OnPropertyChanged(nameof(RecirculationSamplingSummary));
2026-03-09 17:03:13 +08:00
OnPropertyChanged(nameof(PumpFlowLoadDisplay));
OnPropertyChanged(nameof(DrainageFlowLoadDisplay));
OnPropertyChanged(nameof(ReturnFlowLoadDisplay));
OnPropertyChanged(nameof(PumpFlowNormalizedValue));
OnPropertyChanged(nameof(DrainageFlowNormalizedValue));
OnPropertyChanged(nameof(ReturnFlowNormalizedValue));
OnPropertyChanged(nameof(FlowSensorChannels));
OnPropertyChanged(nameof(OtherChannels));
2026-03-11 16:27:00 +08:00
OnPropertyChanged(nameof(PumpControls));
2026-03-20 20:03:26 +08:00
OnPropertyChanged(nameof(IsTelemetryOnline));
OnPropertyChanged(nameof(PlcEndpointDisplay));
OnPropertyChanged(nameof(TelemetryLastUpdatedDisplay));
OnPropertyChanged(nameof(TelemetryStatusDetail));
OnPropertyChanged(nameof(TelemetryAvailabilityDisplay));
OnPropertyChanged(nameof(AlarmSummaryDisplay));
OnPropertyChanged(nameof(TelemetryCoverageDisplay));
2026-03-09 19:06:55 +08:00
OnPropertyChanged(nameof(SelectedItemLiveDisplay));
OnPropertyChanged(nameof(SelectedItemLiveHint));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
2026-03-09 17:03:13 +08:00
2026-03-11 16:27:00 +08:00
CaptureTrendSamples();
2026-03-09 19:06:55 +08:00
SyncRealtimeItems();
2026-03-09 17:03:13 +08:00
}
2026-03-14 15:21:24 +08:00
private void RefreshDeviceStatus()
{
if (!AcquisitionRunning)
{
DeviceStatus = DetectionCompleted ? "采集停止" : "采集暂停";
return;
}
2026-03-20 20:03:26 +08:00
DeviceStatus = _telemetryService.IsLiveConnected
? "PLC在线"
: _telemetryService.LastSuccessfulReadAt.HasValue
? "PLC断连"
: "等待连接";
2026-03-14 15:21:24 +08:00
}
2026-03-09 17:03:13 +08:00
private void RefreshComputedState()
{
2026-03-09 19:06:55 +08:00
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;
2026-03-09 17:03:13 +08:00
}
2026-03-11 16:27:00 +08:00
private void CaptureTrendSamples()
{
2026-03-20 20:03:26 +08:00
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, "双腔插管试验回路流量(两个管腔)");
2026-03-11 16:27:00 +08:00
RaiseTrendPropertyChanges();
}
2026-03-20 20:03:26 +08:00
private void AppendTrendValueIfAvailable(ObservableCollection<double> series, string channelName)
{
if (TryGetChannel(channelName, out var channel) && channel.IsAvailable)
{
AppendTrendValue(series, channel.Value);
}
}
2026-03-11 16:27:00 +08:00
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));
2026-03-14 14:10:52 +08:00
OnPropertyChanged(nameof(PressureTrendCurrentSummary));
OnPropertyChanged(nameof(FlowTrendCurrentSummary));
2026-03-11 16:27:00 +08:00
}
2026-03-09 19:21:25 +08:00
private void RefreshFilteredItemsView()
{
2026-03-10 16:36:40 +08:00
FilteredItemsView.Refresh();
2026-03-09 19:21:25 +08:00
OnPropertyChanged(nameof(FilteredItemSummary));
OnPropertyChanged(nameof(HasFilteredItems));
OnPropertyChanged(nameof(HasItemSearchText));
2026-03-10 16:36:40 +08:00
var filtered = FilteredItemsView.Cast<InspectionItem>().ToList();
2026-03-09 19:21:25 +08:00
if (filtered.Count == 0)
{
2026-03-26 09:28:12 +08:00
SelectedItem = null;
2026-03-09 19:21:25 +08:00
return;
}
if (SelectedItem is null || !filtered.Contains(SelectedItem))
{
SelectedItem = filtered[0];
}
}
2026-03-09 19:06:55 +08:00
private void LoadSelectedItemDraft(InspectionItem item)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
ResultValue = SelectedItemUsesRealtimeValue
? item.Measured
: item.Measured == "待检测" ? string.Empty : item.Measured;
ResultNote = item.Notes;
2026-03-14 14:10:52 +08:00
if (item.Clause == "4.3.4")
{
if (string.IsNullOrWhiteSpace(ResultValue))
{
ResultValue = BuildHemolysisResultText();
}
if (string.IsNullOrWhiteSpace(ResultNote))
{
ResultNote = BuildHemolysisRecordNoteText();
}
_lastAutoHemolysisResult = BuildHemolysisResultText();
_lastAutoHemolysisNote = BuildHemolysisRecordNoteText();
}
2026-03-09 19:06:55 +08:00
ResultOperator = string.IsNullOrWhiteSpace(item.RecordedBy) ? OperatorName : item.RecordedBy;
SelectedResultStatusText = item.Status switch
{
InspectionItemStatus.Warning => "预警",
InspectionItemStatus.Critical => "不合格",
InspectionItemStatus.Pending => "待检",
2026-03-09 17:03:13 +08:00
_ => "合格"
};
}
private TraceEvent NewTrace(string stage, string detail) => new()
{
Timestamp = DateTime.Now,
Stage = stage,
Detail = detail,
2026-03-09 19:06:55 +08:00
Operator = string.IsNullOrWhiteSpace(ResultOperator) ? OperatorName : ResultOperator
2026-03-09 17:03:13 +08:00
};
2026-03-09 19:06:55 +08:00
private void SyncRealtimeItems()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
var pressureItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.1");
if (pressureItem is not null)
2026-03-09 17:03:13 +08:00
{
2026-03-20 20:03:26 +08:00
if (HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
var pressureStatus = GetPressureDropOverallStatus();
2026-03-09 17:03:13 +08:00
2026-03-20 20:03:26 +08:00
pressureItem.Measured = BuildPressureDropMeasuredText();
pressureItem.Notes = BuildPressureDropRecordNote();
pressureItem.RecordedBy = "实时数据";
pressureItem.RecordedAt = DateTime.Now;
pressureItem.Status = pressureStatus;
2026-03-10 16:36:40 +08:00
2026-03-20 20:03:26 +08:00
if (SelectedItem == pressureItem)
2026-03-10 16:36:40 +08:00
{
2026-03-20 20:03:26 +08:00
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;
2026-03-10 16:36:40 +08:00
}
2026-03-09 17:03:13 +08:00
}
2026-03-11 15:06:03 +08:00
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;
}
}
2026-03-10 15:26:28 +08:00
var antiCollapseItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.2");
if (antiCollapseItem is not null)
{
2026-03-20 20:03:26 +08:00
if (HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
UpdateAntiCollapseBaseline();
}
2026-03-10 15:26:28 +08:00
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;
}
}
2026-03-09 19:06:55 +08:00
var recirculationItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.3");
2026-03-20 20:03:26 +08:00
if (recirculationItem is not null && SelectedItem == recirculationItem && HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
2026-03-09 17:03:13 +08:00
{
2026-03-10 16:36:40 +08:00
var suggestedResult = BuildRecirculationMeasuredText();
var suggestedNote = BuildRecirculationRecordNote();
2026-03-09 17:03:13 +08:00
2026-03-10 16:36:40 +08:00
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoRecirculationResult)
2026-03-09 17:03:13 +08:00
{
2026-03-10 16:36:40 +08:00
ResultValue = suggestedResult;
}
2026-03-09 17:03:13 +08:00
2026-03-10 16:36:40 +08:00
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoRecirculationNote)
2026-03-09 17:03:13 +08:00
{
2026-03-10 16:36:40 +08:00
ResultNote = suggestedNote;
}
_lastAutoRecirculationResult = suggestedResult;
_lastAutoRecirculationNote = suggestedNote;
2026-03-09 17:03:13 +08:00
}
}
2026-03-14 14:10:52 +08:00
private string BuildHemolysisResultText() =>
HemolysisRecordTemplate + "\n\n" + BuildHemolysisSamplingTableText() + "\n\n" + BuildHemolysisCalculationSection();
private string BuildHemolysisSamplingTableText()
{
var lines = new List<string>
{
"标准取样节点T0、T30、T180、T360T60/T120/T240/T300 可作为过程观察补充。",
"| 取样序号 | 时间点 (min) | 挂钟时间 | 游离Hb (mg/dL) | Hct | 白细胞 (×10^9/L) | 血小板 (×10^9/L) | Hb (g/dL) | 流量 (L/min) | 压力 (mmHg) | 温度 (°C) | 备注 |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
};
foreach (var entry in HemolysisSamplingEntries)
{
lines.Add($"| {entry.Sequence} | {entry.TimePoint} | {entry.ClockTime} | {FormatHemolysisValue(entry.FreeHemoglobin, "F1")} | {FormatHemolysisValue(entry.Hematocrit, "F2")} | {FormatHemolysisValue(entry.WhiteCellCount, "F1")} | {FormatHemolysisValue(entry.PlateletCount, "F1")} | {FormatHemolysisValue(entry.Hemoglobin, "F1")} | {FormatHemolysisValue(entry.Flow, "F2")} | {FormatHemolysisValue(entry.Pressure, "F0")} | {FormatHemolysisValue(entry.Temperature, "F1")} | {entry.Remarks} |");
}
return string.Join("\n", lines);
}
private string BuildHemolysisCalculationSection()
{
var calculationHematocrit = GetHemolysisCalculationHematocrit();
return
"5. 结果计算\n" +
$"- 标准取样点完成情况:{BuildHemolysisSamplingCompletionSummary()}\n" +
$"- ΔfHb (T360 - T0){FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")}\n" +
"- NIH = ΔfHb × V(L) × (1-Hct) / (Q×T)\n" +
$"- NIH 计算参数V={FormatHemolysisDisplay(HemolysisTestParameters.CircuitPrimingVolume, "F0", "mL")}, Hct={FormatHemolysisDisplay(calculationHematocrit, "F2", string.Empty)}, Q={FormatHemolysisDisplay(HemolysisTestParameters.SetFlow, "F2", "L/min")}, T={FormatHemolysisDisplay(HemolysisTestParameters.RunTimeMinutes, "F0", "min")}\n" +
$"- 计算所得 NIH{FormatHemolysisDisplay(GetHemolysisNih(), "F3", "g/100L")}\n" +
$"- 白细胞减少率:{FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")}\n" +
$"- 血小板减少率:{FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}\n" +
$"- 判定结论:{SelectedResultStatusText}";
}
private string BuildHemolysisRecordNoteText()
{
var calculationHematocrit = GetHemolysisCalculationHematocrit();
return
"标准核对:\n" +
"1. 试验介质应为肝素化牛血、猪血或羊血。\n" +
"2. 两个通用等同回路的初始血液通道试验液容积差不应超过 1%。\n" +
"3. 应记录最大血流量、血中葡萄糖 10 mmol/L、血红蛋白 12 g/dL。\n" +
$"4. 标准取样点完成情况:{BuildHemolysisSamplingCompletionSummary()}。\n" +
$"5. NIH 计算采用 Hct={FormatHemolysisDisplay(calculationHematocrit, "F2", string.Empty)}、V={FormatHemolysisDisplay(HemolysisTestParameters.CircuitPrimingVolume, "F0", "mL")}、Q={FormatHemolysisDisplay(HemolysisTestParameters.SetFlow, "F2", "L/min")}、T={FormatHemolysisDisplay(HemolysisTestParameters.RunTimeMinutes, "F0", "min")}。\n" +
"6. T60、T120、T240、T300 为过程观察补充,不替代标准取样点。";
}
private string BuildHemolysisSamplingCompletionSummary()
{
var requiredPoints = new[] { "T0", "T30", "T180", "T360" };
var completedPoints = requiredPoints
.Where(point => HasHemolysisRequiredSample(GetHemolysisEntry(point)))
.ToList();
var missingPoints = requiredPoints.Except(completedPoints).ToList();
return missingPoints.Count == 0
? "标准取样点已完成 4/4"
: $"标准取样点已完成 {completedPoints.Count}/4缺少 {string.Join("", missingPoints)}";
}
private List<string> GetHemolysisMissingRequiredPoints()
{
var requiredPoints = new[] { "T0", "T30", "T180", "T360" };
return requiredPoints
.Where(point => !HasHemolysisRequiredSample(GetHemolysisEntry(point)))
.ToList();
}
private string BuildHemolysisRequiredPointAlert()
{
var missingPoints = GetHemolysisMissingRequiredPoints();
return missingPoints.Count == 0
? "标准取样点已全部完成。"
: $"请优先补齐标准取样点:{string.Join("", missingPoints)}。";
}
private string BuildHemolysisCalculationSummary() =>
$"{BuildHemolysisSamplingCompletionSummary()}ΔfHb {FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")}NIH {FormatHemolysisDisplay(GetHemolysisNih(), "F3", "g/100L")};白细胞减少率 {FormatHemolysisDisplay(GetHemolysisWhiteCellReduction(), "F1", "%")};血小板减少率 {FormatHemolysisDisplay(GetHemolysisPlateletReduction(), "F1", "%")}";
private HemolysisSamplingEntry? GetHemolysisEntry(string timePointPrefix) =>
HemolysisSamplingEntries.FirstOrDefault(entry => entry.TimePoint.StartsWith(timePointPrefix, StringComparison.OrdinalIgnoreCase));
private static bool HasHemolysisRequiredSample(HemolysisSamplingEntry? entry) =>
entry is not null
&& !string.IsNullOrWhiteSpace(entry.ClockTime)
&& entry.FreeHemoglobin.HasValue;
private double? GetHemolysisInitialFreeHemoglobin() =>
HemolysisTestParameters.InitialFreeHemoglobin ?? GetHemolysisEntry("T0")?.FreeHemoglobin;
private double? GetHemolysisCalculationHematocrit() =>
HemolysisTestParameters.AdjustedHematocrit ?? GetHemolysisEntry("T0")?.Hematocrit;
private double? GetHemolysisDeltaFreeHemoglobin()
{
var start = GetHemolysisEntry("T0")?.FreeHemoglobin;
var end = GetHemolysisEntry("T360")?.FreeHemoglobin;
return start.HasValue && end.HasValue ? end.Value - start.Value : null;
}
private double? GetHemolysisNih()
{
var delta = GetHemolysisDeltaFreeHemoglobin();
var volumeMilliliter = HemolysisTestParameters.CircuitPrimingVolume;
var hematocrit = GetHemolysisCalculationHematocrit();
var flow = HemolysisTestParameters.SetFlow;
var time = HemolysisTestParameters.RunTimeMinutes;
if (!delta.HasValue || !volumeMilliliter.HasValue || !hematocrit.HasValue || !flow.HasValue || !time.HasValue
|| flow.Value <= 0 || time.Value <= 0)
{
return null;
}
var volumeLiter = volumeMilliliter.Value / 1000d;
return delta.Value * volumeLiter * (1d - hematocrit.Value) / (flow.Value * time.Value);
}
private double? GetHemolysisWhiteCellReduction() => CalculateHemolysisReduction(
GetHemolysisEntry("T0")?.WhiteCellCount,
GetHemolysisEntry("T360")?.WhiteCellCount);
private double? GetHemolysisPlateletReduction() => CalculateHemolysisReduction(
GetHemolysisEntry("T0")?.PlateletCount,
GetHemolysisEntry("T360")?.PlateletCount);
private static double? CalculateHemolysisReduction(double? initialValue, double? finalValue)
{
if (!initialValue.HasValue || !finalValue.HasValue || initialValue.Value <= 0)
{
return null;
}
return (initialValue.Value - finalValue.Value) / initialValue.Value * 100d;
}
private static string FormatHemolysisDisplay(double? value, string format, string unit)
{
if (!value.HasValue)
{
return "待计算";
}
return string.IsNullOrWhiteSpace(unit)
? value.Value.ToString(format)
: $"{value.Value.ToString(format)} {unit}";
}
private string BuildFlowTrendCurrentSummary() => SelectedItem?.Clause switch
{
"4.3.3" => $"主泵 {RecirculationPumpFlowDisplay} / 回流 {ReturnFlowDisplay} / 引流 {DrainageFlowDisplay}",
"4.2.3" => $"抗扭结泵 {KinkResistancePumpFlowDisplay}",
"4.3.4" => $"单腔引流/回输 {HemolysisDrainageSingleFlowDisplay} / 双腔插管试验回路 {HemolysisReturnSingleFlowDisplay} / 双腔插管试验回路(两个管腔) {HemolysisDualLumenFlowDisplay}",
_ => $"主泵 {PressureDropPumpFlowDisplay} / 流量偏差 {FlowImbalanceDisplay}"
};
private static string FormatHemolysisDate(DateTime? value) => value?.ToString("yyyy-MM-dd") ?? string.Empty;
private static string FormatHemolysisValue(double? value, string format) => value.HasValue ? value.Value.ToString(format) : string.Empty;
2026-03-20 20:03:26 +08:00
private double ChannelValue(string name) => Channels.First(channel => channel.Name == name).Value;
2026-03-11 16:41:34 +08:00
private double ChannelNormalizedValue(string name) => Channels.FirstOrDefault(channel => channel.Name == name)?.NormalizedValue ?? 0d;
2026-03-20 20:03:26 +08:00
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} 量程";
}
2026-03-11 16:27:00 +08:00
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]);
}
2026-03-10 16:36:40 +08:00
private List<InspectionItem> ActiveItemScope() => FilteredItemsView.Cast<InspectionItem>().ToList();
private bool MatchesFilteredItem(object item) =>
item is InspectionItem inspectionItem
&& MatchesActiveFilter(inspectionItem)
&& MatchesItemSearch(inspectionItem);
2026-03-09 17:03:13 +08:00
2026-03-09 19:06:55 +08:00
private bool MatchesItemSearch(InspectionItem item)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
if (string.IsNullOrWhiteSpace(ItemSearchText))
2026-03-09 17:03:13 +08:00
{
return true;
}
2026-03-09 19:06:55 +08:00
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);
2026-03-09 17:03:13 +08:00
}
2026-03-09 19:21:25 +08:00
private bool MatchesActiveFilter(InspectionItem item) => ActiveFilter switch
{
"待填写" => item.Status == InspectionItemStatus.Pending,
"已完成" => item.Status != InspectionItemStatus.Pending,
"实时监控" => item.CaptureMode == InspectionItemCaptureMode.RealtimeMonitor,
"手动填写" => item.CaptureMode != InspectionItemCaptureMode.RealtimeMonitor,
_ => true
};
2026-03-09 17:03:13 +08:00
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;
}
2026-03-11 16:27:00 +08:00
private static bool IsFlowSensorChannel(DeviceChannel channel) => channel.Unit == "L/min";
2026-03-09 17:03:13 +08:00
2026-03-09 19:06:55 +08:00
private string BuildSelectedItemLiveDisplay()
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
if (SelectedItem is null)
2026-03-09 17:03:13 +08:00
{
2026-03-09 19:06:55 +08:00
return "未选择项目";
2026-03-09 17:03:13 +08:00
}
2026-03-09 19:06:55 +08:00
return (SelectedItem.Clause, SelectedItem.Item) switch
2026-03-09 17:03:13 +08:00
{
2026-03-11 15:06:03 +08:00
("4.2.3", _) => BuildKinkResistanceLiveDisplay(),
2026-03-10 14:35:50 +08:00
("4.3.1", _) => BuildPressureDropLiveDisplay(),
2026-03-10 15:26:28 +08:00
("4.3.2", _) => BuildAntiCollapseLiveDisplay(),
2026-03-10 16:36:40 +08:00
("4.3.3", _) => BuildRecirculationLiveDisplay(),
2026-03-14 14:10:52 +08:00
("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()}",
2026-03-09 19:06:55 +08:00
_ => "当前项目无实时信号,按检测原始记录手动填写。"
2026-03-09 17:03:13 +08:00
};
}
2026-03-10 14:35:50 +08:00
2026-03-11 15:06:03 +08:00
private string BuildKinkResistanceLiveDisplay()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
return "抗扭结主泵实时流量未接入,当前仅可查看已保存采样记录。";
}
2026-03-11 15:06:03 +08:00
return
$"条件:血液/模拟血液 2.0~3.5 mPa·s / 当前温度 {TemperatureDisplay} / 40±1 °C\n" +
$"流量点:{KinkResistanceFlowPointDisplay}\n" +
$"{KinkResistanceMandrelDiameterDisplay}\n" +
2026-03-11 16:27:00 +08:00
$"当前主泵:{KinkResistancePumpFlow:F2} L/min\n" +
2026-03-11 15:06:03 +08:00
$"{BuildKinkResistanceSamplingSummary()}";
}
private string BuildKinkResistanceMeasuredText()
{
var lines = new List<string>
{
"试验条件:试验液体为血液或模拟血液,黏度 2.0~3.5 mPa·s试验温度 40 ℃ ± 1 ℃。",
$"流量点:{KinkResistanceFlowPointDisplay}",
KinkResistanceMandrelDiameterDisplay,
"试验方法:先在插管笔直状态采集基线流量 L0再围绕圆角模板缠绕至少 180°在保持离心泵固定转速条件下采集弯曲流量 L1。"
};
foreach (var entry in KinkResistanceEntries)
{
lines.Add(entry.HasCompleteSample
? $"{entry.Label}:目标 {entry.TargetFlow:F2} L/minL0={entry.BaselineFlow:F2} L/minL1={entry.KinkedFlow:F2} L/min流量降低率={entry.FlowDropRate:F1}%"
: $"{entry.Label}:目标 {entry.TargetFlow:F2} L/min待完成 L0/L1 采样");
}
return string.Join("\n", lines);
}
private string BuildKinkResistanceRecordNote()
{
return
"录入建议:最大流量和制造商规定的最小流量都应完成直管 L0 与扭结后 L1 采样。\n" +
"判定规则:(L0-L1)/L0 不得大于 50%。泵型应为离心泵,且采样时保持固定转速,不应使用滚压式血泵。\n" +
$"{BuildKinkResistanceSamplingSummary()}";
}
private string BuildKinkResistanceSamplingSummary()
{
var completedEntries = KinkResistanceEntries.Where(entry => entry.HasCompleteSample).ToList();
if (completedEntries.Count == 0)
{
return "尚未完成最大/最小流量点的扭结抗性采样。";
}
return string.Join(
"",
completedEntries.Select(entry =>
$"{entry.Label} 降幅 {entry.FlowDropRate:F1}% / {ResolveKinkResistanceStatusText(entry)}"));
}
private void CaptureKinkResistanceBaseline(string label)
{
if (!IsKinkResistanceSelected)
{
LatestAction = "当前选择的不是抗扭结抗性项目。";
return;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
LatestAction = "抗扭结基线采集失败:抗扭结主泵实时流量未接入。";
return;
}
2026-03-11 15:06:03 +08:00
var entry = KinkResistanceEntries.First(item => item.Label == label);
2026-03-11 16:27:00 +08:00
entry.BaselineFlow = KinkResistancePumpFlow;
2026-03-20 20:03:26 +08:00
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
2026-03-11 15:06:03 +08:00
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;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("抗扭结主泵流量"))
{
LatestAction = "抗扭结比较失败:抗扭结主泵实时流量未接入。";
return;
}
2026-03-11 15:06:03 +08:00
var entry = KinkResistanceEntries.First(item => item.Label == label);
if (!entry.HasBaseline)
{
LatestAction = $"请先采集{label}的直管基线 L0。";
return;
}
2026-03-11 16:27:00 +08:00
entry.KinkedFlow = KinkResistancePumpFlow;
2026-03-20 20:03:26 +08:00
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
2026-03-11 15:06:03 +08:00
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 => "预警",
_ => "不合格"
};
2026-03-10 14:35:50 +08:00
private string BuildPressureDropMeasuredText()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"实时压力降数据未接入,无法自动生成记录。当前状态:{TelemetryStatusDetail}";
}
2026-03-10 16:36:40 +08:00
var lines = new List<string>
{
$"试验条件:{PressureDropConditionDisplay}",
$"流量点:{PressureDropFlowPointDisplay}"
};
2026-03-10 14:35:50 +08:00
2026-03-10 16:36:40 +08:00
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}:尚未采样");
}
2026-03-20 20:03:26 +08:00
lines.Add($"当前实时:近端 {ProximalPressureDisplay};远端 {DistalPressureDisplay}ΔP {DeltaPressureDisplay}");
2026-03-10 16:36:40 +08:00
return string.Join("\n", lines);
2026-03-10 14:35:50 +08:00
}
private string BuildPressureDropRecordNote()
{
var pressureStatusText = DeltaPressure switch
{
<= 20 => "实时判定合格",
<= 24 => "实时判定预警,建议补充中间流量点复测",
_ => "实时判定超限,建议复核回路、样品及声明范围"
};
return
$"标准记录建议:在 50% / 75% / 100% 最大标称流量点分别保存压力数据。\n" +
2026-03-10 16:36:40 +08:00
$"制造商声明限值:{PressureDropLimitDisplay}。\n" +
$"已采样流量点:{BuildPressureDropSamplingSummary()}。\n" +
2026-03-20 20:03:26 +08:00
$"当前记录:主泵流量 {PumpFlowDisplay},温度 {TemperatureDisplay}{pressureStatusText}。\n" +
2026-03-10 14:35:50 +08:00
$"制造商声明范围需在正式报告中复核;若压降-流量关系呈非线性,应补充更多流量点。";
}
private string BuildPressureDropLiveDisplay()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"压力降实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
2026-03-10 14:35:50 +08:00
return
$"条件:{PressureDropConditionDisplay}\n" +
$"流量点:{PressureDropFlowPointDisplay}\n" +
2026-03-10 16:36:40 +08:00
$"限值:{PressureDropLimitDisplay}\n" +
2026-03-20 20:03:26 +08:00
$"当前:主泵 {PumpFlowDisplay} / 近端 {ProximalPressureDisplay} / 远端 {DistalPressureDisplay} / ΔP {DeltaPressureDisplay}\n" +
2026-03-10 16:36:40 +08:00
$"{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;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
LatestAction = "压力降采样失败:主泵或压力实时数据未接入。";
return;
}
2026-03-10 16:36:40 +08:00
var entry = PressureDropEntries.First(item => item.Label == label);
entry.ActualPumpFlow = PumpFlow;
entry.ProximalPressure = ChannelValue("近端压力");
entry.DistalPressure = ChannelValue("远端压力");
2026-03-20 20:03:26 +08:00
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
2026-03-10 16:36:40 +08:00
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));
2026-03-10 15:26:28 +08:00
}
private void UpdateAntiCollapseBaseline()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return;
}
var negativePressure = ChannelValueOrDefault("负压辅助引流");
2026-03-10 15:26:28 +08:00
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()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"抗塌陷比较所需的主泵/压力实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
2026-03-10 15:26:28 +08:00
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" +
2026-03-10 16:36:40 +08:00
$"限值:压力降增幅 ≤ {AntiCollapseAllowedIncreaseRate:F1}%\n" +
2026-03-10 15:26:28 +08:00
$"当前:负压 {NegativeAssistPressureDisplay} / 主泵 {PumpFlow:F2} L/min / ΔP {DeltaPressure:F1} mmHg\n" +
$"{baselineText}\n" +
$"{increaseText}";
}
private string BuildAntiCollapseMeasuredText()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("主泵流量", "近端压力", "远端压力"))
{
return $"抗塌陷实时数据未接入,无法自动生成比较结果。当前状态:{TelemetryStatusDetail}";
}
2026-03-10 15:26:28 +08:00
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" +
2026-03-10 16:36:40 +08:00
$"判定规则:负压后压力降增幅不超过基线 {AntiCollapseAllowedIncreaseRate:F1}% 为合格;当前比较结果为 {comparison.StatusText}。";
2026-03-10 15:26:28 +08:00
}
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
{
2026-03-10 16:36:40 +08:00
var rate when rate <= AntiCollapseAllowedIncreaseRate => "合格",
var rate when rate <= AntiCollapseAllowedIncreaseRate + 10 => "预警,建议复测",
2026-03-10 15:26:28 +08:00
_ => "不合格,压降增幅超限"
};
return new AntiCollapseComparison(true, baseline, _antiCollapseBaselineFlow.Value, increase, increaseRate, statusText);
2026-03-10 14:35:50 +08:00
}
2026-03-10 16:36:40 +08:00
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));
2026-03-11 15:06:03 +08:00
OnPropertyChanged(nameof(KinkResistanceFlowPointDisplay));
OnPropertyChanged(nameof(KinkResistanceMandrelDiameterDisplay));
OnPropertyChanged(nameof(KinkResistanceSamplingSummary));
2026-03-10 16:36:40 +08:00
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;
2026-03-11 15:06:03 +08:00
KinkResistanceMinimumFlow = settings.KinkResistanceMinimumFlow;
KinkResistanceOuterDiameter = settings.KinkResistanceOuterDiameter;
2026-03-10 16:36:40 +08:00
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,
2026-03-11 15:06:03 +08:00
KinkResistanceMinimumFlow = KinkResistanceMinimumFlow,
KinkResistanceOuterDiameter = KinkResistanceOuterDiameter,
2026-03-10 16:36:40 +08:00
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()
{
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
{
return $"再循环所需的主泵/引流/回输实时信号未接入。当前链路状态:{TelemetryStatusDetail}";
}
2026-03-10 16:36:40 +08:00
return
$"条件:试验液体为水 + 示踪粒子 / 当前温度 {TemperatureDisplay}\n" +
$"目标流量点:{RecirculationFlowPointDisplay}\n" +
$"{RecirculationLimitDisplay}\n" +
2026-03-11 16:27:00 +08:00
$"当前:主泵 {RecirculationPumpFlow:F2} L/min / 引流 {DrainageFlow:F2} L/min / 回输 {ReturnFlow:F2} L/min / 在线参考 {RecirculationRate:F1}%\n" +
2026-03-10 16:36:40 +08:00
$"{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;
}
2026-03-20 20:03:26 +08:00
if (!HasChannelTelemetry("再循环主泵流量", "静脉引流流量", "动脉回输流量", "再循环率"))
{
LatestAction = "再循环采样失败:主泵/引流/回输实时数据未接入。";
return;
}
2026-03-10 16:36:40 +08:00
var entry = RecirculationEntries.First(item => item.Label == label);
2026-03-11 16:27:00 +08:00
entry.ActualPumpFlow = RecirculationPumpFlow;
2026-03-10 16:36:40 +08:00
entry.DrainageFlow = DrainageFlow;
entry.ReturnFlow = ReturnFlow;
entry.OnlineEstimate = RecirculationRate;
2026-03-20 20:03:26 +08:00
entry.Temperature = ChannelValueOrDefault("模拟血液温度");
2026-03-10 16:36:40 +08:00
entry.SampledAt = DateTime.Now;
var resultText = BuildRecirculationMeasuredText();
var noteText = BuildRecirculationRecordNote();
ResultValue = resultText;
ResultNote = noteText;
_lastAutoRecirculationResult = resultText;
_lastAutoRecirculationNote = noteText;
LatestAction = $"已采集再循环 {entry.Label} 流量点快照,当前在线参考 {RecirculationRate:F1}%。";
2026-03-11 16:27:00 +08:00
TraceEvents.Insert(0, NewTrace("再循环采样", $"{entry.Label} / 主泵 {RecirculationPumpFlow:F2} L/min / 在线参考 {RecirculationRate:F1}%"));
2026-03-10 16:36:40 +08:00
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;
}
}
}
2026-03-14 14:10:52 +08:00
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));
2026-03-26 09:28:12 +08:00
OnPropertyChanged(nameof(HemolysisCalculationDetail));
2026-03-14 14:10:52 +08:00
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;
}
2026-03-10 16:36:40 +08:00
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));
}
}
2026-03-11 15:06:03 +08:00
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));
}
}
2026-03-10 16:36:40 +08:00
private void SyncSpecializedTargetFlows()
{
foreach (var entry in PressureDropEntries)
{
entry.TargetFlow = PressureDropFlowPoint(LabelToRatio(entry.Label));
}
2026-03-11 15:06:03 +08:00
foreach (var entry in KinkResistanceEntries)
{
entry.TargetFlow = entry.Label == "最小流量"
? Math.Clamp(KinkResistanceMinimumFlow, 0d, Math.Max(RatedMaxFlow, 0d))
: RatedMaxFlow;
}
2026-03-10 16:36:40 +08:00
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
};
2026-03-10 14:35:50 +08:00
private double PressureDropFlowPoint(double ratio) =>
2026-03-10 16:36:40 +08:00
Math.Max(RatedMaxFlow, 0) * ratio;
2026-03-10 15:26:28 +08:00
2026-03-26 09:28:12 +08:00
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;
}
2026-03-10 15:26:28 +08:00
private readonly record struct AntiCollapseComparison(
bool HasBaseline,
double BaselinePressureDrop,
double BaselineFlow,
double Increase,
double IncreaseRate,
string StatusText);
2026-03-10 16:36:40 +08:00
2026-03-09 17:03:13 +08:00
}