345 lines
13 KiB
C#
345 lines
13 KiB
C#
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 定义 JSON(2 空格缩进)
|
||
/// 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;
|
||
}
|
||
}
|
||
}
|