This commit is contained in:
xyy
2026-04-12 16:46:55 +08:00
parent ea8e76bff4
commit e774c2374c
11 changed files with 303 additions and 47 deletions

View File

@@ -1,7 +1,6 @@
<Application x:Class="MembranePoreTester.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Views/MainWindow.xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>

View File

@@ -1,4 +1,6 @@
using MembranePoreTester.Communication;
using MembranePoreTester.Models;
using MembranePoreTester.Views;
using Microsoft.Extensions.Configuration;
using OfficeOpenXml;
using System;
@@ -9,18 +11,31 @@ namespace MembranePoreTester
{
public partial class App : Application
{
public static UserRole CurrentUserRole { get; set; } = UserRole.Operator;
public static IPlcService PlcService { get; private set; }
public static PlcConfiguration PlcConfig { get; private set; }
protected async override void OnStartup(StartupEventArgs e)
protected override async void OnStartup(StartupEventArgs e)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
base.OnStartup(e);
// 防止在登录窗口关闭时应用程序因没有窗口而自动退出
ShutdownMode = ShutdownMode.OnExplicitShutdown;
// 显示登录窗口,验证用户身份
var loginWindow = new LoginWindow();
if (loginWindow.ShowDialog() != true)
{
Shutdown();
return;
}
// 登录成功后初始化数据库和PLC连接
using var db = new AppDbContext();
db.Database.EnsureCreated(); // 自动建表
db.Database.EnsureCreated();
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@@ -43,6 +58,12 @@ namespace MembranePoreTester
{
MessageBox.Show($"PLC 连接失败:{ex.Message}");
}
// 启动主窗口,设置为应用程序的主窗口并恢复默认的退出模式
var mainWindow = new MainWindow();
MainWindow = mainWindow;
ShutdownMode = ShutdownMode.OnMainWindowClose;
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)

8
Models/UserRole.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace MembranePoreTester.Models
{
public enum UserRole
{
Operator, // 操作员
Admin // 管理员
}
}

View File

@@ -114,6 +114,8 @@ namespace MembranePoreTester.ViewModels
// 2. 读取当前工位的加压上限(实时)
ushort upperLimitAddress = StationId == 1 ? _plcConfig.PressureUpperLimit
: StationId == 2 ? _plcConfig.PressureUpperLimit2
@@ -121,12 +123,16 @@ namespace MembranePoreTester.ViewModels
double pressureUpperLimit = await _plcService.ReadFloatAsync(upperLimitAddress);
// 3. 如果压力已达到或超过上限,停止采集
if (pressure >= pressureUpperLimit - 3)
if (pressure >= pressureUpperLimit * 1000 - 10 * 1000 && pressure > 0)
{
StopCollecting();
return; // 不再添加当前数据点
}
// 2. 读取当前模式对应的流量
double flow = 0;
if (TestMode.Contains("湿膜"))
@@ -578,19 +584,19 @@ namespace MembranePoreTester.ViewModels
System.Diagnostics.Debug.WriteLine($"P={p.Pressure}, Wet={p.WetFlow}, Dry={p.DryFlow}");
}
var cleanedPoints = CleanDataPoints(originalPoints);
//var cleanedPoints = CleanDataPoints(originalPoints);
if (cleanedPoints.Count < 2)
{
MessageBox.Show("有效数据点不足,至少需要 2 个数据点进行计算。");
return;
}
//if (cleanedPoints.Count < 2)
//{
// MessageBox.Show("有效数据点不足,至少需要 2 个数据点进行计算。");
// return;
//}
int invalidCount = originalPoints.Count - cleanedPoints.Count;
//int invalidCount = originalPoints.Count - cleanedPoints.Count;
// 用清洗后的点替换 ObservableCollection 的内容触发UI更新
Record.DataPoints.Clear();
foreach (var p in cleanedPoints)
foreach (var p in originalPoints)
Record.DataPoints.Add(p);
// 刷新曲线以显示清洗后的数据
@@ -603,11 +609,11 @@ namespace MembranePoreTester.ViewModels
RangePercentage = PoreDistributionAnalysis.CalculatePoreRangePercentage(
Record.DataPoints, Record.PressureUnit, Record.Liquid, LowerPore, UpperPore);
if (invalidCount > 0)
{
MessageBox.Show($"已自动过滤 {invalidCount} 个无效数据点");
System.Diagnostics.Debug.WriteLine($"已自动过滤 {invalidCount} 个无效数据点");
}
//if (invalidCount > 0)
//{
// MessageBox.Show($"已自动过滤 {invalidCount} 个无效数据点");
// System.Diagnostics.Debug.WriteLine($"已自动过滤 {invalidCount} 个无效数据点");
//}
}
//private void Calculate()
@@ -1146,6 +1152,11 @@ namespace MembranePoreTester.ViewModels
//}
public void RefreshPlot()
{
UpdatePlot();
}
private void UpdatePlot()
{
// 确保在 UI 线程执行
@@ -1156,11 +1167,11 @@ namespace MembranePoreTester.ViewModels
}
var sorted = Record.DataPoints.OrderBy(p => p.Pressure).ToList();
if (sorted.Count == 0)
{
PlotModel = null;
return;
}
//if (sorted.Count == 0)
//{
// PlotModel = null;
// return;
//}
var model = new PlotModel
{

33
Views/LoginWindow.xaml Normal file
View File

@@ -0,0 +1,33 @@
<Window x:Class="MembranePoreTester.Views.LoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="登录" Height="250" Width="350"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="膜孔径测试系统" FontSize="18" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,0,0,20"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5">
<Label Width="80" Content="用户名:"/>
<!--<TextBox x:Name="txtUsername" Width="180" Text="admin"/>-->
<ComboBox x:Name="cmbRole" Width="180" SelectedIndex="0">
<ComboBoxItem Content="管理员" Tag="Admin"/>
<ComboBoxItem Content="操作员" Tag="Operator"/>
</ComboBox>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5">
<Label Width="80" Content="密码:"/>
<PasswordBox x:Name="txtPassword" Width="180" PasswordChar="*"/>
</StackPanel>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
<Button x:Name="btnLogin" Content="登录" Width="80" Margin="10" Click="BtnLogin_Click"/>
<Button x:Name="btnChangePwd" Content="修改密码" Width="80" Margin="10" Click="BtnChangePwd_Click"/>
<Button x:Name="btnExit" Content="退出" Width="80" Margin="10" Click="BtnExit_Click"/>
</StackPanel>
</Grid>
</Window>

168
Views/LoginWindow.xaml.cs Normal file
View File

@@ -0,0 +1,168 @@
using System;
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using MembranePoreTester.Models;
namespace MembranePoreTester.Views
{
public partial class LoginWindow : Window
{
private const string PasswordFile = "passwords.json";
public LoginWindow()
{
InitializeComponent();
EnsurePasswordFile();
}
private void EnsurePasswordFile()
{
if (!File.Exists(PasswordFile))
{
var defaultPasswords = new
{
AdminPassword = "admin123",
OperatorPassword = "oper123"
};
string json = JsonSerializer.Serialize(defaultPasswords, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(PasswordFile, json);
}
}
private void BtnLogin_Click(object sender, RoutedEventArgs e)
{
string role = ((ComboBoxItem)cmbRole.SelectedItem).Tag.ToString();
string password = txtPassword.Password;
string expectedPassword = GetPassword(role);
if (password == expectedPassword)
{
App.CurrentUserRole = role == "Admin" ? UserRole.Admin : UserRole.Operator;
DialogResult = true;
Close();
}
else
{
MessageBox.Show("密码错误", "登录失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void BtnChangePwd_Click(object sender, RoutedEventArgs e)
{
string role = ((ComboBoxItem)cmbRole.SelectedItem).Tag.ToString();
string title = role == "Admin" ? "修改管理员密码" : "修改操作员密码";
var dialog = new Window
{
Title = title,
Width = 350,
Height = 250,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this,
ResizeMode = ResizeMode.NoResize,
Content = new StackPanel
{
Margin = new Thickness(20),
Children =
{
new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0,10,0,10),
Children = { new Label { Width = 80, Content = "新密码:" }, new PasswordBox { Name = "newPwd", Width = 180 } } },
new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0,10,0,10),
Children = { new Label { Width = 80, Content = "确认密码:" }, new PasswordBox { Name = "confirmPwd", Width = 180 } } },
new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0,20,0,0),
Children = { new Button { Content = "确定", Width = 80, Margin = new Thickness {Left= 5} }, new Button { Content = "取消", Width = 80, Margin = new Thickness {Left= 5} } } }
}
}
};
var confirmBtn = ((StackPanel)((StackPanel)dialog.Content).Children[2]).Children[0] as Button;
var cancelBtn = ((StackPanel)((StackPanel)dialog.Content).Children[2]).Children[1] as Button;
var newPwdBox = ((StackPanel)((StackPanel)dialog.Content).Children[0]).Children[1] as PasswordBox;
var confirmPwdBox = ((StackPanel)((StackPanel)dialog.Content).Children[1]).Children[1] as PasswordBox;
confirmBtn.Click += (s, ev) =>
{
string newPwd = newPwdBox.Password;
string confirmPwd = confirmPwdBox.Password;
if (string.IsNullOrEmpty(newPwd))
{
MessageBox.Show("密码不能为空", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (newPwd != confirmPwd)
{
MessageBox.Show("两次输入的密码不一致", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (UpdatePassword(role, newPwd))
{
MessageBox.Show("密码修改成功,下次登录生效", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
dialog.Close();
}
else
{
MessageBox.Show("密码修改失败,请检查文件权限", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
};
cancelBtn.Click += (s, ev) => dialog.Close();
dialog.ShowDialog();
}
private void BtnExit_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private string GetPassword(string role)
{
if (!File.Exists(PasswordFile)) return role == "Admin" ? "admin123" : "oper123";
try
{
string json = File.ReadAllText(PasswordFile);
using JsonDocument doc = JsonDocument.Parse(json);
string key = role == "Admin" ? "AdminPassword" : "OperatorPassword";
if (doc.RootElement.TryGetProperty(key, out JsonElement value))
return value.GetString();
return role == "Admin" ? "admin123" : "oper123";
}
catch
{
return role == "Admin" ? "admin123" : "oper123";
}
}
private bool UpdatePassword(string role, string newPassword)
{
try
{
Dictionary<string, string> passwords;
if (File.Exists(PasswordFile))
{
string json = File.ReadAllText(PasswordFile);
passwords = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
}
else
{
passwords = new Dictionary<string, string>();
}
string key = role == "Admin" ? "AdminPassword" : "OperatorPassword";
passwords[key] = newPassword;
string newJson = JsonSerializer.Serialize(passwords, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(PasswordFile, newJson);
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"修改密码失败: {ex.Message}");
return false;
}
}
}
}

View File

@@ -116,7 +116,7 @@
<Border DockPanel.Dock="Top" Background="White" BorderBrush="#E9ECF0" BorderThickness="0,0,0,1" Padding="10,5">
<StackPanel Orientation="Horizontal">
<Button Content="📋 历史记录" Click="OpenHistory_Click" Width="100" HorizontalAlignment="Left" Background="#607D8B"/>
<Button Content="阀门控制" Click="OpenValveControl_Click" Padding="10,5" Margin="5"/>
<Button x:Name="btnValveControl" Content="阀门控制" Click="OpenValveControl_Click" Padding="10,5" Margin="5"/>
</StackPanel>
</Border>

View File

@@ -12,6 +12,19 @@ namespace MembranePoreTester.Views
{
InitializeComponent();
HistoryWindow.LoadRecordEvent += OnLoadRecord;
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// 根据角色设置权限
bool isAdmin = App.CurrentUserRole == Models.UserRole.Admin;
// 禁用/启用阀门控制按钮(假设 x:Name="btnValveControl"
if (btnValveControl != null) btnValveControl.IsEnabled = isAdmin;
//// 如果还有参数设置按钮(例如工具栏上的按钮)
//if (btnParameterSetting != null) btnParameterSetting.IsEnabled = isAdmin;
//// 其他需要限制的全局控件可在此添加
}
private void OnLoadRecord(object sender, LoadRecordEventArgs e)
@@ -32,32 +45,34 @@ namespace MembranePoreTester.Views
}
}
private void OpenHistory_Click(object sender, RoutedEventArgs e)
{
var mainVM = DataContext as MainViewModel;
if (mainVM == null) return;
// 获取当前选中的工位索引(假设选项卡控件是 TabControl名称为 stationTabControl
// 需要在 XAML 中为 TabControl 设置 x:Name="stationTabControl"
int currentStation = stationTabControl.SelectedIndex + 1; // 假设索引从0开始
int currentStation = stationTabControl.SelectedIndex + 1;
var historyWin = new HistoryWindow { SelectedStation = currentStation };
historyWin.ShowDialog();
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
// 仅管理员可使用 Ctrl+P 打开参数设置窗口
if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.P)
{
var win = new ParameterWindow();
win.Owner = this;
win.ShowDialog();
if (App.CurrentUserRole == Models.UserRole.Admin)
{
var win = new ParameterWindow();
win.Owner = this;
win.ShowDialog();
}
else
{
MessageBox.Show("权限不足,无法打开参数设置", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedStation = stationTabControl.SelectedItem as MainViewModel.StationItem;
@@ -72,26 +87,15 @@ namespace MembranePoreTester.Views
var selectedTabItem = innerTabControl.SelectedItem as TabItem;
bool isPoreDistributionActive = selectedTabItem?.Header?.ToString() == "孔分布测试";
// 关键:设置 StationItem 的状态
selectedStation.IsPoreDistributionActive = isPoreDistributionActive;
selectedStation.PoreDistributionVM.IsActive = isPoreDistributionActive;
if (isPoreDistributionActive)
{
// 切换到孔分布时,清空之前的数据
//selectedStation.PoreDistributionVM.ClearData();
}
else
{
// 离开孔分布界面时,停止自动采集
//selectedStation.PoreDistributionVM.StopCollecting();
}
// 清空数据和停止采集的代码已注释,保持原样
}
}
}
}
// 辅助方法:查找 Visual Tree 中的指定类型元素
private T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
@@ -106,6 +110,11 @@ namespace MembranePoreTester.Views
private void OpenValveControl_Click(object sender, RoutedEventArgs e)
{
if (App.CurrentUserRole != Models.UserRole.Admin)
{
MessageBox.Show("权限不足,无法打开阀门控制", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var win = new ValveControlWindow();
win.Owner = this;
win.Show();

View File

@@ -19,6 +19,10 @@ namespace MembranePoreTester.Views
{
System.Diagnostics.Debug.WriteLine($"工位{vm.StationId} IsVisible={this.IsVisible}");
vm.IsActive = this.IsVisible;
if (this.IsVisible)
{
vm.RefreshPlot(); // 新增这一行
}
}
};

View File

@@ -1,4 +1,6 @@
{
"AdminPassword": "admin123",
"OperatorPassword": "oper123",
"PlcSettings": {
// Modbus TCP 连接参数
"IpAddress": "192.168.1.10", // PLC 的 IP 地址

View File

@@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
<PackageReference Include="Microsoft.VisualBasic" Version="10.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NModbus4.NetCore" Version="4.0.0" />
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
<PackageReference Include="SunnyUI" Version="3.9.3" />