添加项目文件。

This commit is contained in:
GukSang.Jin
2026-05-26 15:17:18 +08:00
parent ef2f91e2db
commit fae2a45438
50 changed files with 2547 additions and 0 deletions

15
FootwearTest/App.axaml Normal file
View File

@@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="FootwearTest.App"
xmlns:local="using:FootwearTest"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

61
FootwearTest/App.axaml.cs Normal file
View File

@@ -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<MainWindowViewModel>(),
};
}
base.OnFrameworkInitializationCompleted();
}
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<DeviceSettings>();
services.AddSingleton<TestFormulaService>();
services.AddSingleton<TestRunRepository>();
services.AddSingleton<ReportService>();
services.AddSingleton<ExcelReportService>();
services.AddSingleton<IDeviceClient>(provider =>
{
var settings = provider.GetRequiredService<DeviceSettings>();
return settings.UseSimulator
? new SimulatedDeviceClient(settings)
: new ModbusTcpDeviceClient(settings);
});
services.AddSingleton<DashboardViewModel>();
services.AddSingleton<MethodAViewModel>();
services.AddSingleton<MethodBViewModel>();
services.AddSingleton<HistoryViewModel>();
services.AddSingleton<ReportViewModel>();
services.AddSingleton<SettingsViewModel>();
services.AddSingleton<MainWindowViewModel>();
return services.BuildServiceProvider();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.3" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
</ItemGroup>
</Project>

View File

@@ -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,
"无报警");
}

View File

@@ -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);

25
FootwearTest/Program.cs Normal file
View File

@@ -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<App>()
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()
#endif
.WithInterFont()
.LogToTrace();
}
}

View File

@@ -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;
}

View File

