From d55713542cb3a63997ad8fff9e83f97ad19de201 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Mon, 1 Jun 2026 17:59:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B020260601=EF=BC=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=8C=81=E4=B9=85=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ConeCalorimeter/MainWindow.xaml.cs | 1 + .../Models/ReportManualEntryFieldState.cs | 8 ++ .../Models/ReportManualEntryState.cs | 8 ++ .../IReportInputPersistenceService.cs | 12 +++ .../JsonReportInputPersistenceService.cs | 77 +++++++++++++++++++ ConeCalorimeter/ViewModels/MainViewModel.cs | 2 + .../ViewModels/ReportFieldViewModel.cs | 18 ++++- .../ViewModels/ReportPageViewModel.cs | 71 ++++++++++++++++- 8 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 ConeCalorimeter/Models/ReportManualEntryFieldState.cs create mode 100644 ConeCalorimeter/Models/ReportManualEntryState.cs create mode 100644 ConeCalorimeter/Services/IReportInputPersistenceService.cs create mode 100644 ConeCalorimeter/Services/JsonReportInputPersistenceService.cs diff --git a/ConeCalorimeter/MainWindow.xaml.cs b/ConeCalorimeter/MainWindow.xaml.cs index c6ae0d4..33148cd 100644 --- a/ConeCalorimeter/MainWindow.xaml.cs +++ b/ConeCalorimeter/MainWindow.xaml.cs @@ -31,6 +31,7 @@ namespace ConeCalorimeter experimentDataService, _tcpDeviceConnectionService, new NpoiReportExportService(), + new JsonReportInputPersistenceService(), new NpoiRealtimeDataExportService(), new HelpDialogService()); } diff --git a/ConeCalorimeter/Models/ReportManualEntryFieldState.cs b/ConeCalorimeter/Models/ReportManualEntryFieldState.cs new file mode 100644 index 0000000..514735b --- /dev/null +++ b/ConeCalorimeter/Models/ReportManualEntryFieldState.cs @@ -0,0 +1,8 @@ +namespace ConeCalorimeter.Models; + +public sealed class ReportManualEntryFieldState +{ + public string Value { get; set; } = string.Empty; + + public bool IsAutoFilled { get; set; } +} diff --git a/ConeCalorimeter/Models/ReportManualEntryState.cs b/ConeCalorimeter/Models/ReportManualEntryState.cs new file mode 100644 index 0000000..50227dc --- /dev/null +++ b/ConeCalorimeter/Models/ReportManualEntryState.cs @@ -0,0 +1,8 @@ +namespace ConeCalorimeter.Models; + +public sealed class ReportManualEntryState +{ + public int Version { get; set; } = 1; + + public Dictionary Fields { get; set; } = []; +} diff --git a/ConeCalorimeter/Services/IReportInputPersistenceService.cs b/ConeCalorimeter/Services/IReportInputPersistenceService.cs new file mode 100644 index 0000000..6a21bca --- /dev/null +++ b/ConeCalorimeter/Services/IReportInputPersistenceService.cs @@ -0,0 +1,12 @@ +using ConeCalorimeter.Models; + +namespace ConeCalorimeter.Services; + +public interface IReportInputPersistenceService +{ + string StoragePath { get; } + + IReadOnlyDictionary Load(); + + void Save(IReadOnlyDictionary fields); +} diff --git a/ConeCalorimeter/Services/JsonReportInputPersistenceService.cs b/ConeCalorimeter/Services/JsonReportInputPersistenceService.cs new file mode 100644 index 0000000..bc37b47 --- /dev/null +++ b/ConeCalorimeter/Services/JsonReportInputPersistenceService.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Text.Json; +using ConeCalorimeter.Models; +using Serilog; + +namespace ConeCalorimeter.Services; + +public sealed class JsonReportInputPersistenceService : IReportInputPersistenceService +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true + }; + + public JsonReportInputPersistenceService() + : this(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ConeCalorimeter", + "report-manual-input.json")) + { + } + + public JsonReportInputPersistenceService(string storagePath) + { + StoragePath = storagePath; + } + + public string StoragePath { get; } + + public IReadOnlyDictionary Load() + { + if (!File.Exists(StoragePath)) + { + return new Dictionary(); + } + + try + { + using var stream = File.OpenRead(StoragePath); + var state = JsonSerializer.Deserialize(stream, JsonOptions); + return state?.Fields ?? new Dictionary(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException) + { + Log.Warning(ex, "Report manual input JSON load failed. Path={Path}.", StoragePath); + return new Dictionary(); + } + } + + public void Save(IReadOnlyDictionary fields) + { + var directory = Path.GetDirectoryName(StoragePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + var state = new ReportManualEntryState + { + Fields = fields.ToDictionary( + pair => pair.Key, + pair => new ReportManualEntryFieldState + { + Value = pair.Value.Value ?? string.Empty, + IsAutoFilled = pair.Value.IsAutoFilled + }) + }; + + var tempPath = $"{StoragePath}.tmp"; + using (var stream = File.Create(tempPath)) + { + JsonSerializer.Serialize(stream, state, JsonOptions); + } + + File.Move(tempPath, StoragePath, true); + } +} diff --git a/ConeCalorimeter/ViewModels/MainViewModel.cs b/ConeCalorimeter/ViewModels/MainViewModel.cs index 81a564a..3388b9a 100644 --- a/ConeCalorimeter/ViewModels/MainViewModel.cs +++ b/ConeCalorimeter/ViewModels/MainViewModel.cs @@ -17,6 +17,7 @@ public sealed class MainViewModel : ObservableObject IExperimentDataService experimentDataService, ITcpDeviceConnectionService tcpDeviceConnectionService, IReportExportService reportExportService, + IReportInputPersistenceService reportInputPersistenceService, IRealtimeDataExportService realtimeDataExportService, IHelpDialogService helpDialogService) { @@ -28,6 +29,7 @@ public sealed class MainViewModel : ObservableObject var reportPage = new ReportPageViewModel( experimentDataService, reportExportService, + reportInputPersistenceService, tcpDeviceConnectionService); NavigationItems = []; diff --git a/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs b/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs index 3560152..10ed6d5 100644 --- a/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs +++ b/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs @@ -19,6 +19,12 @@ public sealed class ReportFieldViewModel : ObservableObject public string Label { get; } + public bool IsAutoFilled + { + get => _isAutoFilled; + private set => SetProperty(ref _isAutoFilled, value); + } + public string Value { get => _value; @@ -26,7 +32,7 @@ public sealed class ReportFieldViewModel : ObservableObject { if (SetProperty(ref _value, value) && !_isSettingAutoValue) { - _isAutoFilled = false; + IsAutoFilled = false; } } } @@ -46,6 +52,14 @@ public sealed class ReportFieldViewModel : ObservableObject _isSettingAutoValue = true; Value = value; _isSettingAutoValue = false; - _isAutoFilled = true; + IsAutoFilled = true; + } + + public void SetPersistedValue(string value, bool isAutoFilled) + { + _isSettingAutoValue = true; + Value = value; + _isSettingAutoValue = false; + IsAutoFilled = isAutoFilled; } } diff --git a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs index 7a4793e..774a47b 100644 --- a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs +++ b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Windows; @@ -22,20 +23,25 @@ public sealed class ReportPageViewModel : PageViewModel private readonly IExperimentDataService _experimentDataService; private readonly IReportExportService _reportExportService; + private readonly IReportInputPersistenceService _reportInputPersistenceService; private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; private ModbusFloatByteOrder? _cFactorByteOrder; - private string _statusText = "报表信息可手动录入,导出时会合并当前采集数据。"; + private string _statusText = "报表信息可手动录入,内容会自动保存,导出时合并当前采集数据。"; public ReportPageViewModel( IExperimentDataService experimentDataService, IReportExportService reportExportService, + IReportInputPersistenceService reportInputPersistenceService, ITcpDeviceConnectionService tcpDeviceConnectionService) : base("报表") { _experimentDataService = experimentDataService; _reportExportService = reportExportService; + _reportInputPersistenceService = reportInputPersistenceService; _tcpDeviceConnectionService = tcpDeviceConnectionService; Sections = CreateSections(); + LoadPersistedReportInput(); + AttachReportFieldPersistence(); SummaryItems = [ new ReportSummaryItemViewModel("记录数"), @@ -170,7 +176,7 @@ public sealed class ReportPageViewModel : PageViewModel return; } - RefreshCurrentCValueReportField(overwriteManual: true); + RefreshCurrentCValueReportField(overwriteManual: false); var input = BuildInput(); var defaultName = BuildDefaultFileName(input); var dialog = new SaveFileDialog @@ -237,6 +243,67 @@ public sealed class ReportPageViewModel : PageViewModel return input; } + private void LoadPersistedReportInput() + { + var persistedFields = _reportInputPersistenceService.Load(); + if (persistedFields.Count == 0) + { + return; + } + + foreach (var field in Sections.SelectMany(section => section.Fields)) + { + if (!persistedFields.TryGetValue(field.Key, out var state)) + { + continue; + } + + field.SetPersistedValue(state.Value ?? string.Empty, state.IsAutoFilled); + } + } + + private void AttachReportFieldPersistence() + { + foreach (var field in Sections.SelectMany(section => section.Fields)) + { + field.PropertyChanged += ReportFieldPropertyChanged; + } + } + + private void ReportFieldPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(ReportFieldViewModel.Value) + && e.PropertyName != nameof(ReportFieldViewModel.IsAutoFilled)) + { + return; + } + + SaveReportInput(); + } + + private void SaveReportInput() + { + try + { + var state = Sections + .SelectMany(section => section.Fields) + .ToDictionary( + field => field.Key, + field => new ReportManualEntryFieldState + { + Value = field.Value, + IsAutoFilled = field.IsAutoFilled + }); + + _reportInputPersistenceService.Save(state); + } + catch (Exception ex) + { + StatusText = $"报表录入保存失败:{ex.Message}"; + Log.Error(ex, "Report manual input JSON save failed. Path={Path}.", _reportInputPersistenceService.StoragePath); + } + } + private static string BuildDefaultFileName(ReportInput input) { var name = FirstNonEmpty(input.ReportName, input.FileName, input.SampleName, "锥形量热仪测试报告");