最新更新20260508

This commit is contained in:
GukSang.Jin
2026-05-09 11:39:09 +08:00
parent f14ff344f5
commit 0ea7f83cc2
5 changed files with 784 additions and 921 deletions

View File

@@ -1,7 +1,7 @@
<Window x:Class="Cardiopulmonarybypasssystems.EngineeringRegistersWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="工程寄存器"
Title="泵/流量系数"
Width="920"
Height="640"
MinWidth="860"
@@ -29,19 +29,14 @@
<TextBlock FontSize="20"
FontWeight="Bold"
Foreground="White"
Text="工程寄存器" />
<TextBlock Margin="0,6,0,0"
FontSize="12"
Foreground="#EFFAFC"
Text="长按左上角空白区可关闭窗口。这里只保留 8 泵默认配置和 8 路流量系数配置;流量系数以 PLC 当前寄存器值为准。"
TextWrapping="Wrap" />
Text="泵/流量系数" />
</StackPanel>
<StackPanel DockPanel.Dock="Right"
Orientation="Horizontal"
VerticalAlignment="Center">
<Button Padding="12,6"
Command="{Binding RefreshEngineeringRegistersCommand}"
Content="刷新寄存器"
Content="刷新"
Background="#FFFFFFFF"
Foreground="{StaticResource HeaderBrush}" />
</StackPanel>

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,15 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using Cardiopulmonarybypasssystems.Models;
using Cardiopulmonarybypasssystems.ViewModels;
namespace Cardiopulmonarybypasssystems;
@@ -13,11 +17,23 @@ namespace Cardiopulmonarybypasssystems;
public partial class MainWindow : Window
{
private EngineeringRegistersWindow? _engineeringRegistersWindow;
private readonly List<Button> _navigationButtons = [];
private readonly List<Button> _projectInspectionButtons = [];
private object? _projectInspectionContent;
private object? _realtimeContent;
private object? _manualSupplementContent;
private object? _configurationContent;
private object? _traceContent;
private NavigationPage _currentPage = NavigationPage.RealtimeData;
private InspectionItem? _currentProjectItem;
private bool _isPumpSetpointKeypadOpen;
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
EmbedProjectDetailContent();
ConfigureNavigation(viewModel);
ConfigureTrendBindings();
}
@@ -31,17 +47,525 @@ public partial class MainWindow : Window
Dispatcher.BeginInvoke(() =>
{
if (sender is DataGrid { SelectedItem: not null })
if (sender is DataGrid { SelectedItem: InspectionItem inspectionItem })
{
ProjectDetailTab.IsSelected = true;
NavigateToProjectItem(inspectionItem);
}
}, DispatcherPriority.Background);
}
private void ReturnToProjectListButton_OnClick(object sender, RoutedEventArgs e)
{
ProjectListTab.IsSelected = true;
InspectionItemsGrid.Focus();
NavigateToFixedPage(NavigationPage.RealtimeData);
}
private void EmbedProjectDetailContent()
{
if (ProjectDetailContentHost.Content is not null)
{
return;
}
if (ProjectDetailTab.Content is ScrollViewer detailScrollViewer)
{
var detailContent = detailScrollViewer.Content;
detailScrollViewer.Content = null;
ProjectDetailContentHost.Content = detailContent;
}
else
{
ProjectDetailContentHost.Content = ProjectDetailTab.Content;
}
ProjectDetailTab.Content = null;
RootTabControl.Items.Remove(ProjectDetailTab);
}
private void ConfigureNavigation(MainViewModel viewModel)
{
_realtimeContent = ExtractTabContent(RealtimeDataTab);
_projectInspectionContent = ExtractTabContent(ProjectListTab);
_manualSupplementContent = ExtractTabContent(ManualSupplementTab);
_configurationContent = ExtractTabContent(ConfigurationTab);
_traceContent = ExtractTabContent(TraceTab);
viewModel.PropertyChanged += ViewModel_OnPropertyChanged;
if (viewModel.FilteredItemsView is INotifyCollectionChanged filteredItems)
{
filteredItems.CollectionChanged += (_, _) => RebuildNavigationButtons();
}
RebuildNavigationButtons();
NavigateToFixedPage(NavigationPage.RealtimeData);
}
private static object? ExtractTabContent(TabItem tab)
{
var content = tab.Content;
tab.Content = null;
return content;
}
private void RebuildNavigationButtons()
{
if (DataContext is not MainViewModel viewModel)
{
return;
}
NavigationButtonPanel.Children.Clear();
_navigationButtons.Clear();
_projectInspectionButtons.Clear();
AddNavigationButton("实时数据", new NavigationTarget(NavigationPage.RealtimeData, null));
foreach (var item in viewModel.FilteredItemsView.Cast<InspectionItem>())
{
var button = AddNavigationButton(item.Item, new NavigationTarget(NavigationPage.ProjectItem, item));
_projectInspectionButtons.Add(button);
}
AddNavigationButton("项目补充", new NavigationTarget(NavigationPage.ManualSupplement, null));
AddNavigationButton("配置", new NavigationTarget(NavigationPage.Configuration, null));
AddNavigationButton("追溯", new NavigationTarget(NavigationPage.Trace, null));
if (_currentPage == NavigationPage.ProjectItem
&& (_currentProjectItem is null || !_projectInspectionButtons.Any(button => ReferenceEquals(((NavigationTarget)button.Tag).ProjectItem, _currentProjectItem))))
{
NavigateToFixedPage(NavigationPage.RealtimeData);
return;
}
RefreshNavigationButtonStates();
}
private Button AddNavigationButton(string text, NavigationTarget target)
{
var button = new Button
{
Content = new TextBlock
{
MaxWidth = 132,
Text = text,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap
},
Tag = target,
MinWidth = 92,
MinHeight = 38,
Padding = new Thickness(10, 6, 10, 6),
Margin = new Thickness(0, 0, 6, 6),
FontSize = 14
};
button.Click += NavigationButton_OnClick;
NavigationButtonPanel.Children.Add(button);
_navigationButtons.Add(button);
return button;
}
private void NavigationButton_OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: NavigationTarget target })
{
Navigate(target);
}
}
private void Navigate(NavigationTarget target)
{
if (target.Page == NavigationPage.ProjectItem)
{
NavigateToProjectItem(target.ProjectItem);
return;
}
NavigateToFixedPage(target.Page);
}
private void NavigateToFixedPage(NavigationPage page)
{
_currentPage = page;
_currentProjectItem = null;
MainContentHost.Content = page switch
{
NavigationPage.RealtimeData => _realtimeContent,
NavigationPage.ManualSupplement => _manualSupplementContent,
NavigationPage.Configuration => _configurationContent,
NavigationPage.Trace => _traceContent,
_ => _realtimeContent
};
RefreshNavigationButtonStates();
}
private void NavigateToProjectItem(InspectionItem? item)
{
if (item is null)
{
return;
}
if (DataContext is MainViewModel viewModel && !ReferenceEquals(viewModel.SelectedItem, item))
{
viewModel.SelectedItem = item;
}
_currentPage = NavigationPage.ProjectItem;
_currentProjectItem = item;
MainContentHost.Content = _projectInspectionContent;
RefreshNavigationButtonStates();
}
private void RefreshNavigationButtonStates()
{
foreach (var button in _navigationButtons)
{
if (button.Tag is not NavigationTarget target)
{
continue;
}
var isSelected = target.Page == _currentPage
&& (_currentPage != NavigationPage.ProjectItem || ReferenceEquals(target.ProjectItem, _currentProjectItem));
button.Background = isSelected ? BrushFromRgb(0xE3, 0xF6, 0xEF) : BrushFromRgb(0xE1, 0xE9, 0xEC);
button.Foreground = isSelected ? BrushFromRgb(0x0E, 0x6C, 0x58) : BrushFromRgb(0x54, 0x65, 0x70);
button.BorderBrush = isSelected ? BrushFromRgb(0x6E, 0xB2, 0xA9) : BrushFromRgb(0xC6, 0xD5, 0xDB);
button.FontWeight = isSelected ? FontWeights.Bold : FontWeights.SemiBold;
}
}
private static SolidColorBrush BrushFromRgb(byte red, byte green, byte blue) => new(Color.FromRgb(red, green, blue));
private void ViewModel_OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.SelectedItem) && sender is MainViewModel viewModel)
{
if (viewModel.SelectedItem is not null && _currentPage == NavigationPage.ProjectItem)
{
NavigateToProjectItem(viewModel.SelectedItem);
}
}
}
private enum NavigationPage
{
RealtimeData,
ProjectItem,
ManualSupplement,
Configuration,
Trace
}
private sealed record NavigationTarget(NavigationPage Page, InspectionItem? ProjectItem);
private void PumpSetpointTextBox_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
if (sender is TextBox { DataContext: PumpControlChannel pump })
{
ShowPumpSetpointKeypad(pump);
}
}
private void PumpSetpointTextBox_OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (sender is TextBox { DataContext: PumpControlChannel pump })
{
ShowPumpSetpointKeypad(pump);
}
}
private void ShowPumpSetpointKeypad(PumpControlChannel pump)
{
if (_isPumpSetpointKeypadOpen)
{
return;
}
_isPumpSetpointKeypadOpen = true;
var valueBox = new TextBox
{
Text = string.IsNullOrWhiteSpace(pump.PendingSetpointText) ? "0" : pump.PendingSetpointText.Trim(),
IsReadOnly = true,
Height = 46,
FontSize = 22,
FontWeight = FontWeights.SemiBold,
TextAlignment = TextAlignment.Right,
VerticalContentAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 0, 10),
Padding = new Thickness(10, 0, 10, 0)
};
var errorText = new TextBlock
{
Foreground = Brushes.Firebrick,
FontSize = 12,
MinHeight = 18,
Margin = new Thickness(0, 0, 0, 8)
};
var keypad = new UniformGrid
{
Columns = 3,
Rows = 4,
Margin = new Thickness(0, 0, 0, 12)
};
foreach (var key in new[] { "7", "8", "9", "4", "5", "6", "1", "2", "3", ".", "0", "退格" })
{
keypad.Children.Add(CreateKeypadButton(key, (_, _) =>
{
errorText.Text = string.Empty;
ApplyKeypadInput(valueBox, key);
}));
}
var actions = new Grid();
actions.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
actions.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
actions.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var clearButton = CreateKeypadButton("清空", (_, _) =>
{
errorText.Text = string.Empty;
valueBox.Text = string.Empty;
});
var cancelButton = CreateKeypadButton("取消", (_, _) =>
{
if (Window.GetWindow(valueBox) is Window owner)
{
owner.DialogResult = false;
}
});
var okButton = CreateKeypadButton("确定", (_, _) =>
{
var normalizedValue = NormalizePumpSetpointText(valueBox.Text);
if (!double.TryParse(
normalizedValue,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var parsedValue)
|| parsedValue < 0)
{
errorText.Text = "请输入有效流量";
return;
}
pump.PendingSetpointText = parsedValue.ToString("0.##", CultureInfo.InvariantCulture);
if (DataContext is MainViewModel viewModel
&& viewModel.WritePumpSetpointCommand.CanExecute(pump))
{
viewModel.WritePumpSetpointCommand.Execute(pump);
}
if (Window.GetWindow(valueBox) is Window owner)
{
owner.DialogResult = true;
}
});
Grid.SetColumn(clearButton, 0);
Grid.SetColumn(cancelButton, 1);
Grid.SetColumn(okButton, 2);
actions.Children.Add(clearButton);
actions.Children.Add(cancelButton);
actions.Children.Add(okButton);
var panel = new StackPanel
{
Margin = new Thickness(18)
};
panel.Children.Add(new TextBlock
{
Text = pump.Name,
FontSize = 16,
FontWeight = FontWeights.SemiBold,
Foreground = BrushFromRgb(0x1F, 0x3B, 0x46),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 10)
});
panel.Children.Add(valueBox);
panel.Children.Add(errorText);
panel.Children.Add(keypad);
panel.Children.Add(actions);
var dialog = new Window
{
Owner = this,
Title = "输入目标流量",
Width = 450,
Height = 500,
MinWidth = 280,
MinHeight = 390,
ResizeMode = ResizeMode.NoResize,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = panel
};
dialog.Closed += (_, _) => _isPumpSetpointKeypadOpen = false;
dialog.ShowDialog();
}
private static Button CreateKeypadButton(string text, RoutedEventHandler handler)
{
var button = new Button
{
Content = text,
MinHeight = 46,
Margin = new Thickness(4),
FontSize = 16,
FontWeight = FontWeights.SemiBold,
Background = BrushFromRgb(0xE3, 0xF6, 0xEF),
Foreground = BrushFromRgb(0x1F, 0x3B, 0x46),
BorderBrush = BrushFromRgb(0x9C, 0xCB, 0xBF)
};
button.Click += handler;
return button;
}
private static void ApplyKeypadInput(TextBox valueBox, string key)
{
if (key == "退格")
{
valueBox.Text = valueBox.Text.Length > 0 ? valueBox.Text[..^1] : string.Empty;
return;
}
if (key == "." && valueBox.Text.Contains('.', StringComparison.Ordinal))
{
return;
}
if (valueBox.Text == "0" && key != ".")
{
valueBox.Text = key;
return;
}
valueBox.Text += key;
}
private static string NormalizePumpSetpointText(string value)
{
var normalizedValue = value.Trim();
if (normalizedValue.StartsWith(".", StringComparison.Ordinal))
{
normalizedValue = "0" + normalizedValue;
}
return normalizedValue;
}
private void OpenProjectInfoDialogButton_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainViewModel viewModel || viewModel.SelectedItem is null)
{
return;
}
var item = viewModel.SelectedItem;
var panel = new StackPanel
{
Margin = new Thickness(18)
};
AddDialogSection(panel, "类别", item.Category);
AddDialogSection(panel, "判定要求", item.AcceptanceCriteria);
AddDialogSection(panel, "检验方法", item.TestMethod);
AddDialogSection(panel, "记录要点", item.RecordFocus);
AddDialogSection(panel, "数据来源", viewModel.SelectedItemMeasurementSource);
AddDialogSection(panel, "填写说明", viewModel.RealtimeMeasurementHint);
AddDialogSection(panel, "实时数据预览", viewModel.SelectedItemLiveDisplay);
AddDialogSection(panel, "实时数据说明", viewModel.SelectedItemLiveHint);
var scrollViewer = new ScrollViewer
{
Content = panel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
};
var closeButton = new Button
{
Content = "关闭",
MinWidth = 88,
MinHeight = 34,
Padding = new Thickness(12, 5, 12, 5),
Margin = new Thickness(0, 10, 18, 14),
HorizontalAlignment = HorizontalAlignment.Right,
Background = BrushFromRgb(0xE3, 0xF6, 0xEF),
Foreground = BrushFromRgb(0x1F, 0x3B, 0x46),
BorderBrush = BrushFromRgb(0x9C, 0xCB, 0xBF)
};
var content = new Grid();
content.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
content.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
content.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var title = new TextBlock
{
Text = item.Item,
Margin = new Thickness(18, 16, 18, 6),
FontSize = 18,
FontWeight = FontWeights.Bold,
Foreground = BrushFromRgb(0x1F, 0x3B, 0x46),
TextWrapping = TextWrapping.Wrap
};
Grid.SetRow(title, 0);
Grid.SetRow(scrollViewer, 1);
Grid.SetRow(closeButton, 2);
content.Children.Add(title);
content.Children.Add(scrollViewer);
content.Children.Add(closeButton);
var dialog = new Window
{
Owner = this,
Title = "项目说明",
Width = Math.Min(760, Math.Max(420, ActualWidth - 120)),
Height = Math.Min(620, Math.Max(420, ActualHeight - 140)),
MinWidth = 360,
MinHeight = 320,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = content
};
closeButton.Click += (_, _) => dialog.Close();
dialog.ShowDialog();
}
private static void AddDialogSection(Panel panel, string title, string? value)
{
panel.Children.Add(new TextBlock
{
Text = title,
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Foreground = BrushFromRgb(0x54, 0x65, 0x70),
Margin = new Thickness(0, 0, 0, 4)
});
panel.Children.Add(new TextBlock
{
Text = string.IsNullOrWhiteSpace(value) ? "-" : value.Trim(),
FontSize = 14,
Foreground = BrushFromRgb(0x1F, 0x3B, 0x46),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 12)
});
}
private void OpenEngineeringRegistersButton_OnClick(object sender, RoutedEventArgs e)