@@ -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<string> 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<WorksheetData> CreateSheets(TestRunRecord record)
{
var sheets = new List<WorksheetData>
{
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<IReadOnlyList<object?>>
{
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 gm180 应在 (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×1000R=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<IReadOnlyList<object?>>
{
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<IReadOnlyList<object?>>
{
Row("项目", "数值", "单位/说明"),
};
if (method.Contains("吸湿透水汽", StringComparison.Ordinal))
{
rows.Add(Row("m5", GetDouble(result, "M5"), "gC1 消耗水量"));
rows.Add(Row("m6", GetDouble(result, "M6"), "gC2 蒸发水量"));
rows.Add(Row("m7", GetDouble(result, "M7"), "gC3 蒸发水量"));
rows.Add(Row("m8", GetDouble(result, "M8"), "gC2/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"), "kJ180 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<IReadOnlyList<object?>>
{
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<IReadOnlyList<object?>>
{
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<IReadOnlyList<object?>>
{
Row("路径", "值"),
};
FlattenJson(rows, "", root);
return new WorksheetData("完整原始数据", rows);
}
private static void FlattenJson(List<IReadOnlyList<object?>> 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<WorksheetData> sheets)
{
using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
AddText(archive, "[Content_Types].xml", CreateContentTypes(sheets.Count));
AddText(archive, "_rels/.rels", """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>
""");
AddText(archive, "xl/workbook.xml", CreateWorkbookXml(sheets));
AddText(archive, "xl/_rels/workbook.xml.rels", CreateWorkbookRelationships(sheets.Count));
AddText(archive, "xl/styles.xml", """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="1"><font><sz val="11"/><name val="Microsoft YaHei"/></font></fonts>
<fills count="1"><fill><patternFill patternType="none"/></fill></fills>
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>
""");
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("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
""");
for (var i = 1; i <= sheetCount; i++)
{
builder.Append(CultureInfo.InvariantCulture, $" <Override PartName=\"/xl/worksheets/sheet{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n");
}
builder.Append("</Types>");
return builder.ToString();
}
private static string CreateWorkbookXml(IReadOnlyList<WorksheetData> sheets)
{
var builder = new StringBuilder();
builder.Append("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
""");
for (var i = 0; i < sheets.Count; i++)
{
builder.Append(CultureInfo.InvariantCulture, $" <sheet name=\"{EscapeXml(TrimSheetName(sheets[i].Name))}\" sheetId=\"{i + 1}\" r:id=\"rId{i + 1}\"/>\n");
}
builder.Append("""
</sheets>
</workbook>
""");
return builder.ToString();
}
private static string CreateWorkbookRelationships(int sheetCount)
{
var builder = new StringBuilder();
builder.Append("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
""");
for (var i = 1; i <= sheetCount; i++)
{
builder.Append(CultureInfo.InvariantCulture, $" <Relationship Id=\"rId{i}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet{i}.xml\"/>\n");
}
builder.Append(CultureInfo.InvariantCulture, $" <Relationship Id=\"rId{sheetCount + 1}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n");
builder.Append("</Relationships>");
return builder.ToString();
}
private static string CreateWorksheetXml(WorksheetData sheet)
{
var builder = new StringBuilder();
builder.Append("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
<sheetFormatPr defaultRowHeight="18"/>
<cols>
<col min="1" max="1" width="22" customWidth="1"/>
<col min="2" max="20" width="24" customWidth="1"/>
</cols>
<sheetData>
""");
for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
{
var rowNumber = rowIndex + 1;
builder.Append(CultureInfo.InvariantCulture, $" <row r=\"{rowNumber}\">\n");
var row = sheet.Rows[rowIndex];
for (var columnIndex = 0; columnIndex < row.Count; columnIndex++)
{
AppendCell(builder, row[columnIndex], rowNumber, columnIndex + 1);
}
builder.Append(" </row>\n");
}
builder.Append("""
</sheetData>
</worksheet>
""");
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, $" <c r=\"{reference}\"/>\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, $" <c r=\"{reference}\"><v>{Convert.ToString(value, CultureInfo.InvariantCulture)}</v></c>\n");
break;
default:
builder.Append(CultureInfo.InvariantCulture, $" <c r=\"{reference}\" t=\"inlineStr\"><is><t>{EscapeXml(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")}</t></is></c>\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<object?> 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("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("'", "&apos;", 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<IReadOnlyList<object?>> Rows);
}

View File

@@ -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<DeviceSnapshot>? SnapshotUpdated;
Task ConnectAsync(CancellationToken cancellationToken = default);
Task DisconnectAsync(CancellationToken cancellationToken = default);
Task<DeviceSnapshot> ReadSnapshotAsync(CancellationToken cancellationToken = default);
Task SetOutputsAsync(bool pumpRunning, bool fanRunning, bool heaterRunning, CancellationToken cancellationToken = default);
}

View File

@@ -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<DeviceSnapshot>? 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<DeviceSnapshot> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<DeviceSnapshot>? 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<DeviceSnapshot> 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;
}

View File

@@ -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<double> values)
{
var list = values.ToArray();
return list.Length == 0 ? 0 : list.Average();
}
public double CoefficientOfVariationPercent(IEnumerable<double> 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<double> 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));
}
}

View File

@@ -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<long> 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<IReadOnlyList<TestRunSummary>> GetRecentAsync(int take = 50)
{
var items = new List<TestRunSummary>();
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<TestRunRecord?> 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<TestRunRecord?> 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();
}
}

View File

@@ -0,0 +1,38 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using FootwearTest.ViewModels;
using System;
using System.Diagnostics.CodeAnalysis;
namespace FootwearTest
{
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[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;
}
}
}

View File

@@ -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<TestRunSummary> 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);
}
}
}

View File

@@ -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<TestRunSummary> 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}";
}
}

View File

@@ -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;
}
}

View File

@@ -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<MethodASampleRecord> 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²·℃/WCV {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));
}
}

View File

@@ -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<string> 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} gXT180*={result.XT180Corrected:F2} gm180={result.M180:F2} g"
: $"m180={result.M180:F2} g超出 15±0.9 g结果作废";
MoistureDetailText =
$"m1={result.M1:F3} gm2={result.M2:F3} gm3={result.M3:F3} gm4={result.M4:F3} g\n" +
$"m5={result.M5:F3} gm6={result.M6:F3} gm7={result.M7:F3} gm8={result.M8:F3} gT180={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} WR={result.ThermalResistance:F3} m²·℃/W";
WarmthDetailText = $"P={result.EnergyKilojoules:F1} kJt={result.Seconds:F0} sTf={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));
}
}

View File

@@ -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}";
}
}

View File

