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