View File

@@ -22,6 +22,7 @@ public partial class ValveControlChannel : ObservableObject
? $"停止{Name}"
: $"启动{Name}";
public string CirculatingWaterToggleText => IsOpen ? "停止循环水温" : "启动循环水温";
public string ReservoirToggleText => IsOpen ? "关闭贮液" : "开启贮液";
public string IndicatorColor => !StateAvailable ? "#FF94A6AE" : IsOpen ? "#FF32B06A" : "#FFC8D4DA";
public string StateHint => !StateAvailable ? "未取得 PLC 状态" : $"{Name}已{StateText}";
public bool HideRealtimeCardStateDescription => Key is "TestLoopValve1" or "TestLoopValve2" or "CirculatingWaterTemperature";
@@ -32,6 +33,7 @@ public partial class ValveControlChannel : ObservableObject
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CirculatingWaterToggleText));
OnPropertyChanged(nameof(ReservoirToggleText));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(StateHint));
}
@@ -41,6 +43,7 @@ public partial class ValveControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CirculatingWaterToggleText));
OnPropertyChanged(nameof(ReservoirToggleText));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(StateHint));
}

View File

@@ -70,7 +70,7 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private readonly List<PumpControlChannel> _pumpControls =
[
new() { Key = "NegativeAssistPump", Name = "负压泵", StartAddress = 0 },
new() { Key = "PressureDropPump", Name = "压力降/抗塌陷泵", StartAddress = 1, FlowAddress = FlowRegisters["PressureDropPump"] },
new() { Key = "PressureDropPump", Name = "压力降泵", StartAddress = 1, FlowAddress = FlowRegisters["PressureDropPump"] },
new() { Key = "RecirculationMainPump", Name = "再循环主泵", StartAddress = 2, FlowAddress = FlowRegisters["RecirculationMainPump"] },
new() { Key = "RecirculationReturnPump", Name = "回流泵", StartAddress = 3, FlowAddress = FlowRegisters["RecirculationReturnPump"] },
new() { Key = "RecirculationDrainagePump", Name = "引流泵", StartAddress = 4, FlowAddress = FlowRegisters["RecirculationDrainagePump"] },