@@ -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 = "参数已保存,新的试验流程将使用当前设置";
}
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace FootwearTest.ViewModels
{
public abstract class ViewModelBase : ObservableObject
{
}
}

View File

@@ -0,0 +1,62 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
xmlns:models="using:FootwearTest.Models"
x:Class="FootwearTest.Views.DashboardView"
x:DataType="vm:DashboardViewModel">
<ScrollViewer>
<StackPanel Margin="24" Spacing="18">
<Grid ColumnDefinitions="*,*,*,*">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,0,14,0">
<StackPanel>
<TextBlock Text="假脚温度" Foreground="#5B6775"/>
<TextBlock Text="{Binding Snapshot.FootTemperatureC, StringFormat='{}{0:F2} ℃'}" FontSize="28" FontWeight="SemiBold"/>
</StackPanel>
</Border>
<Border Grid.Column="1" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,0,14,0">
<StackPanel>
<TextBlock Text="环境温湿度" Foreground="#5B6775"/>
<TextBlock Text="{Binding Snapshot.EnvironmentTemperatureC, StringFormat='{}{0:F2} ℃'}" FontSize="28" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Snapshot.EnvironmentHumidityPercent, StringFormat='{}{0:F1}% RH'}" Foreground="#5B6775"/>
</StackPanel>
</Border>
<Border Grid.Column="2" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,0,14,0">
<StackPanel>
<TextBlock Text="风速" Foreground="#5B6775"/>
<TextBlock Text="{Binding Snapshot.AirSpeedMetersPerSecond, StringFormat='{}{0:F2} m/s'}" FontSize="28" FontWeight="SemiBold"/>
</StackPanel>
</Border>
<Border Grid.Column="3" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel>
<TextBlock Text="功率 / 出汗量" Foreground="#5B6775"/>
<TextBlock Text="{Binding Snapshot.PowerWatts, StringFormat='{}{0:F2} W'}" FontSize="28" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Snapshot.WaterLossGramsPerHour, StringFormat='{}{0:F2} g/h'}" Foreground="#5B6775"/>
</StackPanel>
</Border>
</Grid>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<Grid RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="最近试验" FontSize="18" FontWeight="SemiBold"/>
<Button Grid.Column="1" Content="刷新" Command="{Binding RefreshCommand}"/>
</Grid>
<ItemsControl Grid.Row="1" ItemsSource="{Binding RecentRuns}" Margin="0,12,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:TestRunSummary">
<Border BorderBrush="#E4E9EF" BorderThickness="0,0,0,1" Padding="0,10">
<Grid ColumnDefinitions="140,190,*,90">
<TextBlock Text="{Binding CreatedAt, StringFormat='{}{0:MM-dd HH:mm}'}"/>
<TextBlock Grid.Column="1" Text="{Binding Method}"/>
<TextBlock Grid.Column="2" Text="{Binding ResultSummary}" TextWrapping="Wrap"/>
<TextBlock Grid.Column="3" Text="{Binding IsValid, StringFormat='有效: {0}'}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class DashboardView : UserControl
{
public DashboardView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
xmlns:models="using:FootwearTest.Models"
x:Class="FootwearTest.Views.HistoryView"
x:DataType="vm:HistoryViewModel">
<Grid Margin="24" ColumnDefinitions="430,*">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,0,18,0">
<Grid RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="历史记录" FontSize="18" FontWeight="SemiBold"/>
<Button Grid.Column="1" Content="刷新" Command="{Binding LoadCommand}"/>
</Grid>
<ListBox Grid.Row="1" ItemsSource="{Binding Runs}" SelectedItem="{Binding SelectedRun}" Margin="0,12,0,0">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:TestRunSummary">
<StackPanel Margin="0,4">
<TextBlock Text="{Binding Method}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding CreatedAt, StringFormat='{}{0:yyyy-MM-dd HH:mm}'}" Foreground="#5B6775"/>
<TextBlock Text="{Binding ResultSummary}" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<Border Grid.Column="1" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<Grid RowDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="记录报告" FontSize="18" FontWeight="SemiBold"/>
<Button Grid.Column="1" Content="导出 Excel 完整报告" Command="{Binding ExportSelectedExcelCommand}"/>
</Grid>
<TextBlock Grid.Row="1" Text="{Binding ExportStatusText}" Foreground="#1D5F96" TextWrapping="Wrap" Margin="0,10,0,0"/>
<ScrollViewer Grid.Row="2" Margin="0,14,0,0">
<TextBlock Text="{Binding SelectedReportText}" TextWrapping="Wrap" FontFamily="Consolas"/>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class HistoryView : UserControl
{
public HistoryView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,78 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1280" d:DesignHeight="800"
x:Class="FootwearTest.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Width="1280"
Height="800"
MinWidth="1100"
MinHeight="720"
WindowState="Maximized"
Title="整鞋热阻和湿阻测定 HMI">
<Grid Background="#F3F5F7" ColumnDefinitions="220,*">
<Border Grid.Column="0" Background="#17212B">
<Grid RowDefinitions="Auto,*,Auto" Margin="18">
<StackPanel Spacing="4">
<TextBlock Text="FootwearTest" Foreground="White" FontSize="24" FontWeight="SemiBold"/>
<TextBlock Text="GB/T 33393-2023" Foreground="#AAB7C4" FontSize="13"/>
</StackPanel>
<StackPanel Grid.Row="1" Spacing="8" Margin="0,28,0,0">
<Button Command="{Binding ShowDashboardCommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="运行总览" Foreground="White" FontSize="15"/>
</Button>
<Button Command="{Binding ShowMethodACommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="方法 A" Foreground="White" FontSize="15"/>
</Button>
<Button Command="{Binding ShowMethodBCommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="方法 B" Foreground="White" FontSize="15"/>
</Button>
<Button Command="{Binding ShowHistoryCommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="历史记录" Foreground="White" FontSize="15"/>
</Button>
<Button Command="{Binding ShowReportCommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="试验报告" Foreground="White" FontSize="15"/>
</Button>
<Button Command="{Binding ShowSettingsCommand}" HorizontalAlignment="Stretch" Background="#243343" BorderBrush="#34485C" Padding="14,10">
<TextBlock Text="系统设置" Foreground="White" FontSize="15"/>
</Button>
</StackPanel>
<StackPanel Grid.Row="2" Spacing="8">
<TextBlock Text="{Binding ConnectionText}" Foreground="#D9E2EC" TextWrapping="Wrap"/>
<Button Command="{Binding ConnectDeviceCommand}" HorizontalAlignment="Stretch" Background="#1F7A4D" BorderBrush="#2C9962" Padding="14,10">
<TextBlock Text="连接设备" Foreground="White"/>
</Button>
<Button Command="{Binding DisconnectDeviceCommand}" HorizontalAlignment="Stretch" Background="#384858" BorderBrush="#4C6075" Padding="14,10">
<TextBlock Text="断开连接" Foreground="White"/>
</Button>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Column="1" RowDefinitions="76,*">
<Border Background="White" BorderBrush="#D6DDE5" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="24,0">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding CurrentPageTitle}" FontSize="24" FontWeight="SemiBold" Foreground="#16202A"/>
<TextBlock Text="整鞋热阻、湿阻、吸湿透水汽和保暖性能测试" Foreground="#5B6775"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="16" VerticalAlignment="Center">
<TextBlock Text="{Binding Snapshot.FootTemperatureC, StringFormat='假脚 {0:F1} ℃'}" Foreground="#16202A"/>
<TextBlock Text="{Binding Snapshot.EnvironmentTemperatureC, StringFormat='环境 {0:F1} ℃'}" Foreground="#16202A"/>
<TextBlock Text="{Binding Snapshot.EnvironmentHumidityPercent, StringFormat='湿度 {0:F0}%'}" Foreground="#16202A"/>
<TextBlock Text="{Binding Snapshot.AlarmText}" Foreground="#256F46"/>
</StackPanel>
</Grid>
</Border>
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}"/>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace FootwearTest.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,84 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
xmlns:models="using:FootwearTest.Models"
x:Class="FootwearTest.Views.MethodAView"
x:DataType="vm:MethodAViewModel">
<ScrollViewer>
<Grid Margin="24" ColumnDefinitions="360,*">
<StackPanel Spacing="14" Margin="0,0,18,0">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="10">
<TextBlock Text="方法 A 试样与操作" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="试样描述" Foreground="#5B6775"/>
<TextBox Text="{Binding SampleDescription}"/>
<Button Content="测定皮肤湿阻" Command="{Binding MeasureSkinResistanceCommand}"/>
<Button Content="整鞋热阻/湿阻试验" Command="{Binding RunWholeShoeCommand}"/>
<Button Content="停止" Command="{Binding StopCommand}"/>
<TextBlock Text="{Binding StatusText}" Foreground="#1D5F96" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="8">
<TextBlock Text="实时结果" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding SkinMoistureResistance, StringFormat='皮肤湿阻 {0:F3} Pa·m²/W'}"/>
<TextBlock Text="{Binding AverageMoistureResistance, StringFormat='平均湿阻 {0:F3} Pa·m²/W'}"/>
<TextBlock Text="{Binding AverageThermalResistance, StringFormat='平均热阻 {0:F4} m²·℃/W'}"/>
<TextBlock Text="{Binding MoistureCvPercent, StringFormat='湿阻 CV {0:F2}%'}"/>
<TextBlock Text="{Binding ThermalCvPercent, StringFormat='热阻 CV {0:F2}%'}"/>
<TextBlock Text="{Binding ResultText}" TextWrapping="Wrap" FontWeight="SemiBold"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="6">
<TextBlock Text="标准控制点" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="试样 2 只;试验前标准环境调节 24 h" TextWrapping="Wrap"/>
<TextBlock Text="环境 (23±2) ℃ / (50±5)% RH风速 (1.00±0.15) m/s" TextWrapping="Wrap"/>
<TextBlock Text="假脚/模拟皮肤温度 (35.0±0.3) ℃" TextWrapping="Wrap"/>
<TextBlock Text="每分钟记录一次,连续记录至少 30 次;任一测定值偏差不超过 ±10%" TextWrapping="Wrap"/>
</StackPanel>
</Border>
</StackPanel>
<Border Grid.Column="1" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Text="连续采样记录" FontSize="18" FontWeight="SemiBold"/>
<Grid Grid.Row="1" ColumnDefinitions="42,92,78,78,78,78,78,78,88,88" Margin="0,12,0,6">
<TextBlock Text="序号" Foreground="#5B6775"/>
<TextBlock Grid.Column="1" Text="假脚℃" Foreground="#5B6775"/>
<TextBlock Grid.Column="2" Text="环境℃" Foreground="#5B6775"/>
<TextBlock Grid.Column="3" Text="湿度%" Foreground="#5B6775"/>
<TextBlock Grid.Column="4" Text="出汗g/h" Foreground="#5B6775"/>
<TextBlock Grid.Column="5" Text="功率W" Foreground="#5B6775"/>
<TextBlock Grid.Column="6" Text="He W" Foreground="#5B6775"/>
<TextBlock Grid.Column="7" Text="Hd W" Foreground="#5B6775"/>
<TextBlock Grid.Column="8" Text="湿阻" Foreground="#5B6775"/>
<TextBlock Grid.Column="9" Text="热阻" Foreground="#5B6775"/>
</Grid>
<ScrollViewer Grid.Row="2">
<ItemsControl ItemsSource="{Binding Samples}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:MethodASampleRecord">
<Grid ColumnDefinitions="42,92,78,78,78,78,78,78,88,88" Margin="0,0,0,6">
<TextBlock Text="{Binding Index}"/>
<TextBlock Grid.Column="1" Text="{Binding FootTemperatureC, StringFormat='{}{0:F2}'}"/>
<TextBlock Grid.Column="2" Text="{Binding EnvironmentTemperatureC, StringFormat='{}{0:F2}'}"/>
<TextBlock Grid.Column="3" Text="{Binding RelativeHumidityPercent, StringFormat='{}{0:F1}'}"/>
<TextBlock Grid.Column="4" Text="{Binding WaterLossGramsPerHour, StringFormat='{}{0:F2}'}"/>
<TextBlock Grid.Column="5" Text="{Binding PowerWatts, StringFormat='{}{0:F2}'}"/>
<TextBlock Grid.Column="6" Text="{Binding MoistureHeatWatts, StringFormat='{}{0:F3}'}"/>
<TextBlock Grid.Column="7" Text="{Binding DryHeatWatts, StringFormat='{}{0:F3}'}"/>
<TextBlock Grid.Column="8" Text="{Binding MoistureResistance, StringFormat='{}{0:F3}'}"/>
<TextBlock Grid.Column="9" Text="{Binding ThermalResistance, StringFormat='{}{0:F4}'}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class MethodAView : UserControl
{
public MethodAView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,106 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
x:Class="FootwearTest.Views.MethodBView"
x:DataType="vm:MethodBViewModel">
<ScrollViewer>
<Grid Margin="24" ColumnDefinitions="420,*">
<StackPanel Spacing="14" Margin="0,0,18,0">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="10">
<TextBlock Text="方法 B 操作" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="试样描述" Foreground="#5B6775"/>
<TextBox Text="{Binding SampleDescription}"/>
<Button Content="运行吸湿透水汽 180 min 流程" Command="{Binding RunMoistureCommand}"/>
<Button Content="运行保暖性能 180 min 周期" Command="{Binding RunWarmthCommand}"/>
<TextBlock Text="{Binding StatusText}" Foreground="#1D5F96" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="8">
<TextBlock Text="结果" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding MoistureResultText}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding MoistureDetailText}" TextWrapping="Wrap" Foreground="#5B6775"/>
<TextBlock Text="{Binding WarmthResultText}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding WarmthDetailText}" TextWrapping="Wrap" Foreground="#5B6775"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="6">
<TextBlock Text="标准控制点" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="吸湿透水汽:泵流量 (5.0±0.3) cm³/h周期 (180±1) min" TextWrapping="Wrap"/>
<TextBlock Text="称重精确到 0.01 gm180 应在 15±0.9 g 范围内" TextWrapping="Wrap"/>
<TextBlock Text="结果T180* 为透水汽性能XT180* 为吸湿透水汽性能" TextWrapping="Wrap"/>
<TextBlock Text="保暖性能:假脚 (38±1) ℃,连续 2 个 180 min 周期" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="8">
<TextBlock Text="保暖性能输入" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="180 min 能量 P (kJ)" Foreground="#5B6775"/>
<TextBox Text="{Binding WarmthEnergyKilojoules}"/>
<TextBlock Text="周期 t (s)" Foreground="#5B6775"/>
<TextBox Text="{Binding WarmthSeconds}"/>
</StackPanel>
</Border>
</StackPanel>
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="10">
<TextBlock Text="吸湿透水汽称重输入" FontSize="18" FontWeight="SemiBold"/>
<Grid ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock Text="m11 皮肤前"/>
<TextBox Grid.Row="1" Text="{Binding M11}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="1" Text="m12 皮肤后"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding M12}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="2" Text="m21 袜前"/>
<TextBox Grid.Column="2" Grid.Row="1" Text="{Binding M21}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="3" Text="m22 袜后"/>
<TextBox Grid.Column="3" Grid.Row="1" Text="{Binding M22}" Margin="0,0,0,8"/>
<TextBlock Grid.Row="2" Text="m31 鞋前"/>
<TextBox Grid.Row="3" Text="{Binding M31}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="1" Grid.Row="2" Text="m32 鞋后"/>
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding M32}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="2" Grid.Row="2" Text="m41 鞋垫前"/>
<TextBox Grid.Column="2" Grid.Row="3" Text="{Binding M41}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="3" Grid.Row="2" Text="m42 鞋垫后"/>
<TextBox Grid.Column="3" Grid.Row="3" Text="{Binding M42}" Margin="0,0,0,8"/>
<TextBlock Grid.Row="4" Text="m51 C1前"/>
<TextBox Grid.Row="5" Text="{Binding M51}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="1" Grid.Row="4" Text="m52 C1后"/>
<TextBox Grid.Column="1" Grid.Row="5" Text="{Binding M52}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="2" Grid.Row="4" Text="m61 C2前"/>
<TextBox Grid.Column="2" Grid.Row="5" Text="{Binding M61}" Margin="0,0,10,8"/>
<TextBlock Grid.Column="3" Grid.Row="4" Text="m62 C2后"/>
<TextBox Grid.Column="3" Grid.Row="5" Text="{Binding M62}" Margin="0,0,0,8"/>
<TextBlock Grid.Row="6" Text="m71 C3前"/>
<TextBox Grid.Row="7" Text="{Binding M71}" Margin="0,0,10,0"/>
<TextBlock Grid.Column="1" Grid.Row="6" Text="m72 C3后"/>
<TextBox Grid.Column="1" Grid.Row="7" Text="{Binding M72}" Margin="0,0,10,0"/>
</Grid>
</StackPanel>
</Border>
<Border Grid.Row="1" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18" Margin="0,14,0,0">
<StackPanel Spacing="8">
<TextBlock Text="流程记录" FontSize="18" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding ProcedureLog}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap" Margin="0,0,0,4"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
</Grid>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class MethodBView : UserControl
{
public MethodBView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
x:Class="FootwearTest.Views.ReportView"
x:DataType="vm:ReportViewModel">
<Border Margin="24" Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<Grid RowDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Text="试验报告" FontSize="18" FontWeight="SemiBold"/>
<Button Grid.Column="1" Content="加载最新报告" Command="{Binding LoadLatestCommand}"/>
<Button Grid.Column="2" Content="导出 Excel 完整报告" Command="{Binding ExportLatestExcelCommand}" Margin="10,0,0,0"/>
</Grid>
<TextBlock Grid.Row="1" Text="{Binding ExportStatusText}" Foreground="#1D5F96" TextWrapping="Wrap" Margin="0,10,0,0"/>
<ScrollViewer Grid.Row="2" Margin="0,14,0,0">
<TextBlock Text="{Binding ReportText}" TextWrapping="Wrap" FontFamily="Consolas"/>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class ReportView : UserControl
{
public ReportView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:FootwearTest.ViewModels"
x:Class="FootwearTest.Views.SettingsView"
x:DataType="vm:SettingsViewModel">
<ScrollViewer>
<StackPanel Margin="24" Spacing="14" MaxWidth="640">
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="10">
<TextBlock Text="通讯设置" FontSize="18" FontWeight="SemiBold"/>
<CheckBox Content="使用模拟器" IsChecked="{Binding UseSimulator}"/>
<TextBlock Text="Modbus TCP IP" Foreground="#5B6775"/>
<TextBox Text="{Binding Host}"/>
<TextBlock Text="端口" Foreground="#5B6775"/>
<TextBox Text="{Binding Port}"/>
</StackPanel>
</Border>
<Border Background="White" BorderBrush="#DDE3EA" BorderThickness="1" CornerRadius="6" Padding="18">
<StackPanel Spacing="10">
<TextBlock Text="试验参数" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="假脚表面积 S (m²)" Foreground="#5B6775"/>
<TextBox Text="{Binding FootAreaSquareMeters}"/>
<TextBlock Text="方法 A 变异系数阈值 (%)" Foreground="#5B6775"/>
<TextBox Text="{Binding CoefficientOfVariationLimitPercent}"/>
<TextBlock Text="方法 B 泵速 (cm³/h)" Foreground="#5B6775"/>
<TextBox Text="{Binding PumpSpeedCubicCentimetersPerHour}"/>
<Button Content="保存参数" Command="{Binding SaveCommand}" HorizontalAlignment="Left"/>
<TextBlock Text="{Binding SaveStatus}" Foreground="#256F46"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace FootwearTest.Views;
public partial class SettingsView : UserControl
{
public SettingsView()
{
InitializeComponent();
}
}

18
FootwearTest/app.manifest Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="FootwearTest.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

BIN
GBT+33393-2023.pdf Normal file

Binary file not shown.

BIN
tmp/pdfs/gbt33393_p06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
tmp/pdfs/gbt33393_p07.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
tmp/pdfs/gbt33393_p08.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
tmp/pdfs/gbt33393_p09.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

BIN
tmp/pdfs/gbt33393_p13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

BIN
tmp/pdfs/gbt33393_p14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

BIN
tmp/pdfs/gbt33393_p15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
tmp/pdfs/gbt33393_p16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
tmp/pdfs/gbt33393_p17.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="FootwearTest/FootwearTest.csproj" />
</Solution>