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" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 @@
-
+
-
-
+
+