From b6a6febec4c32d7e5fe081c88935c013d909d74e Mon Sep 17 00:00:00 2001 From: zhaotielin Date: Wed, 8 Jul 2026 00:25:45 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=86=E8=8A=82=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/用户使用手册.md | 8 +- SSPCTester.Logic/Models/Results.cs | 4 +- .../Report/ProfessionalReportGenerator.cs | 30 ++-- .../Report/WordReportGenerator.cs | 28 ++- SSPCTester.Tests/WordReportGeneratorTests.cs | 9 + SSPCTester.UI/ViewModels/MainViewModel.cs | 25 +++ .../ViewModels/SettingsViewModel.Manual.cs | 68 +++++++- SSPCTester.UI/Views/MainWindow.xaml | 163 ++++++++++++------ SSPCTester.UI/Views/SettingsManualView.xaml | 6 +- 9 files changed, 264 insertions(+), 77 deletions(-) diff --git a/Docs/用户使用手册.md b/Docs/用户使用手册.md index 688828c..3bab33b 100644 --- a/Docs/用户使用手册.md +++ b/Docs/用户使用手册.md @@ -970,9 +970,9 @@ PowerShell.exe -ExecutionPolicy Bypass -File .\run.ps1 | 阈值 | 默认值 | 含义 | | ----------------- | ------ | -------------------------- | -| `MaxOnTimeMs` | 5.0 ms | 开启时间上限 | +| `MaxOnTimeMs` | 50.0 ms | 开启时间上限 | | `MaxRiseTimeMs` | 3.0 ms | 上升时间上限 | -| `MaxOffTimeMs` | 5.0 ms | 关断时间上限 | +| `MaxOffTimeMs` | 50.0 ms | 关断时间上限 | | `MaxFallTimeMs` | 3.0 ms | 下降时间上限 | > **【提示】** 阈值随项目保存(位于 `project.json` 的 `CurveThresholds` 字段),可针对不同型号被测件调整。 @@ -1734,8 +1734,8 @@ $$ | 编号 | 中文名 | 单位 | 定义 | 测试方法 | 判定准则 | 软件位置 | | ---- | ------------------ | ---- | ----------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- | --------------- | -| 1.1 | 开启时间 | ms | 控制信号有效 → 输出电压上升到 90% Vmax 的时间 | 高速采集 + 上升沿百分位检测 | ≤ MaxOnTimeMs(默认 5) | 第 10 章 | -| 1.2 | 关闭时间 | ms | 控制信号撤销 → 输出电压下降到 10% Vmax 的时间 | 高速采集 + 下降沿百分位检测 | ≤ MaxOffTimeMs(默认 5) | 第 10 章 | +| 1.1 | 开启时间 | ms | 控制信号有效 → 输出电压上升到 90% Vmax 的时间 | 高速采集 + 上升沿百分位检测 | ≤ MaxOnTimeMs(默认 50) | 第 10 章 | +| 1.2 | 关闭时间 | ms | 控制信号撤销 → 输出电压下降到 10% Vmax 的时间 | 高速采集 + 下降沿百分位检测 | ≤ MaxOffTimeMs(默认 50) | 第 10 章 | | 1.3 | 控制延迟时间 | ms | 控制端电平变化 → 内部驱动信号到达 | 厂商方法;本软件等同于"开启时间"近似 | 由型号规范确定 | 第 10 章(近似) | | 1.4 | 释放延迟时间 | ms | 控制端撤销 → 内部驱动撤销 | 同上 | 由型号规范确定 | 第 10 章(近似) | | 1.5 | 负载电压上升/下降时间 | ms | 负载侧电压 10% – 90% Vmax 的过渡时间 | 高速采集 + 双阈值检测 | ≤ MaxRiseTimeMs / MaxFallTimeMs(默认 3)| 第 10 章 | diff --git a/SSPCTester.Logic/Models/Results.cs b/SSPCTester.Logic/Models/Results.cs index 7d551e6..116219f 100644 --- a/SSPCTester.Logic/Models/Results.cs +++ b/SSPCTester.Logic/Models/Results.cs @@ -43,9 +43,9 @@ public partial class CurveResultRow : ObservableObject /// 曲线测试阈值(合格判定)。 public sealed class CurveThresholds { - public double MaxOnTimeMs { get; set; } = 5.0; + public double MaxOnTimeMs { get; set; } = 50.0; public double MaxRiseTimeMs { get; set; } = 3.0; - public double MaxOffTimeMs { get; set; } = 5.0; + public double MaxOffTimeMs { get; set; } = 50.0; public double MaxFallTimeMs { get; set; } = 3.0; } diff --git a/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs b/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs index 411f898..bc7e0d6 100644 --- a/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs +++ b/SSPCTester.Logic/Report/ProfessionalReportGenerator.cs @@ -62,7 +62,16 @@ public sealed class ReportGenerator column.Item().PaddingTop(45).Element(container => CoverIdentity(container, p, reportNo)); column.Item().PaddingTop(34).Element(container => OverallPanel(container, p)); column.Item().ExtendVertical(); - column.Item().PaddingBottom(18).BorderTop(1).BorderColor(Line).PaddingTop(14).Table(table => + }); + + page.Footer().Element(container => CoverFooter(container, p)); + } + + private static void CoverFooter(IContainer container, Project p) + { + container.Column(column => + { + column.Item().BorderTop(1).BorderColor(Line).PaddingTop(14).Table(table => { table.ColumnsDefinition(columns => { @@ -74,13 +83,12 @@ public sealed class ReportGenerator CoverSignCell(table, "审核", ""); CoverSignCell(table, "批准", ""); }); - }); - - page.Footer().Row(row => - { - row.RelativeItem().Text("本报告仅对所检样品及记录数据负责").FontSize(8).FontColor(Muted); - row.ConstantItem(180).AlignRight().Text($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm}") - .FontSize(8).FontColor(Muted); + column.Item().PaddingTop(12).Row(row => + { + row.RelativeItem().Text("本报告仅对所检样品及记录数据负责").FontSize(8).FontColor(Muted); + row.ConstantItem(180).AlignRight().Text($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm}") + .FontSize(8).FontColor(Muted); + }); }); } @@ -204,7 +212,7 @@ public sealed class ReportGenerator Section(column, section++, "开启与关断曲线", "SWITCHING CHARACTERISTICS"); if (p.CurveResults.Count > 0) { - string criteria = $"判据:开启≤{p.CurveThresholds.MaxOnTimeMs:F1}ms,上升≤{p.CurveThresholds.MaxRiseTimeMs:F1}ms,关断≤{p.CurveThresholds.MaxOffTimeMs:F1}ms,下降≤{p.CurveThresholds.MaxFallTimeMs:F1}ms"; + string criteria = $"判据:开启时间≤{p.CurveThresholds.MaxOnTimeMs:F1}ms,上升时间≤{p.CurveThresholds.MaxRiseTimeMs:F1}ms,关断时间≤{p.CurveThresholds.MaxOffTimeMs:F1}ms,下降时间≤{p.CurveThresholds.MaxFallTimeMs:F1}ms"; column.Item().Element(x => ModuleSummary(x, criteria, p.CurveResults.Select(r => r.Status))); column.Item().Element(x => CurveTable(x, p)); } @@ -462,14 +470,14 @@ public sealed class ReportGenerator private static void OverallPanel(IContainer container, Project p) { container.Border(1.2f).BorderColor(StatusColor(p.OverallStatus)) - .Background(StatusBackground(p.OverallStatus)).Padding(14).Row(row => + .Background(StatusBackground(p.OverallStatus)).Padding(12).Row(row => { row.RelativeItem().Column(column => { column.Item().Text("综合检测结论").FontSize(9).FontColor(Muted); column.Item().PaddingTop(4).Text(ConclusionText(p.OverallStatus)).FontSize(9); }); - row.ConstantItem(110).AlignRight().AlignMiddle().Text(StatusText(p.OverallStatus)) + row.ConstantItem(72).PaddingLeft(10).AlignRight().AlignMiddle().Text(StatusText(p.OverallStatus)) .FontSize(18).Bold().FontColor(StatusColor(p.OverallStatus)); }); } diff --git a/SSPCTester.Logic/Report/WordReportGenerator.cs b/SSPCTester.Logic/Report/WordReportGenerator.cs index cad86ff..0c7500d 100644 --- a/SSPCTester.Logic/Report/WordReportGenerator.cs +++ b/SSPCTester.Logic/Report/WordReportGenerator.cs @@ -34,10 +34,11 @@ public sealed class WordReportGenerator Body body = main.Document.Body!; string reportNo = BuildReportNumber(project); + string? firstPageFooterId = project.Report.Cover ? AddFirstPageFooter(main) : null; if (project.Report.Cover) ComposeCover(body, project, reportNo); ComposeBody(body, main, project, reportNo); - body.Append(PageSectionProperties()); + body.Append(PageSectionProperties(firstPageFooterId)); main.Document.Save(); } @@ -95,11 +96,17 @@ public sealed class WordReportGenerator }), Spacer(18), ConclusionBox(p), - Spacer(52), - SignatureTable(), PageBreak()); } + private static string AddFirstPageFooter(MainDocumentPart main) + { + var footerPart = main.AddNewPart(); + footerPart.Footer = new Footer(SignatureTable(), FooterParagraph()); + footerPart.Footer.Save(); + return main.GetIdOfPart(footerPart); + } + private void ComposeBody(Body body, MainDocumentPart main, Project p, string reportNo) { body.Append(HeaderParagraph("固态功率控制器动态参数检测报告", reportNo)); @@ -388,10 +395,21 @@ public sealed class WordReportGenerator private static Paragraph FooterParagraph() => Paragraph($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm} 本报告仅对所检样品及记录数据负责。", 16, Muted, JustificationValues.Center); - private static SectionProperties PageSectionProperties() => - new( + private static SectionProperties PageSectionProperties(string? firstPageFooterId = null) + { + var properties = new SectionProperties(); + if (!string.IsNullOrWhiteSpace(firstPageFooterId)) + { + properties.Append( + new FooterReference { Type = HeaderFooterValues.First, Id = firstPageFooterId }, + new TitlePage()); + } + + properties.Append( new PageSize { Width = 11906U, Height = 16838U }, new PageMargin { Top = 900, Right = 850U, Bottom = 900, Left = 850U, Header = 450U, Footer = 450U, Gutter = 0U }); + return properties; + } private static long CmToEmu(double cm) => (long)Math.Round(cm * 360000.0); diff --git a/SSPCTester.Tests/WordReportGeneratorTests.cs b/SSPCTester.Tests/WordReportGeneratorTests.cs index 0a44113..dfb69f7 100644 --- a/SSPCTester.Tests/WordReportGeneratorTests.cs +++ b/SSPCTester.Tests/WordReportGeneratorTests.cs @@ -39,6 +39,15 @@ public sealed class WordReportGeneratorTests .ToArray(); Assert.Contains("SimSun", fontNames); Assert.Equal(2, document.MainDocumentPart.ImageParts.Count()); + + var sectionProperties = document.MainDocumentPart.Document.Body!.Descendants().Single(); + var firstFooterReference = sectionProperties.Elements() + .Single(x => x.Type?.Value == HeaderFooterValues.First); + Assert.Contains(sectionProperties.Elements(), _ => true); + + var firstFooter = (FooterPart)document.MainDocumentPart.GetPartById(firstFooterReference.Id!); + Assert.Single(firstFooter.Footer!.Elements()); + Assert.Equal(2, firstFooter.Footer!.Descendants().Count()); } finally { diff --git a/SSPCTester.UI/ViewModels/MainViewModel.cs b/SSPCTester.UI/ViewModels/MainViewModel.cs index fcbc0e1..c266b72 100644 --- a/SSPCTester.UI/ViewModels/MainViewModel.cs +++ b/SSPCTester.UI/ViewModels/MainViewModel.cs @@ -377,10 +377,35 @@ public partial class MainViewModel : ObservableObject _viewBeforeSettings = CurrentView; var vm = _sp.GetRequiredService(); vm.SetCloseAction(CloseSettings); + vm.SetProjectRefreshAction(RefreshProjectsFromSettingsAsync, CanRefreshProjectsFromSettings); CurrentView = vm; DevicePanelOpen = false; } + private bool CanRefreshProjectsFromSettings() + { + if (State != AppState.Testing) return true; + + _logSink.Add(UiLogLevel.Warning, "测试中不可切换项目目录,请退出测试后再刷新工程。"); + return false; + } + + private async Task RefreshProjectsFromSettingsAsync(string projectsRoot) + { + if (!CanRefreshProjectsFromSettings()) return; + if (string.IsNullOrWhiteSpace(projectsRoot)) + { + _logSink.Add(UiLogLevel.Error, "项目数据路径为空,无法刷新工程。"); + return; + } + + Directory.CreateDirectory(projectsRoot); + _store.ProjectsRoot = projectsRoot; + CurrentProject = null; + _viewBeforeSettings = _sp.GetRequiredService(); + await LoadProjectsAsync(); + } + [RelayCommand] private void OpenSettingsFromDevicePanel() { diff --git a/SSPCTester.UI/ViewModels/SettingsViewModel.Manual.cs b/SSPCTester.UI/ViewModels/SettingsViewModel.Manual.cs index 0cf94ed..046023d 100644 --- a/SSPCTester.UI/ViewModels/SettingsViewModel.Manual.cs +++ b/SSPCTester.UI/ViewModels/SettingsViewModel.Manual.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.Win32; using SSPCTester.Devices.Config; using SSPCTester.Devices.Interfaces; +using SSPCTester.Logic.Storage; using SSPCTester.UI.Services; namespace SSPCTester.UI.ViewModels; @@ -25,14 +26,18 @@ public partial class SettingsViewModel : ObservableObject private readonly StorageOptions _runtimeStorage; private readonly ReportOptions _runtimeReport; private readonly UserSettingsStore _settingsStore; + private readonly ProjectStore _projectStore; private Action? _closeAction; + private Func? _canRefreshProjects; + private Func? _projectRefreshAction; public SettingsViewModel( IOptions devOpts, IOptions storeOpts, IOptions reportOpts, ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, ISafetyDio safetyDio, UiLogSink log, - UserSettingsStore settingsStore) + UserSettingsStore settingsStore, + ProjectStore projectStore) { _runtimeDevices = devOpts.Value; _runtimeStorage = storeOpts.Value; @@ -44,6 +49,7 @@ public partial class SettingsViewModel : ObservableObject _safetyDio = safetyDio; _log = log; _settingsStore = settingsStore; + _projectStore = projectStore; Devices = CloneDeviceOptions(_runtimeDevices); Storage = CloneStorageOptions(_runtimeStorage); @@ -141,6 +147,12 @@ public partial class SettingsViewModel : ObservableObject public void SetCloseAction(Action closeAction) => _closeAction = closeAction; + public void SetProjectRefreshAction(Func refreshAction, Func? canRefresh = null) + { + _projectRefreshAction = refreshAction; + _canRefreshProjects = canRefresh; + } + public void NotifyDraftChanged() { ApplyDaqChannelEditorsToDraft(); @@ -154,17 +166,29 @@ public partial class SettingsViewModel : ObservableObject private void Close() => _closeAction?.Invoke(); [RelayCommand] - private async Task SaveSettings() + private async Task SaveSettings() => + await SaveSettingsCore(logSaved: true); + + private async Task SaveSettingsCore(bool logSaved) { ApplyDaqChannelEditorsToDraft(); Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels); ApplyToRuntime(Devices, _runtimeDevices); ApplyToRuntime(Storage, _runtimeStorage); ApplyToRuntime(Report, _runtimeReport); - await _settingsStore.SaveAsync().ConfigureAwait(false); + ApplyProjectsRootToStore(); + await _settingsStore.SaveAsync(); OnPropertyChanged(nameof(HasUnsavedChanges)); OnPropertyChanged(nameof(DaqValidationMessage)); - _log.Add(UiLogLevel.Success, "设置已保存。重新连接请单独点击“重新连接”。"); + if (logSaved) + _log.Add(UiLogLevel.Success, "设置已保存。重新连接请单独点击“重新连接”。"); + } + + private void ApplyProjectsRootToStore() + { + _projectStore.ProjectsRoot = _runtimeStorage.ProjectsRoot; + if (!string.IsNullOrWhiteSpace(_projectStore.ProjectsRoot)) + Directory.CreateDirectory(_projectStore.ProjectsRoot); } [RelayCommand] @@ -350,6 +374,42 @@ public partial class SettingsViewModel : ObservableObject [RelayCommand] private void OpenProjectsDirectory() => OpenDirectory(Storage.ProjectsRoot); + [RelayCommand] + private async Task RefreshProjects() + { + NotifyDraftChanged(); + if (string.IsNullOrWhiteSpace(Storage.ProjectsRoot)) + { + _log.Add(UiLogLevel.Error, "项目数据路径为空,无法刷新工程。"); + return; + } + + if (_canRefreshProjects?.Invoke() == false) + return; + + if (_projectRefreshAction == null) + { + _log.Add(UiLogLevel.Warning, "刷新工程功能尚未初始化。"); + return; + } + + try + { + await SaveSettingsCore(logSaved: false); + await _projectRefreshAction(_projectStore.ProjectsRoot); + await RunOnUiAsync(() => + { + OnPropertyChanged(nameof(HasUnsavedChanges)); + OnPropertyChanged(nameof(DaqValidationMessage)); + }); + _log.Add(UiLogLevel.Success, "工程列表已刷新。"); + } + catch (Exception ex) + { + _log.Add(UiLogLevel.Error, $"刷新工程失败:{ex.Message}"); + } + } + [RelayCommand] private void OpenReportsDirectory() => OpenDirectory(Report.SavePath); diff --git a/SSPCTester.UI/Views/MainWindow.xaml b/SSPCTester.UI/Views/MainWindow.xaml index 6df516f..ecdc84c 100644 --- a/SSPCTester.UI/Views/MainWindow.xaml +++ b/SSPCTester.UI/Views/MainWindow.xaml @@ -37,8 +37,48 @@ FontSize="{StaticResource FsTitle}" FontWeight="Bold" /> - + + + + + + + + + + + + + + + + + + @@ -106,33 +146,38 @@ - - - - - - - - + + + + + + + + + + + + + @@ -167,19 +212,53 @@ + @@ -273,18 +352,6 @@ - - diff --git a/SSPCTester.UI/Views/SettingsManualView.xaml b/SSPCTester.UI/Views/SettingsManualView.xaml index 42fee4c..e29e7a1 100644 --- a/SSPCTester.UI/Views/SettingsManualView.xaml +++ b/SSPCTester.UI/Views/SettingsManualView.xaml @@ -277,12 +277,12 @@ - + -