refactor: organize cnc view model folders

This commit is contained in:
zhengxuan.zhang
2026-08-10 14:02:01 +08:00
parent 5cfc7077d4
commit 5cacd4ddd7
20 changed files with 3082 additions and 3082 deletions
@@ -1,54 +1,54 @@
// ── CNC 流水线步骤 ViewModel ──
// 检测模块流水线中的单一步骤(具体图像处理算子)的轻量封装,
// 在树形结构中作为三级节点显示。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测模块流水线步骤的轻量 ViewModel,用于在 CNC 树形结构中作为三级节点显示。
/// Lightweight ViewModel for a pipeline step inside an InspectionModule node,
/// displayed as a 3rd-level node in the CNC tree.
/// </summary>
public class CncPipelineStepViewModel : BindableBase
{
private string _displayName;
private bool _isEnabled;
private string _iconPath;
public CncPipelineStepViewModel(string displayName, bool isEnabled, string iconPath = null)
{
_displayName = displayName;
_isEnabled = isEnabled;
_iconPath = iconPath ?? string.Empty;
}
/// <summary>算子显示名称(中文)| Operator display name</summary>
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
/// <summary>是否启用 | Whether the step is enabled</summary>
public bool IsEnabled
{
get => _isEnabled;
set
{
if (SetProperty(ref _isEnabled, value))
RaisePropertyChanged(nameof(StateText));
}
}
/// <summary>图标路径 | Icon path</summary>
public string IconPath
{
get => _iconPath;
set => SetProperty(ref _iconPath, value);
}
/// <summary>状态文字(已启用 / 已停用)| State text</summary>
public string StateText => _isEnabled ? "已启用" : "已停用";
}
// ── CNC 流水线步骤 ViewModel ──
// 检测模块流水线中的单一步骤(具体图像处理算子)的轻量封装,
// 在树形结构中作为三级节点显示。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测模块流水线步骤的轻量 ViewModel,用于在 CNC 树形结构中作为三级节点显示。
/// Lightweight ViewModel for a pipeline step inside an InspectionModule node,
/// displayed as a 3rd-level node in the CNC tree.
/// </summary>
public class CncPipelineStepViewModel : BindableBase
{
private string _displayName;
private bool _isEnabled;
private string _iconPath;
public CncPipelineStepViewModel(string displayName, bool isEnabled, string iconPath = null)
{
_displayName = displayName;
_isEnabled = isEnabled;
_iconPath = iconPath ?? string.Empty;
}
/// <summary>算子显示名称(中文)| Operator display name</summary>
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
/// <summary>是否启用 | Whether the step is enabled</summary>
public bool IsEnabled
{
get => _isEnabled;
set
{
if (SetProperty(ref _isEnabled, value))
RaisePropertyChanged(nameof(StateText));
}
}
/// <summary>图标路径 | Icon path</summary>
public string IconPath
{
get => _iconPath;
set => SetProperty(ref _iconPath, value);
}
/// <summary>状态文字(已启用 / 已停用)| State text</summary>
public string StateText => _isEnabled ? "已启用" : "已停用";
}
}
@@ -1,34 +1,34 @@
// ── CNC 程序树根节点 ViewModel ──
// 程序树的顶层根节点,包含子节点列表(CncNodeViewModel),
// 负责子节点的添加/移除通知。
using Prism.Mvvm;
using System.Collections.ObjectModel;
namespace XplorePlane.ViewModels.Cnc
{
public class CncProgramTreeRootViewModel : BindableBase
{
private string _displayName;
private bool _isExpanded = true;
public CncProgramTreeRootViewModel(string displayName, ObservableCollection<CncNodeViewModel> children)
{
_displayName = displayName;
Children = children;
}
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public ObservableCollection<CncNodeViewModel> Children { get; }
}
// ── CNC 程序树根节点 ViewModel ──
// 程序树的顶层根节点,包含子节点列表(CncNodeViewModel),
// 负责子节点的添加/移除通知。
using Prism.Mvvm;
using System.Collections.ObjectModel;
namespace XplorePlane.ViewModels.Cnc
{
public class CncProgramTreeRootViewModel : BindableBase
{
private string _displayName;
private bool _isExpanded = true;
public CncProgramTreeRootViewModel(string displayName, ObservableCollection<CncNodeViewModel> children)
{
_displayName = displayName;
Children = children;
}
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public ObservableCollection<CncNodeViewModel> Children { get; }
}
}
@@ -1,102 +1,102 @@
// ── 检测指标行 ViewModel ──
// 将 InspectionMetricResult 模型封装为可绑定的行数据,
// 在检测执行结果列表中显示指标名称、测量值、容差范围等。
using Prism.Mvvm;
using System.Windows.Media;
using XplorePlane.Themes;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测指标行 ViewModel,将 InspectionMetricResult 模型封装为可绑定的 WPF ViewModel
/// Inspection metric row ViewModel that wraps an InspectionMetricResult model into a bindable WPF ViewModel
/// </summary>
public class InspectionMetricRowViewModel : BindableBase
{
private readonly InspectionMetricResult _model;
/// <summary>
/// 构造函数,从 InspectionMetricResult 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from an InspectionMetricResult model
/// </summary>
public InspectionMetricRowViewModel(InspectionMetricResult model)
{
_model = model;
}
/// <summary>底层检测指标模型(只读)| Underlying inspection metric model (read-only)</summary>
public InspectionMetricResult Model => _model;
/// <summary>指标名称 | Metric name</summary>
public string MetricName => _model.MetricName;
/// <summary>实测值 | Measured metric value</summary>
public double MetricValue => _model.MetricValue;
/// <summary>单位 | Unit of measurement</summary>
public string Unit => _model.Unit;
/// <summary>下限 | Lower limit (nullable)</summary>
public double? LowerLimit => _model.LowerLimit;
/// <summary>上限 | Upper limit (nullable)</summary>
public double? UpperLimit => _model.UpperLimit;
/// <summary>单指标判定 | Individual metric pass/fail status</summary>
public bool IsPass => _model.IsPass;
/// <summary>显示顺序 | Display order for sorting</summary>
public int DisplayOrder => _model.DisplayOrder;
/// <summary>
/// 下限文本(null 时显示 "—"
/// Lower limit text (displays "—" when null)
/// </summary>
public string LowerLimitText => _model.LowerLimit.HasValue ? _model.LowerLimit.Value.ToString("F2") : "—";
/// <summary>
/// 上限文本(null 时显示 "—"
/// Upper limit text (displays "—" when null)
/// </summary>
public string UpperLimitText => _model.UpperLimit.HasValue ? _model.UpperLimit.Value.ToString("F2") : "—";
/// <summary>
/// 实测值是否超出范围(用于加粗字体触发器)
/// Whether the measured value is out of range (used for bold font trigger)
/// </summary>
public bool IsValueOutOfRange
{
get
{
// 如果下限不为空且实测值小于下限,则超出范围
// If lower limit is not null and measured value is less than lower limit, it's out of range
if (_model.LowerLimit.HasValue && _model.MetricValue < _model.LowerLimit.Value)
{
return true;
}
// 如果上限不为空且实测值大于上限,则超出范围
// If upper limit is not null and measured value is greater than upper limit, it's out of range
if (_model.UpperLimit.HasValue && _model.MetricValue > _model.UpperLimit.Value)
{
return true;
}
return false;
}
}
/// <summary>
/// 行背景色(IsPass=true → NOVA success 容器色,IsPass=false → NOVA error 容器色)
/// Row background color (IsPass=true → NOVA success container, IsPass=false → NOVA error container)
/// </summary>
/// <remarks>
/// 此前硬编码 #E8F5E9/#FFEBEE,与 Converters.cs 中 PassFailColorConverter
/// 等判定色转换器各自维护一套色值。现改为从 Nova 令牌取色,与全应用的
/// 通过/失败语义色统一。若资源未加载(极端情况),回退到浅灰,
/// 不使用原硬编码色,避免同一处出现两套配色来源。
/// </remarks>
public Brush RowBackground =>
NovaResourceHelper.FindBrush(_model.IsPass ? "NovaSuccessContainerBrush" : "NovaErrorContainerBrush");
}
// ── 检测指标行 ViewModel ──
// 将 InspectionMetricResult 模型封装为可绑定的行数据,
// 在检测执行结果列表中显示指标名称、测量值、容差范围等。
using Prism.Mvvm;
using System.Windows.Media;
using XplorePlane.Themes;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测指标行 ViewModel,将 InspectionMetricResult 模型封装为可绑定的 WPF ViewModel
/// Inspection metric row ViewModel that wraps an InspectionMetricResult model into a bindable WPF ViewModel
/// </summary>
public class InspectionMetricRowViewModel : BindableBase
{
private readonly InspectionMetricResult _model;
/// <summary>
/// 构造函数,从 InspectionMetricResult 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from an InspectionMetricResult model
/// </summary>
public InspectionMetricRowViewModel(InspectionMetricResult model)
{
_model = model;
}
/// <summary>底层检测指标模型(只读)| Underlying inspection metric model (read-only)</summary>
public InspectionMetricResult Model => _model;
/// <summary>指标名称 | Metric name</summary>
public string MetricName => _model.MetricName;
/// <summary>实测值 | Measured metric value</summary>
public double MetricValue => _model.MetricValue;
/// <summary>单位 | Unit of measurement</summary>
public string Unit => _model.Unit;
/// <summary>下限 | Lower limit (nullable)</summary>
public double? LowerLimit => _model.LowerLimit;
/// <summary>上限 | Upper limit (nullable)</summary>
public double? UpperLimit => _model.UpperLimit;
/// <summary>单指标判定 | Individual metric pass/fail status</summary>
public bool IsPass => _model.IsPass;
/// <summary>显示顺序 | Display order for sorting</summary>
public int DisplayOrder => _model.DisplayOrder;
/// <summary>
/// 下限文本(null 时显示 "—"
/// Lower limit text (displays "—" when null)
/// </summary>
public string LowerLimitText => _model.LowerLimit.HasValue ? _model.LowerLimit.Value.ToString("F2") : "—";
/// <summary>
/// 上限文本(null 时显示 "—"
/// Upper limit text (displays "—" when null)
/// </summary>
public string UpperLimitText => _model.UpperLimit.HasValue ? _model.UpperLimit.Value.ToString("F2") : "—";
/// <summary>
/// 实测值是否超出范围(用于加粗字体触发器)
/// Whether the measured value is out of range (used for bold font trigger)
/// </summary>
public bool IsValueOutOfRange
{
get
{
// 如果下限不为空且实测值小于下限,则超出范围
// If lower limit is not null and measured value is less than lower limit, it's out of range
if (_model.LowerLimit.HasValue && _model.MetricValue < _model.LowerLimit.Value)
{
return true;
}
// 如果上限不为空且实测值大于上限,则超出范围
// If upper limit is not null and measured value is greater than upper limit, it's out of range
if (_model.UpperLimit.HasValue && _model.MetricValue > _model.UpperLimit.Value)
{
return true;
}
return false;
}
}
/// <summary>
/// 行背景色(IsPass=true → NOVA success 容器色,IsPass=false → NOVA error 容器色)
/// Row background color (IsPass=true → NOVA success container, IsPass=false → NOVA error container)
/// </summary>
/// <remarks>
/// 此前硬编码 #E8F5E9/#FFEBEE,与 Converters.cs 中 PassFailColorConverter
/// 等判定色转换器各自维护一套色值。现改为从 Nova 令牌取色,与全应用的
/// 通过/失败语义色统一。若资源未加载(极端情况),回退到浅灰,
/// 不使用原硬编码色,避免同一处出现两套配色来源。
/// </remarks>
public Brush RowBackground =>
NovaResourceHelper.FindBrush(_model.IsPass ? "NovaSuccessContainerBrush" : "NovaErrorContainerBrush");
}
}
@@ -1,394 +1,394 @@
// ── 检测节点卡片 ViewModel ──
// CNC 执行时各检测节点运行状态监控的 ViewModel。
// 绑定到运行视图中的校验卡片面板,实时显示每个检测子节点的状态/进度/数据。
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using XplorePlane.Helpers;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测节点卡片 ViewModel,包装单个 InspectionNodeResult 并提供图像切换和快照查看功能
/// Inspection node card ViewModel that wraps a single InspectionNodeResult and provides image toggle and snapshot viewing
/// </summary>
public class InspectionNodeCardViewModel : BindableBase
{
private readonly InspectionNodeResult _nodeResult;
private readonly IXpDataPathService _dataPathService;
private readonly string _imageBaseDirectory;
private readonly PipelineExecutionSnapshot _snapshot;
private readonly InspectionAssetRecord _resultImageAsset;
private readonly InspectionAssetRecord _inputImageAsset;
private readonly IReadOnlyList<InspectionResultTable> _resultTables;
private BitmapSource _resultImage;
private BitmapSource _inputImage;
private BitmapSource _currentImage;
private bool _isShowingInputImage;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="nodeResult">节点结果数据 | Node result data</param>
/// <param name="metrics">该节点的所有指标 | All metrics for this node</param>
/// <param name="assets">该节点的所有资产 | All assets for this node</param>
/// <param name="snapshot">Pipeline 快照(可为 null| Pipeline snapshot (can be null)</param>
/// <param name="resultTables">该节点的结构化明细表(可为 null| Structured result tables for this node (can be null)</param>
/// <param name="dataPathService">数据路径服务 | Data path service</param>
/// <param name="imageBaseDirectory">图像基目录覆盖(本地导入时使用;为空则用 {DataPath}\InspectionResults| Image base override (used for local import; defaults to {DataPath}\InspectionResults)</param>
public InspectionNodeCardViewModel(
InspectionNodeResult nodeResult,
IEnumerable<InspectionMetricResult> metrics,
IEnumerable<InspectionAssetRecord> assets,
PipelineExecutionSnapshot snapshot,
IEnumerable<InspectionResultTable> resultTables,
IXpDataPathService dataPathService,
string imageBaseDirectory = null)
{
_nodeResult = nodeResult ?? throw new ArgumentNullException(nameof(nodeResult));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_imageBaseDirectory = string.IsNullOrWhiteSpace(imageBaseDirectory)
// ── 检测节点卡片 ViewModel ──
// CNC 执行时各检测节点运行状态监控的 ViewModel。
// 绑定到运行视图中的校验卡片面板,实时显示每个检测子节点的状态/进度/数据。
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using XplorePlane.Helpers;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测节点卡片 ViewModel,包装单个 InspectionNodeResult 并提供图像切换和快照查看功能
/// Inspection node card ViewModel that wraps a single InspectionNodeResult and provides image toggle and snapshot viewing
/// </summary>
public class InspectionNodeCardViewModel : BindableBase
{
private readonly InspectionNodeResult _nodeResult;
private readonly IXpDataPathService _dataPathService;
private readonly string _imageBaseDirectory;
private readonly PipelineExecutionSnapshot _snapshot;
private readonly InspectionAssetRecord _resultImageAsset;
private readonly InspectionAssetRecord _inputImageAsset;
private readonly IReadOnlyList<InspectionResultTable> _resultTables;
private BitmapSource _resultImage;
private BitmapSource _inputImage;
private BitmapSource _currentImage;
private bool _isShowingInputImage;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="nodeResult">节点结果数据 | Node result data</param>
/// <param name="metrics">该节点的所有指标 | All metrics for this node</param>
/// <param name="assets">该节点的所有资产 | All assets for this node</param>
/// <param name="snapshot">Pipeline 快照(可为 null| Pipeline snapshot (can be null)</param>
/// <param name="resultTables">该节点的结构化明细表(可为 null| Structured result tables for this node (can be null)</param>
/// <param name="dataPathService">数据路径服务 | Data path service</param>
/// <param name="imageBaseDirectory">图像基目录覆盖(本地导入时使用;为空则用 {DataPath}\InspectionResults| Image base override (used for local import; defaults to {DataPath}\InspectionResults)</param>
public InspectionNodeCardViewModel(
InspectionNodeResult nodeResult,
IEnumerable<InspectionMetricResult> metrics,
IEnumerable<InspectionAssetRecord> assets,
PipelineExecutionSnapshot snapshot,
IEnumerable<InspectionResultTable> resultTables,
IXpDataPathService dataPathService,
string imageBaseDirectory = null)
{
_nodeResult = nodeResult ?? throw new ArgumentNullException(nameof(nodeResult));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_imageBaseDirectory = string.IsNullOrWhiteSpace(imageBaseDirectory)
? _dataPathService.DataPath
: imageBaseDirectory;
_snapshot = snapshot;
// 查找资产记录
// Find asset records
var assetList = assets?.ToList() ?? new List<InspectionAssetRecord>();
_resultImageAsset = assetList.FirstOrDefault(a => a.NodeId == nodeResult.NodeId && a.AssetType == InspectionAssetType.NodeResultImage);
_inputImageAsset = assetList.FirstOrDefault(a => a.NodeId == nodeResult.NodeId && a.AssetType == InspectionAssetType.NodeInputImage);
// 构建指标列表(按 DisplayOrder 升序,同序按 MetricName 升序)
// Build metrics list (sorted by DisplayOrder ascending, then by MetricName ascending)
var metricList = metrics?
.Where(m => m.NodeId == nodeResult.NodeId)
.OrderBy(m => m.DisplayOrder)
.ThenBy(m => m.MetricName)
.Select(m => new InspectionMetricRowViewModel(m))
.ToList() ?? new List<InspectionMetricRowViewModel>();
Metrics = new ObservableCollection<InspectionMetricRowViewModel>(metricList);
// 筛选属于本节点的结构化明细表,并按 DisplayOrder/TableName 排序
// Filter structured result tables that belong to this node, sorted by DisplayOrder/TableName
_resultTables = resultTables?
.Where(t => t.NodeId == nodeResult.NodeId)
.OrderBy(t => t.DisplayOrder)
.ThenBy(t => t.TableName)
.ToList()
as IReadOnlyList<InspectionResultTable>
?? Array.Empty<InspectionResultTable>();
// 为 UI 绑定构建列信息,按 DisplayOrder 取第一个有数据的表(UI 一次只显示一个明细表)
// Build column info for UI binding from the first table with data (UI shows one detail table at a time)
if (_resultTables.Count > 0 && _resultTables[0].Columns?.Count > 0)
{
ResultTableColumns = _resultTables[0].Columns;
ResultTableRows = new ObservableCollection<Dictionary<string, object>>(
_resultTables[0].Rows ?? Array.Empty<Dictionary<string, object>>());
}
else
{
ResultTableColumns = Array.Empty<InspectionResultTableColumn>();
ResultTableRows = new ObservableCollection<Dictionary<string, object>>();
}
// 初始化命令
// Initialize commands
ToggleImageCommand = new DelegateCommand(ExecuteToggleImage, CanExecuteToggleImage);
ViewSnapshotCommand = new DelegateCommand(ExecuteViewSnapshot, CanExecuteViewSnapshot);
// 异步加载图像
// Load images asynchronously
_ = LoadImagesAsync();
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>节点序号 | Node index</summary>
public int NodeIndex => _nodeResult.NodeIndex;
/// <summary>节点名称 | Node name</summary>
public string NodeName => _nodeResult.NodeName;
/// <summary>Pipeline 名称 | Pipeline name</summary>
public string PipelineName => _nodeResult.PipelineName;
/// <summary>节点判定 | Node pass status</summary>
public bool NodePass => _nodeResult.NodePass;
/// <summary>节点状态 | Node status</summary>
public InspectionNodeStatus Status => _nodeResult.Status;
/// <summary>耗时(毫秒)| Duration in milliseconds</summary>
public long DurationMs => _nodeResult.DurationMs;
/// <summary>
/// 判定标签文字("Pass" / "Fail"
/// Pass label text ("Pass" / "Fail")
/// </summary>
public string PassLabel => NodePass ? "Pass" : "Fail";
// 注:PassLabelColor 曾在此定义(硬编码 #2E7D32/#C62828),但未被任何
// XAML 绑定引用——检测节点卡片实际走的是 PassFailColorConverter
// (绑定 NodePass),已改用 Nova 令牌。该死代码属性已删除,避免继续
// 维护一套从未生效的重复配色。
/// <summary>
/// 是否有资产缺失警告(Status == AssetMissing
/// Whether there is an asset missing warning (Status == AssetMissing)
/// </summary>
public bool HasAssetMissingWarning => Status == InspectionNodeStatus.AssetMissing;
/// <summary>
/// Pipeline 版本标识(hash 前 8 位,空则 "--"
/// Pipeline version identifier (first 8 chars of hash, or "--" if empty)
/// </summary>
public string PipelineVersionShort
{
get
{
if (string.IsNullOrWhiteSpace(_nodeResult.PipelineVersionHash))
{
return "--";
}
return _nodeResult.PipelineVersionHash.Length >= 8
? _nodeResult.PipelineVersionHash.Substring(0, 8)
: _nodeResult.PipelineVersionHash;
}
}
/// <summary>
/// 是否有 Pipeline 快照
/// Whether pipeline snapshot exists
/// </summary>
public bool HasPipelineSnapshot => _snapshot != null && !string.IsNullOrWhiteSpace(_snapshot.PipelineDefinitionJson);
/// <summary>
/// 格式化的 Pipeline 定义 JSON2 空格缩进)
/// Formatted pipeline definition JSON (2-space indentation)
/// </summary>
public string PipelineDefinitionJson
{
get
{
if (_snapshot == null || string.IsNullOrWhiteSpace(_snapshot.PipelineDefinitionJson))
{
return string.Empty;
}
try
{
// 尝试解析并格式化 JSON
// Try to parse and format JSON
var parsed = JToken.Parse(_snapshot.PipelineDefinitionJson);
return parsed.ToString(Formatting.Indented);
}
catch
{
// 解析失败,返回原始字符串
// Parsing failed, return original string
return _snapshot.PipelineDefinitionJson;
}
}
}
/// <summary>
/// 节点结果图(可为 null
/// Node result image (can be null)
/// </summary>
public BitmapSource ResultImage
{
get => _resultImage;
private set => SetProperty(ref _resultImage, value);
}
/// <summary>
/// 节点输入图(可为 null
/// Node input image (can be null)
/// </summary>
public BitmapSource InputImage
{
get => _inputImage;
private set => SetProperty(ref _inputImage, value);
}
/// <summary>
/// 当前显示的图像(默认为结果图)
/// Currently displayed image (defaults to result image)
/// </summary>
public BitmapSource CurrentImage
{
get => _currentImage;
private set => SetProperty(ref _currentImage, value);
}
/// <summary>
/// 是否正在显示输入图
/// Whether currently showing input image
/// </summary>
public bool IsShowingInputImage
{
get => _isShowingInputImage;
private set => SetProperty(ref _isShowingInputImage, value);
}
/// <summary>
/// 是否有输入图
/// Whether input image exists
/// </summary>
public bool HasInputImage => InputImage != null;
/// <summary>
/// 指标列表(按 DisplayOrder 升序,同序按 MetricName 升序)
/// Metrics list (sorted by DisplayOrder ascending, then by MetricName ascending)
/// </summary>
public ObservableCollection<InspectionMetricRowViewModel> Metrics { get; }
/// <summary>
/// 是否有结构化明细表(如 BGA焊球明细、QFN引脚明细)
/// Whether structured result tables exist (e.g. BGA ball details, QFN lead details)
/// </summary>
public bool HasResultTables => ResultTableRows != null && ResultTableRows.Count > 0;
/// <summary>
/// 当前明细表的列定义(Key为列键,Name为显示名,含Unit)
/// Column definitions for the current result table
/// </summary>
public IReadOnlyList<InspectionResultTableColumn> ResultTableColumns { get; }
/// <summary>
/// 当前明细表的行数据(每个 Dictionary 代表一行,Key 为列键)
/// Row data for the current result table (each Dictionary is a row, Key is column key)
/// </summary>
public ObservableCollection<Dictionary<string, object>> ResultTableRows { get; }
// ── 命令 | Commands ────────────────────────────────────────────
/// <summary>
/// 切换图像命令(在输入图和结果图之间切换)
/// Toggle image command (switch between input and result images)
/// </summary>
public DelegateCommand ToggleImageCommand { get; }
/// <summary>
/// 查看快照命令(打开 SnapshotViewerWindow
/// View snapshot command (open SnapshotViewerWindow)
/// </summary>
public DelegateCommand ViewSnapshotCommand { get; }
// ── 私有方法 | Private Methods ─────────────────────────────────
/// <summary>
/// 异步加载图像
/// Load images asynchronously
/// </summary>
private async Task LoadImagesAsync()
{
// 加载结果图
// Load result image
if (_resultImageAsset != null && !string.IsNullOrWhiteSpace(_resultImageAsset.RelativePath))
{
var resultImagePath = Path.Combine(_imageBaseDirectory, _resultImageAsset.RelativePath);
ResultImage = await ImageLoader.LoadBitmapSafeAsync(resultImagePath);
}
// 加载输入图
// Load input image
if (_inputImageAsset != null && !string.IsNullOrWhiteSpace(_inputImageAsset.RelativePath))
{
var inputImagePath = Path.Combine(_imageBaseDirectory, _inputImageAsset.RelativePath);
InputImage = await ImageLoader.LoadBitmapSafeAsync(inputImagePath);
}
// 设置当前图像为结果图
// Set current image to result image
CurrentImage = ResultImage;
IsShowingInputImage = false;
// 通知命令状态可能已更改
// Notify that command state may have changed
ToggleImageCommand.RaiseCanExecuteChanged();
}
/// <summary>
/// 执行切换图像命令
/// Execute toggle image command
/// </summary>
private void ExecuteToggleImage()
{
if (IsShowingInputImage)
{
// 切换到结果图
// Switch to result image
CurrentImage = ResultImage;
IsShowingInputImage = false;
}
else
{
// 切换到输入图
// Switch to input image
CurrentImage = InputImage;
IsShowingInputImage = true;
}
}
/// <summary>
/// 判断是否可以执行切换图像命令
/// Determine whether toggle image command can be executed
/// </summary>
private bool CanExecuteToggleImage()
{
return InputImage != null && ResultImage != null;
}
/// <summary>
/// 执行查看快照命令
/// Execute view snapshot command
/// </summary>
private void ExecuteViewSnapshot()
{
if (!HasPipelineSnapshot)
{
return;
}
// 创建并显示快照查看器窗口
// Create and show snapshot viewer window
// 注意:这里需要在 Views/Cnc 中实现 SnapshotViewerWindow
// Note: SnapshotViewerWindow needs to be implemented in Views/Cnc
try
{
var window = new Views.Cnc.SnapshotViewerWindow(PipelineDefinitionJson, PipelineName);
window.Show();
}
catch (Exception ex)
{
// 如果窗口尚未实现,静默失败
// Silently fail if window is not yet implemented
System.Diagnostics.Debug.WriteLine($"Failed to open snapshot viewer: {ex.Message}");
}
}
/// <summary>
/// 判断是否可以执行查看快照命令
/// Determine whether view snapshot command can be executed
/// </summary>
private bool CanExecuteViewSnapshot()
{
return HasPipelineSnapshot;
}
}
: imageBaseDirectory;
_snapshot = snapshot;
// 查找资产记录
// Find asset records
var assetList = assets?.ToList() ?? new List<InspectionAssetRecord>();
_resultImageAsset = assetList.FirstOrDefault(a => a.NodeId == nodeResult.NodeId && a.AssetType == InspectionAssetType.NodeResultImage);
_inputImageAsset = assetList.FirstOrDefault(a => a.NodeId == nodeResult.NodeId && a.AssetType == InspectionAssetType.NodeInputImage);
// 构建指标列表(按 DisplayOrder 升序,同序按 MetricName 升序)
// Build metrics list (sorted by DisplayOrder ascending, then by MetricName ascending)
var metricList = metrics?
.Where(m => m.NodeId == nodeResult.NodeId)
.OrderBy(m => m.DisplayOrder)
.ThenBy(m => m.MetricName)
.Select(m => new InspectionMetricRowViewModel(m))
.ToList() ?? new List<InspectionMetricRowViewModel>();
Metrics = new ObservableCollection<InspectionMetricRowViewModel>(metricList);
// 筛选属于本节点的结构化明细表,并按 DisplayOrder/TableName 排序
// Filter structured result tables that belong to this node, sorted by DisplayOrder/TableName
_resultTables = resultTables?
.Where(t => t.NodeId == nodeResult.NodeId)
.OrderBy(t => t.DisplayOrder)
.ThenBy(t => t.TableName)
.ToList()
as IReadOnlyList<InspectionResultTable>
?? Array.Empty<InspectionResultTable>();
// 为 UI 绑定构建列信息,按 DisplayOrder 取第一个有数据的表(UI 一次只显示一个明细表)
// Build column info for UI binding from the first table with data (UI shows one detail table at a time)
if (_resultTables.Count > 0 && _resultTables[0].Columns?.Count > 0)
{
ResultTableColumns = _resultTables[0].Columns;
ResultTableRows = new ObservableCollection<Dictionary<string, object>>(
_resultTables[0].Rows ?? Array.Empty<Dictionary<string, object>>());
}
else
{
ResultTableColumns = Array.Empty<InspectionResultTableColumn>();
ResultTableRows = new ObservableCollection<Dictionary<string, object>>();
}
// 初始化命令
// Initialize commands
ToggleImageCommand = new DelegateCommand(ExecuteToggleImage, CanExecuteToggleImage);
ViewSnapshotCommand = new DelegateCommand(ExecuteViewSnapshot, CanExecuteViewSnapshot);
// 异步加载图像
// Load images asynchronously
_ = LoadImagesAsync();
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>节点序号 | Node index</summary>
public int NodeIndex => _nodeResult.NodeIndex;
/// <summary>节点名称 | Node name</summary>
public string NodeName => _nodeResult.NodeName;
/// <summary>Pipeline 名称 | Pipeline name</summary>
public string PipelineName => _nodeResult.PipelineName;
/// <summary>节点判定 | Node pass status</summary>
public bool NodePass => _nodeResult.NodePass;
/// <summary>节点状态 | Node status</summary>
public InspectionNodeStatus Status => _nodeResult.Status;
/// <summary>耗时(毫秒)| Duration in milliseconds</summary>
public long DurationMs => _nodeResult.DurationMs;
/// <summary>
/// 判定标签文字("Pass" / "Fail"
/// Pass label text ("Pass" / "Fail")
/// </summary>
public string PassLabel => NodePass ? "Pass" : "Fail";
// 注:PassLabelColor 曾在此定义(硬编码 #2E7D32/#C62828),但未被任何
// XAML 绑定引用——检测节点卡片实际走的是 PassFailColorConverter
// (绑定 NodePass),已改用 Nova 令牌。该死代码属性已删除,避免继续
// 维护一套从未生效的重复配色。
/// <summary>
/// 是否有资产缺失警告(Status == AssetMissing
/// Whether there is an asset missing warning (Status == AssetMissing)
/// </summary>
public bool HasAssetMissingWarning => Status == InspectionNodeStatus.AssetMissing;
/// <summary>
/// Pipeline 版本标识(hash 前 8 位,空则 "--"
/// Pipeline version identifier (first 8 chars of hash, or "--" if empty)
/// </summary>
public string PipelineVersionShort
{
get
{
if (string.IsNullOrWhiteSpace(_nodeResult.PipelineVersionHash))
{
return "--";
}
return _nodeResult.PipelineVersionHash.Length >= 8
? _nodeResult.PipelineVersionHash.Substring(0, 8)
: _nodeResult.PipelineVersionHash;
}
}
/// <summary>
/// 是否有 Pipeline 快照
/// Whether pipeline snapshot exists
/// </summary>
public bool HasPipelineSnapshot => _snapshot != null && !string.IsNullOrWhiteSpace(_snapshot.PipelineDefinitionJson);
/// <summary>
/// 格式化的 Pipeline 定义 JSON2 空格缩进)
/// Formatted pipeline definition JSON (2-space indentation)
/// </summary>
public string PipelineDefinitionJson
{
get
{
if (_snapshot == null || string.IsNullOrWhiteSpace(_snapshot.PipelineDefinitionJson))
{
return string.Empty;
}
try
{
// 尝试解析并格式化 JSON
// Try to parse and format JSON
var parsed = JToken.Parse(_snapshot.PipelineDefinitionJson);
return parsed.ToString(Formatting.Indented);
}
catch
{
// 解析失败,返回原始字符串
// Parsing failed, return original string
return _snapshot.PipelineDefinitionJson;
}
}
}
/// <summary>
/// 节点结果图(可为 null
/// Node result image (can be null)
/// </summary>
public BitmapSource ResultImage
{
get => _resultImage;
private set => SetProperty(ref _resultImage, value);
}
/// <summary>
/// 节点输入图(可为 null
/// Node input image (can be null)
/// </summary>
public BitmapSource InputImage
{
get => _inputImage;
private set => SetProperty(ref _inputImage, value);
}
/// <summary>
/// 当前显示的图像(默认为结果图)
/// Currently displayed image (defaults to result image)
/// </summary>
public BitmapSource CurrentImage
{
get => _currentImage;
private set => SetProperty(ref _currentImage, value);
}
/// <summary>
/// 是否正在显示输入图
/// Whether currently showing input image
/// </summary>
public bool IsShowingInputImage
{
get => _isShowingInputImage;
private set => SetProperty(ref _isShowingInputImage, value);
}
/// <summary>
/// 是否有输入图
/// Whether input image exists
/// </summary>
public bool HasInputImage => InputImage != null;
/// <summary>
/// 指标列表(按 DisplayOrder 升序,同序按 MetricName 升序)
/// Metrics list (sorted by DisplayOrder ascending, then by MetricName ascending)
/// </summary>
public ObservableCollection<InspectionMetricRowViewModel> Metrics { get; }
/// <summary>
/// 是否有结构化明细表(如 BGA焊球明细、QFN引脚明细)
/// Whether structured result tables exist (e.g. BGA ball details, QFN lead details)
/// </summary>
public bool HasResultTables => ResultTableRows != null && ResultTableRows.Count > 0;
/// <summary>
/// 当前明细表的列定义(Key为列键,Name为显示名,含Unit)
/// Column definitions for the current result table
/// </summary>
public IReadOnlyList<InspectionResultTableColumn> ResultTableColumns { get; }
/// <summary>
/// 当前明细表的行数据(每个 Dictionary 代表一行,Key 为列键)
/// Row data for the current result table (each Dictionary is a row, Key is column key)
/// </summary>
public ObservableCollection<Dictionary<string, object>> ResultTableRows { get; }
// ── 命令 | Commands ────────────────────────────────────────────
/// <summary>
/// 切换图像命令(在输入图和结果图之间切换)
/// Toggle image command (switch between input and result images)
/// </summary>
public DelegateCommand ToggleImageCommand { get; }
/// <summary>
/// 查看快照命令(打开 SnapshotViewerWindow
/// View snapshot command (open SnapshotViewerWindow)
/// </summary>
public DelegateCommand ViewSnapshotCommand { get; }
// ── 私有方法 | Private Methods ─────────────────────────────────
/// <summary>
/// 异步加载图像
/// Load images asynchronously
/// </summary>
private async Task LoadImagesAsync()
{
// 加载结果图
// Load result image
if (_resultImageAsset != null && !string.IsNullOrWhiteSpace(_resultImageAsset.RelativePath))
{
var resultImagePath = Path.Combine(_imageBaseDirectory, _resultImageAsset.RelativePath);
ResultImage = await ImageLoader.LoadBitmapSafeAsync(resultImagePath);
}
// 加载输入图
// Load input image
if (_inputImageAsset != null && !string.IsNullOrWhiteSpace(_inputImageAsset.RelativePath))
{
var inputImagePath = Path.Combine(_imageBaseDirectory, _inputImageAsset.RelativePath);
InputImage = await ImageLoader.LoadBitmapSafeAsync(inputImagePath);
}
// 设置当前图像为结果图
// Set current image to result image
CurrentImage = ResultImage;
IsShowingInputImage = false;
// 通知命令状态可能已更改
// Notify that command state may have changed
ToggleImageCommand.RaiseCanExecuteChanged();
}
/// <summary>
/// 执行切换图像命令
/// Execute toggle image command
/// </summary>
private void ExecuteToggleImage()
{
if (IsShowingInputImage)
{
// 切换到结果图
// Switch to result image
CurrentImage = ResultImage;
IsShowingInputImage = false;
}
else
{
// 切换到输入图
// Switch to input image
CurrentImage = InputImage;
IsShowingInputImage = true;
}
}
/// <summary>
/// 判断是否可以执行切换图像命令
/// Determine whether toggle image command can be executed
/// </summary>
private bool CanExecuteToggleImage()
{
return InputImage != null && ResultImage != null;
}
/// <summary>
/// 执行查看快照命令
/// Execute view snapshot command
/// </summary>
private void ExecuteViewSnapshot()
{
if (!HasPipelineSnapshot)
{
return;
}
// 创建并显示快照查看器窗口
// Create and show snapshot viewer window
// 注意:这里需要在 Views/Cnc 中实现 SnapshotViewerWindow
// Note: SnapshotViewerWindow needs to be implemented in Views/Cnc
try
{
var window = new Views.Cnc.SnapshotViewerWindow(PipelineDefinitionJson, PipelineName);
window.Show();
}
catch (Exception ex)
{
// 如果窗口尚未实现,静默失败
// Silently fail if window is not yet implemented
System.Diagnostics.Debug.WriteLine($"Failed to open snapshot viewer: {ex.Message}");
}
}
/// <summary>
/// 判断是否可以执行查看快照命令
/// Determine whether view snapshot command can be executed
/// </summary>
private bool CanExecuteViewSnapshot()
{
return HasPipelineSnapshot;
}
}
}
@@ -1,124 +1,124 @@
// ── 检测结果审查 ViewModel ──
// 窗口右半部分的审查面板,展示已选节点在选中运行中的各 Tile 判定结果。
// 按检测子节点分组,每个子节点显示其对应的指标列表。
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC 运行时结果复核窗的 ViewModel。
/// 提供只读展示数据(明细表 + 汇总 + 整体判定),
/// 操作员改判状态与备注由窗体交互收集(见 InspectionReviewWindow)。
/// </summary>
public sealed class InspectionReviewViewModel : BindableBase
{
public InspectionReviewViewModel(InspectionReviewRequest request)
{
ArgumentNullException.ThrowIfNull(request);
NodeName = request.NodeName ?? string.Empty;
OverallPass = request.OverallPass;
Tables = request.Tables ?? new List<InspectionResultTable>();
Metrics = request.Metrics ?? new List<InspectionMetricResult>();
BuildSummary();
}
/// <summary>检测模块名称。</summary>
public string NodeName { get; }
/// <summary>算法整体判定是否合格。</summary>
public bool OverallPass { get; }
/// <summary>结构化明细表(供窗体动态建列展示)。</summary>
public IReadOnlyList<InspectionResultTable> Tables { get; }
/// <summary>指标集合。</summary>
public IReadOnlyList<InspectionMetricResult> Metrics { get; }
/// <summary>窗体标题。</summary>
public string Title => string.IsNullOrWhiteSpace(NodeName) ? "检测结果复核" : $"检测结果复核 - {NodeName}";
// ── 汇总 ──
/// <summary>数量项标签(焊球数 / 引脚数 / 数量)。</summary>
public string CountLabel { get; private set; } = "数量";
/// <summary>项目总数。</summary>
public int CountValue { get; private set; }
/// <summary>不合格项数。</summary>
public int FailValue { get; private set; }
/// <summary>缺失项数(如缺球)。</summary>
public int MissingValue { get; private set; }
/// <summary>整体判定文本(PASS / FAILED)。</summary>
public string ResultText => OverallPass ? "PASS" : "FAILED";
/// <summary>整体判定是否合格(供 XAML 着色)。</summary>
public bool IsPass => OverallPass;
private void BuildSummary()
{
var primary = Tables.OrderByDescending(t => t.Rows?.Count ?? 0).FirstOrDefault();
// 数量标签与总数:优先取指标,回退到明细行数
if (TryGetMetric("BgaCount", out var bgaCount))
{
CountLabel = "焊球数";
CountValue = (int)Math.Round(bgaCount);
}
else if (TryGetMetric("QfnLeadCount", out var leadCount))
{
CountLabel = "引脚数";
CountValue = (int)Math.Round(leadCount);
}
else
{
CountLabel = "数量";
CountValue = primary?.Rows?.Count ?? 0;
}
// 不合格 / 缺失:从明细行的判定列统计
int fail = 0, missing = 0;
if (primary?.Rows != null)
{
foreach (var row in primary.Rows)
{
if (row == null || !row.TryGetValue("Classification", out var clsObj))
continue;
var cls = Convert.ToString(clsObj, CultureInfo.InvariantCulture)?.Trim().ToUpperInvariant() ?? string.Empty;
if (cls == "MISS" || cls == "MISSING")
missing++;
else if (cls.StartsWith("FAIL", StringComparison.Ordinal))
fail++;
}
}
// 若明细无判定列,回退用指标的不合格数
if (fail == 0 && TryGetMetric("QfnLeadFailCount", out var failMetric))
fail = (int)Math.Round(failMetric);
FailValue = fail;
MissingValue = missing;
}
private bool TryGetMetric(string key, out double value)
{
var metric = Metrics?.FirstOrDefault(m => string.Equals(m.MetricKey, key, StringComparison.Ordinal));
if (metric != null)
{
value = metric.MetricValue;
return true;
}
value = 0;
return false;
}
}
// ── 检测结果审查 ViewModel ──
// 窗口右半部分的审查面板,展示已选节点在选中运行中的各 Tile 判定结果。
// 按检测子节点分组,每个子节点显示其对应的指标列表。
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// CNC 运行时结果复核窗的 ViewModel。
/// 提供只读展示数据(明细表 + 汇总 + 整体判定),
/// 操作员改判状态与备注由窗体交互收集(见 InspectionReviewWindow)。
/// </summary>
public sealed class InspectionReviewViewModel : BindableBase
{
public InspectionReviewViewModel(InspectionReviewRequest request)
{
ArgumentNullException.ThrowIfNull(request);
NodeName = request.NodeName ?? string.Empty;
OverallPass = request.OverallPass;
Tables = request.Tables ?? new List<InspectionResultTable>();
Metrics = request.Metrics ?? new List<InspectionMetricResult>();
BuildSummary();
}
/// <summary>检测模块名称。</summary>
public string NodeName { get; }
/// <summary>算法整体判定是否合格。</summary>
public bool OverallPass { get; }
/// <summary>结构化明细表(供窗体动态建列展示)。</summary>
public IReadOnlyList<InspectionResultTable> Tables { get; }
/// <summary>指标集合。</summary>
public IReadOnlyList<InspectionMetricResult> Metrics { get; }
/// <summary>窗体标题。</summary>
public string Title => string.IsNullOrWhiteSpace(NodeName) ? "检测结果复核" : $"检测结果复核 - {NodeName}";
// ── 汇总 ──
/// <summary>数量项标签(焊球数 / 引脚数 / 数量)。</summary>
public string CountLabel { get; private set; } = "数量";
/// <summary>项目总数。</summary>
public int CountValue { get; private set; }
/// <summary>不合格项数。</summary>
public int FailValue { get; private set; }
/// <summary>缺失项数(如缺球)。</summary>
public int MissingValue { get; private set; }
/// <summary>整体判定文本(PASS / FAILED)。</summary>
public string ResultText => OverallPass ? "PASS" : "FAILED";
/// <summary>整体判定是否合格(供 XAML 着色)。</summary>
public bool IsPass => OverallPass;
private void BuildSummary()
{
var primary = Tables.OrderByDescending(t => t.Rows?.Count ?? 0).FirstOrDefault();
// 数量标签与总数:优先取指标,回退到明细行数
if (TryGetMetric("BgaCount", out var bgaCount))
{
CountLabel = "焊球数";
CountValue = (int)Math.Round(bgaCount);
}
else if (TryGetMetric("QfnLeadCount", out var leadCount))
{
CountLabel = "引脚数";
CountValue = (int)Math.Round(leadCount);
}
else
{
CountLabel = "数量";
CountValue = primary?.Rows?.Count ?? 0;
}
// 不合格 / 缺失:从明细行的判定列统计
int fail = 0, missing = 0;
if (primary?.Rows != null)
{
foreach (var row in primary.Rows)
{
if (row == null || !row.TryGetValue("Classification", out var clsObj))
continue;
var cls = Convert.ToString(clsObj, CultureInfo.InvariantCulture)?.Trim().ToUpperInvariant() ?? string.Empty;
if (cls == "MISS" || cls == "MISSING")
missing++;
else if (cls.StartsWith("FAIL", StringComparison.Ordinal))
fail++;
}
}
// 若明细无判定列,回退用指标的不合格数
if (fail == 0 && TryGetMetric("QfnLeadFailCount", out var failMetric))
fail = (int)Math.Round(failMetric);
FailValue = fail;
MissingValue = missing;
}
private bool TryGetMetric(string key, out double value)
{
var metric = Metrics?.FirstOrDefault(m => string.Equals(m.MetricKey, key, StringComparison.Ordinal));
if (metric != null)
{
value = metric.MetricValue;
return true;
}
value = 0;
return false;
}
}
}
@@ -1,123 +1,123 @@
// ── 检测运行事件行 ViewModel ──
// 运行记录中每条事件/日志的封装,绑定到事件列表表格的行显示。
// 包含时间戳、事件类型图标、事件描述等信息。
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Windows.Media;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 运行事件行 ViewModel,包装 InspectionRunEvent 供事件时间线绑定
/// Run event row ViewModel that wraps InspectionRunEvent for event timeline binding
/// </summary>
public class InspectionRunEventRowViewModel : BindableBase
{
private readonly InspectionRunEvent _event;
private readonly Dictionary<Guid, string> _nodeIdToNameMap;
private bool _isExpanded;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="eventRecord">运行事件记录 | Run event record</param>
/// <param name="nodeIdToNameMap">节点 ID 到节点名称的映射字典 | Dictionary mapping node IDs to node names</param>
public InspectionRunEventRowViewModel(InspectionRunEvent eventRecord, Dictionary<Guid, string> nodeIdToNameMap)
{
_event = eventRecord ?? throw new ArgumentNullException(nameof(eventRecord));
_nodeIdToNameMap = nodeIdToNameMap ?? new Dictionary<Guid, string>();
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>事件类型 | Event type</summary>
public InspectionRunEventType EventType => _event.EventType;
/// <summary>
/// 事件时间(本地时区,格式 HH:mm:ss.fff
/// Event time in local timezone (format: HH:mm:ss.fff)
/// </summary>
public string EventTimeLocal => _event.EventTime.ToLocalTime().ToString("HH:mm:ss.fff");
/// <summary>
/// 关联节点显示名称
/// 规则:如果 NodeId 不为空,优先显示节点名称;若无法解析则显示 NodeId 前 8 位;若 NodeId 为空则返回空字符串
/// Associated node display name
/// Rules: If NodeId is not null, display node name if available; otherwise display first 8 chars of NodeId; if NodeId is null return empty string
/// </summary>
public string NodeDisplayName
{
get
{
if (!_event.NodeId.HasValue)
{
return string.Empty;
}
// 尝试从映射字典中获取节点名称
// Try to get node name from the mapping dictionary
if (_nodeIdToNameMap.TryGetValue(_event.NodeId.Value, out string nodeName) && !string.IsNullOrWhiteSpace(nodeName))
{
return nodeName;
}
// 无法解析节点名称,返回 NodeId 前 8 位
// Cannot resolve node name, return first 8 chars of NodeId
return _event.NodeId.Value.ToString().Substring(0, 8);
}
}
/// <summary>
/// 格式化的 Payload JSON2 空格缩进)
/// Formatted payload JSON (2-space indentation)
/// </summary>
public string PayloadFormatted
{
get
{
if (string.IsNullOrWhiteSpace(_event.PayloadJson))
{
return string.Empty;
}
try
{
// 尝试解析并格式化 JSON
// Try to parse and format JSON
var parsed = JToken.Parse(_event.PayloadJson);
return parsed.ToString(Formatting.Indented);
}
catch
{
// 解析失败,返回原始字符串
// Parsing failed, return original string
return _event.PayloadJson;
}
}
}
/// <summary>
/// 是否有 Payload 数据
/// Whether payload data exists
/// </summary>
public bool HasPayload => !string.IsNullOrWhiteSpace(_event.PayloadJson);
// 注:IconColor 曾在此定义(硬编码 #1565C0/#2E7D32/#C62828/#E65100/
// #9E9E9E),但未被任何 XAML 绑定引用——事件时间线实际走的是
// EventTypeIconConverter(绑定 EventType),已改用 Nova 令牌。
// 该死代码属性已删除,避免继续维护一套从未生效的重复配色。
/// <summary>
/// 是否展开显示 Payload(用于 UI 绑定)
/// Whether to expand and display payload (for UI binding)
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
}
// ── 检测运行事件行 ViewModel ──
// 运行记录中每条事件/日志的封装,绑定到事件列表表格的行显示。
// 包含时间戳、事件类型图标、事件描述等信息。
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Windows.Media;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 运行事件行 ViewModel,包装 InspectionRunEvent 供事件时间线绑定
/// Run event row ViewModel that wraps InspectionRunEvent for event timeline binding
/// </summary>
public class InspectionRunEventRowViewModel : BindableBase
{
private readonly InspectionRunEvent _event;
private readonly Dictionary<Guid, string> _nodeIdToNameMap;
private bool _isExpanded;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="eventRecord">运行事件记录 | Run event record</param>
/// <param name="nodeIdToNameMap">节点 ID 到节点名称的映射字典 | Dictionary mapping node IDs to node names</param>
public InspectionRunEventRowViewModel(InspectionRunEvent eventRecord, Dictionary<Guid, string> nodeIdToNameMap)
{
_event = eventRecord ?? throw new ArgumentNullException(nameof(eventRecord));
_nodeIdToNameMap = nodeIdToNameMap ?? new Dictionary<Guid, string>();
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>事件类型 | Event type</summary>
public InspectionRunEventType EventType => _event.EventType;
/// <summary>
/// 事件时间(本地时区,格式 HH:mm:ss.fff
/// Event time in local timezone (format: HH:mm:ss.fff)
/// </summary>
public string EventTimeLocal => _event.EventTime.ToLocalTime().ToString("HH:mm:ss.fff");
/// <summary>
/// 关联节点显示名称
/// 规则:如果 NodeId 不为空,优先显示节点名称;若无法解析则显示 NodeId 前 8 位;若 NodeId 为空则返回空字符串
/// Associated node display name
/// Rules: If NodeId is not null, display node name if available; otherwise display first 8 chars of NodeId; if NodeId is null return empty string
/// </summary>
public string NodeDisplayName
{
get
{
if (!_event.NodeId.HasValue)
{
return string.Empty;
}
// 尝试从映射字典中获取节点名称
// Try to get node name from the mapping dictionary
if (_nodeIdToNameMap.TryGetValue(_event.NodeId.Value, out string nodeName) && !string.IsNullOrWhiteSpace(nodeName))
{
return nodeName;
}
// 无法解析节点名称,返回 NodeId 前 8 位
// Cannot resolve node name, return first 8 chars of NodeId
return _event.NodeId.Value.ToString().Substring(0, 8);
}
}
/// <summary>
/// 格式化的 Payload JSON2 空格缩进)
/// Formatted payload JSON (2-space indentation)
/// </summary>
public string PayloadFormatted
{
get
{
if (string.IsNullOrWhiteSpace(_event.PayloadJson))
{
return string.Empty;
}
try
{
// 尝试解析并格式化 JSON
// Try to parse and format JSON
var parsed = JToken.Parse(_event.PayloadJson);
return parsed.ToString(Formatting.Indented);
}
catch
{
// 解析失败,返回原始字符串
// Parsing failed, return original string
return _event.PayloadJson;
}
}
}
/// <summary>
/// 是否有 Payload 数据
/// Whether payload data exists
/// </summary>
public bool HasPayload => !string.IsNullOrWhiteSpace(_event.PayloadJson);
// 注:IconColor 曾在此定义(硬编码 #1565C0/#2E7D32/#C62828/#E65100/
// #9E9E9E),但未被任何 XAML 绑定引用——事件时间线实际走的是
// EventTypeIconConverter(绑定 EventType),已改用 Nova 令牌。
// 该死代码属性已删除,避免继续维护一套从未生效的重复配色。
/// <summary>
/// 是否展开显示 Payload(用于 UI 绑定)
/// Whether to expand and display payload (for UI binding)
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
}
}
@@ -1,65 +1,65 @@
// ── 检测运行行 ViewModel ──
// 运行列表中的单条执行记录封装。绑定到运行历史表格的行显示,
// 包含运行时间、判定结果(OK/NG)、操作等列。
using Prism.Mvvm;
using System;
using System.Windows.Media;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测实例行 ViewModel,包装 InspectionRunRecord 供 RunList 行绑定
/// Inspection run row ViewModel that wraps InspectionRunRecord for RunList row binding
/// </summary>
public class InspectionRunRowViewModel : BindableBase
{
private readonly InspectionRunRecord _record;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="record">检测实例记录 | Inspection run record</param>
public InspectionRunRowViewModel(InspectionRunRecord record)
{
_record = record ?? throw new ArgumentNullException(nameof(record));
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>运行 ID | Run ID</summary>
public Guid RunId => _record.RunId;
/// <summary>程序名 | Program name</summary>
public string ProgramName => _record.ProgramName;
/// <summary>工件号 | Workpiece ID</summary>
public string WorkpieceId => _record.WorkpieceId;
/// <summary>序列号 | Serial number</summary>
public string SerialNumber => _record.SerialNumber;
/// <summary>
/// 开始时间(本地时区,格式 yyyy-MM-dd HH:mm:ss
/// Start time in local timezone (format: yyyy-MM-dd HH:mm:ss)
/// </summary>
public string StartedAtLocal => _record.StartedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
/// <summary>整体判定 | Overall pass</summary>
public bool OverallPass => _record.OverallPass;
/// <summary>运行状态 | Run status</summary>
public InspectionRunStatus Status => _record.Status;
/// <summary>
/// 判定标签文字("Pass" / "Fail"
/// Pass label text ("Pass" / "Fail")
/// </summary>
public string PassLabel => OverallPass ? "Pass" : "Fail";
// 注:PassLabelColor / StatusWarningColor 曾在此定义(硬编码 #2E7D32/
// #C62828/#E65100),但未被任何 XAML 绑定引用——RunList 实际走的是
// PassFailColorConverter(绑定 OverallPass)与 RunStatusColorConverter
// (绑定 Status),两者均已改用 Nova 令牌。这两个死代码属性已删除,
// 避免继续维护一套从未生效的重复配色。
}
// ── 检测运行行 ViewModel ──
// 运行列表中的单条执行记录封装。绑定到运行历史表格的行显示,
// 包含运行时间、判定结果(OK/NG)、操作等列。
using Prism.Mvvm;
using System;
using System.Windows.Media;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 检测实例行 ViewModel,包装 InspectionRunRecord 供 RunList 行绑定
/// Inspection run row ViewModel that wraps InspectionRunRecord for RunList row binding
/// </summary>
public class InspectionRunRowViewModel : BindableBase
{
private readonly InspectionRunRecord _record;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="record">检测实例记录 | Inspection run record</param>
public InspectionRunRowViewModel(InspectionRunRecord record)
{
_record = record ?? throw new ArgumentNullException(nameof(record));
}
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>运行 ID | Run ID</summary>
public Guid RunId => _record.RunId;
/// <summary>程序名 | Program name</summary>
public string ProgramName => _record.ProgramName;
/// <summary>工件号 | Workpiece ID</summary>
public string WorkpieceId => _record.WorkpieceId;
/// <summary>序列号 | Serial number</summary>
public string SerialNumber => _record.SerialNumber;
/// <summary>
/// 开始时间(本地时区,格式 yyyy-MM-dd HH:mm:ss
/// Start time in local timezone (format: yyyy-MM-dd HH:mm:ss)
/// </summary>
public string StartedAtLocal => _record.StartedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
/// <summary>整体判定 | Overall pass</summary>
public bool OverallPass => _record.OverallPass;
/// <summary>运行状态 | Run status</summary>
public InspectionRunStatus Status => _record.Status;
/// <summary>
/// 判定标签文字("Pass" / "Fail"
/// Pass label text ("Pass" / "Fail")
/// </summary>
public string PassLabel => OverallPass ? "Pass" : "Fail";
// 注:PassLabelColor / StatusWarningColor 曾在此定义(硬编码 #2E7D32/
// #C62828/#E65100),但未被任何 XAML 绑定引用——RunList 实际走的是
// PassFailColorConverter(绑定 OverallPass)与 RunStatusColorConverter
// (绑定 Status),两者均已改用 Nova 令牌。这两个死代码属性已删除,
// 避免继续维护一套从未生效的重复配色。
}
}
@@ -1,186 +1,186 @@
// ── 矩阵单元格 ViewModel ──
// 将 MatrixCell 模型封装为可绑定 ViewModel,每个单元格对应一个检测位置。
// 在矩阵编辑器中显示 Cell 名称/状态/坐标预览等。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 矩阵单元格 ViewModel,将 MatrixCell 模型封装为可绑定的 WPF ViewModel
/// Matrix cell ViewModel that wraps a MatrixCell model into a bindable WPF ViewModel
/// </summary>
public class MatrixCellViewModel : BindableBase
{
private int _row;
private int _column;
private double _offsetX;
private double _offsetY;
private bool _isEnabled;
private MatrixCellStatus _status;
private string _errorMessage;
private bool _isSelected;
private int _position;
private MatrixCellTeachState _teachState;
private double _absoluteX;
private double _absoluteY;
private bool _hasCoordinate;
/// <summary>
/// 构造函数,从 MatrixCell 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a MatrixCell model
/// </summary>
public MatrixCellViewModel(MatrixCell model)
{
Model = model;
_row = model.Row;
_column = model.Column;
_offsetX = model.OffsetX;
_offsetY = model.OffsetY;
_isEnabled = model.IsEnabled;
_status = model.Status;
_errorMessage = model.ErrorMessage;
_teachState = MatrixCellTeachState.Auto;
}
/// <summary>底层矩阵单元格模型(只读)| Underlying matrix cell model (read-only)</summary>
public MatrixCell Model { get; }
/// <summary>单元格行索引 | Cell row index</summary>
public int Row
{
get => _row;
set => SetProperty(ref _row, value);
}
/// <summary>单元格列索引 | Cell column index</summary>
public int Column
{
get => _column;
set => SetProperty(ref _column, value);
}
/// <summary>计算后的 X 偏移量 | Calculated X offset</summary>
public double OffsetX
{
get => _offsetX;
set => SetProperty(ref _offsetX, value);
}
/// <summary>计算后的 Y 偏移量 | Calculated Y offset</summary>
public double OffsetY
{
get => _offsetY;
set => SetProperty(ref _offsetY, value);
}
/// <summary>单元格是否启用检测 | Whether the cell is enabled for inspection</summary>
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
}
/// <summary>单元格执行状态 | Cell execution status</summary>
public MatrixCellStatus Status
{
get => _status;
set => SetProperty(ref _status, value);
}
/// <summary>错误信息 | Error message if any</summary>
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
/// <summary>单元格是否被选中 | Whether the cell is currently selected in the UI</summary>
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
/// <summary>单元格 1 基位置编号(行优先)| 1-based position number (row-major)</summary>
public int Position
{
get => _position;
set
{
if (SetProperty(ref _position, value))
{
RaisePropertyChanged(nameof(PositionLabel));
RaisePropertyChanged(nameof(PositionShort));
}
}
}
/// <summary>位置显示标签,如 "位置1 / Position 1" | Position display label</summary>
public string PositionLabel => $"位置{Position} / Position {Position}";
/// <summary>位置短标签,如 "位置1" | Short position label</summary>
public string PositionShort => $"位置{Position}";
/// <summary>
/// 示教来源状态:手动示教 / 当前待示教 / 自动推算
/// Teaching source state: Taught / Current / Auto
/// </summary>
public MatrixCellTeachState TeachState
{
get => _teachState;
set
{
if (SetProperty(ref _teachState, value))
{
RaisePropertyChanged(nameof(TeachStateLabel));
}
}
}
/// <summary>示教状态显示文本 | Teach state display text</summary>
public string TeachStateLabel => _teachState switch
{
MatrixCellTeachState.Taught => "已示教",
MatrixCellTeachState.Current => "当前",
_ => "自动生成"
};
/// <summary>该位置的实际机台 X 坐标(mm| Actual stage X coordinate (mm)</summary>
public double AbsoluteX
{
get => _absoluteX;
set
{
if (SetProperty(ref _absoluteX, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>该位置的实际机台 Y 坐标(mm| Actual stage Y coordinate (mm)</summary>
public double AbsoluteY
{
get => _absoluteY;
set
{
if (SetProperty(ref _absoluteY, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>是否已具备有效的机台坐标(存在示教参考点后为 true| Whether a valid stage coordinate is available</summary>
public bool HasCoordinate
{
get => _hasCoordinate;
set
{
if (SetProperty(ref _hasCoordinate, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>实际机台坐标显示文本(mm),无参考点时为 "--" | Actual stage coordinate label (mm)</summary>
public string CoordinateLabel => _hasCoordinate
? $"X:{_absoluteX:F3} Y:{_absoluteY:F3}"
: "--";
}
// ── 矩阵单元格 ViewModel ──
// 将 MatrixCell 模型封装为可绑定 ViewModel,每个单元格对应一个检测位置。
// 在矩阵编辑器中显示 Cell 名称/状态/坐标预览等。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 矩阵单元格 ViewModel,将 MatrixCell 模型封装为可绑定的 WPF ViewModel
/// Matrix cell ViewModel that wraps a MatrixCell model into a bindable WPF ViewModel
/// </summary>
public class MatrixCellViewModel : BindableBase
{
private int _row;
private int _column;
private double _offsetX;
private double _offsetY;
private bool _isEnabled;
private MatrixCellStatus _status;
private string _errorMessage;
private bool _isSelected;
private int _position;
private MatrixCellTeachState _teachState;
private double _absoluteX;
private double _absoluteY;
private bool _hasCoordinate;
/// <summary>
/// 构造函数,从 MatrixCell 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a MatrixCell model
/// </summary>
public MatrixCellViewModel(MatrixCell model)
{
Model = model;
_row = model.Row;
_column = model.Column;
_offsetX = model.OffsetX;
_offsetY = model.OffsetY;
_isEnabled = model.IsEnabled;
_status = model.Status;
_errorMessage = model.ErrorMessage;
_teachState = MatrixCellTeachState.Auto;
}
/// <summary>底层矩阵单元格模型(只读)| Underlying matrix cell model (read-only)</summary>
public MatrixCell Model { get; }
/// <summary>单元格行索引 | Cell row index</summary>
public int Row
{
get => _row;
set => SetProperty(ref _row, value);
}
/// <summary>单元格列索引 | Cell column index</summary>
public int Column
{
get => _column;
set => SetProperty(ref _column, value);
}
/// <summary>计算后的 X 偏移量 | Calculated X offset</summary>
public double OffsetX
{
get => _offsetX;
set => SetProperty(ref _offsetX, value);
}
/// <summary>计算后的 Y 偏移量 | Calculated Y offset</summary>
public double OffsetY
{
get => _offsetY;
set => SetProperty(ref _offsetY, value);
}
/// <summary>单元格是否启用检测 | Whether the cell is enabled for inspection</summary>
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
}
/// <summary>单元格执行状态 | Cell execution status</summary>
public MatrixCellStatus Status
{
get => _status;
set => SetProperty(ref _status, value);
}
/// <summary>错误信息 | Error message if any</summary>
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
/// <summary>单元格是否被选中 | Whether the cell is currently selected in the UI</summary>
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
/// <summary>单元格 1 基位置编号(行优先)| 1-based position number (row-major)</summary>
public int Position
{
get => _position;
set
{
if (SetProperty(ref _position, value))
{
RaisePropertyChanged(nameof(PositionLabel));
RaisePropertyChanged(nameof(PositionShort));
}
}
}
/// <summary>位置显示标签,如 "位置1 / Position 1" | Position display label</summary>
public string PositionLabel => $"位置{Position} / Position {Position}";
/// <summary>位置短标签,如 "位置1" | Short position label</summary>
public string PositionShort => $"位置{Position}";
/// <summary>
/// 示教来源状态:手动示教 / 当前待示教 / 自动推算
/// Teaching source state: Taught / Current / Auto
/// </summary>
public MatrixCellTeachState TeachState
{
get => _teachState;
set
{
if (SetProperty(ref _teachState, value))
{
RaisePropertyChanged(nameof(TeachStateLabel));
}
}
}
/// <summary>示教状态显示文本 | Teach state display text</summary>
public string TeachStateLabel => _teachState switch
{
MatrixCellTeachState.Taught => "已示教",
MatrixCellTeachState.Current => "当前",
_ => "自动生成"
};
/// <summary>该位置的实际机台 X 坐标(mm| Actual stage X coordinate (mm)</summary>
public double AbsoluteX
{
get => _absoluteX;
set
{
if (SetProperty(ref _absoluteX, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>该位置的实际机台 Y 坐标(mm| Actual stage Y coordinate (mm)</summary>
public double AbsoluteY
{
get => _absoluteY;
set
{
if (SetProperty(ref _absoluteY, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>是否已具备有效的机台坐标(存在示教参考点后为 true| Whether a valid stage coordinate is available</summary>
public bool HasCoordinate
{
get => _hasCoordinate;
set
{
if (SetProperty(ref _hasCoordinate, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>实际机台坐标显示文本(mm),无参考点时为 "--" | Actual stage coordinate label (mm)</summary>
public string CoordinateLabel => _hasCoordinate
? $"X:{_absoluteX:F3} Y:{_absoluteY:F3}"
: "--";
}
}
@@ -1,127 +1,127 @@
// ── 矩阵示教点 ViewModel ──
// 示教点 = 操作员手动移动机台后记录的机械位置(锚点)。
// 系统通过示教点插值自动生成其余单元格坐标。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 推荐示教点条目 ViewModel。
/// 表示矩阵编排流程中操作员需要手动移动机台并记录位置的一个"锚点"。
/// 系统仅需示教这些关键点(如 2×2 的 1、4 或 3×3 的 1、3、7),
/// 其余位置由 <see cref="MatrixEditorViewModel"/> 自动推算偏移。
/// A recommended teach point in the matrix teaching workflow.
/// </summary>
public class MatrixTeachPointViewModel : BindableBase
{
private TeachPointStatus _status;
private double _recordedX;
private double _recordedY;
private bool _hasRecorded;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="position">1 基位置编号(行优先)| 1-based position number (row-major)</param>
/// <param name="row">0 基行索引 | 0-based row index</param>
/// <param name="column">0 基列索引 | 0-based column index</param>
public MatrixTeachPointViewModel(int position, int row, int column)
{
Position = position;
Row = row;
Column = column;
_status = TeachPointStatus.Pending;
}
/// <summary>1 基位置编号 | 1-based position number</summary>
public int Position { get; }
/// <summary>0 基行索引 | 0-based row index</summary>
public int Row { get; }
/// <summary>0 基列索引 | 0-based column index</summary>
public int Column { get; }
/// <summary>位置显示标签,如 "位置1" | Position display label</summary>
public string PositionLabel => $"位置{Position}";
/// <summary>示教点状态:待示教 / 当前 / 已示教 | Teach point status</summary>
public TeachPointStatus Status
{
get => _status;
set
{
if (SetProperty(ref _status, value))
{
RaisePropertyChanged(nameof(StatusLabel));
RaisePropertyChanged(nameof(IsCurrent));
RaisePropertyChanged(nameof(IsTaught));
}
}
}
/// <summary>状态显示文本 | Status display text</summary>
public string StatusLabel => _status switch
{
TeachPointStatus.Taught => "已示教",
TeachPointStatus.Current => "当前",
_ => "待示教"
};
/// <summary>是否为当前待记录的示教点 | Whether this is the current teach point</summary>
public bool IsCurrent => _status == TeachPointStatus.Current;
/// <summary>是否已完成示教 | Whether this teach point has been taught</summary>
public bool IsTaught => _status == TeachPointStatus.Taught;
/// <summary>是否已记录实际坐标 | Whether an actual coordinate has been recorded</summary>
public bool HasRecorded
{
get => _hasRecorded;
set
{
if (SetProperty(ref _hasRecorded, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>记录的载物台 X 坐标(mm| Recorded stage X (mm)</summary>
public double RecordedX
{
get => _recordedX;
set
{
if (SetProperty(ref _recordedX, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>记录的载物台 Y 坐标(mm| Recorded stage Y (mm)</summary>
public double RecordedY
{
get => _recordedY;
set
{
if (SetProperty(ref _recordedY, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>坐标显示文本(未记录时为 "未记录"| Coordinate display text</summary>
public string CoordinateLabel => _hasRecorded
? $"X:{_recordedX:F3} Y:{_recordedY:F3} mm"
: "未记录";
/// <summary>
/// 记录一次实际位置(单位 mm,保留 3 位小数),并标记为已示教。
/// Record an actual position (in mm, rounded to 3 decimal places) and mark as taught.
/// </summary>
public void Record(double stageXmm, double stageYmm)
{
RecordedX = System.Math.Round(stageXmm, 3, System.MidpointRounding.AwayFromZero);
RecordedY = System.Math.Round(stageYmm, 3, System.MidpointRounding.AwayFromZero);
HasRecorded = true;
Status = TeachPointStatus.Taught;
}
}
// ── 矩阵示教点 ViewModel ──
// 示教点 = 操作员手动移动机台后记录的机械位置(锚点)。
// 系统通过示教点插值自动生成其余单元格坐标。
using Prism.Mvvm;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 推荐示教点条目 ViewModel。
/// 表示矩阵编排流程中操作员需要手动移动机台并记录位置的一个"锚点"。
/// 系统仅需示教这些关键点(如 2×2 的 1、4 或 3×3 的 1、3、7),
/// 其余位置由 <see cref="MatrixEditorViewModel"/> 自动推算偏移。
/// A recommended teach point in the matrix teaching workflow.
/// </summary>
public class MatrixTeachPointViewModel : BindableBase
{
private TeachPointStatus _status;
private double _recordedX;
private double _recordedY;
private bool _hasRecorded;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="position">1 基位置编号(行优先)| 1-based position number (row-major)</param>
/// <param name="row">0 基行索引 | 0-based row index</param>
/// <param name="column">0 基列索引 | 0-based column index</param>
public MatrixTeachPointViewModel(int position, int row, int column)
{
Position = position;
Row = row;
Column = column;
_status = TeachPointStatus.Pending;
}
/// <summary>1 基位置编号 | 1-based position number</summary>
public int Position { get; }
/// <summary>0 基行索引 | 0-based row index</summary>
public int Row { get; }
/// <summary>0 基列索引 | 0-based column index</summary>
public int Column { get; }
/// <summary>位置显示标签,如 "位置1" | Position display label</summary>
public string PositionLabel => $"位置{Position}";
/// <summary>示教点状态:待示教 / 当前 / 已示教 | Teach point status</summary>
public TeachPointStatus Status
{
get => _status;
set
{
if (SetProperty(ref _status, value))
{
RaisePropertyChanged(nameof(StatusLabel));
RaisePropertyChanged(nameof(IsCurrent));
RaisePropertyChanged(nameof(IsTaught));
}
}
}
/// <summary>状态显示文本 | Status display text</summary>
public string StatusLabel => _status switch
{
TeachPointStatus.Taught => "已示教",
TeachPointStatus.Current => "当前",
_ => "待示教"
};
/// <summary>是否为当前待记录的示教点 | Whether this is the current teach point</summary>
public bool IsCurrent => _status == TeachPointStatus.Current;
/// <summary>是否已完成示教 | Whether this teach point has been taught</summary>
public bool IsTaught => _status == TeachPointStatus.Taught;
/// <summary>是否已记录实际坐标 | Whether an actual coordinate has been recorded</summary>
public bool HasRecorded
{
get => _hasRecorded;
set
{
if (SetProperty(ref _hasRecorded, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>记录的载物台 X 坐标(mm| Recorded stage X (mm)</summary>
public double RecordedX
{
get => _recordedX;
set
{
if (SetProperty(ref _recordedX, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>记录的载物台 Y 坐标(mm| Recorded stage Y (mm)</summary>
public double RecordedY
{
get => _recordedY;
set
{
if (SetProperty(ref _recordedY, value))
RaisePropertyChanged(nameof(CoordinateLabel));
}
}
/// <summary>坐标显示文本(未记录时为 "未记录"| Coordinate display text</summary>
public string CoordinateLabel => _hasRecorded
? $"X:{_recordedX:F3} Y:{_recordedY:F3} mm"
: "未记录";
/// <summary>
/// 记录一次实际位置(单位 mm,保留 3 位小数),并标记为已示教。
/// Record an actual position (in mm, rounded to 3 decimal places) and mark as taught.
/// </summary>
public void Record(double stageXmm, double stageYmm)
{
RecordedX = System.Math.Round(stageXmm, 3, System.MidpointRounding.AwayFromZero);
RecordedY = System.Math.Round(stageYmm, 3, System.MidpointRounding.AwayFromZero);
HasRecorded = true;
Status = TeachPointStatus.Taught;
}
}
}
@@ -1,132 +1,132 @@
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.Measurement;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 测量统计 ViewModel,显示测量统计数据并支持筛选
/// Measurement statistics ViewModel that displays measurement statistics and supports filtering
/// </summary>
public class MeasurementStatsViewModel : BindableBase
{
private readonly IMeasurementDataService _measurementDataService;
private readonly ILoggerService _logger;
// ── 统计属性字段 | Statistics property fields ──
private int _totalCount;
private int _passCount;
private int _failCount;
private double _passRate;
// ── 筛选属性字段 | Filter property fields ──
private string _recipeName;
private DateTime? _fromDate;
private DateTime? _toDate;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public MeasurementStatsViewModel(
IMeasurementDataService measurementDataService,
ILoggerService logger)
{
_measurementDataService = measurementDataService ?? throw new ArgumentNullException(nameof(measurementDataService));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MeasurementStatsViewModel>();
// ── 命令初始化 | Command initialization ──
RefreshCommand = new DelegateCommand(async () => await ExecuteRefreshAsync());
_logger.Info("MeasurementStatsViewModel 已初始化 | MeasurementStatsViewModel initialized");
}
// ── 统计属性 | Statistics properties ────────────────────────────
/// <summary>总测量数 | Total measurement count</summary>
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
/// <summary>合格数 | Pass count</summary>
public int PassCount
{
get => _passCount;
set => SetProperty(ref _passCount, value);
}
/// <summary>不合格数 | Fail count</summary>
public int FailCount
{
get => _failCount;
set => SetProperty(ref _failCount, value);
}
/// <summary>合格率(0.0 ~ 1.0| Pass rate (0.0 to 1.0)</summary>
public double PassRate
{
get => _passRate;
set => SetProperty(ref _passRate, value);
}
// ── 筛选属性 | Filter properties ────────────────────────────────
/// <summary>按配方名称筛选 | Filter by recipe name</summary>
public string RecipeName
{
get => _recipeName;
set => SetProperty(ref _recipeName, value);
}
/// <summary>筛选起始日期 | Filter start date</summary>
public DateTime? FromDate
{
get => _fromDate;
set => SetProperty(ref _fromDate, value);
}
/// <summary>筛选结束日期 | Filter end date</summary>
public DateTime? ToDate
{
get => _toDate;
set => SetProperty(ref _toDate, value);
}
// ── 命令 | Commands ─────────────────────────────────────────────
/// <summary>刷新统计数据命令 | Refresh statistics command</summary>
public DelegateCommand RefreshCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 从服务获取统计数据并更新属性
/// Fetch statistics from service and update properties
/// </summary>
private async Task ExecuteRefreshAsync()
{
try
{
var stats = await _measurementDataService.GetStatisticsAsync(RecipeName, FromDate, ToDate);
TotalCount = stats.TotalCount;
PassCount = stats.PassCount;
FailCount = stats.FailCount;
PassRate = stats.PassRate;
_logger.Info("统计数据已刷新 | Statistics refreshed: Total={TotalCount}, Pass={PassCount}, Fail={FailCount}, Rate={PassRate:P1}",
stats.TotalCount, stats.PassCount, stats.FailCount, stats.PassRate);
}
catch (Exception ex)
{
_logger.Error(ex, "刷新统计数据失败 | Failed to refresh statistics");
}
}
}
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.Measurement;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 测量统计 ViewModel,显示测量统计数据并支持筛选
/// Measurement statistics ViewModel that displays measurement statistics and supports filtering
/// </summary>
public class MeasurementStatsViewModel : BindableBase
{
private readonly IMeasurementDataService _measurementDataService;
private readonly ILoggerService _logger;
// ── 统计属性字段 | Statistics property fields ──
private int _totalCount;
private int _passCount;
private int _failCount;
private double _passRate;
// ── 筛选属性字段 | Filter property fields ──
private string _recipeName;
private DateTime? _fromDate;
private DateTime? _toDate;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public MeasurementStatsViewModel(
IMeasurementDataService measurementDataService,
ILoggerService logger)
{
_measurementDataService = measurementDataService ?? throw new ArgumentNullException(nameof(measurementDataService));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MeasurementStatsViewModel>();
// ── 命令初始化 | Command initialization ──
RefreshCommand = new DelegateCommand(async () => await ExecuteRefreshAsync());
_logger.Info("MeasurementStatsViewModel 已初始化 | MeasurementStatsViewModel initialized");
}
// ── 统计属性 | Statistics properties ────────────────────────────
/// <summary>总测量数 | Total measurement count</summary>
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
/// <summary>合格数 | Pass count</summary>
public int PassCount
{
get => _passCount;
set => SetProperty(ref _passCount, value);
}
/// <summary>不合格数 | Fail count</summary>
public int FailCount
{
get => _failCount;
set => SetProperty(ref _failCount, value);
}
/// <summary>合格率(0.0 ~ 1.0| Pass rate (0.0 to 1.0)</summary>
public double PassRate
{
get => _passRate;
set => SetProperty(ref _passRate, value);
}
// ── 筛选属性 | Filter properties ────────────────────────────────
/// <summary>按配方名称筛选 | Filter by recipe name</summary>
public string RecipeName
{
get => _recipeName;
set => SetProperty(ref _recipeName, value);
}
/// <summary>筛选起始日期 | Filter start date</summary>
public DateTime? FromDate
{
get => _fromDate;
set => SetProperty(ref _fromDate, value);
}
/// <summary>筛选结束日期 | Filter end date</summary>
public DateTime? ToDate
{
get => _toDate;
set => SetProperty(ref _toDate, value);
}
// ── 命令 | Commands ─────────────────────────────────────────────
/// <summary>刷新统计数据命令 | Refresh statistics command</summary>
public DelegateCommand RefreshCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 从服务获取统计数据并更新属性
/// Fetch statistics from service and update properties
/// </summary>
private async Task ExecuteRefreshAsync()
{
try
{
var stats = await _measurementDataService.GetStatisticsAsync(RecipeName, FromDate, ToDate);
TotalCount = stats.TotalCount;
PassCount = stats.PassCount;
FailCount = stats.FailCount;
PassRate = stats.PassRate;
_logger.Info("统计数据已刷新 | Statistics refreshed: Total={TotalCount}, Pass={PassCount}, Fail={FailCount}, Rate={PassRate:P1}",
stats.TotalCount, stats.PassCount, stats.FailCount, stats.PassRate);
}
catch (Exception ex)
{
_logger.Error(ex, "刷新统计数据失败 | Failed to refresh statistics");
}
}
}
}