更新20260601,数据持久处理

This commit is contained in:
GukSang.Jin
2026-06-01 17:59:44 +08:00
parent f5f4dc73ac
commit d55713542c
8 changed files with 193 additions and 4 deletions

View File

@@ -31,6 +31,7 @@ namespace ConeCalorimeter
experimentDataService,
_tcpDeviceConnectionService,
new NpoiReportExportService(),
new JsonReportInputPersistenceService(),
new NpoiRealtimeDataExportService(),
new HelpDialogService());
}

View File

@@ -0,0 +1,8 @@
namespace ConeCalorimeter.Models;
public sealed class ReportManualEntryFieldState
{
public string Value { get; set; } = string.Empty;
public bool IsAutoFilled { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace ConeCalorimeter.Models;
public sealed class ReportManualEntryState
{
public int Version { get; set; } = 1;
public Dictionary<string, ReportManualEntryFieldState> Fields { get; set; } = [];
}

View File

@@ -0,0 +1,12 @@
using ConeCalorimeter.Models;
namespace ConeCalorimeter.Services;
public interface IReportInputPersistenceService
{
string StoragePath { get; }
IReadOnlyDictionary<string, ReportManualEntryFieldState> Load();
void Save(IReadOnlyDictionary<string, ReportManualEntryFieldState> fields);
}

View File

@@ -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<string, ReportManualEntryFieldState> Load()
{
if (!File.Exists(StoragePath))
{
return new Dictionary<string, ReportManualEntryFieldState>();
}
try
{
using var stream = File.OpenRead(StoragePath);
var state = JsonSerializer.Deserialize<ReportManualEntryState>(stream, JsonOptions);
return state?.Fields ?? new Dictionary<string, ReportManualEntryFieldState>();
}
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<string, ReportManualEntryFieldState>();
}
}
public void Save(IReadOnlyDictionary<string, ReportManualEntryFieldState> 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);
}
}

View File

@@ -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 = [];

View File

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

View File

@@ -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, "锥形量热仪测试报告");