334 lines
12 KiB
C#
334 lines
12 KiB
C#
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))
|
||
{
|
||
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();
|
||
_settingsStore.ScheduleSave();
|
||
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.SerialNumber}");
|
||
lines.Add($"设备名称:{info.ProductName}");
|
||
lines.Add($"设备型号:{info.Model}");
|
||
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, byte[]>> BuildCurveImagesAsync(Project project)
|
||
{
|
||
var images = new Dictionary<int, byte[]>();
|
||
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] = CurveImageRenderer.RenderChannel(
|
||
row.Channel,
|
||
rising,
|
||
falling,
|
||
row.ViewStartMs,
|
||
row.ViewEndMs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Add(UiLogLevel.Warning, $"CH{row.Channel} 曲线插图生成失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
return images;
|
||
}
|
||
}
|
||
|
||
public sealed record ReportFormatChoice(ReportOutputFormat Format, string DisplayName);
|