SSPC-Tester/SSPCTester.UI/ViewModels/ReportViewModel.cs
2026-07-03 14:16:15 +08:00

343 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Options;
using SSPCTester.Logic.Models;
using SSPCTester.Logic.Report;
using SSPCTester.Logic.Storage;
using SSPCTester.UI.Services;
namespace SSPCTester.UI.ViewModels;
/// <summary>测试报告页。</summary>
public partial class ReportViewModel : ObservableObject
{
private readonly ReportGenerator _gen;
private readonly WordReportGenerator _wordGen;
private readonly ReportOptions _opts;
private readonly ProjectStore _store;
private readonly UiLogSink _log;
private readonly UserSettingsStore _settingsStore;
public ObservableCollection<ReportFileDescriptor> ReportFiles { get; } = new();
public IReadOnlyList<ReportFormatChoice> FormatChoices { get; } = new[]
{
new ReportFormatChoice(ReportOutputFormat.Pdf, "PDF"),
new ReportFormatChoice(ReportOutputFormat.Word, "Word"),
new ReportFormatChoice(ReportOutputFormat.Both, "PDF + Word")
};
[ObservableProperty] private Project? _project;
[ObservableProperty] private string _lastOutputPath = "";
[ObservableProperty] private ReportFileDescriptor? _selectedReportFile;
[ObservableProperty] private ReportFormatChoice? _selectedFormatChoice;
[ObservableProperty] private Uri? _previewSource;
[ObservableProperty] private string _previewMessage = "暂无可预览的报告。";
[ObservableProperty] private bool _hasPreview;
[ObservableProperty] private bool _hasWordInfo;
[ObservableProperty] private string _wordInfoText = "";
[ObservableProperty] private string _selectedFilePath = "";
public ReportViewModel(
ReportGenerator gen,
WordReportGenerator wordGen,
IOptions<ReportOptions> opts,
ProjectStore store,
UiLogSink log,
UserSettingsStore settingsStore)
{
_gen = gen;
_wordGen = wordGen;
_opts = opts.Value;
_store = store;
_log = log;
_settingsStore = settingsStore;
SelectedFormatChoice = FormatChoices.First(x => x.Format == ReportFileHelper.ParseFormat(_opts.Format));
}
public void SetProject(Project p)
{
Project = p;
if (string.IsNullOrWhiteSpace(p.Report.FileName) ||
string.Equals(p.Report.FileName, "report.pdf", StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.Report.FileName, "report_{yyyyMMdd_HHmmss}.pdf", StringComparison.OrdinalIgnoreCase))
{
p.Report.FileName = ReportFileHelper.DefaultTemplate;
}
RefreshReportFiles(selectLatest: true);
}
[RelayCommand]
private async Task Generate()
{
if (Project == null) return;
try
{
await _store.SaveAsync(Project);
string dir = ReportDirectory(Project);
Directory.CreateDirectory(dir);
DateTime now = DateTime.Now;
var format = CurrentFormat;
string? pdfPath = null;
string? wordPath = null;
var curveImages = await BuildCurveImagesAsync(Project);
if (format is ReportOutputFormat.Pdf or ReportOutputFormat.Both)
{
pdfPath = Path.Combine(dir, ReportFileHelper.ResolveFileName(Project, now, ".pdf"));
_gen.CurveImages = curveImages;
_gen.Generate(Project, pdfPath);
}
if (format is ReportOutputFormat.Word or ReportOutputFormat.Both)
{
wordPath = Path.Combine(dir, ReportFileHelper.ResolveFileName(Project, now, ".docx"));
_wordGen.CurveImages = curveImages;
_wordGen.Generate(Project, wordPath);
}
string primaryPath = pdfPath ?? wordPath!;
ReportFileHelper.SaveInfo(BuildInfo(Project, format, now, pdfPath, wordPath), ReportFileHelper.BuildInfoPath(primaryPath));
if (wordPath != null && !string.Equals(ReportFileHelper.BuildInfoPath(wordPath), ReportFileHelper.BuildInfoPath(primaryPath), StringComparison.OrdinalIgnoreCase))
ReportFileHelper.SaveInfo(BuildInfo(Project, format, now, pdfPath, wordPath), ReportFileHelper.BuildInfoPath(wordPath));
LastOutputPath = primaryPath;
RefreshReportFiles(primaryPath);
_log.Add(UiLogLevel.Success, "报告已生成:" + primaryPath);
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error, "生成报告失败:" + ex.Message);
}
}
[RelayCommand]
private void OpenFolder()
{
if (Project == null) return;
var dir = ReportDirectory(Project);
if (Directory.Exists(dir))
System.Diagnostics.Process.Start("explorer.exe", dir);
}
[RelayCommand]
private void RefreshReports() => RefreshReportFiles(selectLatest: true);
[RelayCommand]
private void OpenSelectedFile()
{
if (SelectedReportFile == null || !File.Exists(SelectedReportFile.FullPath)) return;
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(SelectedReportFile.FullPath)
{
UseShellExecute = true
});
}
partial void OnSelectedFormatChoiceChanged(ReportFormatChoice? value)
{
if (value == null) return;
_opts.Format = value.Format.ToString();
RefreshReportFiles(selectLatest: true);
}
partial void OnSelectedReportFileChanged(ReportFileDescriptor? value)
{
if (value == null)
{
ClearPreview("暂无可预览的报告。");
return;
}
SelectedFilePath = value.FullPath;
if (value.IsPdf)
LoadPreview(value.FullPath);
else
LoadWordInfo(value.FullPath);
}
public void PreviewUnavailable(string message) => ClearPreview(message);
private void RefreshReportFiles(string? selectPath = null, bool selectLatest = false)
{
ReportFiles.Clear();
var project = Project;
if (project == null)
{
ClearPreview("暂无项目,无法预览报告。");
return;
}
foreach (var file in ReportFileHelper.EnumerateReportFiles(ReportDirectory(project), CurrentFormat))
ReportFiles.Add(file);
SelectedReportFile = selectPath == null
? selectLatest ? DefaultSelectedReportFile() : SelectedReportFile
: ReportFiles.FirstOrDefault(x => string.Equals(x.FullPath, selectPath, StringComparison.OrdinalIgnoreCase));
if (SelectedReportFile == null)
ClearPreview(CurrentFormat == ReportOutputFormat.Word
? "当前报告目录下暂无 Word 报告。生成报告后将在这里显示文件信息。"
: "当前报告目录下暂无报告。生成报告后将在这里预览或显示文件信息。");
}
private void LoadPreview(string path)
{
if (!File.Exists(path))
{
ClearPreview("选中的报告文件不存在。");
return;
}
LastOutputPath = path;
PreviewSource = new Uri(path);
PreviewMessage = "";
HasPreview = true;
var info = ReportFileHelper.LoadInfoForReport(path);
if (!string.IsNullOrWhiteSpace(info?.WordPath))
SetWordInfo(info, path);
else
HasWordInfo = false;
}
private void LoadWordInfo(string path)
{
if (!File.Exists(path))
{
ClearPreview("选中的 Word 报告文件不存在。");
return;
}
LastOutputPath = path;
HasPreview = false;
PreviewSource = null;
PreviewMessage = "";
SetWordInfo(ReportFileHelper.LoadInfoForReport(path), path);
}
private void ClearPreview(string message)
{
HasPreview = false;
PreviewSource = null;
PreviewMessage = message;
HasWordInfo = false;
WordInfoText = "";
SelectedFilePath = "";
}
private string ReportDirectory(Project project) =>
string.IsNullOrWhiteSpace(project.FolderPath) ? _opts.SavePath : project.FolderPath;
private ReportOutputFormat CurrentFormat => SelectedFormatChoice?.Format ?? ReportFileHelper.ParseFormat(_opts.Format);
private ReportFileDescriptor? DefaultSelectedReportFile()
{
if (CurrentFormat == ReportOutputFormat.Both)
return ReportFiles.FirstOrDefault(x => x.IsPdf) ?? ReportFiles.FirstOrDefault();
return ReportFiles.FirstOrDefault();
}
private static ReportInfo BuildInfo(Project project, ReportOutputFormat format, DateTime generatedAt, string? pdfPath, string? wordPath) =>
new()
{
ProjectName = project.Name,
ProductName = project.Dut.ProductName,
Model = project.Dut.Model,
SerialNumber = project.Dut.SerialNumber,
GeneratedAt = generatedAt,
Format = format.ToString(),
PdfPath = pdfPath,
WordPath = wordPath,
OverallStatus = project.OverallStatus.ToString(),
Sections = new ReportSectionInfo
{
Cover = project.Report.Cover,
Basic = project.Report.Basic,
Curve = project.Report.Curve,
Power = project.Report.Power,
Protect = project.Report.Protect
}
};
private void SetWordInfo(ReportInfo? info, string selectedPath)
{
var file = new FileInfo(selectedPath);
string? wordPath = info?.WordPath;
string? pdfPath = info?.PdfPath;
var lines = new List<string>
{
$"文件名:{file.Name}",
$"完整路径:{file.FullName}",
$"文件大小:{FormatBytes(file.Length)}",
$"修改时间:{file.LastWriteTime:yyyy-MM-dd HH:mm:ss}",
$"格式:{(file.Extension.Equals(".docx", StringComparison.OrdinalIgnoreCase) ? "Word" : "PDF")}"
};
if (info != null)
{
lines.Add($"项目名称:{info.ProjectName}");
lines.Add($"设备名称:{info.ProductName}");
lines.Add($"设备型号:{info.Model}");
lines.Add($"设备编号:{info.SerialNumber}");
lines.Add($"生成时间:{info.GeneratedAt:yyyy-MM-dd HH:mm:ss}");
lines.Add($"输出格式:{info.Format}");
lines.Add($"综合判定:{info.OverallStatus}");
lines.Add($"关联 PDF{(string.IsNullOrWhiteSpace(pdfPath) ? "" : File.Exists(pdfPath) ? pdfPath : "")}");
lines.Add($"关联 Word{(string.IsNullOrWhiteSpace(wordPath) ? "" : File.Exists(wordPath) ? wordPath : "")}");
}
else
{
lines.Add("报告信息文件:未找到,已显示文件系统信息。");
}
WordInfoText = string.Join(Environment.NewLine, lines);
HasWordInfo = true;
}
private static string FormatBytes(long bytes)
{
if (bytes >= 1024 * 1024) return $"{bytes / 1024d / 1024d:F2} MB";
if (bytes >= 1024) return $"{bytes / 1024d:F1} KB";
return $"{bytes} B";
}
private async Task<Dictionary<int, CurveReportImages>> BuildCurveImagesAsync(Project project)
{
var images = new Dictionary<int, CurveReportImages>();
foreach (var row in project.CurveResults.OrderBy(x => x.Channel))
{
try
{
var rising = await WaveformStore.LoadAsync(project, row.RisingWaveformPath);
var falling = await WaveformStore.LoadAsync(project, row.FallingWaveformPath);
if (rising == null && falling == null) continue;
images[row.Channel] = new CurveReportImages(
rising == null
? null
: CurveImageRenderer.RenderRisingChannel(
row.Channel,
rising,
row.RisingViewStartMs,
row.RisingViewEndMs),
falling == null
? null
: CurveImageRenderer.RenderFallingChannel(
row.Channel,
falling,
row.FallingViewStartMs,
row.FallingViewEndMs));
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Warning, $"CH{row.Channel} 曲线插图生成失败:{ex.Message}");
}
}
return images;
}
}
public sealed record ReportFormatChoice(ReportOutputFormat Format, string DisplayName);