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; /// 测试报告页。 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 ReportFiles { get; } = new(); public IReadOnlyList 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 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(); 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 { $"文件名:{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> BuildCurveImagesAsync(Project project) { var images = new Dictionary(); 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.ViewStartMs, row.RisingViewEndMs ?? row.ViewEndMs), falling == null ? null : CurveImageRenderer.RenderFallingChannel( row.Channel, falling, row.FallingViewStartMs ?? row.ViewStartMs, row.FallingViewEndMs ?? row.ViewEndMs)); } catch (Exception ex) { _log.Add(UiLogLevel.Warning, $"CH{row.Channel} 曲线插图生成失败:{ex.Message}"); } } return images; } } public sealed record ReportFormatChoice(ReportOutputFormat Format, string DisplayName);