Files
XplorePlane/XplorePlane/ViewModels/Cnc/InspectionNodeCardViewModel.cs
T
zhengxuan.zhang 8b29285d03 CNC结果预览
2026-05-12 00:29:21 +08:00

345 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.Models;
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 PipelineExecutionSnapshot _snapshot;
private readonly InspectionAssetRecord _resultImageAsset;
private readonly InspectionAssetRecord _inputImageAsset;
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="dataPathService">数据路径服务 | Data path service</param>
public InspectionNodeCardViewModel(
InspectionNodeResult nodeResult,
IEnumerable<InspectionMetricResult> metrics,
IEnumerable<InspectionAssetRecord> assets,
PipelineExecutionSnapshot snapshot,
IXpDataPathService dataPathService)
{
_nodeResult = nodeResult ?? throw new ArgumentNullException(nameof(nodeResult));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_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);
// 初始化命令
// 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";
/// <summary>
/// 判定标签颜色(绿色 #2E7D32 / 红色 #C62828
/// Pass label color (green #2E7D32 / red #C62828)
/// </summary>
public Brush PassLabelColor => NodePass
? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2E7D32"))
: new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C62828"));
/// <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; }
// ── 命令 | 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(_dataPathService.DataPath, _resultImageAsset.RelativePath);
ResultImage = await ImageLoader.LoadBitmapSafeAsync(resultImagePath);
}
// 加载输入图
// Load input image
if (_inputImageAsset != null && !string.IsNullOrWhiteSpace(_inputImageAsset.RelativePath))
{
var inputImagePath = Path.Combine(_dataPathService.DataPath, _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;
}
}
}