diff --git a/FootwearTest/App.axaml b/FootwearTest/App.axaml new file mode 100644 index 0000000..c81a6c3 --- /dev/null +++ b/FootwearTest/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/FootwearTest/App.axaml.cs b/FootwearTest/App.axaml.cs new file mode 100644 index 0000000..5c0d76f --- /dev/null +++ b/FootwearTest/App.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using FootwearTest.Services; +using FootwearTest.ViewModels; +using FootwearTest.Views; +using Microsoft.Extensions.DependencyInjection; + +namespace FootwearTest +{ + public partial class App : Application + { + private ServiceProvider? _serviceProvider; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + _serviceProvider = ConfigureServices(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = _serviceProvider.GetRequiredService(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + private static ServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => + { + var settings = provider.GetRequiredService(); + return settings.UseSimulator + ? new SimulatedDeviceClient(settings) + : new ModbusTcpDeviceClient(settings); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services.BuildServiceProvider(); + } + } +} diff --git a/FootwearTest/Assets/avalonia-logo.ico b/FootwearTest/Assets/avalonia-logo.ico new file mode 100644 index 0000000..f7da8bb Binary files /dev/null and b/FootwearTest/Assets/avalonia-logo.ico differ diff --git a/FootwearTest/FootwearTest.csproj b/FootwearTest/FootwearTest.csproj new file mode 100644 index 0000000..689e873 --- /dev/null +++ b/FootwearTest/FootwearTest.csproj @@ -0,0 +1,28 @@ + + + WinExe + net10.0 + enable + app.manifest + true + + + + + + + + + + + + + + None + All + + + + + + diff --git a/FootwearTest/Models/DeviceSnapshot.cs b/FootwearTest/Models/DeviceSnapshot.cs new file mode 100644 index 0000000..e7232e0 --- /dev/null +++ b/FootwearTest/Models/DeviceSnapshot.cs @@ -0,0 +1,34 @@ +using System; + +namespace FootwearTest.Models; + +public sealed record DeviceSnapshot( + DateTime Timestamp, + double FootTemperatureC, + double EnvironmentTemperatureC, + double EnvironmentHumidityPercent, + double AirSpeedMetersPerSecond, + double PowerWatts, + double EnergyKilojoules, + double WaterLossGramsPerHour, + double PumpSpeedCubicCentimetersPerHour, + bool PumpRunning, + bool FanRunning, + bool HeaterRunning, + string AlarmText) +{ + public static DeviceSnapshot Initial { get; } = new( + DateTime.Now, + 35.0, + 23.0, + 50.0, + 1.10, + 4.5, + 0.0, + 5.0, + 5.0, + false, + false, + false, + "无报警"); +} diff --git a/FootwearTest/Models/TestRecords.cs b/FootwearTest/Models/TestRecords.cs new file mode 100644 index 0000000..e2fb43a --- /dev/null +++ b/FootwearTest/Models/TestRecords.cs @@ -0,0 +1,70 @@ +using System; + +namespace FootwearTest.Models; + +public sealed record TestRunRecord( + long Id, + string Method, + string SampleDescription, + string Conditions, + string ResultSummary, + string DataJson, + bool IsValid, + DateTime CreatedAt); + +public sealed record TestRunSummary( + long Id, + string Method, + string SampleDescription, + string ResultSummary, + bool IsValid, + DateTime CreatedAt); + +public sealed record MethodASampleRecord( + int Index, + DateTime Timestamp, + double FootTemperatureC, + double EnvironmentTemperatureC, + double RelativeHumidityPercent, + double WaterLossGramsPerHour, + double PowerWatts, + double MoistureHeatWatts, + double DryHeatWatts, + double MoistureResistance, + double ThermalResistance); + +public sealed record MethodAResult( + string Stage, + double SkinMoistureResistance, + double AverageMoistureResistance, + double AverageThermalResistance, + double MoistureCoefficientOfVariation, + double ThermalCoefficientOfVariation, + bool PassedDeviationCheck); + +public sealed record MethodBMoistureResult( + double M1, + double M2, + double M3, + double M4, + double M5, + double M6, + double M7, + double M8, + double M180, + double T180, + double M1Corrected, + double M2Corrected, + double M3Corrected, + double M4Corrected, + double T180Corrected, + double XT180Corrected, + bool IsValid); + +public sealed record MethodBWarmthResult( + double EnergyKilojoules, + double Seconds, + double HeatWatts, + double FootTemperatureC, + double ChamberTemperatureC, + double ThermalResistance); diff --git a/FootwearTest/Program.cs b/FootwearTest/Program.cs new file mode 100644 index 0000000..f32bcbd --- /dev/null +++ b/FootwearTest/Program.cs @@ -0,0 +1,25 @@ +using Avalonia; +using System; + +namespace FootwearTest +{ + internal sealed class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() +#if DEBUG + .WithDeveloperTools() +#endif + .WithInterFont() + .LogToTrace(); + } +} diff --git a/FootwearTest/Services/DeviceSettings.cs b/FootwearTest/Services/DeviceSettings.cs new file mode 100644 index 0000000..52d6fc5 --- /dev/null +++ b/FootwearTest/Services/DeviceSettings.cs @@ -0,0 +1,19 @@ +namespace FootwearTest.Services; + +public sealed class DeviceSettings +{ + public string Host { get; set; } = "127.0.0.1"; + public int Port { get; set; } = 502; + public bool UseSimulator { get; set; } = true; + public int PollIntervalMilliseconds { get; set; } = 1000; + public double FootAreaSquareMeters { get; set; } = 0.055; + public double MethodATargetTemperatureC { get; set; } = 35.0; + public double MethodBMoistureTargetTemperatureC { get; set; } = 35.0; + public double MethodBWarmthTargetTemperatureC { get; set; } = 38.0; + public double EnvironmentTemperatureC { get; set; } = 23.0; + public double EnvironmentHumidityPercent { get; set; } = 50.0; + public double MethodAAirSpeedMetersPerSecond { get; set; } = 1.00; + public double MethodBAirSpeedMetersPerSecond { get; set; } = 1.10; + public double PumpSpeedCubicCentimetersPerHour { get; set; } = 5.0; + public double CoefficientOfVariationLimitPercent { get; set; } = 8.0; +} diff --git a/FootwearTest/Services/ExcelReportService.cs b/FootwearTest/Services/ExcelReportService.cs new file mode 100644 index 0000000..f528e88 --- /dev/null +++ b/FootwearTest/Services/ExcelReportService.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public sealed class ExcelReportService +{ + public async Task ExportAsync(TestRunRecord record) + { + var directory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), + "整鞋试验报告"); + Directory.CreateDirectory(directory); + + var fileName = $"{record.CreatedAt:yyyyMMdd_HHmmss}_{SanitizeFileName(record.Method)}_{record.Id}.xlsx"; + var path = Path.Combine(directory, fileName); + await using var stream = File.Create(path); + WriteWorkbook(stream, CreateSheets(record)); + return path; + } + + private static IReadOnlyList CreateSheets(TestRunRecord record) + { + var sheets = new List + { + CreateCoverSheet(record), + CreateStandardChecklistSheet(record), + }; + + using var document = JsonDocument.Parse(record.DataJson); + var root = document.RootElement; + if (record.Method.StartsWith("方法 A", StringComparison.Ordinal)) + { + sheets.Add(CreateMethodAResultSheet(root)); + sheets.Add(CreateMethodASampleSheet(root)); + } + else if (record.Method.StartsWith("方法 B", StringComparison.Ordinal)) + { + sheets.Add(CreateMethodBResultSheet(root, record.Method)); + sheets.Add(CreateMethodBMassSheet(root)); + sheets.Add(CreateProcedureSheet(root)); + } + + sheets.Add(CreateRawJsonSheet(root)); + return sheets; + } + + private static WorksheetData CreateCoverSheet(TestRunRecord record) + { + return new WorksheetData( + "报告封面", + [ + Row("GB/T 33393-2023 整鞋试验报告"), + Row("报告项目", "内容"), + Row("记录编号", record.Id), + Row("文件编号", "GB/T 33393-2023"), + Row("使用的试验方法", record.Method), + Row("试样描述", record.SampleDescription), + Row("试验条件", record.Conditions), + Row("试验结果", record.ResultSummary), + Row("试验日期", record.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)), + Row("与试验方法的偏差", record.IsValid ? "无已知偏差" : "结果被标记为需复核或重测"), + ]); + } + + private static WorksheetData CreateStandardChecklistSheet(TestRunRecord record) + { + var rows = new List> + { + Row("GB/T 33393-2023 核对项"), + Row("类别", "标准要求", "本次记录"), + Row("报告", "报告包含文件编号、方法、试样描述、试验条件、试验结果、日期和偏差", "已写入报告封面"), + }; + + if (record.Method.StartsWith("方法 A", StringComparison.Ordinal)) + { + rows.Add(Row("方法 A 试样", "试样数量 2 只;试验前按 GB/T 22049 标准环境调节 24 h", record.SampleDescription)); + rows.Add(Row("方法 A 环境", "环境 (23±2) ℃、(50±5)% RH;空气流速 (1.00±0.15) m/s", record.Conditions)); + rows.Add(Row("方法 A 假脚", "假脚/模拟皮肤温度 (35.0±0.3) ℃;水汽化热 0.672 W·h/g", record.Conditions)); + rows.Add(Row("方法 A 采样", "每分钟记录一次,连续记录至少 30 次;结果取连续 30 次平均值", "见“方法A连续数据”")); + rows.Add(Row("方法 A 偏差", "任一测定值与两只鞋测试结果平均值的偏差不应超过 ±10%", record.IsValid ? "通过" : "需复核或重测")); + } + else if (record.Method.Contains("吸湿透水汽", StringComparison.Ordinal)) + { + rows.Add(Row("方法 B 试样", "满帮鞋同号两只;试验前在 (23±2) ℃、(50±5)% RH 标准环境调节", record.SampleDescription)); + rows.Add(Row("方法 B 供水", "泵流量 (5.0±0.3) cm3/h;测试周期 (180±1) min", record.Conditions)); + rows.Add(Row("方法 B 称重", "m11-m72 均精确到 0.01 g;m180 应在 (15±0.9) g 范围内", record.IsValid ? "m180 有效" : "m180 超限")); + rows.Add(Row("方法 B 结果", "透水汽性能 T180*;吸湿透水汽性能 XT180*=T180*+m3*+m4*,保留两位小数", record.ResultSummary)); + } + else + { + rows.Add(Row("方法 B 保暖", "假脚温度 (38±1) ℃;测试周期 (180±1) min,连续测试 2 个周期", record.Conditions)); + rows.Add(Row("方法 B 热阻", "Q=P/t×1000;R=S×(Tf-Tc)/Q,结果保留三位小数", record.ResultSummary)); + } + + return new WorksheetData("标准核对", rows); + } + + private static WorksheetData CreateMethodAResultSheet(JsonElement root) + { + var result = root.GetProperty("Result"); + return new WorksheetData( + "方法A结果", + [ + Row("项目", "数值", "单位/说明"), + Row("阶段", GetString(result, "Stage"), ""), + Row("皮肤湿阻 Res", GetDouble(result, "SkinMoistureResistance"), "Pa·m2/W"), + Row("整鞋平均湿阻 Re", GetDouble(result, "AverageMoistureResistance"), "Pa·m2/W"), + Row("整鞋平均热阻 Rt", GetDouble(result, "AverageThermalResistance"), "m2·℃/W"), + Row("湿阻变异系数", GetDouble(result, "MoistureCoefficientOfVariation"), "%"), + Row("热阻变异系数", GetDouble(result, "ThermalCoefficientOfVariation"), "%"), + Row("±10% 偏差检查", GetBool(result, "PassedDeviationCheck") ? "通过" : "需复核或重测", ""), + Row("湿热量 He", "He=λ×Q,λ=0.672 W·h/g", "W"), + Row("湿阻 Re", "Re=A×(Psi×RHsi-Pa×RHa)/He-Res", "Pa·m2/W"), + Row("干热量 Hd", "Hd=Hs-He", "W"), + Row("热阻 Rt", "Rt=A×(Ts-Ta)/Hd", "m2·℃/W"), + ]); + } + + private static WorksheetData CreateMethodASampleSheet(JsonElement root) + { + var rows = new List> + { + Row("序号", "时间", "假脚温度 ℃", "环境温度 ℃", "相对湿度 %", "出汗量 g/h", "加热功率 W", "湿热量 He W", "干热量 Hd W", "湿阻 Pa·m2/W", "热阻 m2·℃/W"), + }; + + if (root.TryGetProperty("Samples", out var samples) && samples.ValueKind == JsonValueKind.Array) + { + foreach (var sample in samples.EnumerateArray()) + { + var waterLoss = GetDouble(sample, "WaterLossGramsPerHour"); + var power = GetDouble(sample, "PowerWatts"); + var moistureHeat = sample.TryGetProperty("MoistureHeatWatts", out _) ? GetDouble(sample, "MoistureHeatWatts") : Math.Round(0.672 * Math.Max(waterLoss, 0.001), 3); + var dryHeat = sample.TryGetProperty("DryHeatWatts", out _) ? GetDouble(sample, "DryHeatWatts") : Math.Round(Math.Max(power - moistureHeat, 0.001), 3); + rows.Add(Row( + GetDouble(sample, "Index"), + GetString(sample, "Timestamp"), + GetDouble(sample, "FootTemperatureC"), + GetDouble(sample, "EnvironmentTemperatureC"), + GetDouble(sample, "RelativeHumidityPercent"), + waterLoss, + power, + moistureHeat, + dryHeat, + GetDouble(sample, "MoistureResistance"), + GetDouble(sample, "ThermalResistance"))); + } + } + + return new WorksheetData("方法A连续数据", rows); + } + + private static WorksheetData CreateMethodBResultSheet(JsonElement root, string method) + { + var result = root.GetProperty("Result"); + var rows = new List> + { + Row("项目", "数值", "单位/说明"), + }; + + if (method.Contains("吸湿透水汽", StringComparison.Ordinal)) + { + rows.Add(Row("m5", GetDouble(result, "M5"), "g,C1 消耗水量")); + rows.Add(Row("m6", GetDouble(result, "M6"), "g,C2 蒸发水量")); + rows.Add(Row("m7", GetDouble(result, "M7"), "g,C3 蒸发水量")); + rows.Add(Row("m8", GetDouble(result, "M8"), "g,C2/C3 平均蒸发量")); + rows.Add(Row("m180", GetDouble(result, "M180"), "g,泵进鞋腔内水总质量,15±0.9 g")); + rows.Add(Row("m1", GetDouble(result, "M1"), "g,模拟皮肤质量变化")); + rows.Add(Row("m2", GetDouble(result, "M2"), "g,标准长筒袜质量变化")); + rows.Add(Row("m3", GetDouble(result, "M3"), "g,样品鞋质量变化")); + rows.Add(Row("m4", GetDouble(result, "M4"), "g,鞋垫质量变化")); + rows.Add(Row("T180", GetDouble(result, "T180"), "g,散发到环境中的水汽质量")); + rows.Add(Row("m1*", GetDouble(result, "M1Corrected"), "g,按 15 g 修正")); + rows.Add(Row("m2*", GetDouble(result, "M2Corrected"), "g,按 15 g 修正")); + rows.Add(Row("m3*", GetDouble(result, "M3Corrected"), "g,按 15 g 修正")); + rows.Add(Row("m4*", GetDouble(result, "M4Corrected"), "g,按 15 g 修正")); + rows.Add(Row("T180*", GetDouble(result, "T180Corrected"), "g,透水汽性能")); + rows.Add(Row("XT180*", GetDouble(result, "XT180Corrected"), "g,吸湿透水汽性能")); + rows.Add(Row("有效性", GetBool(result, "IsValid") ? "有效" : "m180 超出 15±0.9 g", "")); + } + else + { + rows.Add(Row("P", GetDouble(result, "EnergyKilojoules"), "kJ,180 min 能量消耗")); + rows.Add(Row("t", GetDouble(result, "Seconds"), "s,测试周期")); + rows.Add(Row("Q", GetDouble(result, "HeatWatts"), "W,单位时间热量消耗")); + rows.Add(Row("Tf", GetDouble(result, "FootTemperatureC"), "℃,假脚表面温度平均值")); + rows.Add(Row("Tc", GetDouble(result, "ChamberTemperatureC"), "℃,测试箱环境温度平均值")); + rows.Add(Row("R", GetDouble(result, "ThermalResistance"), "m2·℃/W,保暖性能热阻值")); + } + + return new WorksheetData("方法B结果", rows); + } + + private static WorksheetData CreateMethodBMassSheet(JsonElement root) + { + var rows = new List> + { + Row("字段", "数值 g", "说明"), + }; + + if (root.TryGetProperty("Masses", out var masses)) + { + rows.Add(Row("m11", GetDouble(masses, "M11"), "模拟皮肤测试前质量")); + rows.Add(Row("m12", GetDouble(masses, "M12"), "模拟皮肤测试后质量")); + rows.Add(Row("m21", GetDouble(masses, "M21"), "标准长筒袜测试前质量")); + rows.Add(Row("m22", GetDouble(masses, "M22"), "标准长筒袜测试后质量")); + rows.Add(Row("m31", GetDouble(masses, "M31"), "样品鞋测试前质量")); + rows.Add(Row("m32", GetDouble(masses, "M32"), "样品鞋测试后质量")); + rows.Add(Row("m41", GetDouble(masses, "M41"), "鞋垫测试前质量")); + rows.Add(Row("m42", GetDouble(masses, "M42"), "鞋垫测试后质量")); + rows.Add(Row("m51", GetDouble(masses, "M51"), "C1 测试前质量")); + rows.Add(Row("m52", GetDouble(masses, "M52"), "C1 测试后质量")); + rows.Add(Row("m61", GetDouble(masses, "M61"), "C2 测试前质量")); + rows.Add(Row("m62", GetDouble(masses, "M62"), "C2 测试后质量")); + rows.Add(Row("m71", GetDouble(masses, "M71"), "C3 测试前质量")); + rows.Add(Row("m72", GetDouble(masses, "M72"), "C3 测试后质量")); + } + + return new WorksheetData("方法B称重", rows); + } + + private static WorksheetData CreateProcedureSheet(JsonElement root) + { + var rows = new List> + { + Row("序号", "流程记录"), + }; + + if (root.TryGetProperty("ProcedureLog", out var log) && log.ValueKind == JsonValueKind.Array) + { + var index = 1; + foreach (var item in log.EnumerateArray()) + { + rows.Add(Row(index++, item.GetString() ?? "")); + } + } + + return new WorksheetData("流程记录", rows); + } + + private static WorksheetData CreateRawJsonSheet(JsonElement root) + { + var rows = new List> + { + Row("路径", "值"), + }; + FlattenJson(rows, "", root); + return new WorksheetData("完整原始数据", rows); + } + + private static void FlattenJson(List> rows, string path, JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var nextPath = string.IsNullOrWhiteSpace(path) ? property.Name : $"{path}.{property.Name}"; + FlattenJson(rows, nextPath, property.Value); + } + break; + case JsonValueKind.Array: + var index = 0; + foreach (var item in element.EnumerateArray()) + { + FlattenJson(rows, $"{path}[{index++}]", item); + } + break; + case JsonValueKind.Number: + rows.Add(Row(path, element.TryGetDouble(out var number) ? number : element.GetRawText())); + break; + case JsonValueKind.True: + case JsonValueKind.False: + rows.Add(Row(path, element.GetBoolean() ? "true" : "false")); + break; + case JsonValueKind.String: + rows.Add(Row(path, element.GetString() ?? "")); + break; + default: + rows.Add(Row(path, "")); + break; + } + } + + private static void WriteWorkbook(Stream stream, IReadOnlyList sheets) + { + using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true); + AddText(archive, "[Content_Types].xml", CreateContentTypes(sheets.Count)); + AddText(archive, "_rels/.rels", """ + + + + + """); + AddText(archive, "xl/workbook.xml", CreateWorkbookXml(sheets)); + AddText(archive, "xl/_rels/workbook.xml.rels", CreateWorkbookRelationships(sheets.Count)); + AddText(archive, "xl/styles.xml", """ + + + + + + + + + + """); + + for (var i = 0; i < sheets.Count; i++) + { + AddText(archive, $"xl/worksheets/sheet{i + 1}.xml", CreateWorksheetXml(sheets[i])); + } + } + + private static string CreateContentTypes(int sheetCount) + { + var builder = new StringBuilder(); + builder.Append(""" + + + + + + + """); + for (var i = 1; i <= sheetCount; i++) + { + builder.Append(CultureInfo.InvariantCulture, $" \n"); + } + + builder.Append(""); + return builder.ToString(); + } + + private static string CreateWorkbookXml(IReadOnlyList sheets) + { + var builder = new StringBuilder(); + builder.Append(""" + + + + """); + for (var i = 0; i < sheets.Count; i++) + { + builder.Append(CultureInfo.InvariantCulture, $" \n"); + } + + builder.Append(""" + + + """); + return builder.ToString(); + } + + private static string CreateWorkbookRelationships(int sheetCount) + { + var builder = new StringBuilder(); + builder.Append(""" + + + """); + for (var i = 1; i <= sheetCount; i++) + { + builder.Append(CultureInfo.InvariantCulture, $" \n"); + } + + builder.Append(CultureInfo.InvariantCulture, $" \n"); + builder.Append(""); + return builder.ToString(); + } + + private static string CreateWorksheetXml(WorksheetData sheet) + { + var builder = new StringBuilder(); + builder.Append(""" + + + + + + + + + + """); + + for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++) + { + var rowNumber = rowIndex + 1; + builder.Append(CultureInfo.InvariantCulture, $" \n"); + var row = sheet.Rows[rowIndex]; + for (var columnIndex = 0; columnIndex < row.Count; columnIndex++) + { + AppendCell(builder, row[columnIndex], rowNumber, columnIndex + 1); + } + + builder.Append(" \n"); + } + + builder.Append(""" + + + """); + return builder.ToString(); + } + + private static void AppendCell(StringBuilder builder, object? value, int row, int column) + { + var reference = $"{ColumnName(column)}{row}"; + switch (value) + { + case null: + builder.Append(CultureInfo.InvariantCulture, $" \n"); + break; + case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal: + builder.Append(CultureInfo.InvariantCulture, $" {Convert.ToString(value, CultureInfo.InvariantCulture)}\n"); + break; + default: + builder.Append(CultureInfo.InvariantCulture, $" {EscapeXml(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")}\n"); + break; + } + } + + private static void AddText(ZipArchive archive, string name, string content) + { + var entry = archive.CreateEntry(name, CompressionLevel.Optimal); + using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + writer.Write(content); + } + + private static IReadOnlyList Row(params object?[] values) => values; + + private static double GetDouble(JsonElement element, string property) + { + return element.TryGetProperty(property, out var value) && value.TryGetDouble(out var number) ? number : 0.0; + } + + private static string GetString(JsonElement element, string property) + { + return element.TryGetProperty(property, out var value) ? value.ToString() : ""; + } + + private static bool GetBool(JsonElement element, string property) + { + return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.True; + } + + private static string ColumnName(int column) + { + var name = ""; + while (column > 0) + { + column--; + name = (char)('A' + column % 26) + name; + column /= 26; + } + + return name; + } + + private static string EscapeXml(string value) + { + return value + .Replace("&", "&", StringComparison.Ordinal) + .Replace("<", "<", StringComparison.Ordinal) + .Replace(">", ">", StringComparison.Ordinal) + .Replace("\"", """, StringComparison.Ordinal) + .Replace("'", "'", StringComparison.Ordinal); + } + + private static string TrimSheetName(string name) + { + return name.Length <= 31 ? name : name[..31]; + } + + private static string SanitizeFileName(string value) + { + var invalid = Path.GetInvalidFileNameChars(); + return new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray()); + } + + private sealed record WorksheetData(string Name, IReadOnlyList> Rows); +} diff --git a/FootwearTest/Services/IDeviceClient.cs b/FootwearTest/Services/IDeviceClient.cs new file mode 100644 index 0000000..38d83b9 --- /dev/null +++ b/FootwearTest/Services/IDeviceClient.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public interface IDeviceClient +{ + bool IsConnected { get; } + string ConnectionText { get; } + DeviceSnapshot LastSnapshot { get; } + event EventHandler? SnapshotUpdated; + + Task ConnectAsync(CancellationToken cancellationToken = default); + Task DisconnectAsync(CancellationToken cancellationToken = default); + Task ReadSnapshotAsync(CancellationToken cancellationToken = default); + Task SetOutputsAsync(bool pumpRunning, bool fanRunning, bool heaterRunning, CancellationToken cancellationToken = default); +} diff --git a/FootwearTest/Services/ModbusTcpDeviceClient.cs b/FootwearTest/Services/ModbusTcpDeviceClient.cs new file mode 100644 index 0000000..678bef1 --- /dev/null +++ b/FootwearTest/Services/ModbusTcpDeviceClient.cs @@ -0,0 +1,60 @@ +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public sealed class ModbusTcpDeviceClient : IDeviceClient +{ + private readonly DeviceSettings _settings; + private TcpClient? _tcpClient; + + public ModbusTcpDeviceClient(DeviceSettings settings) + { + _settings = settings; + LastSnapshot = DeviceSnapshot.Initial with { AlarmText = "Modbus 点表未配置,当前仅验证连接" }; + } + + public bool IsConnected => _tcpClient?.Connected == true; + public string ConnectionText => IsConnected ? $"Modbus TCP {_settings.Host}:{_settings.Port} 已连接" : "Modbus TCP 未连接"; + public DeviceSnapshot LastSnapshot { get; private set; } + public event EventHandler? SnapshotUpdated; + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + _tcpClient?.Dispose(); + _tcpClient = new TcpClient(); + await _tcpClient.ConnectAsync(_settings.Host, _settings.Port, cancellationToken); + LastSnapshot = LastSnapshot with { Timestamp = DateTime.Now, AlarmText = "等待配置真实 Modbus 寄存器点表" }; + SnapshotUpdated?.Invoke(this, LastSnapshot); + } + + public Task DisconnectAsync(CancellationToken cancellationToken = default) + { + _tcpClient?.Dispose(); + _tcpClient = null; + return Task.CompletedTask; + } + + public Task ReadSnapshotAsync(CancellationToken cancellationToken = default) + { + LastSnapshot = LastSnapshot with { Timestamp = DateTime.Now }; + SnapshotUpdated?.Invoke(this, LastSnapshot); + return Task.FromResult(LastSnapshot); + } + + public Task SetOutputsAsync(bool pumpRunning, bool fanRunning, bool heaterRunning, CancellationToken cancellationToken = default) + { + LastSnapshot = LastSnapshot with + { + Timestamp = DateTime.Now, + PumpRunning = pumpRunning, + FanRunning = fanRunning, + HeaterRunning = heaterRunning + }; + SnapshotUpdated?.Invoke(this, LastSnapshot); + return Task.CompletedTask; + } +} diff --git a/FootwearTest/Services/ReportService.cs b/FootwearTest/Services/ReportService.cs new file mode 100644 index 0000000..7fcb672 --- /dev/null +++ b/FootwearTest/Services/ReportService.cs @@ -0,0 +1,26 @@ +using System.Text; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public sealed class ReportService +{ + public string CreateTextReport(TestRunRecord record) + { + var builder = new StringBuilder(); + builder.AppendLine("GB/T 33393-2023 整鞋热阻和湿阻测定报告"); + builder.AppendLine(); + builder.AppendLine($"记录编号: {record.Id}"); + builder.AppendLine($"文件编号: GB/T 33393-2023"); + builder.AppendLine($"使用的试验方法: {record.Method}"); + builder.AppendLine($"试样描述: {record.SampleDescription}"); + builder.AppendLine($"试验条件: {record.Conditions}"); + builder.AppendLine($"试验结果: {record.ResultSummary}"); + builder.AppendLine($"试验日期: {record.CreatedAt:yyyy-MM-dd HH:mm:ss}"); + builder.AppendLine($"与方法偏差: {(record.IsValid ? "无已知偏差" : "结果被标记为需复核或重测")}"); + builder.AppendLine(); + builder.AppendLine("原始数据摘要:"); + builder.AppendLine(record.DataJson); + return builder.ToString(); + } +} diff --git a/FootwearTest/Services/SimulatedDeviceClient.cs b/FootwearTest/Services/SimulatedDeviceClient.cs new file mode 100644 index 0000000..7833996 --- /dev/null +++ b/FootwearTest/Services/SimulatedDeviceClient.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public sealed class SimulatedDeviceClient : IDeviceClient +{ + private readonly DeviceSettings _settings; + private readonly Random _random = new(); + private bool _pumpRunning; + private bool _fanRunning; + private bool _heaterRunning; + private double _energyKilojoules; + + public SimulatedDeviceClient(DeviceSettings settings) + { + _settings = settings; + LastSnapshot = DeviceSnapshot.Initial; + } + + public bool IsConnected { get; private set; } + public string ConnectionText => IsConnected ? "模拟器已连接" : "未连接"; + public DeviceSnapshot LastSnapshot { get; private set; } + public event EventHandler? SnapshotUpdated; + + public Task ConnectAsync(CancellationToken cancellationToken = default) + { + IsConnected = true; + return ReadSnapshotAsync(cancellationToken); + } + + public Task DisconnectAsync(CancellationToken cancellationToken = default) + { + IsConnected = false; + _pumpRunning = false; + _fanRunning = false; + _heaterRunning = false; + return Task.CompletedTask; + } + + public Task SetOutputsAsync(bool pumpRunning, bool fanRunning, bool heaterRunning, CancellationToken cancellationToken = default) + { + _pumpRunning = pumpRunning; + _fanRunning = fanRunning; + _heaterRunning = heaterRunning; + return ReadSnapshotAsync(cancellationToken); + } + + public Task ReadSnapshotAsync(CancellationToken cancellationToken = default) + { + var targetFoot = _heaterRunning ? _settings.MethodATargetTemperatureC : _settings.EnvironmentTemperatureC; + var previous = LastSnapshot; + var foot = previous.FootTemperatureC + (targetFoot - previous.FootTemperatureC) * 0.18 + Noise(0.08); + var chamber = _settings.EnvironmentTemperatureC + Noise(0.12); + var humidity = _settings.EnvironmentHumidityPercent + Noise(0.35); + var airSpeed = (_fanRunning ? _settings.MethodBAirSpeedMetersPerSecond : 0.05) + Noise(0.03); + var waterLoss = _pumpRunning ? _settings.PumpSpeedCubicCentimetersPerHour + Noise(0.12) : Math.Max(0, Noise(0.03)); + var power = _heaterRunning ? 4.2 + Math.Max(0, targetFoot - chamber) * 0.03 + Noise(0.12) : 0.0; + + _energyKilojoules += power * _settings.PollIntervalMilliseconds / 1_000_000.0; + LastSnapshot = new DeviceSnapshot( + DateTime.Now, + Math.Round(foot, 2), + Math.Round(chamber, 2), + Math.Round(humidity, 2), + Math.Round(Math.Max(0, airSpeed), 2), + Math.Round(Math.Max(0, power), 2), + Math.Round(_energyKilojoules, 3), + Math.Round(Math.Max(0, waterLoss), 2), + _settings.PumpSpeedCubicCentimetersPerHour, + _pumpRunning, + _fanRunning, + _heaterRunning, + "无报警"); + + SnapshotUpdated?.Invoke(this, LastSnapshot); + return Task.FromResult(LastSnapshot); + } + + private double Noise(double amplitude) => (_random.NextDouble() * 2.0 - 1.0) * amplitude; +} diff --git a/FootwearTest/Services/TestFormulaService.cs b/FootwearTest/Services/TestFormulaService.cs new file mode 100644 index 0000000..a5f11bf --- /dev/null +++ b/FootwearTest/Services/TestFormulaService.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FootwearTest.Models; + +namespace FootwearTest.Services; + +public sealed class TestFormulaService +{ + private const double VaporizationHeatWhPerGram = 0.672; + + public double CalculateSkinMoistureResistance( + double footAreaSquareMeters, + double skinSaturatedVaporPressurePa, + double skinRelativeHumidity, + double environmentSaturatedVaporPressurePa, + double environmentRelativeHumidity, + double nakedFootSweatGramsPerHour) + { + return footAreaSquareMeters * + (skinSaturatedVaporPressurePa * skinRelativeHumidity - environmentSaturatedVaporPressurePa * environmentRelativeHumidity) / + (VaporizationHeatWhPerGram * Math.Max(nakedFootSweatGramsPerHour, 0.001)); + } + + public MethodASampleRecord CalculateMethodASample( + int index, + DeviceSnapshot snapshot, + double footAreaSquareMeters, + double skinMoistureResistance, + double skinSaturatedVaporPressurePa = 5623.0, + double environmentSaturatedVaporPressurePa = 2809.0) + { + var he = VaporizationHeatWhPerGram * Math.Max(snapshot.WaterLossGramsPerHour, 0.001); + var re = footAreaSquareMeters * + (skinSaturatedVaporPressurePa - environmentSaturatedVaporPressurePa * snapshot.EnvironmentHumidityPercent / 100.0) / + he - skinMoistureResistance; + var hd = Math.Max(snapshot.PowerWatts - he, 0.001); + var rt = footAreaSquareMeters * (snapshot.FootTemperatureC - snapshot.EnvironmentTemperatureC) / hd; + + return new MethodASampleRecord( + index, + snapshot.Timestamp, + snapshot.FootTemperatureC, + snapshot.EnvironmentTemperatureC, + snapshot.EnvironmentHumidityPercent, + snapshot.WaterLossGramsPerHour, + snapshot.PowerWatts, + Math.Round(he, 3), + Math.Round(hd, 3), + Math.Round(re, 3), + Math.Round(rt, 4)); + } + + public double Average(IEnumerable values) + { + var list = values.ToArray(); + return list.Length == 0 ? 0 : list.Average(); + } + + public double CoefficientOfVariationPercent(IEnumerable values) + { + var list = values.ToArray(); + if (list.Length < 2) + { + return 0; + } + + var average = list.Average(); + if (Math.Abs(average) < 0.000001) + { + return 0; + } + + var variance = list.Sum(value => Math.Pow(value - average, 2)) / (list.Length - 1); + return Math.Sqrt(variance) / Math.Abs(average) * 100.0; + } + + public bool PassesTenPercentDeviation(IEnumerable values) + { + var list = values.ToArray(); + if (list.Length == 0) + { + return false; + } + + var average = list.Average(); + return list.All(value => Math.Abs(value - average) <= Math.Abs(average) * 0.10); + } + + public MethodBMoistureResult CalculateMethodBMoisture( + double m11, + double m12, + double m21, + double m22, + double m31, + double m32, + double m41, + double m42, + double m51, + double m52, + double m61, + double m62, + double m71, + double m72) + { + var m5 = m51 - m52; + var m6 = m61 - m62; + var m7 = m71 - m72; + var m8 = (m6 + m7) / 2.0; + var m180 = m5 - m8; + var scale = 15.0 / Math.Max(m180, 0.001); + + var m1 = m12 - m11; + var m2 = m22 - m21; + var m3 = m32 - m31; + var m4 = m42 - m41; + var t180 = m180 - m1 - m2 - m3 - m4; + + return new MethodBMoistureResult( + Math.Round(m1, 3), + Math.Round(m2, 3), + Math.Round(m3, 3), + Math.Round(m4, 3), + Math.Round(m5, 3), + Math.Round(m6, 3), + Math.Round(m7, 3), + Math.Round(m8, 3), + Math.Round(m180, 3), + Math.Round(t180, 3), + Math.Round(m1 * scale, 3), + Math.Round(m2 * scale, 3), + Math.Round(m3 * scale, 3), + Math.Round(m4 * scale, 3), + Math.Round(t180 * scale, 3), + Math.Round((t180 + m3 + m4) * scale, 3), + m180 >= 14.1 && m180 <= 15.9); + } + + public MethodBWarmthResult CalculateMethodBWarmth( + double energyKilojoules, + double seconds, + double footAreaSquareMeters, + double footTemperatureC, + double chamberTemperatureC) + { + var heatWatts = energyKilojoules / Math.Max(seconds, 1.0) * 1000.0; + var resistance = footAreaSquareMeters * (footTemperatureC - chamberTemperatureC) / Math.Max(heatWatts, 0.001); + return new MethodBWarmthResult( + Math.Round(energyKilojoules, 1), + seconds, + Math.Round(heatWatts, 3), + Math.Round(footTemperatureC, 2), + Math.Round(chamberTemperatureC, 2), + Math.Round(resistance, 3)); + } +} diff --git a/FootwearTest/Services/TestRunRepository.cs b/FootwearTest/Services/TestRunRepository.cs new file mode 100644 index 0000000..7537a9d --- /dev/null +++ b/FootwearTest/Services/TestRunRepository.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FootwearTest.Models; +using Microsoft.Data.Sqlite; + +namespace FootwearTest.Services; + +public sealed class TestRunRepository +{ + private readonly string _databasePath; + + public TestRunRepository() + { + var directory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "FootwearTest"); + Directory.CreateDirectory(directory); + _databasePath = Path.Combine(directory, "footwear-test.db"); + EnsureCreated(); + } + + public async Task SaveAsync(TestRunRecord record) + { + await using var connection = CreateConnection(); + await connection.OpenAsync(); + var command = connection.CreateCommand(); + command.CommandText = """ + INSERT INTO TestRuns(Method, SampleDescription, Conditions, ResultSummary, DataJson, IsValid, CreatedAt) + VALUES($method, $sample, $conditions, $summary, $data, $valid, $createdAt); + SELECT last_insert_rowid(); + """; + command.Parameters.AddWithValue("$method", record.Method); + command.Parameters.AddWithValue("$sample", record.SampleDescription); + command.Parameters.AddWithValue("$conditions", record.Conditions); + command.Parameters.AddWithValue("$summary", record.ResultSummary); + command.Parameters.AddWithValue("$data", record.DataJson); + command.Parameters.AddWithValue("$valid", record.IsValid ? 1 : 0); + command.Parameters.AddWithValue("$createdAt", record.CreatedAt.ToString("O")); + var id = (long)(await command.ExecuteScalarAsync() ?? 0L); + return id; + } + + public async Task> GetRecentAsync(int take = 50) + { + var items = new List(); + await using var connection = CreateConnection(); + await connection.OpenAsync(); + var command = connection.CreateCommand(); + command.CommandText = """ + SELECT Id, Method, SampleDescription, ResultSummary, IsValid, CreatedAt + FROM TestRuns + ORDER BY Id DESC + LIMIT $take; + """; + command.Parameters.AddWithValue("$take", take); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + items.Add(new TestRunSummary( + reader.GetInt64(0), + reader.GetString(1), + reader.GetString(2), + reader.GetString(3), + reader.GetInt32(4) == 1, + DateTime.Parse(reader.GetString(5)))); + } + + return items; + } + + public async Task GetLatestAsync() + { + await using var connection = CreateConnection(); + await connection.OpenAsync(); + var command = connection.CreateCommand(); + command.CommandText = """ + SELECT Id, Method, SampleDescription, Conditions, ResultSummary, DataJson, IsValid, CreatedAt + FROM TestRuns + ORDER BY Id DESC + LIMIT 1; + """; + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? ReadRecord(reader) : null; + } + + public async Task GetByIdAsync(long id) + { + await using var connection = CreateConnection(); + await connection.OpenAsync(); + var command = connection.CreateCommand(); + command.CommandText = """ + SELECT Id, Method, SampleDescription, Conditions, ResultSummary, DataJson, IsValid, CreatedAt + FROM TestRuns + WHERE Id = $id; + """; + command.Parameters.AddWithValue("$id", id); + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? ReadRecord(reader) : null; + } + + private TestRunRecord ReadRecord(SqliteDataReader reader) + { + return new TestRunRecord( + reader.GetInt64(0), + reader.GetString(1), + reader.GetString(2), + reader.GetString(3), + reader.GetString(4), + reader.GetString(5), + reader.GetInt32(6) == 1, + DateTime.Parse(reader.GetString(7))); + } + + private SqliteConnection CreateConnection() => new($"Data Source={_databasePath}"); + + private void EnsureCreated() + { + using var connection = CreateConnection(); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = """ + CREATE TABLE IF NOT EXISTS TestRuns + ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Method TEXT NOT NULL, + SampleDescription TEXT NOT NULL, + Conditions TEXT NOT NULL, + ResultSummary TEXT NOT NULL, + DataJson TEXT NOT NULL, + IsValid INTEGER NOT NULL, + CreatedAt TEXT NOT NULL + ); + """; + command.ExecuteNonQuery(); + } +} diff --git a/FootwearTest/ViewLocator.cs b/FootwearTest/ViewLocator.cs new file mode 100644 index 0000000..c9c2cbf --- /dev/null +++ b/FootwearTest/ViewLocator.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using FootwearTest.ViewModels; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace FootwearTest +{ + /// + /// Given a view model, returns the corresponding view if possible. + /// + [RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] + public class ViewLocator : IDataTemplate + { + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } + } +} diff --git a/FootwearTest/ViewModels/DashboardViewModel.cs b/FootwearTest/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..916e658 --- /dev/null +++ b/FootwearTest/ViewModels/DashboardViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class DashboardViewModel : ViewModelBase +{ + private readonly TestRunRepository _repository; + + public DashboardViewModel(TestRunRepository repository) + { + _repository = repository; + RefreshCommand = new AsyncRelayCommand(RefreshAsync); + _ = RefreshAsync(); + } + + private DeviceSnapshot _snapshot = DeviceSnapshot.Initial; + + public ObservableCollection RecentRuns { get; } = []; + public IAsyncRelayCommand RefreshCommand { get; } + + public DeviceSnapshot Snapshot + { + get => _snapshot; + set => SetProperty(ref _snapshot, value); + } + + private async Task RefreshAsync() + { + RecentRuns.Clear(); + foreach (var run in await _repository.GetRecentAsync(8)) + { + RecentRuns.Add(run); + } + } +} diff --git a/FootwearTest/ViewModels/HistoryViewModel.cs b/FootwearTest/ViewModels/HistoryViewModel.cs new file mode 100644 index 0000000..147a3d2 --- /dev/null +++ b/FootwearTest/ViewModels/HistoryViewModel.cs @@ -0,0 +1,97 @@ +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class HistoryViewModel : ViewModelBase +{ + private readonly TestRunRepository _repository; + private readonly ReportService _reportService; + private readonly ExcelReportService _excelReportService; + + public HistoryViewModel(TestRunRepository repository, ReportService reportService, ExcelReportService excelReportService) + { + _repository = repository; + _reportService = reportService; + _excelReportService = excelReportService; + LoadCommand = new AsyncRelayCommand(LoadAsync); + ExportSelectedExcelCommand = new AsyncRelayCommand(ExportSelectedExcelAsync); + _ = LoadAsync(); + } + + public ObservableCollection Runs { get; } = []; + public IAsyncRelayCommand LoadCommand { get; } + public IAsyncRelayCommand ExportSelectedExcelCommand { get; } + + private TestRunSummary? _selectedRun; + private string _selectedReportText = "选择一条历史记录查看报告。"; + private string _exportStatusText = "选择历史记录后可导出完整 Excel 报告。"; + + public TestRunSummary? SelectedRun + { + get => _selectedRun; + set + { + if (SetProperty(ref _selectedRun, value)) + { + _ = LoadSelectedReportAsync(value); + } + } + } + + public string SelectedReportText + { + get => _selectedReportText; + set => SetProperty(ref _selectedReportText, value); + } + + public string ExportStatusText + { + get => _exportStatusText; + set => SetProperty(ref _exportStatusText, value); + } + + private async Task LoadAsync() + { + Runs.Clear(); + foreach (var run in await _repository.GetRecentAsync()) + { + Runs.Add(run); + } + } + + private async Task LoadSelectedReportAsync(TestRunSummary? value) + { + if (value is null) + { + SelectedReportText = "选择一条历史记录查看报告。"; + return; + } + + var record = await _repository.GetByIdAsync(value.Id); + SelectedReportText = record is null ? "记录不存在。" : _reportService.CreateTextReport(record); + ExportStatusText = record is null ? "记录不存在,无法导出。" : "可导出所选记录的完整 Excel 报告。"; + } + + private async Task ExportSelectedExcelAsync() + { + if (SelectedRun is null) + { + ExportStatusText = "请先选择一条历史记录。"; + return; + } + + var record = await _repository.GetByIdAsync(SelectedRun.Id); + if (record is null) + { + ExportStatusText = "记录不存在,无法导出。"; + return; + } + + var path = await _excelReportService.ExportAsync(record); + ExportStatusText = $"已导出 Excel: {path}"; + } +} diff --git a/FootwearTest/ViewModels/MainWindowViewModel.cs b/FootwearTest/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..1442dae --- /dev/null +++ b/FootwearTest/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + private readonly IDeviceClient _deviceClient; + private readonly DispatcherTimer _pollTimer; + + public MainWindowViewModel( + IDeviceClient deviceClient, + DashboardViewModel dashboard, + MethodAViewModel methodA, + MethodBViewModel methodB, + HistoryViewModel history, + ReportViewModel report, + SettingsViewModel settings) + { + _deviceClient = deviceClient; + Dashboard = dashboard; + MethodA = methodA; + MethodB = methodB; + History = history; + Report = report; + Settings = settings; + CurrentPage = Dashboard; + CurrentPageTitle = "运行总览"; + ShowDashboardCommand = new RelayCommand(ShowDashboard); + ShowMethodACommand = new RelayCommand(ShowMethodA); + ShowMethodBCommand = new RelayCommand(ShowMethodB); + ShowHistoryCommand = new RelayCommand(ShowHistory); + ShowReportCommand = new RelayCommand(ShowReport); + ShowSettingsCommand = new RelayCommand(ShowSettings); + ConnectDeviceCommand = new AsyncRelayCommand(ConnectDeviceAsync); + DisconnectDeviceCommand = new AsyncRelayCommand(DisconnectDeviceAsync); + + _pollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _pollTimer.Tick += async (_, _) => await RefreshSnapshotAsync(); + _pollTimer.Start(); + } + + public DashboardViewModel Dashboard { get; } + public MethodAViewModel MethodA { get; } + public MethodBViewModel MethodB { get; } + public HistoryViewModel History { get; } + public ReportViewModel Report { get; } + public SettingsViewModel Settings { get; } + public IRelayCommand ShowDashboardCommand { get; } + public IRelayCommand ShowMethodACommand { get; } + public IRelayCommand ShowMethodBCommand { get; } + public IRelayCommand ShowHistoryCommand { get; } + public IRelayCommand ShowReportCommand { get; } + public IRelayCommand ShowSettingsCommand { get; } + public IAsyncRelayCommand ConnectDeviceCommand { get; } + public IAsyncRelayCommand DisconnectDeviceCommand { get; } + + private ViewModelBase _currentPage = null!; + private string _currentPageTitle = ""; + private DeviceSnapshot _snapshot = DeviceSnapshot.Initial; + private string _connectionText = "未连接"; + + public ViewModelBase CurrentPage + { + get => _currentPage; + set => SetProperty(ref _currentPage, value); + } + + public string CurrentPageTitle + { + get => _currentPageTitle; + set => SetProperty(ref _currentPageTitle, value); + } + + public DeviceSnapshot Snapshot + { + get => _snapshot; + set => SetProperty(ref _snapshot, value); + } + + public string ConnectionText + { + get => _connectionText; + set => SetProperty(ref _connectionText, value); + } + + private void ShowDashboard() => Navigate(Dashboard, "运行总览"); + + private void ShowMethodA() => Navigate(MethodA, "方法 A - 热阻/湿阻"); + + private void ShowMethodB() => Navigate(MethodB, "方法 B - 吸湿透水汽/保暖"); + + private void ShowHistory() => Navigate(History, "历史记录"); + + private void ShowReport() => Navigate(Report, "试验报告"); + + private void ShowSettings() => Navigate(Settings, "系统设置"); + + private async Task ConnectDeviceAsync() + { + await _deviceClient.ConnectAsync(); + await RefreshSnapshotAsync(); + } + + private async Task DisconnectDeviceAsync() + { + await _deviceClient.DisconnectAsync(); + ConnectionText = _deviceClient.ConnectionText; + } + + private void Navigate(ViewModelBase page, string title) + { + CurrentPage = page; + CurrentPageTitle = title; + } + + private async Task RefreshSnapshotAsync() + { + if (_deviceClient.IsConnected) + { + Snapshot = await _deviceClient.ReadSnapshotAsync(); + Dashboard.Snapshot = Snapshot; + } + + ConnectionText = _deviceClient.ConnectionText; + } +} diff --git a/FootwearTest/ViewModels/MethodAViewModel.cs b/FootwearTest/ViewModels/MethodAViewModel.cs new file mode 100644 index 0000000..74e515b --- /dev/null +++ b/FootwearTest/ViewModels/MethodAViewModel.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class MethodAViewModel : ViewModelBase +{ + private readonly IDeviceClient _deviceClient; + private readonly DeviceSettings _settings; + private readonly TestFormulaService _formulaService; + private readonly TestRunRepository _repository; + + public MethodAViewModel( + IDeviceClient deviceClient, + DeviceSettings settings, + TestFormulaService formulaService, + TestRunRepository repository) + { + _deviceClient = deviceClient; + _settings = settings; + _formulaService = formulaService; + _repository = repository; + MeasureSkinResistanceCommand = new AsyncRelayCommand(MeasureSkinResistanceAsync); + RunWholeShoeCommand = new AsyncRelayCommand(RunWholeShoeAsync); + StopCommand = new AsyncRelayCommand(StopAsync); + } + + public ObservableCollection Samples { get; } = []; + public IAsyncRelayCommand MeasureSkinResistanceCommand { get; } + public IAsyncRelayCommand RunWholeShoeCommand { get; } + public IAsyncRelayCommand StopCommand { get; } + + private string _sampleDescription = "整鞋样品 1# / 2#"; + private string _statusText = "待机"; + private bool _isRunning; + private double _skinMoistureResistance = 6.0; + private double _averageMoistureResistance; + private double _averageThermalResistance; + private double _moistureCvPercent; + private double _thermalCvPercent; + private string _resultText = "尚未开始试验"; + + public string SampleDescription + { + get => _sampleDescription; + set => SetProperty(ref _sampleDescription, value); + } + + public string StatusText + { + get => _statusText; + set => SetProperty(ref _statusText, value); + } + + public bool IsRunning + { + get => _isRunning; + set => SetProperty(ref _isRunning, value); + } + + public double SkinMoistureResistance + { + get => _skinMoistureResistance; + set => SetProperty(ref _skinMoistureResistance, value); + } + + public double AverageMoistureResistance + { + get => _averageMoistureResistance; + set => SetProperty(ref _averageMoistureResistance, value); + } + + public double AverageThermalResistance + { + get => _averageThermalResistance; + set => SetProperty(ref _averageThermalResistance, value); + } + + public double MoistureCvPercent + { + get => _moistureCvPercent; + set => SetProperty(ref _moistureCvPercent, value); + } + + public double ThermalCvPercent + { + get => _thermalCvPercent; + set => SetProperty(ref _thermalCvPercent, value); + } + + public string ResultText + { + get => _resultText; + set => SetProperty(ref _resultText, value); + } + + private async Task MeasureSkinResistanceAsync() + { + if (IsRunning) + { + return; + } + + await EnsureConnectedAsync(); + IsRunning = true; + StatusText = "皮肤湿阻测定中"; + Samples.Clear(); + await _deviceClient.SetOutputsAsync(true, true, true); + + for (var i = 1; i <= 30; i++) + { + await Task.Delay(60); + var snapshot = await _deviceClient.ReadSnapshotAsync(); + var value = _formulaService.CalculateSkinMoistureResistance( + _settings.FootAreaSquareMeters, + 5620.0, + 1.0, + 2809.0, + snapshot.EnvironmentHumidityPercent / 100.0, + snapshot.WaterLossGramsPerHour); + SkinMoistureResistance = Math.Round(value, 3); + Samples.Add(_formulaService.CalculateMethodASample(i, snapshot, _settings.FootAreaSquareMeters, SkinMoistureResistance)); + } + + await _deviceClient.SetOutputsAsync(false, true, false); + UpdateMethodAResult("皮肤湿阻"); + await SaveRunAsync("方法 A - 皮肤湿阻", true); + StatusText = "皮肤湿阻测定完成"; + IsRunning = false; + } + + private async Task RunWholeShoeAsync() + { + if (IsRunning) + { + return; + } + + await EnsureConnectedAsync(); + IsRunning = true; + StatusText = "整鞋热阻/湿阻测定中"; + Samples.Clear(); + await _deviceClient.SetOutputsAsync(true, true, true); + + for (var i = 1; i <= 30; i++) + { + await Task.Delay(80); + var snapshot = await _deviceClient.ReadSnapshotAsync(); + Samples.Add(_formulaService.CalculateMethodASample(i, snapshot, _settings.FootAreaSquareMeters, SkinMoistureResistance)); + UpdateMethodAResult("整鞋热阻/湿阻"); + } + + await _deviceClient.SetOutputsAsync(false, true, false); + var passed = _formulaService.PassesTenPercentDeviation(Samples.Select(sample => sample.ThermalResistance)); + UpdateMethodAResult("整鞋热阻/湿阻"); + await SaveRunAsync("方法 A - 整鞋热阻/湿阻", passed); + StatusText = passed ? "试验完成" : "结果偏差超过 ±10%,建议重测"; + IsRunning = false; + } + + private async Task StopAsync() + { + await _deviceClient.SetOutputsAsync(false, false, false); + IsRunning = false; + StatusText = "已停止"; + } + + private async Task EnsureConnectedAsync() + { + if (!_deviceClient.IsConnected) + { + await _deviceClient.ConnectAsync(); + } + } + + private void UpdateMethodAResult(string stage) + { + AverageMoistureResistance = Math.Round(_formulaService.Average(Samples.Select(sample => sample.MoistureResistance)), 3); + AverageThermalResistance = Math.Round(_formulaService.Average(Samples.Select(sample => sample.ThermalResistance)), 4); + MoistureCvPercent = Math.Round(_formulaService.CoefficientOfVariationPercent(Samples.Select(sample => sample.MoistureResistance)), 2); + ThermalCvPercent = Math.Round(_formulaService.CoefficientOfVariationPercent(Samples.Select(sample => sample.ThermalResistance)), 2); + ResultText = $"{stage}: 湿阻 {AverageMoistureResistance:F3} Pa·m²/W,热阻 {AverageThermalResistance:F4} m²·℃/W,CV {ThermalCvPercent:F2}%"; + } + + private async Task SaveRunAsync(string method, bool isValid) + { + var result = new MethodAResult( + method, + SkinMoistureResistance, + AverageMoistureResistance, + AverageThermalResistance, + MoistureCvPercent, + ThermalCvPercent, + isValid); + var data = JsonSerializer.Serialize(new { Result = result, Samples }, new JsonSerializerOptions { WriteIndented = true }); + await _repository.SaveAsync(new TestRunRecord( + 0, + method, + SampleDescription, + $"假脚 {_settings.MethodATargetTemperatureC:F1} ℃;环境 {_settings.EnvironmentTemperatureC:F1} ℃ / {_settings.EnvironmentHumidityPercent:F0}%;风速 {_settings.MethodAAirSpeedMetersPerSecond:F2} m/s", + ResultText, + data, + isValid, + DateTime.Now)); + } +} diff --git a/FootwearTest/ViewModels/MethodBViewModel.cs b/FootwearTest/ViewModels/MethodBViewModel.cs new file mode 100644 index 0000000..8e0ca2c --- /dev/null +++ b/FootwearTest/ViewModels/MethodBViewModel.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class MethodBViewModel : ViewModelBase +{ + private readonly IDeviceClient _deviceClient; + private readonly DeviceSettings _settings; + private readonly TestFormulaService _formulaService; + private readonly TestRunRepository _repository; + + public MethodBViewModel( + IDeviceClient deviceClient, + DeviceSettings settings, + TestFormulaService formulaService, + TestRunRepository repository) + { + _deviceClient = deviceClient; + _settings = settings; + _formulaService = formulaService; + _repository = repository; + RunMoistureCommand = new AsyncRelayCommand(RunMoistureAsync); + RunWarmthCommand = new AsyncRelayCommand(RunWarmthAsync); + } + + public ObservableCollection ProcedureLog { get; } = []; + public IAsyncRelayCommand RunMoistureCommand { get; } + public IAsyncRelayCommand RunWarmthCommand { get; } + + private string _sampleDescription = "满帮鞋同号两只"; + private string _statusText = "待机"; + private string _moistureResultText = "尚未计算"; + private string _warmthResultText = "尚未计算"; + private string _moistureDetailText = "m180、T180*、XT180* 完成后显示。"; + private string _warmthDetailText = "Q、R 完成后显示。"; + private double _m11 = 6.00; + private double _m12 = 7.10; + private double _m21 = 14.00; + private double _m22 = 16.40; + private double _m31 = 410.00; + private double _m32 = 413.20; + private double _m41 = 18.00; + private double _m42 = 19.00; + private double _m51 = 100.00; + private double _m52 = 84.70; + private double _m61 = 100.00; + private double _m62 = 99.80; + private double _m71 = 100.00; + private double _m72 = 99.78; + private double _warmthEnergyKilojoules = 48.2; + private double _warmthSeconds = 10800; + + public string SampleDescription { get => _sampleDescription; set => SetProperty(ref _sampleDescription, value); } + public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); } + public string MoistureResultText { get => _moistureResultText; set => SetProperty(ref _moistureResultText, value); } + public string WarmthResultText { get => _warmthResultText; set => SetProperty(ref _warmthResultText, value); } + public string MoistureDetailText { get => _moistureDetailText; set => SetProperty(ref _moistureDetailText, value); } + public string WarmthDetailText { get => _warmthDetailText; set => SetProperty(ref _warmthDetailText, value); } + public double M11 { get => _m11; set => SetProperty(ref _m11, value); } + public double M12 { get => _m12; set => SetProperty(ref _m12, value); } + public double M21 { get => _m21; set => SetProperty(ref _m21, value); } + public double M22 { get => _m22; set => SetProperty(ref _m22, value); } + public double M31 { get => _m31; set => SetProperty(ref _m31, value); } + public double M32 { get => _m32; set => SetProperty(ref _m32, value); } + public double M41 { get => _m41; set => SetProperty(ref _m41, value); } + public double M42 { get => _m42; set => SetProperty(ref _m42, value); } + public double M51 { get => _m51; set => SetProperty(ref _m51, value); } + public double M52 { get => _m52; set => SetProperty(ref _m52, value); } + public double M61 { get => _m61; set => SetProperty(ref _m61, value); } + public double M62 { get => _m62; set => SetProperty(ref _m62, value); } + public double M71 { get => _m71; set => SetProperty(ref _m71, value); } + public double M72 { get => _m72; set => SetProperty(ref _m72, value); } + public double WarmthEnergyKilojoules { get => _warmthEnergyKilojoules; set => SetProperty(ref _warmthEnergyKilojoules, value); } + public double WarmthSeconds { get => _warmthSeconds; set => SetProperty(ref _warmthSeconds, value); } + + private async Task RunMoistureAsync() + { + await EnsureConnectedAsync(); + ProcedureLog.Clear(); + StatusText = "方法 B 吸湿透水汽流程运行中"; + ProcedureLog.Add("固定假脚并定位鞋头至风扇 450±10 mm。"); + ProcedureLog.Add("预热并排空水管空气,确认 C1/C2/C3 称重。"); + await _deviceClient.SetOutputsAsync(false, true, true); + await Task.Delay(250); + ProcedureLog.Add("恢复假脚温度稳定,启动泵并进入 180 min 正式周期。"); + await _deviceClient.SetOutputsAsync(true, true, true); + await Task.Delay(500); + ProcedureLog.Add("模拟 180 min 周期完成,关闭泵、风扇及温控。"); + await _deviceClient.SetOutputsAsync(false, false, false); + + var result = _formulaService.CalculateMethodBMoisture(M11, M12, M21, M22, M31, M32, M41, M42, M51, M52, M61, M62, M71, M72); + MoistureResultText = result.IsValid + ? $"T180*={result.T180Corrected:F2} g,XT180*={result.XT180Corrected:F2} g,m180={result.M180:F2} g" + : $"m180={result.M180:F2} g,超出 15±0.9 g,结果作废"; + MoistureDetailText = + $"m1={result.M1:F3} g,m2={result.M2:F3} g,m3={result.M3:F3} g,m4={result.M4:F3} g\n" + + $"m5={result.M5:F3} g,m6={result.M6:F3} g,m7={result.M7:F3} g,m8={result.M8:F3} g,T180={result.T180:F3} g"; + StatusText = result.IsValid ? "吸湿透水汽测试完成" : "吸湿透水汽测试需重测"; + + await SaveRunAsync("方法 B - 吸湿透水汽性能", MoistureResultText, result, result.IsValid); + } + + private async Task RunWarmthAsync() + { + await EnsureConnectedAsync(); + ProcedureLog.Clear(); + StatusText = "方法 B 保暖性能流程运行中"; + ProcedureLog.Add("密封假脚微径管道,穿戴模拟皮肤、标准长筒袜和样品鞋。"); + ProcedureLog.Add("开启风扇和温控,稳定到 38±1 ℃。"); + await _deviceClient.SetOutputsAsync(false, true, true); + await Task.Delay(500); + ProcedureLog.Add("记录两个 180 min 周期能量消耗,生成热阻结果。"); + await _deviceClient.SetOutputsAsync(false, false, false); + + var result = _formulaService.CalculateMethodBWarmth( + WarmthEnergyKilojoules, + WarmthSeconds, + _settings.FootAreaSquareMeters, + _settings.MethodBWarmthTargetTemperatureC, + _settings.EnvironmentTemperatureC); + WarmthResultText = $"Q={result.HeatWatts:F3} W,R={result.ThermalResistance:F3} m²·℃/W"; + WarmthDetailText = $"P={result.EnergyKilojoules:F1} kJ,t={result.Seconds:F0} s,Tf={result.FootTemperatureC:F2} ℃,Tc={result.ChamberTemperatureC:F2} ℃"; + StatusText = "保暖性能测试完成"; + + await SaveRunAsync("方法 B - 保暖性能", WarmthResultText, result, true); + } + + private async Task EnsureConnectedAsync() + { + if (!_deviceClient.IsConnected) + { + await _deviceClient.ConnectAsync(); + } + } + + private async Task SaveRunAsync(string method, string summary, object result, bool isValid) + { + var data = JsonSerializer.Serialize(new + { + Result = result, + Masses = new { M11, M12, M21, M22, M31, M32, M41, M42, M51, M52, M61, M62, M71, M72 }, + ProcedureLog + }, new JsonSerializerOptions { WriteIndented = true }); + + await _repository.SaveAsync(new TestRunRecord( + 0, + method, + SampleDescription, + $"环境 {_settings.EnvironmentTemperatureC:F1} ℃ / {_settings.EnvironmentHumidityPercent:F0}%;风速 {_settings.MethodBAirSpeedMetersPerSecond:F2} m/s;周期 180 min", + summary, + data, + isValid, + DateTime.Now)); + } +} diff --git a/FootwearTest/ViewModels/ReportViewModel.cs b/FootwearTest/ViewModels/ReportViewModel.cs new file mode 100644 index 0000000..fb9cb98 --- /dev/null +++ b/FootwearTest/ViewModels/ReportViewModel.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Models; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class ReportViewModel : ViewModelBase +{ + private readonly TestRunRepository _repository; + private readonly ReportService _reportService; + private readonly ExcelReportService _excelReportService; + + public ReportViewModel(TestRunRepository repository, ReportService reportService, ExcelReportService excelReportService) + { + _repository = repository; + _reportService = reportService; + _excelReportService = excelReportService; + LoadLatestCommand = new AsyncRelayCommand(LoadLatestAsync); + ExportLatestExcelCommand = new AsyncRelayCommand(ExportLatestExcelAsync); + _ = LoadLatestAsync(); + } + + public IAsyncRelayCommand LoadLatestCommand { get; } + public IAsyncRelayCommand ExportLatestExcelCommand { get; } + + private string _reportText = "暂无报告。完成一次试验后会自动生成报告文本。"; + private string _exportStatusText = "Excel 报告将导出到桌面“整鞋试验报告”文件夹。"; + private TestRunRecord? _latestRecord; + + public string ReportText + { + get => _reportText; + set => SetProperty(ref _reportText, value); + } + + public string ExportStatusText + { + get => _exportStatusText; + set => SetProperty(ref _exportStatusText, value); + } + + private async Task LoadLatestAsync() + { + var record = await _repository.GetLatestAsync(); + _latestRecord = record; + ReportText = record is null ? "暂无报告。完成一次试验后会自动生成报告文本。" : _reportService.CreateTextReport(record); + ExportStatusText = record is null ? "暂无可导出的报告。" : "Excel 报告将导出到桌面“整鞋试验报告”文件夹。"; + } + + private async Task ExportLatestExcelAsync() + { + var record = _latestRecord ?? await _repository.GetLatestAsync(); + if (record is null) + { + ExportStatusText = "暂无可导出的报告。"; + return; + } + + var path = await _excelReportService.ExportAsync(record); + ExportStatusText = $"已导出 Excel: {path}"; + } +} diff --git a/FootwearTest/ViewModels/SettingsViewModel.cs b/FootwearTest/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..05b5884 --- /dev/null +++ b/FootwearTest/ViewModels/SettingsViewModel.cs @@ -0,0 +1,50 @@ +using CommunityToolkit.Mvvm.Input; +using FootwearTest.Services; + +namespace FootwearTest.ViewModels; + +public partial class SettingsViewModel : ViewModelBase +{ + private readonly DeviceSettings _settings; + + public SettingsViewModel(DeviceSettings settings) + { + _settings = settings; + Host = settings.Host; + Port = settings.Port; + UseSimulator = settings.UseSimulator; + FootAreaSquareMeters = settings.FootAreaSquareMeters; + CoefficientOfVariationLimitPercent = settings.CoefficientOfVariationLimitPercent; + PumpSpeedCubicCentimetersPerHour = settings.PumpSpeedCubicCentimetersPerHour; + SaveCommand = new RelayCommand(Save); + } + + public IRelayCommand SaveCommand { get; } + + private string _host = ""; + private int _port; + private bool _useSimulator; + private double _footAreaSquareMeters; + private double _coefficientOfVariationLimitPercent; + private double _pumpSpeedCubicCentimetersPerHour; + private string _saveStatus = "参数未保存"; + + public string Host { get => _host; set => SetProperty(ref _host, value); } + public int Port { get => _port; set => SetProperty(ref _port, value); } + public bool UseSimulator { get => _useSimulator; set => SetProperty(ref _useSimulator, value); } + public double FootAreaSquareMeters { get => _footAreaSquareMeters; set => SetProperty(ref _footAreaSquareMeters, value); } + public double CoefficientOfVariationLimitPercent { get => _coefficientOfVariationLimitPercent; set => SetProperty(ref _coefficientOfVariationLimitPercent, value); } + public double PumpSpeedCubicCentimetersPerHour { get => _pumpSpeedCubicCentimetersPerHour; set => SetProperty(ref _pumpSpeedCubicCentimetersPerHour, value); } + public string SaveStatus { get => _saveStatus; set => SetProperty(ref _saveStatus, value); } + + private void Save() + { + _settings.Host = Host; + _settings.Port = Port; + _settings.UseSimulator = UseSimulator; + _settings.FootAreaSquareMeters = FootAreaSquareMeters; + _settings.CoefficientOfVariationLimitPercent = CoefficientOfVariationLimitPercent; + _settings.PumpSpeedCubicCentimetersPerHour = PumpSpeedCubicCentimetersPerHour; + SaveStatus = "参数已保存,新的试验流程将使用当前设置"; + } +} diff --git a/FootwearTest/ViewModels/ViewModelBase.cs b/FootwearTest/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..31195e5 --- /dev/null +++ b/FootwearTest/ViewModels/ViewModelBase.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace FootwearTest.ViewModels +{ + public abstract class ViewModelBase : ObservableObject + { + } +} diff --git a/FootwearTest/Views/DashboardView.axaml b/FootwearTest/Views/DashboardView.axaml new file mode 100644 index 0000000..652ebaa --- /dev/null +++ b/FootwearTest/Views/DashboardView.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FootwearTest/Views/MainWindow.axaml.cs b/FootwearTest/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..3d0d6e5 --- /dev/null +++ b/FootwearTest/Views/MainWindow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace FootwearTest.Views +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/FootwearTest/Views/MethodAView.axaml b/FootwearTest/Views/MethodAView.axaml new file mode 100644 index 0000000..970f962 --- /dev/null +++ b/FootwearTest/Views/MethodAView.axaml @@ -0,0 +1,84 @@ + + + + + + + + + +