From 27aa99057fc3f37861f7bd0a0538d78092d1e4cf Mon Sep 17 00:00:00 2001 From: xyy <544939200@qq.com> Date: Wed, 6 May 2026 16:41:32 +0800 Subject: [PATCH] --- App.xaml.cs | 92 +++++++- Helpers/ValueConverters.cs | 27 ++- Models/TestBatch.cs | 36 ++- Services/ExcelExportService.cs | 285 +++++++++++++++++++++-- ViewModels/MainViewModel.cs | 15 ++ ViewModels/StationViewModel.cs | 359 ++++++++++++++++++++++++---- Views/HistoryWindow.xaml | 381 ++++++++++++++++++++++++++---- Views/HistoryWindow.xaml.cs | 77 ++++-- Views/MainWindow.xaml | 411 +++++++++++++++++++++------------ Views/MainWindow.xaml.cs | 5 - Views/SettingsWindow.xaml | 5 +- 11 files changed, 1393 insertions(+), 300 deletions(-) diff --git a/App.xaml.cs b/App.xaml.cs index 31357e7..aa6ffc1 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -28,15 +28,63 @@ namespace TabletTester2025 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); var configuration = builder.Build(); - // 数据库初始化并确保表结构最新 + // 数据库初始化(手动建表) var connectionString = configuration.GetConnectionString("DefaultConnection") ?? "Data Source=TabletTests.db"; - using (var db = new AppDbContext(connectionString)) + using (var connection = new Microsoft.Data.Sqlite.SqliteConnection(connectionString)) { - db.Database.EnsureCreated(); - // 自动添加缺失的列 - EnsureColumnsExist(connectionString); - } + connection.Open(); + var cmd = connection.CreateCommand(); + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS TestBatches ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + TestTime TEXT NOT NULL, + StationId INTEGER NOT NULL, + SampleName TEXT, + -- 硬度 + HardnessAvg REAL, + HardnessRSD REAL, + HardnessMax REAL, + HardnessMin REAL, + HardnessTestCount INTEGER, + + -- 脆碎度 + FriabilityLoss REAL, + FriabilityTargetRpm REAL, + FriabilityTargetTimeSec INTEGER, + FriabilityClockwise INTEGER, -- bool 存储为 0/1 + FriabilityRemainingRounds INTEGER, + WeightBefore REAL, + WeightAfter REAL, + + -- 崩解 + DisintegrationTimeSec REAL, + RemainingTubesAtEnd INTEGER, + DisintegrationTargetFreq REAL, + DisintegrationTemp REAL, + + -- 溶出 + DissolutionRate30Min REAL, + DissolutionTargetRpm REAL, + DissolutionRSquared REAL, + DissolutionSampleInterval INTEGER, + DissolutionUpDownFreq REAL, + + -- 综合 + IsQualified INTEGER, + + -- 各项目单独合格标志 + HardnessPass INTEGER, + FriabilityPass INTEGER, + DisintegrationPass INTEGER, + DissolutionPass INTEGER, + + -- 测试类型(用于区分报表) + TestType TEXT +); + "; + cmd.ExecuteNonQuery(); + } // 绑定药典参数 configuration.GetSection("PharmaStandard").Bind(CurrentPharmaParams); @@ -76,13 +124,33 @@ namespace TabletTester2025 private void EnsureColumnsExist(string connectionString) { - // 定义需要确保存在的列 (表名, 列名, 列类型, 默认值) var requiredColumns = new[] - { - ("TestBatches", "HardnessRSD", "REAL", "0"), - ("TestBatches", "RemainingTubesAtEnd", "INTEGER", "0"), - // 如果将来还有新列,继续添加 - }; + { + ("TestBatches", "HardnessMax", "REAL", "0"), + ("TestBatches", "HardnessMin", "REAL", "0"), + ("TestBatches", "HardnessTestCount", "INTEGER", "6"), + ("TestBatches", "FriabilityTargetRpm", "REAL", "25"), + ("TestBatches", "FriabilityTargetTimeSec", "INTEGER", "240"), + ("TestBatches", "FriabilityClockwise", "INTEGER", "1"), + ("TestBatches", "FriabilityRemainingRounds", "INTEGER", "100"), + ("TestBatches", "WeightBefore", "REAL", "0"), + ("TestBatches", "WeightAfter", "REAL", "0"), + ("TestBatches", "DisintegrationTargetFreq", "REAL", "31"), + ("TestBatches", "DisintegrationTemp", "REAL", "37"), + ("TestBatches", "DissolutionTargetRpm", "REAL", "50"), + ("TestBatches", "DissolutionRSquared", "REAL", "0"), + ("TestBatches", "DissolutionSampleInterval", "INTEGER", "5"), + ("TestBatches", "DissolutionUpDownFreq", "REAL", "32"), + ("TestBatches", "HardnessRSD", "REAL", "0"), // 已存在但确保 + ("TestBatches", "RemainingTubesAtEnd", "INTEGER", "0"), + // 新增合格列 + ("TestBatches", "HardnessPass", "INTEGER", "0"), + ("TestBatches", "FriabilityPass", "INTEGER", "0"), + ("TestBatches", "DisintegrationPass", "INTEGER", "0"), + ("TestBatches", "DissolutionPass", "INTEGER", "0"), + + ("TestBatches", "TestType", "TEXT", "") + }; using var connection = new SqliteConnection(connectionString); connection.Open(); diff --git a/Helpers/ValueConverters.cs b/Helpers/ValueConverters.cs index 48a445c..0bd7523 100644 --- a/Helpers/ValueConverters.cs +++ b/Helpers/ValueConverters.cs @@ -32,7 +32,7 @@ namespace TabletTester2025.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { bool flag = (bool)value; - return flag ? "Green" : "LightGray"; + return flag ? "Green" : "Red"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); @@ -48,4 +48,29 @@ namespace TabletTester2025.Helpers public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } + + public class BoolToTextConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool isClockwise = (bool)value; + return isClockwise ? "顺时针" : "逆时针"; + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + // 用于将 bool 方向转换为 "顺时针"/"逆时针" + public class BoolToDirectionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool isClockwise = (bool)value; + return isClockwise ? "顺时针" : "逆时针"; + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + } \ No newline at end of file diff --git a/Models/TestBatch.cs b/Models/TestBatch.cs index d95bab3..bd6c6fc 100644 --- a/Models/TestBatch.cs +++ b/Models/TestBatch.cs @@ -8,12 +8,46 @@ namespace TabletTester2025.Models public DateTime TestTime { get; set; } public int StationId { get; set; } public string SampleName { get; set; } + + // 硬度 public double HardnessAvg { get; set; } public double HardnessRSD { get; set; } + public double HardnessMax { get; set; } + public double HardnessMin { get; set; } + public int HardnessTestCount { get; set; } + + // 脆碎度 public double FriabilityLoss { get; set; } + public double FriabilityTargetRpm { get; set; } + public int FriabilityTargetTimeSec { get; set; } + public bool FriabilityClockwise { get; set; } + public int FriabilityRemainingRounds { get; set; } + public double WeightBefore { get; set; } + public double WeightAfter { get; set; } + + // 崩解 public double DisintegrationTimeSec { get; set; } - public int RemainingTubesAtEnd { get; set; } // 未崩解管数 + public int RemainingTubesAtEnd { get; set; } + public double DisintegrationTargetFreq { get; set; } + public double DisintegrationTemp { get; set; } + + // 溶出 public double DissolutionRate30Min { get; set; } + public double DissolutionTargetRpm { get; set; } + public double DissolutionRSquared { get; set; } + public int DissolutionSampleInterval { get; set; } + public double DissolutionUpDownFreq { get; set; } + + // 综合 public bool IsQualified { get; set; } + + + + public bool HardnessPass { get; set; } + public bool FriabilityPass { get; set; } + public bool DisintegrationPass { get; set; } + public bool DissolutionPass { get; set; } + + public string TestType { get; set; } // "硬度", "脆碎度", "崩解", "溶出" } } \ No newline at end of file diff --git a/Services/ExcelExportService.cs b/Services/ExcelExportService.cs index 14eb1a4..8401246 100644 --- a/Services/ExcelExportService.cs +++ b/Services/ExcelExportService.cs @@ -7,20 +7,177 @@ namespace TabletTester2025.Services { public class ExcelExportService { + public void ExportToExcel(IEnumerable batches, string filePath) { using var package = new ExcelPackage(new FileInfo(filePath)); - var sheet = package.Workbook.Worksheets.Add("检测记录"); - sheet.Cells[1, 1].Value = "时间"; + + // 按测试类型分组 + var hardnessData = batches.Where(b => b.TestType == "硬度").ToList(); + var friabilityData = batches.Where(b => b.TestType == "脆碎度").ToList(); + var disintegrationData = batches.Where(b => b.TestType == "崩解").ToList(); + var dissolutionData = batches.Where(b => b.TestType == "溶出").ToList(); + + // 硬度表 + if (hardnessData.Any()) + { + var sheet = package.Workbook.Worksheets.Add("硬度报表"); + // 表头 + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "平均值(N)"; + sheet.Cells[1, 5].Value = "RSD(%)"; + sheet.Cells[1, 6].Value = "最大值(N)"; + sheet.Cells[1, 7].Value = "最小值(N)"; + sheet.Cells[1, 8].Value = "测试次数"; + sheet.Cells[1, 9].Value = "合格"; + int row = 2; + foreach (var b in hardnessData) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.HardnessAvg; + sheet.Cells[row, 5].Value = b.HardnessRSD; + sheet.Cells[row, 6].Value = b.HardnessMax; + sheet.Cells[row, 7].Value = b.HardnessMin; + sheet.Cells[row, 8].Value = b.HardnessTestCount; + sheet.Cells[row, 9].Value = b.HardnessPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + } + else + { + // 如果没有数据也创建空表(可选),根据需求决定 + var sheet = package.Workbook.Worksheets.Add("硬度报表"); + sheet.Cells[1, 1].Value = "无硬度测试数据"; + } + + // 脆碎度表 + if (friabilityData.Any()) + { + var sheet = package.Workbook.Worksheets.Add("脆碎度报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "失重率(%)"; + sheet.Cells[1, 5].Value = "设定转速(r/min)"; + sheet.Cells[1, 6].Value = "方向"; + sheet.Cells[1, 7].Value = "总圈数"; + sheet.Cells[1, 8].Value = "前重(g)"; + sheet.Cells[1, 9].Value = "后重(g)"; + sheet.Cells[1, 10].Value = "合格"; + int row = 2; + foreach (var b in friabilityData) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.FriabilityLoss; + sheet.Cells[row, 5].Value = b.FriabilityTargetRpm; + sheet.Cells[row, 6].Value = b.FriabilityClockwise ? "顺时针" : "逆时针"; + sheet.Cells[row, 7].Value = b.FriabilityRemainingRounds; + sheet.Cells[row, 8].Value = b.WeightBefore; + sheet.Cells[row, 9].Value = b.WeightAfter; + sheet.Cells[row, 10].Value = b.FriabilityPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + } + else + { + var sheet = package.Workbook.Worksheets.Add("脆碎度报表"); + sheet.Cells[1, 1].Value = "无脆碎度测试数据"; + } + + // 崩解表 + if (disintegrationData.Any()) + { + var sheet = package.Workbook.Worksheets.Add("崩解报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "崩解时间(秒)"; + sheet.Cells[1, 5].Value = "剩余未崩解管"; + sheet.Cells[1, 6].Value = "设定升降频率"; + sheet.Cells[1, 7].Value = "水浴温度(℃)"; + sheet.Cells[1, 8].Value = "合格"; + int row = 2; + foreach (var b in disintegrationData) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.DisintegrationTimeSec; + sheet.Cells[row, 5].Value = b.RemainingTubesAtEnd; + sheet.Cells[row, 6].Value = b.DisintegrationTargetFreq; + sheet.Cells[row, 7].Value = b.DisintegrationTemp; + sheet.Cells[row, 8].Value = b.DisintegrationPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + } + else + { + var sheet = package.Workbook.Worksheets.Add("崩解报表"); + sheet.Cells[1, 1].Value = "无崩解测试数据"; + } + + // 溶出表 + if (dissolutionData.Any()) + { + var sheet = package.Workbook.Worksheets.Add("溶出报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "30min溶出度(%)"; + sheet.Cells[1, 5].Value = "设定转速(r/min)"; + sheet.Cells[1, 6].Value = "R²"; + sheet.Cells[1, 7].Value = "取样间隔(min)"; + sheet.Cells[1, 8].Value = "升降频率"; + sheet.Cells[1, 9].Value = "合格"; + int row = 2; + foreach (var b in dissolutionData) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.DissolutionRate30Min; + sheet.Cells[row, 5].Value = b.DissolutionTargetRpm; + sheet.Cells[row, 6].Value = b.DissolutionRSquared; + sheet.Cells[row, 7].Value = b.DissolutionSampleInterval; + sheet.Cells[row, 8].Value = b.DissolutionUpDownFreq; + sheet.Cells[row, 9].Value = b.DissolutionPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + } + else + { + var sheet = package.Workbook.Worksheets.Add("溶出报表"); + sheet.Cells[1, 1].Value = "无溶出测试数据"; + } + + package.Save(); + } + + public void ExportHardnessToExcel(IEnumerable batches, string filePath) + { + using var package = new ExcelPackage(new FileInfo(filePath)); + var sheet = package.Workbook.Worksheets.Add("硬度报表"); + // 只导出硬度相关列 + sheet.Cells[1, 1].Value = "检测时间"; sheet.Cells[1, 2].Value = "工位"; - sheet.Cells[1, 3].Value = "样品"; - sheet.Cells[1, 4].Value = "硬度平均值(N)"; - sheet.Cells[1, 5].Value = "硬度RSD(%)"; - sheet.Cells[1, 6].Value = "脆碎度失重(%)"; - sheet.Cells[1, 7].Value = "崩解时间(秒)"; - sheet.Cells[1, 8].Value = "剩余未崩解管数"; - sheet.Cells[1, 9].Value = "溶出度(30min %)"; - sheet.Cells[1, 10].Value = "合格"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "平均值(N)"; + sheet.Cells[1, 5].Value = "RSD(%)"; + sheet.Cells[1, 6].Value = "最大值(N)"; + sheet.Cells[1, 7].Value = "最小值(N)"; + sheet.Cells[1, 8].Value = "测试次数"; + sheet.Cells[1, 9].Value = "合格"; + int row = 2; foreach (var b in batches) { @@ -29,15 +186,113 @@ namespace TabletTester2025.Services sheet.Cells[row, 3].Value = b.SampleName; sheet.Cells[row, 4].Value = b.HardnessAvg; sheet.Cells[row, 5].Value = b.HardnessRSD; - sheet.Cells[row, 6].Value = b.FriabilityLoss; - sheet.Cells[row, 7].Value = b.DisintegrationTimeSec; - sheet.Cells[row, 8].Value = b.RemainingTubesAtEnd; - sheet.Cells[row, 9].Value = b.DissolutionRate30Min; - sheet.Cells[row, 10].Value = b.IsQualified ? "合格" : "不合格"; + sheet.Cells[row, 6].Value = b.HardnessMax; + sheet.Cells[row, 7].Value = b.HardnessMin; + sheet.Cells[row, 8].Value = b.HardnessTestCount; + sheet.Cells[row, 9].Value = b.HardnessPass ? "合格" : "不合格"; row++; } sheet.Cells.AutoFitColumns(); package.Save(); } + + public void ExportFriabilityToExcel(IEnumerable batches, string filePath) + { + using var package = new ExcelPackage(new FileInfo(filePath)); + var sheet = package.Workbook.Worksheets.Add("脆碎度报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "失重率(%)"; + sheet.Cells[1, 5].Value = "设定转速(r/min)"; + sheet.Cells[1, 6].Value = "方向"; + sheet.Cells[1, 7].Value = "总圈数"; + sheet.Cells[1, 8].Value = "前重(g)"; + sheet.Cells[1, 9].Value = "后重(g)"; + sheet.Cells[1, 10].Value = "合格"; + + int row = 2; + foreach (var b in batches) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.FriabilityLoss; + sheet.Cells[row, 5].Value = b.FriabilityTargetRpm; + sheet.Cells[row, 6].Value = b.FriabilityClockwise ? "顺时针" : "逆时针"; + sheet.Cells[row, 7].Value = b.FriabilityRemainingRounds; // 总圈数 + sheet.Cells[row, 8].Value = b.WeightBefore; + sheet.Cells[row, 9].Value = b.WeightAfter; + sheet.Cells[row, 10].Value = b.FriabilityPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + package.Save(); + } + + public void ExportDisintegrationToExcel(IEnumerable batches, string filePath) + { + using var package = new ExcelPackage(new FileInfo(filePath)); + var sheet = package.Workbook.Worksheets.Add("崩解报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "崩解时间(秒)"; + sheet.Cells[1, 5].Value = "剩余未崩解管"; + sheet.Cells[1, 6].Value = "设定升降频率"; + sheet.Cells[1, 7].Value = "水浴温度(℃)"; + sheet.Cells[1, 8].Value = "合格"; + + int row = 2; + foreach (var b in batches) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.DisintegrationTimeSec; + sheet.Cells[row, 5].Value = b.RemainingTubesAtEnd; + sheet.Cells[row, 6].Value = b.DisintegrationTargetFreq; + sheet.Cells[row, 7].Value = b.DisintegrationTemp; + sheet.Cells[row, 8].Value = b.DisintegrationPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + package.Save(); + } + + public void ExportDissolutionToExcel(IEnumerable batches, string filePath) + { + using var package = new ExcelPackage(new FileInfo(filePath)); + var sheet = package.Workbook.Worksheets.Add("溶出报表"); + sheet.Cells[1, 1].Value = "检测时间"; + sheet.Cells[1, 2].Value = "工位"; + sheet.Cells[1, 3].Value = "样品名称"; + sheet.Cells[1, 4].Value = "30min溶出度(%)"; + sheet.Cells[1, 5].Value = "设定转速(r/min)"; + sheet.Cells[1, 6].Value = "R²"; + sheet.Cells[1, 7].Value = "取样间隔(min)"; + sheet.Cells[1, 8].Value = "升降频率"; + sheet.Cells[1, 9].Value = "合格"; + + int row = 2; + foreach (var b in batches) + { + sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss"); + sheet.Cells[row, 2].Value = b.StationId; + sheet.Cells[row, 3].Value = b.SampleName; + sheet.Cells[row, 4].Value = b.DissolutionRate30Min; + sheet.Cells[row, 5].Value = b.DissolutionTargetRpm; + sheet.Cells[row, 6].Value = b.DissolutionRSquared; + sheet.Cells[row, 7].Value = b.DissolutionSampleInterval; + sheet.Cells[row, 8].Value = b.DissolutionUpDownFreq; + sheet.Cells[row, 9].Value = b.DissolutionPass ? "合格" : "不合格"; + row++; + } + sheet.Cells.AutoFitColumns(); + package.Save(); + } + + + } } \ No newline at end of file diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 52359e2..afe37bf 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input; using System; using System.Collections.ObjectModel; using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; using TabletTester2025.Models; using TabletTester2025.Services; @@ -18,6 +19,8 @@ namespace TabletTester2025.ViewModels private readonly PlcConfiguration _plcConfig; private DispatcherTimer _timer; + + [ObservableProperty] private string _plcStatus = "断开"; [ObservableProperty] private string _currentTime; [ObservableProperty] private string _globalAlarm; @@ -26,6 +29,10 @@ namespace TabletTester2025.ViewModels public IAsyncRelayCommand ExportAllCommand { get; } + public IAsyncRelayCommand OpenSettingsCommand { get; } + public IAsyncRelayCommand OpenHistoryCommand { get; } + public IAsyncRelayCommand OpenCalibrationCommand { get; } + public MainViewModel(IPlcService plc, DatabaseService db, ExcelExportService excel, AlarmService alarm, PlcConfiguration plcConfig) { _plc = plc; @@ -46,6 +53,14 @@ namespace TabletTester2025.ViewModels _timer.Tick += OnTimerTick; _ = ConnectToPlc(); _timer.Start(); + + + + + // 在构造函数中 + OpenSettingsCommand = new AsyncRelayCommand(() => { new SettingsWindow().ShowDialog(); return Task.CompletedTask; }); + OpenHistoryCommand = new AsyncRelayCommand(() => { new HistoryWindow().ShowDialog(); return Task.CompletedTask; }); + OpenCalibrationCommand = new AsyncRelayCommand(() => { MessageBox.Show("校准功能待实现"); return Task.CompletedTask; }); } private async Task ConnectToPlc() diff --git a/ViewModels/StationViewModel.cs b/ViewModels/StationViewModel.cs index ae444e9..db70d0c 100644 --- a/ViewModels/StationViewModel.cs +++ b/ViewModels/StationViewModel.cs @@ -1,12 +1,13 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using OxyPlot; +using OxyPlot.Series; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; -using OxyPlot; -using OxyPlot.Series; using TabletTester2025.Models; using TabletTester2025.Services; @@ -19,8 +20,12 @@ namespace TabletTester2025.ViewModels private readonly DatabaseService _db; private readonly ExcelExportService _excel; private readonly AlarmService _alarm; + private readonly BalanceService _balance; // ✅ 新增天平服务 private DispatcherTimer _disintegrationTimer; + private List _dissolutionTimes = new List(); + private List _dissolutionValues = new List(); + public int StationId { get; } [ObservableProperty] private TestType _currentTest; @@ -49,6 +54,53 @@ namespace TabletTester2025.ViewModels // 溶出 [ObservableProperty] private double _dissolutionRpm; [ObservableProperty] private double _dissolutionPercent; + + // 硬度相关新增 + [ObservableProperty] private int _hardnessTestCount = 6; + [ObservableProperty] private int _hardnessIntervalSec = 2; + [ObservableProperty] private int _hardnessCurrentCount; + [ObservableProperty] private double _hardnessMax; + [ObservableProperty] private double _hardnessMin; + + + [ObservableProperty] private bool _disintegrationPass; // 新增 + [ObservableProperty] private bool _dissolutionPass; // 新增 + + public IAsyncRelayCommand HardnessUpCommand { get; } + public IAsyncRelayCommand HardnessDownCommand { get; } + public IAsyncRelayCommand HardnessResetCommand { get; } + public IAsyncRelayCommand PrintHardnessCommand { get; } + + // 脆碎度新增 + [ObservableProperty] private double _friabilityTargetRpm = 25; + [ObservableProperty] private int _friabilityTargetTimeSec = 240; + [ObservableProperty] private bool _friabilityClockwise = true; + [ObservableProperty] private bool _friabilityCounterClockwise; + [ObservableProperty] private double _friabilityCurrentRpm; + [ObservableProperty] private int _friabilityRemainingRounds = 100; + + public IAsyncRelayCommand StopFriabilityCommand { get; } + public IAsyncRelayCommand ResetFriabilityCommand { get; } + public IAsyncRelayCommand PrintFriabilityCommand { get; } + + // 溶出度新增 + [ObservableProperty] private double _dissolutionUpDownFreq = 32; + [ObservableProperty] private int _dissolutionSampleInterval = 5; + [ObservableProperty] private double _dissolutionTargetRpm = 50; + [ObservableProperty] private double _dissolutionElapsedTime; + [ObservableProperty] private double _dissolutionCountdown; + [ObservableProperty] private double _dissolutionRSquared; + + public IAsyncRelayCommand DissolutionUpCommand { get; } + public IAsyncRelayCommand DissolutionDownCommand { get; } + public IAsyncRelayCommand StopDissolutionCommand { get; } + public IAsyncRelayCommand PrintDissolutionCommand { get; } + + // 崩解新增 + [ObservableProperty] private double _disintegrationTargetFreq = 31; + public IAsyncRelayCommand StopDisintegrationCommand { get; } + public IAsyncRelayCommand PrintDisintegrationCommand { get; } + public PlotModel DissolutionPlotModel { get; } private LineSeries _dissolutionSeries; private DateTime _dissolutionStartTime; @@ -60,6 +112,8 @@ namespace TabletTester2025.ViewModels public IAsyncRelayCommand StartDissolutionCommand { get; } public IAsyncRelayCommand ExportHistoryCommand { get; } + [ObservableProperty] private string _localAlarm; + public StationViewModel(int id, IPlcService plc, PlcConfiguration plcConfig, DatabaseService db, ExcelExportService excel, AlarmService alarm) { StationId = id; @@ -68,6 +122,7 @@ namespace TabletTester2025.ViewModels _db = db; _excel = excel; _alarm = alarm; + _balance = new BalanceService(); // 实例化天平服务(模拟) StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync); StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync); @@ -75,10 +130,67 @@ namespace TabletTester2025.ViewModels StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync); ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync); - // 溶出曲线:时间 vs 溶出度 + // 溶出曲线 DissolutionPlotModel = new PlotModel { Title = $"工位{StationId} 溶出曲线" }; _dissolutionSeries = new LineSeries { Title = "溶出度 (%)", Color = OxyColors.Green }; DissolutionPlotModel.Series.Add(_dissolutionSeries); + + // 硬度命令 + HardnessUpCommand = new AsyncRelayCommand(async () => + { + await _plc.WriteCoilAsync(0x20, true); + await Task.Delay(100); + await _plc.WriteCoilAsync(0x20, false); + }); + HardnessDownCommand = new AsyncRelayCommand(async () => + { + await _plc.WriteCoilAsync(0x21, true); + await Task.Delay(100); + await _plc.WriteCoilAsync(0x21, false); + }); + HardnessResetCommand = new AsyncRelayCommand(() => + { + _hardnessResults.Clear(); + HardnessValue = 0; + HardnessAvg = 0; + HardnessRSD = 0; + HardnessCurrentCount = 0; + HardnessMax = 0; + HardnessMin = 0; + Phase = TestPhase.Idle; + return Task.CompletedTask; + }); + PrintHardnessCommand = new AsyncRelayCommand(async () => await PrintReport("硬度")); + + // 脆碎度命令 + StopFriabilityCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; }); + ResetFriabilityCommand = new AsyncRelayCommand(() => + { + FriabilityRemainingRounds = 100; + LossPercent = 0; + WeightBefore = 0; + WeightAfter = 0; + return Task.CompletedTask; + }); + PrintFriabilityCommand = new AsyncRelayCommand(async () => await PrintReport("脆碎度")); + + // 溶出度命令 + DissolutionUpCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x22, true)); + DissolutionDownCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x23, true)); + StopDissolutionCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; }); + PrintDissolutionCommand = new AsyncRelayCommand(async () => await PrintReport("溶出度")); + + // 崩解命令 + StopDisintegrationCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; }); + PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解")); + } + + private async Task PrintReport(string testName) + { + await App.Current.Dispatcher.InvokeAsync(() => + { + MessageBox.Show($"打印{testName}报告 - 工位{StationId}", "打印", MessageBoxButton.OK, MessageBoxImage.Information); + }); } public async Task UpdateRealTimeData() @@ -122,13 +234,14 @@ namespace TabletTester2025.ViewModels private async Task RunHardnessAsync() { + if (Phase != TestPhase.Idle) return; + CurrentTest = TestType.Hardness; + Phase = TestPhase.Running; + HardnessPass = false; // 添加这一行 + _hardnessResults.Clear(); + try { - if (Phase != TestPhase.Idle) return; - CurrentTest = TestType.Hardness; - Phase = TestPhase.Running; - _hardnessResults.Clear(); - int count = App.CurrentPharmaParams.HardnessTestCount; double min = App.CurrentPharmaParams.HardnessMin_N; double max = App.CurrentPharmaParams.HardnessMax_N; @@ -151,12 +264,21 @@ namespace TabletTester2025.ViewModels HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100; HardnessPass = HardnessAvg >= min && HardnessAvg <= max; Phase = TestPhase.Completed; - await SaveBatchResult(); } catch (Exception ex) { + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"硬度测试出错:{ex.Message}")); Phase = TestPhase.Error; - System.Windows.MessageBox.Show($"硬度测试出错:{ex.Message}\n{ex.StackTrace}", "错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } + finally + { + Phase = TestPhase.Idle; + + // 在保存前设置崩解合格标志:剩余管数为0 且 崩解时间未超时 + //DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds); + + + await SaveBatchResult(); } } @@ -165,16 +287,35 @@ namespace TabletTester2025.ViewModels if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Friability; Phase = TestPhase.Running; + FriabilityPass = false; // 添加这一行 + try + { + // 通过天平读取前重 + WeightBefore = await _balance.ReadWeightAsync(); + await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true); - // 模拟称重(实际可通过串口天平) - WeightBefore = 6.5; - await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true); - await Task.Delay(TimeSpan.FromMinutes(4)); - WeightAfter = WeightBefore * (1 - 0.008); - LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100; - FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; - Phase = TestPhase.Completed; - await SaveBatchResult(); + // 根据设定的转速和总圈数计算运行时间 + int totalRounds = 100; + double rpm = FriabilityTargetRpm; + int durationMs = (int)((totalRounds / rpm) * 60 * 1000); + await Task.Delay(durationMs); + + WeightAfter = await _balance.ReadWeightAsync(); + LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100; + FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; + Phase = TestPhase.Completed; + } + catch (Exception ex) + { + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"脆碎度测试出错: {ex.Message}")); + Phase = TestPhase.Error; + } + finally + { + Phase = TestPhase.Idle; + + await SaveBatchResult(); + } } private async Task RunDisintegrationAsync() @@ -182,20 +323,42 @@ namespace TabletTester2025.ViewModels if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Disintegration; Phase = TestPhase.Running; + DisintegrationPass = false; // 添加这一行 TubesCompleted = new bool[6]; RemainingTubes = 6; - _disintegrationSeconds = 0; + DisintegrationSeconds = 0; _disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _disintegrationTimer.Tick += (s, e) => { if (Phase == TestPhase.Running) DisintegrationSeconds = _disintegrationSeconds + 1; }; + _disintegrationTimer.Tick += (s, e) => + { + if (Phase == TestPhase.Running) + DisintegrationSeconds++; + }; _disintegrationTimer.Start(); - await _plc.WriteCoilAsync(_plcConfig.DisintegrationStartCoil, true); - int maxSec = App.CurrentPharmaParams.DisintegrationMaxSeconds; - while (RemainingTubes > 0 && _disintegrationSeconds < maxSec && Phase == TestPhase.Running) - await Task.Delay(500); - _disintegrationTimer.Stop(); - Phase = TestPhase.Completed; - await SaveBatchResult(); + try + { + await _plc.WriteCoilAsync(_plcConfig.DisintegrationStartCoil, true); + int maxSec = App.CurrentPharmaParams.DisintegrationMaxSeconds; + while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running) + { + await Task.Delay(500); + } + _disintegrationTimer.Stop(); + Phase = TestPhase.Completed; + } + catch (Exception ex) + { + _disintegrationTimer.Stop(); + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}")); + Phase = TestPhase.Error; + } + finally + { + Phase = TestPhase.Idle; + DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds); + + await SaveBatchResult(); + } } private async Task RunDissolutionAsync() @@ -203,65 +366,159 @@ namespace TabletTester2025.ViewModels if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Dissolution; Phase = TestPhase.Running; + DissolutionPass = false; // 添加这一行 _dissolutionStartTime = DateTime.Now; _dissolutionSeries.Points.Clear(); - await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true); + _dissolutionTimes.Clear(); + _dissolutionValues.Clear(); - var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes; - double prevMin = 0; - foreach (var t in sampleTimes) + try { - int delayMs = (int)((t - prevMin) * 60 * 1000); - if (delayMs > 0) await Task.Delay(delayMs); - double value = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent); - DissolutionPercent = value; - prevMin = t; - // 弹出取样提示(可改为非阻塞提示) - App.Current.Dispatcher.Invoke(() => + await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true); + var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes; + double prevMin = 0; + foreach (var t in sampleTimes) { - System.Windows.MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%"); - }); + int delayMs = (int)((t - prevMin) * 60 * 1000); + if (delayMs > 0) await Task.Delay(delayMs); + double value = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent); + DissolutionPercent = value; + _dissolutionTimes.Add(t); + _dissolutionValues.Add(value); + prevMin = t; + await App.Current.Dispatcher.InvokeAsync(() => + { + MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%"); + }); + } + // 计算 R² + _dissolutionRSquared = CalculateRSquared(_dissolutionTimes, _dissolutionValues); + bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min; + Phase = TestPhase.Completed; + } + catch (Exception ex) + { + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"溶出测试出错: {ex.Message}")); + Phase = TestPhase.Error; + } + finally + { + Phase = TestPhase.Idle; + DissolutionPass = (DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min); + await SaveBatchResult(); } - bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min; - Phase = TestPhase.Completed; - await SaveBatchResult(pass); } private async Task SaveBatchResult(bool? forcedQualified = null) { try { + // 计算溶出曲线的 R²(需在 RunDissolutionAsync 中计算后存入 _dissolutionRSquared) + double rsquared = DissolutionRSquared; // 确保此属性在溶出结束后被赋值 + var batch = new TestBatch { TestTime = DateTime.Now, StationId = StationId, SampleName = $"样品-{StationId}", + + // 硬度 HardnessAvg = HardnessAvg, HardnessRSD = HardnessRSD, + HardnessMax = HardnessMax, + HardnessMin = HardnessMin, + HardnessTestCount = HardnessTestCount, + + // 脆碎度 FriabilityLoss = LossPercent, + FriabilityTargetRpm = FriabilityTargetRpm, + FriabilityTargetTimeSec = FriabilityTargetTimeSec, + FriabilityClockwise = FriabilityClockwise, + FriabilityRemainingRounds = FriabilityRemainingRounds, + WeightBefore = WeightBefore, + WeightAfter = WeightAfter, + + // 崩解 DisintegrationTimeSec = DisintegrationSeconds, RemainingTubesAtEnd = RemainingTubes, + DisintegrationTargetFreq = DisintegrationTargetFreq, + DisintegrationTemp = DisintegrationTemp, + + // 溶出 DissolutionRate30Min = DissolutionPercent, - IsQualified = forcedQualified ?? (HardnessPass && FriabilityPass && RemainingTubes == 0 && DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min) + DissolutionTargetRpm = DissolutionTargetRpm, + DissolutionRSquared = rsquared, + DissolutionSampleInterval = DissolutionSampleInterval, + DissolutionUpDownFreq = DissolutionUpDownFreq, + HardnessPass = HardnessPass, + FriabilityPass = FriabilityPass, + DisintegrationPass = DisintegrationPass, + DissolutionPass = DissolutionPass, + TestType = CurrentTest switch + { + TestType.Hardness => "硬度", + TestType.Friability => "脆碎度", + TestType.Disintegration => "崩解", + TestType.Dissolution => "溶出", + _ => "" + }, + + IsQualified = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass }; await Task.Run(() => _db.InsertBatch(batch)); - if (!batch.IsQualified) - _alarm.RaiseAlarm($"工位{StationId} 测试不合格"); - else - _alarm.ClearAlarm(); + + + await Application.Current.Dispatcher.InvokeAsync(() => + { + // 获取当前测试项目是否合格 + bool currentPass = CurrentTest switch + { + TestType.Hardness => HardnessPass, + TestType.Friability => FriabilityPass, + TestType.Disintegration => DisintegrationPass, + TestType.Dissolution => DissolutionPass, + _ => false + }; + string projectName = CurrentTest switch + { + TestType.Hardness => "硬度", + TestType.Friability => "脆碎度", + TestType.Disintegration => "崩解", + TestType.Dissolution => "溶出", + _ => "" + }; + LocalAlarm = currentPass ? $"{projectName}测试合格" : $"{projectName}测试不合格"; + }); } catch (Exception ex) { - System.Windows.MessageBox.Show($"保存测试结果失败:{ex.Message}\n{ex.InnerException?.Message}", "数据库错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}")); } } + + private double CalculateRSquared(List timeMinutes, List concentration) + { + if (timeMinutes.Count < 2) return 1; + int n = timeMinutes.Count; + double sumX = timeMinutes.Sum(); + double sumY = concentration.Sum(); + double sumXY = timeMinutes.Zip(concentration, (x, y) => x * y).Sum(); + double sumX2 = timeMinutes.Select(x => x * x).Sum(); + double sumY2 = concentration.Select(y => y * y).Sum(); + + double numerator = (n * sumXY - sumX * sumY); + double denominator = Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); + double r = numerator / denominator; + return r * r; + } + private async Task ExportHistoryAsync() { var batches = await Task.Run(() => _db.GetBatches(StationId, 100)); string path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"工位{StationId}_检测记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); _excel.ExportToExcel(batches, path); - System.Windows.MessageBox.Show($"导出成功: {path}"); + await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"导出成功: {path}")); } private double StandardDeviation(List values) diff --git a/Views/HistoryWindow.xaml b/Views/HistoryWindow.xaml index e2c5bd1..0747254 100644 --- a/Views/HistoryWindow.xaml +++ b/Views/HistoryWindow.xaml @@ -2,62 +2,343 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:helpers="clr-namespace:TabletTester2025.Helpers" - Title="历史检测记录" Height="600" Width="1000" - WindowStartupLocation="CenterOwner"> + Title="历史检测记录" Width="1024" MinHeight="768" + WindowState="Maximized" WindowStartupLocation="CenterScreen" + Background="#F5F7FA"> + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - + + + + - - - - - - - - - -