更新2026
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
|
||||
public ushort HardnessShishilizhi { get; set; }
|
||||
// 兼容旧代码:硬度完成线圈与溶出或硬度实时值寄存器地址
|
||||
public ushort HardnessOver { get; set; }
|
||||
public ushort HardnessCompleteCoil { get; set; }
|
||||
public ushort HardnessPoSun { get; set; }
|
||||
// 脆碎度
|
||||
|
||||
@@ -69,6 +69,20 @@ namespace TabletTester2025.Models
|
||||
public bool DisintegrationPass { get; set; }
|
||||
public bool DissolutionPass { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string HardnessPassText => ToPassText(HardnessPass);
|
||||
|
||||
[NotMapped]
|
||||
public string FriabilityPassText => ToPassText(FriabilityPass);
|
||||
|
||||
[NotMapped]
|
||||
public string DisintegrationPassText => ToPassText(DisintegrationPass);
|
||||
|
||||
[NotMapped]
|
||||
public string DissolutionPassText => ToPassText(DissolutionPass);
|
||||
|
||||
public string TestType { get; set; } // "硬度", "脆碎度", "崩解", "溶出"
|
||||
|
||||
private static string ToPassText(bool value) => value ? "合格" : "不合格";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,29 @@
|
||||
using OfficeOpenXml;
|
||||
using OfficeOpenXml;
|
||||
using OfficeOpenXml.Drawing.Chart;
|
||||
using OfficeOpenXml.Style;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TabletTester2025.Models;
|
||||
|
||||
namespace TabletTester2025.Services
|
||||
{
|
||||
public class ExcelExportService
|
||||
{
|
||||
|
||||
public void ExportToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
|
||||
// 按测试类型分组
|
||||
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 = "平均值(N)";
|
||||
sheet.Cells[1, 4].Value = "RSD(%)";
|
||||
sheet.Cells[1, 5].Value = "最大值(N)";
|
||||
sheet.Cells[1, 6].Value = "最小值(N)";
|
||||
sheet.Cells[1, 7].Value = "测试次数";
|
||||
sheet.Cells[1, 8].Value = "内控下限(N)";
|
||||
sheet.Cells[1, 9].Value = "内控上限(N)";
|
||||
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.SampleName;
|
||||
sheet.Cells[row, 3].Value = b.HardnessAvg;
|
||||
sheet.Cells[row, 4].Value = b.HardnessRSD;
|
||||
sheet.Cells[row, 5].Value = b.HardnessMax;
|
||||
sheet.Cells[row, 6].Value = b.HardnessMin;
|
||||
sheet.Cells[row, 7].Value = b.HardnessTestCount;
|
||||
sheet.Cells[row, 8].Value = b.HardnessInternalMin;
|
||||
sheet.Cells[row, 9].Value = b.HardnessInternalMax;
|
||||
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 = "设定转速(r/min)";
|
||||
sheet.Cells[1, 5].Value = "试验转数";
|
||||
sheet.Cells[1, 6].Value = "前重(g)";
|
||||
sheet.Cells[1, 7].Value = "后重(g)";
|
||||
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.SampleName;
|
||||
sheet.Cells[row, 3].Value = b.FriabilityLoss;
|
||||
sheet.Cells[row, 4].Value = b.FriabilityTargetRpm;
|
||||
sheet.Cells[row, 5].Value = b.FriabilityTargetRounds;
|
||||
sheet.Cells[row, 6].Value = b.WeightBefore;
|
||||
sheet.Cells[row, 7].Value = b.WeightAfter;
|
||||
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 = "水浴温度(℃)";
|
||||
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.SampleName;
|
||||
sheet.Cells[row, 3].Value = b.DisintegrationDosageForm;
|
||||
sheet.Cells[row, 4].Value = b.DisintegrationLimitSeconds;
|
||||
sheet.Cells[row, 5].Value = b.DisintegrationTimeSec;
|
||||
sheet.Cells[row, 6].Value = b.RemainingTubesAtEnd;
|
||||
sheet.Cells[row, 7].Value = b.DisintegrationTemp;
|
||||
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²";
|
||||
sheet.Cells[1, 6].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.SampleName;
|
||||
sheet.Cells[row, 3].Value = b.DissolutionChannel;
|
||||
sheet.Cells[row, 4].Value = b.DissolutionRate30Min;
|
||||
sheet.Cells[row, 5].Value = b.DissolutionRSquared;
|
||||
sheet.Cells[row, 6].Value = b.DissolutionSampleSummary;
|
||||
row++;
|
||||
}
|
||||
sheet.Cells.AutoFitColumns();
|
||||
AddDissolutionSamplesSheet(package, dissolutionData);
|
||||
}
|
||||
else
|
||||
{
|
||||
var sheet = package.Workbook.Worksheets.Add("溶出报表");
|
||||
sheet.Cells[1, 1].Value = "无溶出测试数据";
|
||||
}
|
||||
AddHardnessSheet(package, hardnessData);
|
||||
AddFriabilitySheet(package, friabilityData);
|
||||
AddDisintegrationSheet(package, disintegrationData);
|
||||
AddDissolutionSheet(package, dissolutionData);
|
||||
|
||||
package.Save();
|
||||
}
|
||||
@@ -153,20 +31,46 @@ namespace TabletTester2025.Services
|
||||
public void ExportHardnessToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
AddHardnessSheet(package, batches);
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportFriabilityToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
AddFriabilitySheet(package, batches);
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportDisintegrationToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
AddDisintegrationSheet(package, batches);
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportDissolutionToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
AddDissolutionSheet(package, batches);
|
||||
package.Save();
|
||||
}
|
||||
|
||||
private static void AddHardnessSheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
var data = batches.ToList();
|
||||
var sheet = package.Workbook.Worksheets.Add("硬度报表");
|
||||
// 只导出硬度相关列
|
||||
sheet.Cells[1, 1].Value = "检测时间";
|
||||
sheet.Cells[1, 2].Value = "样品名称";
|
||||
sheet.Cells[1, 3].Value = "平均值(N)";
|
||||
sheet.Cells[1, 4].Value = "RSD(%)";
|
||||
sheet.Cells[1, 5].Value = "最大值(N)";
|
||||
sheet.Cells[1, 6].Value = "最小值(N)";
|
||||
sheet.Cells[1, 7].Value = "测试次数";
|
||||
sheet.Cells[1, 8].Value = "内控下限(N)";
|
||||
sheet.Cells[1, 9].Value = "内控上限(N)";
|
||||
WriteHeader(sheet, "检测时间", "样品名称", "平均值(N)", "RSD(%)", "最大值(N)", "最小值(N)", "测试次数", "内控下限(N)", "内控上限(N)", "判定");
|
||||
|
||||
if (data.Count == 0)
|
||||
{
|
||||
sheet.Cells[2, 1].Value = "无硬度测试数据";
|
||||
sheet.Cells.AutoFitColumns();
|
||||
return;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
foreach (var b in batches)
|
||||
foreach (var b in data)
|
||||
{
|
||||
sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
sheet.Cells[row, 2].Value = b.SampleName;
|
||||
@@ -177,26 +81,28 @@ namespace TabletTester2025.Services
|
||||
sheet.Cells[row, 7].Value = b.HardnessTestCount;
|
||||
sheet.Cells[row, 8].Value = b.HardnessInternalMin;
|
||||
sheet.Cells[row, 9].Value = b.HardnessInternalMax;
|
||||
sheet.Cells[row, 10].Value = b.HardnessPassText;
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportFriabilityToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
private static void AddFriabilitySheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
var data = batches.ToList();
|
||||
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 = "设定转速(r/min)";
|
||||
sheet.Cells[1, 5].Value = "试验转数";
|
||||
sheet.Cells[1, 6].Value = "前重(g)";
|
||||
sheet.Cells[1, 7].Value = "后重(g)";
|
||||
WriteHeader(sheet, "检测时间", "样品名称", "失重率(%)", "设定转速(r/min)", "试验转数", "前重(g)", "后重(g)", "判定");
|
||||
|
||||
if (data.Count == 0)
|
||||
{
|
||||
sheet.Cells[2, 1].Value = "无脆碎度测试数据";
|
||||
sheet.Cells.AutoFitColumns();
|
||||
return;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
foreach (var b in batches)
|
||||
foreach (var b in data)
|
||||
{
|
||||
sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
sheet.Cells[row, 2].Value = b.SampleName;
|
||||
@@ -205,26 +111,28 @@ namespace TabletTester2025.Services
|
||||
sheet.Cells[row, 5].Value = b.FriabilityTargetRounds;
|
||||
sheet.Cells[row, 6].Value = b.WeightBefore;
|
||||
sheet.Cells[row, 7].Value = b.WeightAfter;
|
||||
sheet.Cells[row, 8].Value = b.FriabilityPassText;
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportDisintegrationToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
private static void AddDisintegrationSheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
var data = batches.ToList();
|
||||
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 = "水浴温度(℃)";
|
||||
WriteHeader(sheet, "检测时间", "样品名称", "剂型规则", "时限(秒)", "崩解时间(秒)", "剩余未崩解管", "水浴温度(℃)", "判定");
|
||||
|
||||
if (data.Count == 0)
|
||||
{
|
||||
sheet.Cells[2, 1].Value = "无崩解测试数据";
|
||||
sheet.Cells.AutoFitColumns();
|
||||
return;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
foreach (var b in batches)
|
||||
foreach (var b in data)
|
||||
{
|
||||
sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
sheet.Cells[row, 2].Value = b.SampleName;
|
||||
@@ -233,25 +141,28 @@ namespace TabletTester2025.Services
|
||||
sheet.Cells[row, 5].Value = b.DisintegrationTimeSec;
|
||||
sheet.Cells[row, 6].Value = b.RemainingTubesAtEnd;
|
||||
sheet.Cells[row, 7].Value = b.DisintegrationTemp;
|
||||
sheet.Cells[row, 8].Value = b.DisintegrationPassText;
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
package.Save();
|
||||
}
|
||||
|
||||
public void ExportDissolutionToExcel(IEnumerable<TestBatch> batches, string filePath)
|
||||
private static void AddDissolutionSheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
using var package = new ExcelPackage(new FileInfo(filePath));
|
||||
var data = batches.ToList();
|
||||
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²";
|
||||
sheet.Cells[1, 6].Value = "取样明细";
|
||||
WriteHeader(sheet, "检测时间", "样品名称", "通道", "30min溶出度(%)", "R²", "取样明细", "判定");
|
||||
|
||||
if (data.Count == 0)
|
||||
{
|
||||
sheet.Cells[2, 1].Value = "无溶出测试数据";
|
||||
sheet.Cells.AutoFitColumns();
|
||||
return;
|
||||
}
|
||||
|
||||
int row = 2;
|
||||
foreach (var b in batches)
|
||||
foreach (var b in data)
|
||||
{
|
||||
sheet.Cells[row, 1].Value = b.TestTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
sheet.Cells[row, 2].Value = b.SampleName;
|
||||
@@ -259,34 +170,23 @@ namespace TabletTester2025.Services
|
||||
sheet.Cells[row, 4].Value = b.DissolutionRate30Min;
|
||||
sheet.Cells[row, 5].Value = b.DissolutionRSquared;
|
||||
sheet.Cells[row, 6].Value = b.DissolutionSampleSummary;
|
||||
sheet.Cells[row, 7].Value = b.DissolutionPassText;
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
AddDissolutionSamplesSheet(package, batches);
|
||||
package.Save();
|
||||
AddDissolutionSamplesSheet(package, data);
|
||||
AddDissolutionChartSheet(package, data);
|
||||
}
|
||||
|
||||
private static void AddDissolutionSamplesSheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
var samples = batches
|
||||
.SelectMany(batch => (batch.DissolutionSamples ?? Enumerable.Empty<DissolutionSamplePoint>())
|
||||
.Select(sample => new { Batch = batch, Sample = sample }))
|
||||
.OrderBy(x => x.Batch.TestTime)
|
||||
.ThenBy(x => x.Sample.Channel)
|
||||
.ThenBy(x => x.Sample.ScheduledTimeMin)
|
||||
.ToList();
|
||||
|
||||
var samples = GetDissolutionSampleRows(batches).ToList();
|
||||
if (samples.Count == 0)
|
||||
return;
|
||||
|
||||
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 = "计划时间(min)";
|
||||
sheet.Cells[1, 5].Value = "实际时间(min)";
|
||||
sheet.Cells[1, 6].Value = "溶出度(%)";
|
||||
sheet.Cells[1, 7].Value = "记录时间";
|
||||
WriteHeader(sheet, "检测时间", "样品名称", "通道", "计划时间(min)", "实际时间(min)", "溶出度(%)", "记录时间");
|
||||
|
||||
int row = 2;
|
||||
foreach (var item in samples)
|
||||
@@ -304,7 +204,103 @@ namespace TabletTester2025.Services
|
||||
sheet.Cells.AutoFitColumns();
|
||||
}
|
||||
|
||||
private static void AddDissolutionChartSheet(ExcelPackage package, IEnumerable<TestBatch> batches)
|
||||
{
|
||||
var seriesGroups = GetDissolutionSampleRows(batches)
|
||||
.Where(x => x.Sample.Percent.HasValue
|
||||
&& double.IsFinite(x.Sample.ScheduledTimeMin)
|
||||
&& x.Sample.ScheduledTimeMin >= 0
|
||||
&& double.IsFinite(x.Sample.Percent.Value))
|
||||
.GroupBy(x => new
|
||||
{
|
||||
x.Batch.Id,
|
||||
x.Batch.TestTime,
|
||||
x.Batch.SampleName,
|
||||
x.Sample.ChannelName
|
||||
})
|
||||
.Select(group => new
|
||||
{
|
||||
Name = $"{group.Key.TestTime:MM-dd HH:mm} {group.Key.SampleName} {group.Key.ChannelName}",
|
||||
Points = group
|
||||
.OrderBy(x => x.Sample.ScheduledTimeMin)
|
||||
.Select(x => new { Time = x.Sample.ScheduledTimeMin, Percent = x.Sample.Percent!.Value })
|
||||
.ToList()
|
||||
})
|
||||
.Where(group => group.Points.Count > 0)
|
||||
.ToList();
|
||||
|
||||
var sheet = package.Workbook.Worksheets.Add("溶出曲线");
|
||||
if (seriesGroups.Count == 0)
|
||||
{
|
||||
sheet.Cells[1, 1].Value = "无可绘制溶出曲线数据";
|
||||
sheet.Cells.AutoFitColumns();
|
||||
return;
|
||||
}
|
||||
|
||||
int column = 1;
|
||||
foreach (var group in seriesGroups)
|
||||
{
|
||||
sheet.Cells[1, column].Value = $"{group.Name} 时间(min)";
|
||||
sheet.Cells[1, column + 1].Value = $"{group.Name} 溶出度(%)";
|
||||
|
||||
int row = 2;
|
||||
foreach (var point in group.Points)
|
||||
{
|
||||
sheet.Cells[row, column].Value = point.Time;
|
||||
sheet.Cells[row, column + 1].Value = point.Percent;
|
||||
row++;
|
||||
}
|
||||
|
||||
column += 3;
|
||||
}
|
||||
|
||||
using (var header = sheet.Cells[1, 1, 1, Math.Max(1, column - 2)])
|
||||
{
|
||||
header.Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var chart = sheet.Drawings.AddChart("DissolutionCurve", eChartType.LineMarkers);
|
||||
chart.Title.Text = "溶出曲线";
|
||||
chart.SetPosition(1, 0, column, 0);
|
||||
chart.SetSize(900, 420);
|
||||
chart.XAxis.Title.Text = "计划时间(min)";
|
||||
chart.YAxis.Title.Text = "溶出度(%)";
|
||||
chart.YAxis.MinValue = 0;
|
||||
chart.YAxis.MaxValue = 150;
|
||||
chart.Legend.Position = eLegendPosition.Bottom;
|
||||
|
||||
column = 1;
|
||||
foreach (var group in seriesGroups)
|
||||
{
|
||||
int endRow = group.Points.Count + 1;
|
||||
var series = chart.Series.Add(
|
||||
sheet.Cells[2, column + 1, endRow, column + 1],
|
||||
sheet.Cells[2, column, endRow, column]);
|
||||
series.Header = group.Name;
|
||||
column += 3;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
}
|
||||
|
||||
private static IEnumerable<(TestBatch Batch, DissolutionSamplePoint Sample)> GetDissolutionSampleRows(IEnumerable<TestBatch> batches)
|
||||
{
|
||||
return batches
|
||||
.SelectMany(batch => (batch.DissolutionSamples ?? Enumerable.Empty<DissolutionSamplePoint>())
|
||||
.Select(sample => (Batch: batch, Sample: sample)))
|
||||
.OrderBy(x => x.Batch.TestTime)
|
||||
.ThenBy(x => x.Sample.Channel)
|
||||
.ThenBy(x => x.Sample.ScheduledTimeMin);
|
||||
}
|
||||
|
||||
private static void WriteHeader(ExcelWorksheet sheet, params string[] headers)
|
||||
{
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
sheet.Cells[1, i + 1].Value = headers[i];
|
||||
|
||||
using var range = sheet.Cells[1, 1, 1, headers.Length];
|
||||
range.Style.Font.Bold = true;
|
||||
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
Services/TestCalculationService.cs
Normal file
147
Services/TestCalculationService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TabletTester2025.Models;
|
||||
|
||||
namespace TabletTester2025.Services
|
||||
{
|
||||
public readonly record struct HardnessStatistics(
|
||||
double Average,
|
||||
double RsdPercent,
|
||||
double Maximum,
|
||||
double Minimum,
|
||||
int Count,
|
||||
bool IsPass);
|
||||
|
||||
public static class TestCalculationService
|
||||
{
|
||||
public static HardnessStatistics CalculateHardness(
|
||||
IReadOnlyCollection<double> values,
|
||||
double internalMin,
|
||||
double internalMax,
|
||||
int requiredCount)
|
||||
{
|
||||
var validValues = values
|
||||
.Where(value => double.IsFinite(value) && value > 0)
|
||||
.ToList();
|
||||
|
||||
if (validValues.Count == 0)
|
||||
return new HardnessStatistics(0, 0, 0, 0, 0, false);
|
||||
|
||||
double average = validValues.Average();
|
||||
double standardDeviation = CalculateSampleStandardDeviation(validValues, average);
|
||||
double rsd = average == 0 ? 0 : standardDeviation / average * 100;
|
||||
bool countMet = validValues.Count >= Math.Max(1, requiredCount);
|
||||
bool inRange = validValues.All(value => value >= internalMin && value <= internalMax);
|
||||
|
||||
return new HardnessStatistics(
|
||||
average,
|
||||
rsd,
|
||||
validValues.Max(),
|
||||
validValues.Min(),
|
||||
validValues.Count,
|
||||
countMet && inRange);
|
||||
}
|
||||
|
||||
public static double CalculateFriabilityLossPercent(double weightBefore, double weightAfter)
|
||||
{
|
||||
if (!double.IsFinite(weightBefore) || !double.IsFinite(weightAfter) || weightBefore <= 0)
|
||||
throw new InvalidOperationException("脆碎前重量必须大于0");
|
||||
|
||||
if (weightAfter < 0)
|
||||
throw new InvalidOperationException("脆碎后重量数据异常");
|
||||
|
||||
if (weightAfter > weightBefore)
|
||||
throw new InvalidOperationException("脆碎后重量不能大于初始重量");
|
||||
|
||||
return (weightBefore - weightAfter) / weightBefore * 100;
|
||||
}
|
||||
|
||||
public static bool TryGetDissolutionRateAt30Min(
|
||||
IReadOnlyList<double> times,
|
||||
IReadOnlyList<double> values,
|
||||
out double rate)
|
||||
{
|
||||
rate = 0;
|
||||
if (times.Count == 0 || times.Count != values.Count)
|
||||
return false;
|
||||
|
||||
var points = times
|
||||
.Zip(values, (time, value) => new { Time = time, Value = value })
|
||||
.Where(point => double.IsFinite(point.Time)
|
||||
&& double.IsFinite(point.Value)
|
||||
&& point.Time >= 0
|
||||
&& point.Value >= 0
|
||||
&& point.Value <= 150)
|
||||
.OrderBy(point => point.Time)
|
||||
.ToList();
|
||||
|
||||
if (points.Count == 0)
|
||||
return false;
|
||||
|
||||
var exact = points.FirstOrDefault(point => Math.Abs(point.Time - 30) < 0.0001);
|
||||
if (exact != null)
|
||||
{
|
||||
rate = exact.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
var before = points.LastOrDefault(point => point.Time < 30);
|
||||
var after = points.FirstOrDefault(point => point.Time > 30);
|
||||
if (before == null || after == null || after.Time <= before.Time)
|
||||
return false;
|
||||
|
||||
double fraction = (30 - before.Time) / (after.Time - before.Time);
|
||||
rate = before.Value + (after.Value - before.Value) * fraction;
|
||||
return double.IsFinite(rate);
|
||||
}
|
||||
|
||||
public static double CalculateRSquared(IReadOnlyList<double> timeMinutes, IReadOnlyList<double> concentration)
|
||||
{
|
||||
if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count)
|
||||
return 0;
|
||||
|
||||
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));
|
||||
if (denominator <= 0 || double.IsNaN(denominator))
|
||||
return 0;
|
||||
|
||||
double r = numerator / denominator;
|
||||
double result = r * r;
|
||||
return double.IsFinite(result) ? result : 0;
|
||||
}
|
||||
|
||||
public static bool ResolveCurrentTestQualified(
|
||||
TestType currentTest,
|
||||
bool hardnessPass,
|
||||
bool friabilityPass,
|
||||
bool disintegrationPass,
|
||||
bool dissolutionPass)
|
||||
{
|
||||
return currentTest switch
|
||||
{
|
||||
TestType.Hardness => hardnessPass,
|
||||
TestType.Friability => friabilityPass,
|
||||
TestType.Disintegration => disintegrationPass,
|
||||
TestType.Dissolution => dissolutionPass,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateSampleStandardDeviation(IReadOnlyList<double> values, double average)
|
||||
{
|
||||
if (values.Count < 2)
|
||||
return 0;
|
||||
|
||||
double sum = values.Sum(value => Math.Pow(value - average, 2));
|
||||
return Math.Sqrt(sum / (values.Count - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,7 +386,7 @@ namespace TabletTester2025.ViewModels
|
||||
if (completed && !TubesCompleted[i])
|
||||
{
|
||||
TubesCompleted[i] = true;
|
||||
RemainingTubes = 6 - TubesCompleted.Count(c => c);
|
||||
RemainingTubes = TubesCompleted.Length - TubesCompleted.Count(c => c);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -405,16 +405,28 @@ namespace TabletTester2025.ViewModels
|
||||
DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp);
|
||||
|
||||
if (_isDissolution1Running)
|
||||
{
|
||||
await ReadDissolutionChannelAsync(1);
|
||||
await CheckDissolutionSampleAsync(1);
|
||||
}
|
||||
|
||||
if (_isDissolution2Running)
|
||||
{
|
||||
await ReadDissolutionChannelAsync(2);
|
||||
await CheckDissolutionSampleAsync(2);
|
||||
}
|
||||
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
|
||||
private async Task CheckDissolutionSampleAsync(int channel)
|
||||
{
|
||||
if (channel == 2 && _plcConfig.Dissolution2Percent == 0)
|
||||
{
|
||||
DissolutionCurveStatus = "溶出2溶出度寄存器未配置,未启用取样计算";
|
||||
return;
|
||||
}
|
||||
|
||||
ushort coilAddress = channel == 1
|
||||
? _plcConfig.Dissolution1SampleAckCoil
|
||||
: _plcConfig.Dissolution2SampleAckCoil;
|
||||
@@ -1130,41 +1142,115 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private async Task RunHardnessAsync()
|
||||
{
|
||||
// 点击启动才执行一次
|
||||
if (Phase != TestPhase.Idle) return;
|
||||
CurrentTest = TestType.Hardness;
|
||||
Phase = TestPhase.Running;
|
||||
HardnessPass = false;
|
||||
_hardnessResults.Clear();
|
||||
HardnessCurrentCount = 0;
|
||||
HardnessAvg = 0;
|
||||
HardnessRSD = 0;
|
||||
HardnessMax = 0;
|
||||
HardnessMin = 0;
|
||||
|
||||
try
|
||||
{
|
||||
int count = Math.Max(1, HardnessTestCount);
|
||||
double min = HardnessInternalMin;
|
||||
double max = HardnessInternalMax;
|
||||
ushort completeCoil = ResolveHardnessCompleteCoil();
|
||||
if (completeCoil == 0)
|
||||
throw new InvalidOperationException("硬度完成线圈未配置");
|
||||
if (_plcConfig.HardnessMax == 0)
|
||||
throw new InvalidOperationException("硬度最大力寄存器未配置");
|
||||
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)HardnessSudu);
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)HardnessWeiyi);
|
||||
|
||||
double currentSpeed = HardnessSudu;
|
||||
double currentWeiyi = HardnessWeiyi;
|
||||
if (await _plc.ReadCoilAsync(completeCoil))
|
||||
await WaitForCoilStateAsync(completeCoil, false, TimeSpan.FromSeconds(10), "硬度完成信号未复位");
|
||||
|
||||
await PulseCoilAsync(_plcConfig.HardnessStartCoil);
|
||||
|
||||
// 写入PLC
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)currentSpeed);
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)currentWeiyi);
|
||||
await _plc.WriteCoilAsync(_plcConfig.HardnessStartCoil, true);//启动
|
||||
|
||||
while (Phase == TestPhase.Running && _hardnessResults.Count < count)
|
||||
{
|
||||
await WaitForCoilStateAsync(completeCoil, true, TimeSpan.FromSeconds(120), "等待硬度完成信号超时");
|
||||
double value = await ReadHardnessResultAsync();
|
||||
_hardnessResults.Add(value);
|
||||
ApplyHardnessStatistics(count);
|
||||
await WaitForCoilStateAsync(completeCoil, false, TimeSpan.FromSeconds(10), "硬度完成信号未回落");
|
||||
}
|
||||
|
||||
if (_hardnessResults.Count < count)
|
||||
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
||||
|
||||
ApplyHardnessStatistics(count);
|
||||
Phase = TestPhase.Completed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show($"硬度测试出错: {ex.Message}"));
|
||||
Phase = TestPhase.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
bool resultReady = Phase == TestPhase.Completed;
|
||||
Phase = TestPhase.Idle;
|
||||
if (resultReady)
|
||||
await SaveBatchResult();
|
||||
}
|
||||
}
|
||||
|
||||
private ushort ResolveHardnessCompleteCoil()
|
||||
{
|
||||
return _plcConfig.HardnessOver != 0
|
||||
? _plcConfig.HardnessOver
|
||||
: _plcConfig.HardnessCompleteCoil;
|
||||
}
|
||||
|
||||
private async Task<double> ReadHardnessResultAsync()
|
||||
{
|
||||
double value = await _plc.ReadFloatAsync(_plcConfig.HardnessMax);
|
||||
if (!double.IsFinite(value) || value <= 0)
|
||||
throw new InvalidOperationException("硬度最大力数据异常");
|
||||
|
||||
HardnessValue = value;
|
||||
HardnessShishilizhi = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
private async Task WaitForCoilStateAsync(ushort coilAddress, bool expectedState, TimeSpan timeout, string timeoutMessage)
|
||||
{
|
||||
DateTime deadline = DateTime.Now.Add(timeout);
|
||||
while (Phase == TestPhase.Running && DateTime.Now <= deadline)
|
||||
{
|
||||
if (await _plc.ReadCoilAsync(coilAddress) == expectedState)
|
||||
return;
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
if (Phase != TestPhase.Running)
|
||||
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
||||
|
||||
throw new TimeoutException(timeoutMessage);
|
||||
}
|
||||
|
||||
private void ApplyHardnessStatistics(int requiredCount)
|
||||
{
|
||||
var stats = TestCalculationService.CalculateHardness(
|
||||
_hardnessResults,
|
||||
HardnessInternalMin,
|
||||
HardnessInternalMax,
|
||||
requiredCount);
|
||||
|
||||
HardnessAvg = stats.Average;
|
||||
HardnessRSD = stats.RsdPercent;
|
||||
HardnessMax = stats.Maximum;
|
||||
HardnessMin = stats.Minimum;
|
||||
HardnessCurrentCount = stats.Count;
|
||||
HardnessPass = stats.IsPass;
|
||||
}
|
||||
|
||||
/// 脆碎度测试主逻辑(实时状态显示)
|
||||
|
||||
private async Task RunFriabilityAsync()
|
||||
@@ -1197,7 +1283,7 @@ namespace TabletTester2025.ViewModels
|
||||
FriabilityTargetTimeSec = (int)Math.Ceiling(totalRounds / rpm * 60);
|
||||
FriabilityRemainingRounds = totalRounds;
|
||||
FriabilityCurrentRpm = rpm;
|
||||
await _plc.WriteCoilAsync(startCoil, true);
|
||||
await PulseCoilAsync(startCoil);
|
||||
int durationMs = (int)((totalRounds / rpm) * 60 * 1000); // 总运行时间(毫秒)
|
||||
|
||||
for (int i = 0; i < durationMs; i += 100)
|
||||
@@ -1222,11 +1308,8 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
double weightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量");
|
||||
SetFriabilityWeightFromPlc(weightAfter: weightAfter);
|
||||
if (WeightAfter > WeightBefore)
|
||||
throw new InvalidOperationException("脆碎后重量不能大于初始重量");
|
||||
|
||||
FriabilityCurrentRpm = rpm;
|
||||
LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率
|
||||
LossPercent = TestCalculationService.CalculateFriabilityLossPercent(WeightBefore, WeightAfter);
|
||||
FriabilityPass = LossPercent <= FriabilityMaxLossPercent; //标准值
|
||||
resultReady = true;
|
||||
// 标记测试为已完成
|
||||
@@ -1241,6 +1324,12 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_plcConfig.FriabilityStartCoil != 0)
|
||||
{
|
||||
try { await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, false); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
Phase = TestPhase.Idle;
|
||||
FriabilityRemainingRounds = FriabilityTargetRounds;
|
||||
if (resultReady)
|
||||
@@ -1266,14 +1355,17 @@ namespace TabletTester2025.ViewModels
|
||||
CurrentTest = TestType.Disintegration;
|
||||
Phase = TestPhase.Running;
|
||||
DisintegrationPass = false; // 添加这一行
|
||||
TubesCompleted = new bool[6];
|
||||
RemainingTubes = 6;
|
||||
int tubeCount = Math.Max(1, _plcConfig.DisintegrationCompleteCoils.Length);
|
||||
TubesCompleted = new bool[tubeCount];
|
||||
RemainingTubes = tubeCount;
|
||||
DisintegrationSeconds = 0;
|
||||
bool resultReady = false;
|
||||
DateTime startedAt = DateTime.Now;
|
||||
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_disintegrationTimer.Tick += (s, e) =>
|
||||
{
|
||||
if (Phase == TestPhase.Running)
|
||||
DisintegrationSeconds++;
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
};
|
||||
_disintegrationTimer.Start();
|
||||
|
||||
@@ -1286,10 +1378,13 @@ namespace TabletTester2025.ViewModels
|
||||
DisintegrationTimeMin = maxSec / 60.0;
|
||||
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
|
||||
{
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
await Task.Delay(500);
|
||||
}
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
_disintegrationTimer.Stop();
|
||||
Phase = TestPhase.Completed;
|
||||
resultReady = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1303,7 +1398,7 @@ namespace TabletTester2025.ViewModels
|
||||
int maxSec = ResolveDisintegrationLimitSeconds();
|
||||
DisintegrationPass = !_discardDisintegrationResult && RemainingTubes == 0 && DisintegrationSeconds <= maxSec;
|
||||
|
||||
if (!_discardDisintegrationResult)
|
||||
if (resultReady && !_discardDisintegrationResult)
|
||||
await SaveBatchResult();
|
||||
|
||||
_discardDisintegrationResult = false;
|
||||
@@ -1312,6 +1407,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private async Task StopDisintegrationAsync()
|
||||
{
|
||||
_discardDisintegrationResult = CurrentTest == TestType.Disintegration && Phase == TestPhase.Running;
|
||||
try
|
||||
{
|
||||
await PulseCoilAsync(_plcConfig.DisintegrationStopCoil);
|
||||
@@ -1401,7 +1497,10 @@ namespace TabletTester2025.ViewModels
|
||||
DissolutionPass = false;
|
||||
ResetDissolutionChannel(2);
|
||||
ResetDissolutionSampleState(2);
|
||||
if (_plcConfig.Dissolution2Percent != 0)
|
||||
CreateDissolutionSampleSchedule(2);
|
||||
else
|
||||
DissolutionCurveStatus = "溶出2溶出度寄存器未配置,仅执行设备控制,不保存计算结果";
|
||||
_dissolution2StartTime = DateTime.Now;
|
||||
_isDissolution2Running = true;
|
||||
DissolutionPlotModel.Title = "溶出曲线";
|
||||
@@ -1487,7 +1586,14 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
|
||||
_dissolutionResultChannel = $"溶出{channel}";
|
||||
_dissolutionResultRate30Min = GetDissolutionRateAt30Min(times, values);
|
||||
if (!TestCalculationService.TryGetDissolutionRateAt30Min(times, values, out double rate30Min))
|
||||
{
|
||||
DissolutionCurveStatus = $"溶出{channel}缺少有效30min溶出度,未保存结果";
|
||||
LocalAlarm = DissolutionCurveStatus;
|
||||
return;
|
||||
}
|
||||
|
||||
_dissolutionResultRate30Min = rate30Min;
|
||||
_dissolutionResultRSquared = CalculateRSquared(times, values);
|
||||
DissolutionPercent = _dissolutionResultRate30Min;
|
||||
DissolutionRSquared = _dissolutionResultRSquared;
|
||||
@@ -1500,26 +1606,6 @@ namespace TabletTester2025.ViewModels
|
||||
await SaveBatchResult();
|
||||
}
|
||||
|
||||
private static double GetDissolutionRateAt30Min(List<double> times, List<double> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
return 0;
|
||||
|
||||
int index = 0;
|
||||
double nearestDistance = double.MaxValue;
|
||||
for (int i = 0; i < times.Count; i++)
|
||||
{
|
||||
double distance = Math.Abs(times[i] - 30);
|
||||
if (distance < nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
return values[index];
|
||||
}
|
||||
|
||||
private async Task RunDissolutionAsync()
|
||||
{
|
||||
await StartDissolution1Async();
|
||||
@@ -1545,7 +1631,8 @@ namespace TabletTester2025.ViewModels
|
||||
// 硬度
|
||||
HardnessAvg = HardnessAvg,
|
||||
HardnessRSD = HardnessRSD,
|
||||
|
||||
HardnessMax = HardnessMax,
|
||||
HardnessMin = HardnessMin,
|
||||
HardnessTestCount = HardnessTestCount,
|
||||
HardnessInternalMin = HardnessInternalMin,
|
||||
HardnessInternalMax = HardnessInternalMax,
|
||||
@@ -1592,7 +1679,12 @@ namespace TabletTester2025.ViewModels
|
||||
_ => ""
|
||||
},
|
||||
|
||||
IsQualified = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass
|
||||
IsQualified = TestCalculationService.ResolveCurrentTestQualified(
|
||||
CurrentTest,
|
||||
HardnessPass,
|
||||
FriabilityPass,
|
||||
DisintegrationPass,
|
||||
DissolutionPass)
|
||||
};
|
||||
var dissolutionSamples = CurrentTest == TestType.Dissolution
|
||||
? DissolutionSamplePoints
|
||||
@@ -1625,22 +1717,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private double CalculateRSquared(List<double> timeMinutes, List<double> concentration)
|
||||
{
|
||||
if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count) return 0;
|
||||
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));
|
||||
if (denominator <= 0 || double.IsNaN(denominator))
|
||||
return 0;
|
||||
|
||||
double r = numerator / denominator;
|
||||
double result = r * r;
|
||||
return double.IsFinite(result) ? result : 0;
|
||||
return TestCalculationService.CalculateRSquared(timeMinutes, concentration);
|
||||
}
|
||||
|
||||
private async Task ExportHistoryAsync()
|
||||
@@ -1656,7 +1733,7 @@ namespace TabletTester2025.ViewModels
|
||||
if (values.Count == 0) return 0;
|
||||
double avg = values.Average();
|
||||
double sum = values.Sum(v => Math.Pow(v - avg, 2));
|
||||
return Math.Sqrt(sum / values.Count);
|
||||
return values.Count < 2 ? 0 : Math.Sqrt(sum / (values.Count - 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
<DataGridTextColumn Header="测试次数" Binding="{Binding HardnessTestCount}" Width="70"/>
|
||||
<DataGridTextColumn Header="内控下限(N)" Binding="{Binding HardnessInternalMin, StringFormat=F1}" Width="100"/>
|
||||
<DataGridTextColumn Header="内控上限(N)" Binding="{Binding HardnessInternalMax, StringFormat=F1}" Width="100"/>
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding HardnessPassText}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
@@ -199,6 +200,7 @@
|
||||
<DataGridTextColumn Header="试验转数" Binding="{Binding FriabilityTargetRounds}" Width="80"/>
|
||||
<DataGridTextColumn Header="前重(g)" Binding="{Binding WeightBefore, StringFormat=F3}" Width="90"/>
|
||||
<DataGridTextColumn Header="后重(g)" Binding="{Binding WeightAfter, StringFormat=F3}" Width="90"/>
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding FriabilityPassText}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
@@ -222,6 +224,7 @@
|
||||
<DataGridTextColumn Header="崩解时间(秒)" Binding="{Binding DisintegrationTimeSec}" Width="100"/>
|
||||
<DataGridTextColumn Header="剩余未崩解管" Binding="{Binding RemainingTubesAtEnd}" Width="110"/>
|
||||
<DataGridTextColumn Header="水浴温度(℃)" Binding="{Binding DisintegrationTemp, StringFormat=F1}" Width="110"/>
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding DisintegrationPassText}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
@@ -244,6 +247,7 @@
|
||||
<DataGridTextColumn Header="30min溶出度(%)" Binding="{Binding DissolutionRate30Min, StringFormat=F1}" Width="130"/>
|
||||
<DataGridTextColumn Header="R²" Binding="{Binding DissolutionRSquared, StringFormat=F4}" Width="80"/>
|
||||
<DataGridTextColumn Header="取样明细" Binding="{Binding DissolutionSampleSummary}" Width="*"/>
|
||||
<DataGridTextColumn Header="判定" Binding="{Binding DissolutionPassText}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="脆碎度 - 通则0923">
|
||||
<GroupBox Header="脆碎度">
|
||||
<StackPanel>
|
||||
<WrapPanel>
|
||||
<StackPanel Style="{StaticResource ParamRow}">
|
||||
@@ -108,12 +108,12 @@
|
||||
<TextBox x:Name="FriabilityMaxLossBox"/>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
<TextBlock Text="通则默认:转速25±1 r/min,转动100次,减失重量不得过1.0%,且不得检出断裂、龟裂或粉碎的片。"
|
||||
<TextBlock Text="默认:转速25±1 r/min,转动100次,减失重量不得过1.0%,且不得检出断裂、龟裂或粉碎的片。"
|
||||
Style="{StaticResource StandardNote}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="崩解时限 - 通则0921">
|
||||
<GroupBox Header="崩解">
|
||||
<StackPanel>
|
||||
<WrapPanel>
|
||||
<StackPanel Style="{StaticResource ParamRow}">
|
||||
@@ -142,12 +142,12 @@
|
||||
<TextBox x:Name="DisintegrationTempBox"/>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
<TextBlock Text="通则默认:升降频率30-32次/min,介质温度37±1℃。不同剂型按药典或品种正文规定时限执行。"
|
||||
<TextBlock Text="默认:升降频率30-32次/min,介质温度37±1℃。不同剂型按药典或品种正文规定时限执行。"
|
||||
Style="{StaticResource StandardNote}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="溶出度 - 通则0931">
|
||||
<GroupBox Header="溶出度">
|
||||
<StackPanel>
|
||||
<WrapPanel>
|
||||
<StackPanel Style="{StaticResource ParamRow}">
|
||||
@@ -181,7 +181,7 @@
|
||||
Width="430"
|
||||
helpers:NumericInput.IsEnabled="False"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="通则默认:普通制剂通常取6片,溶出介质温度37±0.5℃;Q值、介质、转速和取样点应按具体品种正文或企业批准标准录入。"
|
||||
<TextBlock Text="默认:普通制剂通常取6片,溶出介质温度37±0.5℃;Q值、介质、转速和取样点应按具体品种正文或企业批准标准录入。"
|
||||
Style="{StaticResource StandardNote}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
Reference in New Issue
Block a user