#0050 新增CNC视图
This commit is contained in:
@@ -8,7 +8,12 @@ using XplorePlane.Views;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Matrix;
|
||||
using XplorePlane.Services.Measurement;
|
||||
using XplorePlane.Services.Recipe;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using Prism.Ioc;
|
||||
using Prism.DryIoc;
|
||||
using Prism.Modularity;
|
||||
@@ -247,6 +252,20 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterSingleton<DumpConfig>(() => XP.Common.Helpers.ConfigLoader.LoadDumpConfig());
|
||||
containerRegistry.RegisterSingleton<IDumpService, DumpService>();
|
||||
|
||||
// ── CNC / 矩阵编排 / 测量数据服务(单例)──
|
||||
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
|
||||
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
|
||||
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
|
||||
|
||||
// ── CNC / 矩阵 ViewModel(瞬态)──
|
||||
containerRegistry.Register<CncEditorViewModel>();
|
||||
containerRegistry.Register<MatrixEditorViewModel>();
|
||||
containerRegistry.Register<MeasurementStatsViewModel>();
|
||||
|
||||
// ── CNC / 矩阵导航视图 ──
|
||||
containerRegistry.RegisterForNavigation<CncPageView>();
|
||||
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
||||
|
||||
Log.Information("依赖注入容器配置完成");
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +1,15 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 程序状态变更事件 | CNC program changed event
|
||||
/// 当 CNC 程序被修改时通过 IEventAggregator 发布 | Published via IEventAggregator when CNC program is modified
|
||||
/// </summary>
|
||||
public class CncProgramChangedEvent : PubSubEvent<CncProgramChangedPayload>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>CNC 程序变更载荷 | CNC program changed payload</summary>
|
||||
public record CncProgramChangedPayload(string ProgramName, bool IsModified);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Prism.Events;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵执行进度事件 | Matrix execution progress event
|
||||
/// 矩阵执行过程中通过 IEventAggregator 发布进度更新 | Published via IEventAggregator during matrix execution
|
||||
/// </summary>
|
||||
public class MatrixExecutionProgressEvent : PubSubEvent<MatrixExecutionProgressPayload>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>矩阵执行进度载荷 | Matrix execution progress payload</summary>
|
||||
public record MatrixExecutionProgressPayload(
|
||||
int CurrentRow,
|
||||
int CurrentColumn,
|
||||
int TotalCells,
|
||||
int CompletedCells,
|
||||
MatrixCellStatus Status
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
// ── CNC 节点类型枚举 | CNC Node Type Enumeration ──────────────────
|
||||
|
||||
/// <summary>CNC 节点类型 | CNC node type</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum CncNodeType
|
||||
{
|
||||
ReferencePoint,
|
||||
SaveNodeWithImage,
|
||||
SaveNode,
|
||||
SavePosition,
|
||||
InspectionModule,
|
||||
InspectionMarker,
|
||||
PauseDialog,
|
||||
WaitDelay,
|
||||
CompleteProgram
|
||||
}
|
||||
|
||||
// ── CNC 节点基类与派生类型 | CNC Node Base & Derived Types ────────
|
||||
|
||||
/// <summary>
|
||||
/// CNC 节点抽象基类(不可变)| CNC node abstract base (immutable)
|
||||
/// 使用 System.Text.Json 多态序列化 | Uses System.Text.Json polymorphic serialization
|
||||
/// </summary>
|
||||
[JsonDerivedType(typeof(ReferencePointNode), "ReferencePoint")]
|
||||
[JsonDerivedType(typeof(SaveNodeWithImageNode), "SaveNodeWithImage")]
|
||||
[JsonDerivedType(typeof(SaveNodeNode), "SaveNode")]
|
||||
[JsonDerivedType(typeof(SavePositionNode), "SavePosition")]
|
||||
[JsonDerivedType(typeof(InspectionModuleNode), "InspectionModule")]
|
||||
[JsonDerivedType(typeof(InspectionMarkerNode), "InspectionMarker")]
|
||||
[JsonDerivedType(typeof(PauseDialogNode), "PauseDialog")]
|
||||
[JsonDerivedType(typeof(WaitDelayNode), "WaitDelay")]
|
||||
[JsonDerivedType(typeof(CompleteProgramNode), "CompleteProgram")]
|
||||
public abstract record CncNode(
|
||||
Guid Id,
|
||||
int Index,
|
||||
CncNodeType NodeType,
|
||||
string Name
|
||||
);
|
||||
|
||||
/// <summary>参考点节点 | Reference point node</summary>
|
||||
public record ReferencePointNode(
|
||||
Guid Id, int Index, string Name,
|
||||
double XM, double YM, double ZT, double ZD, double TiltD, double Dist
|
||||
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
|
||||
|
||||
/// <summary>保存节点(含图像)| Save node with image</summary>
|
||||
public record SaveNodeWithImageNode(
|
||||
Guid Id, int Index, string Name,
|
||||
MotionState MotionState,
|
||||
RaySourceState RaySourceState,
|
||||
DetectorState DetectorState,
|
||||
string ImageFileName
|
||||
) : CncNode(Id, Index, CncNodeType.SaveNodeWithImage, Name);
|
||||
|
||||
/// <summary>保存节点(不含图像)| Save node without image</summary>
|
||||
public record SaveNodeNode(
|
||||
Guid Id, int Index, string Name,
|
||||
MotionState MotionState,
|
||||
RaySourceState RaySourceState,
|
||||
DetectorState DetectorState
|
||||
) : CncNode(Id, Index, CncNodeType.SaveNode, Name);
|
||||
|
||||
/// <summary>保存位置节点 | Save position node</summary>
|
||||
public record SavePositionNode(
|
||||
Guid Id, int Index, string Name,
|
||||
MotionState MotionState
|
||||
) : CncNode(Id, Index, CncNodeType.SavePosition, Name);
|
||||
|
||||
/// <summary>检测模块节点 | Inspection module node</summary>
|
||||
public record InspectionModuleNode(
|
||||
Guid Id, int Index, string Name,
|
||||
PipelineModel Pipeline
|
||||
) : CncNode(Id, Index, CncNodeType.InspectionModule, Name);
|
||||
|
||||
/// <summary>检测标记节点 | Inspection marker node</summary>
|
||||
public record InspectionMarkerNode(
|
||||
Guid Id, int Index, string Name,
|
||||
string MarkerType,
|
||||
double MarkerX, double MarkerY
|
||||
) : CncNode(Id, Index, CncNodeType.InspectionMarker, Name);
|
||||
|
||||
/// <summary>停顿对话框节点 | Pause dialog node</summary>
|
||||
public record PauseDialogNode(
|
||||
Guid Id, int Index, string Name,
|
||||
string DialogTitle,
|
||||
string DialogMessage
|
||||
) : CncNode(Id, Index, CncNodeType.PauseDialog, Name);
|
||||
|
||||
/// <summary>等待延时节点 | Wait delay node</summary>
|
||||
public record WaitDelayNode(
|
||||
Guid Id, int Index, string Name,
|
||||
int DelayMilliseconds
|
||||
) : CncNode(Id, Index, CncNodeType.WaitDelay, Name);
|
||||
|
||||
/// <summary>完成程序节点 | Complete program node</summary>
|
||||
public record CompleteProgramNode(
|
||||
Guid Id, int Index, string Name
|
||||
) : CncNode(Id, Index, CncNodeType.CompleteProgram, Name);
|
||||
|
||||
// ── CNC 程序 | CNC Program ────────────────────────────────────────
|
||||
|
||||
/// <summary>CNC 程序(不可变)| CNC program (immutable)</summary>
|
||||
public record CncProgram(
|
||||
Guid Id,
|
||||
string Name,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt,
|
||||
IReadOnlyList<CncNode> Nodes
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
// ── 矩阵单元格状态枚举 | Matrix Cell Status Enumeration ──────────
|
||||
|
||||
/// <summary>矩阵单元格执行状态 | Matrix cell execution status</summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum MatrixCellStatus
|
||||
{
|
||||
NotExecuted,
|
||||
Executing,
|
||||
Completed,
|
||||
Error,
|
||||
Disabled
|
||||
}
|
||||
|
||||
// ── 矩阵数据模型 | Matrix Data Models ────────────────────────────
|
||||
|
||||
/// <summary>矩阵单元格(不可变)| Matrix cell (immutable)</summary>
|
||||
public record MatrixCell(
|
||||
int Row,
|
||||
int Column,
|
||||
double OffsetX,
|
||||
double OffsetY,
|
||||
bool IsEnabled,
|
||||
MatrixCellStatus Status,
|
||||
string ErrorMessage
|
||||
);
|
||||
|
||||
/// <summary>矩阵布局(不可变)| Matrix layout (immutable)</summary>
|
||||
public record MatrixLayout(
|
||||
Guid Id,
|
||||
int Rows,
|
||||
int Columns,
|
||||
double RowSpacing,
|
||||
double ColumnSpacing,
|
||||
double StartOffsetX,
|
||||
double StartOffsetY,
|
||||
string CncProgramPath,
|
||||
IReadOnlyList<MatrixCell> Cells
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
// ── 测量数据模型 | Measurement Data Models ────────────────────────
|
||||
|
||||
/// <summary>测量记录 | Measurement record</summary>
|
||||
public class MeasurementRecord
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string RecipeName { get; set; } = string.Empty;
|
||||
public int StepIndex { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public double ResultValue { get; set; }
|
||||
public bool IsPass { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>测量统计(不可变)| Measurement statistics (immutable)</summary>
|
||||
public record MeasurementStatistics(
|
||||
int TotalCount,
|
||||
int PassCount,
|
||||
int FailCount,
|
||||
double PassRate
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
|
||||
namespace XplorePlane.Services.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 程序管理服务实现。
|
||||
/// 负责程序创建、节点编辑(插入/删除/移动 + 自动重编号)、JSON 序列化/反序列化和 .xp 文件读写。
|
||||
/// CNC program management service implementation.
|
||||
/// Handles program creation, node editing (insert/remove/move + auto-renumber), JSON serialization and .xp file I/O.
|
||||
/// </summary>
|
||||
public class CncProgramService : ICncProgramService
|
||||
{
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// ── 序列化配置 | Serialization options ──
|
||||
private static readonly JsonSerializerOptions CncJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public CncProgramService(
|
||||
IAppStateService appStateService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appStateService);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_appStateService = appStateService;
|
||||
_logger = logger.ForModule<CncProgramService>();
|
||||
|
||||
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncProgram CreateProgram(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var program = new CncProgram(
|
||||
Id: Guid.NewGuid(),
|
||||
Name: name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Nodes: Array.Empty<CncNode>());
|
||||
|
||||
_logger.Info("已创建 CNC 程序 | Created CNC program: {ProgramName}, Id={ProgramId}", name, program.Id);
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncNode CreateNode(CncNodeType type)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
const int defaultIndex = 0;
|
||||
|
||||
CncNode node = type switch
|
||||
{
|
||||
// 参考点:捕获 6 轴位置 | Reference point: capture 6-axis position
|
||||
CncNodeType.ReferencePoint => CreateReferencePointNode(id, defaultIndex),
|
||||
|
||||
// 保存节点(含图像):捕获全部设备状态 | Save node with image: capture all device states
|
||||
CncNodeType.SaveNodeWithImage => CreateSaveNodeWithImageNode(id, defaultIndex),
|
||||
|
||||
// 保存节点(不含图像):捕获全部设备状态 | Save node: capture all device states
|
||||
CncNodeType.SaveNode => CreateSaveNodeNode(id, defaultIndex),
|
||||
|
||||
// 保存位置:仅捕获运动状态 | Save position: capture motion state only
|
||||
CncNodeType.SavePosition => CreateSavePositionNode(id, defaultIndex),
|
||||
|
||||
// 检测模块:空 Pipeline | Inspection module: empty pipeline
|
||||
CncNodeType.InspectionModule => new InspectionModuleNode(
|
||||
id, defaultIndex, $"检测模块_{defaultIndex}",
|
||||
Pipeline: new PipelineModel()),
|
||||
|
||||
// 检测标记:默认标记值 | Inspection marker: default marker values
|
||||
CncNodeType.InspectionMarker => new InspectionMarkerNode(
|
||||
id, defaultIndex, $"检测标记_{defaultIndex}",
|
||||
MarkerType: "Default",
|
||||
MarkerX: 0.0, MarkerY: 0.0),
|
||||
|
||||
// 停顿对话框:默认标题/消息 | Pause dialog: default title/message
|
||||
CncNodeType.PauseDialog => new PauseDialogNode(
|
||||
id, defaultIndex, $"停顿_{defaultIndex}",
|
||||
DialogTitle: "暂停",
|
||||
DialogMessage: "请确认后继续"),
|
||||
|
||||
// 等待延时:默认 1000ms | Wait delay: default 1000ms
|
||||
CncNodeType.WaitDelay => new WaitDelayNode(
|
||||
id, defaultIndex, $"延时_{defaultIndex}",
|
||||
DelayMilliseconds: 1000),
|
||||
|
||||
// 完成程序:默认名称 | Complete program: default name
|
||||
CncNodeType.CompleteProgram => new CompleteProgramNode(
|
||||
id, defaultIndex, "完成程序"),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, $"不支持的节点类型 | Unsupported node type: {type}")
|
||||
};
|
||||
|
||||
_logger.Info("已创建节点 | Created node: Type={NodeType}, Id={NodeId}", type, id);
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
// 防止重复插入 CompleteProgram | Prevent duplicate CompleteProgram insertion
|
||||
if (node.NodeType == CncNodeType.CompleteProgram &&
|
||||
program.Nodes.Any(n => n.NodeType == CncNodeType.CompleteProgram))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"CNC 程序中已存在完成程序节点,不允许重复插入 | A CompleteProgram node already exists, duplicate insertion is not allowed");
|
||||
}
|
||||
|
||||
var nodes = new List<CncNode>(program.Nodes);
|
||||
|
||||
// afterIndex = -1 表示插入到开头 | afterIndex = -1 means insert at the beginning
|
||||
int insertAt = afterIndex + 1;
|
||||
if (insertAt < 0) insertAt = 0;
|
||||
if (insertAt > nodes.Count) insertAt = nodes.Count;
|
||||
|
||||
nodes.Insert(insertAt, node);
|
||||
|
||||
var updated = program with
|
||||
{
|
||||
Nodes = RenumberNodes(nodes),
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_logger.Info("已插入节点 | Inserted node: Type={NodeType}, AfterIndex={AfterIndex}, Program={ProgramName}",
|
||||
node.NodeType, afterIndex, program.Name);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncProgram RemoveNode(CncProgram program, int index)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
if (index < 0 || index >= program.Nodes.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index),
|
||||
$"索引超出范围 | Index out of range: {index}, Count={program.Nodes.Count}");
|
||||
|
||||
var nodes = new List<CncNode>(program.Nodes);
|
||||
nodes.RemoveAt(index);
|
||||
|
||||
var updated = program with
|
||||
{
|
||||
Nodes = RenumberNodes(nodes),
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_logger.Info("已移除节点 | Removed node: Index={Index}, Program={ProgramName}",
|
||||
index, program.Name);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(oldIndex),
|
||||
$"旧索引超出范围 | Old index out of range: {oldIndex}, Count={program.Nodes.Count}");
|
||||
|
||||
if (newIndex < 0 || newIndex >= program.Nodes.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(newIndex),
|
||||
$"新索引超出范围 | New index out of range: {newIndex}, Count={program.Nodes.Count}");
|
||||
|
||||
var nodes = new List<CncNode>(program.Nodes);
|
||||
var node = nodes[oldIndex];
|
||||
nodes.RemoveAt(oldIndex);
|
||||
nodes.Insert(newIndex, node);
|
||||
|
||||
var updated = program with
|
||||
{
|
||||
Nodes = RenumberNodes(nodes),
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_logger.Info("已移动节点 | Moved node: OldIndex={OldIndex} -> NewIndex={NewIndex}, Program={ProgramName}",
|
||||
oldIndex, newIndex, program.Name);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(CncProgram program, string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
|
||||
// 路径安全性验证:检查路径遍历 | Path safety: check for path traversal
|
||||
if (filePath.Contains("..", StringComparison.Ordinal))
|
||||
throw new UnauthorizedAccessException(
|
||||
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
|
||||
|
||||
var json = Serialize(program);
|
||||
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("已保存 CNC 程序到文件 | Saved CNC program to file: {FilePath}, Program={ProgramName}",
|
||||
filePath, program.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CncProgram> LoadAsync(string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
|
||||
// 路径安全性验证 | Path safety check
|
||||
if (filePath.Contains("..", StringComparison.Ordinal))
|
||||
throw new UnauthorizedAccessException(
|
||||
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException(
|
||||
$"CNC 程序文件不存在 | CNC program file not found: {filePath}", filePath);
|
||||
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"无法读取 CNC 程序文件 | Cannot read CNC program file: {filePath}", ex);
|
||||
}
|
||||
|
||||
var program = Deserialize(json);
|
||||
|
||||
_logger.Info("已加载 CNC 程序 | Loaded CNC program: {FilePath}, Program={ProgramName}, Nodes={NodeCount}",
|
||||
filePath, program.Name, program.Nodes.Count);
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Serialize(CncProgram program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
return JsonSerializer.Serialize(program, CncJsonOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CncProgram Deserialize(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
throw new InvalidDataException("JSON 字符串为空 | JSON string is empty");
|
||||
|
||||
CncProgram program;
|
||||
try
|
||||
{
|
||||
program = JsonSerializer.Deserialize<CncProgram>(json, CncJsonOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"CNC 程序 JSON 格式无效 | Invalid CNC program JSON format: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
if (program is null)
|
||||
throw new InvalidDataException("反序列化结果为 null | Deserialization result is null");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(program.Name))
|
||||
throw new InvalidDataException("CNC 程序数据不完整,缺少名称 | CNC program data incomplete, missing Name");
|
||||
|
||||
if (program.Nodes is null)
|
||||
throw new InvalidDataException("CNC 程序数据不完整,缺少节点列表 | CNC program data incomplete, missing Nodes");
|
||||
|
||||
// 对节点应用值域校验(如 WaitDelay 截断)| Apply value range validation to nodes
|
||||
var validatedNodes = ValidateNodes(program.Nodes);
|
||||
return program with { Nodes = validatedNodes };
|
||||
}
|
||||
|
||||
// ── 常量 | Constants ──
|
||||
|
||||
/// <summary>等待延时最小值(毫秒)| Wait delay minimum (ms)</summary>
|
||||
private const int WaitDelayMin = 0;
|
||||
|
||||
/// <summary>等待延时最大值(毫秒)| Wait delay maximum (ms)</summary>
|
||||
private const int WaitDelayMax = 300000;
|
||||
|
||||
// ── 内部辅助方法 | Internal helper methods ──
|
||||
|
||||
/// <summary>
|
||||
/// 将等待延时值截断到 [0, 300000] 范围 | Clamp wait delay value to [0, 300000] range
|
||||
/// </summary>
|
||||
internal static int ClampWaitDelay(int value) =>
|
||||
Math.Clamp(value, WaitDelayMin, WaitDelayMax);
|
||||
|
||||
/// <summary>
|
||||
/// 对反序列化后的节点列表应用值域校验(如 WaitDelay 截断)
|
||||
/// Apply value range validation to deserialized nodes (e.g. WaitDelay clamping)
|
||||
/// </summary>
|
||||
private static IReadOnlyList<CncNode> ValidateNodes(IReadOnlyList<CncNode> nodes)
|
||||
{
|
||||
var result = new List<CncNode>(nodes.Count);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node is WaitDelayNode wdn)
|
||||
{
|
||||
var clamped = ClampWaitDelay(wdn.DelayMilliseconds);
|
||||
result.Add(clamped != wdn.DelayMilliseconds ? wdn with { DelayMilliseconds = clamped } : wdn);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(node);
|
||||
}
|
||||
}
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新编号节点索引为连续序列 0, 1, 2, ..., N-1(record 不可变,使用 with 语法)
|
||||
/// Renumber node indices to consecutive sequence 0, 1, 2, ..., N-1 (records are immutable, use with syntax)
|
||||
/// </summary>
|
||||
private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes)
|
||||
{
|
||||
var result = new List<CncNode>(nodes.Count);
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
result.Add(nodes[i] with { Index = i });
|
||||
}
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>创建参考点节点 | Create reference point node</summary>
|
||||
private ReferencePointNode CreateReferencePointNode(Guid id, int index)
|
||||
{
|
||||
var motion = _appStateService.MotionState;
|
||||
return new ReferencePointNode(
|
||||
id, index, $"参考点_{index}",
|
||||
XM: motion.XM,
|
||||
YM: motion.YM,
|
||||
ZT: motion.ZT,
|
||||
ZD: motion.ZD,
|
||||
TiltD: motion.TiltD,
|
||||
Dist: motion.Dist);
|
||||
}
|
||||
|
||||
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
|
||||
private SaveNodeWithImageNode CreateSaveNodeWithImageNode(Guid id, int index)
|
||||
{
|
||||
return new SaveNodeWithImageNode(
|
||||
id, index, $"保存节点(图像)_{index}",
|
||||
MotionState: _appStateService.MotionState,
|
||||
RaySourceState: _appStateService.RaySourceState,
|
||||
DetectorState: _appStateService.DetectorState,
|
||||
ImageFileName: "");
|
||||
}
|
||||
|
||||
/// <summary>创建保存节点(不含图像)| Create save node without image</summary>
|
||||
private SaveNodeNode CreateSaveNodeNode(Guid id, int index)
|
||||
{
|
||||
return new SaveNodeNode(
|
||||
id, index, $"保存节点_{index}",
|
||||
MotionState: _appStateService.MotionState,
|
||||
RaySourceState: _appStateService.RaySourceState,
|
||||
DetectorState: _appStateService.DetectorState);
|
||||
}
|
||||
|
||||
/// <summary>创建保存位置节点 | Create save position node</summary>
|
||||
private SavePositionNode CreateSavePositionNode(Guid id, int index)
|
||||
{
|
||||
return new SavePositionNode(
|
||||
id, index, $"保存位置_{index}",
|
||||
MotionState: _appStateService.MotionState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
|
||||
/// CNC program management service interface for creation, node editing, serialization and file I/O
|
||||
/// </summary>
|
||||
public interface ICncProgramService
|
||||
{
|
||||
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
|
||||
CncProgram CreateProgram(string name);
|
||||
|
||||
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
|
||||
CncNode CreateNode(CncNodeType type);
|
||||
|
||||
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
|
||||
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
|
||||
|
||||
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
|
||||
CncProgram RemoveNode(CncProgram program, int index);
|
||||
|
||||
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
|
||||
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
|
||||
|
||||
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary>
|
||||
Task SaveAsync(CncProgram program, string filePath);
|
||||
|
||||
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
|
||||
Task<CncProgram> LoadAsync(string filePath);
|
||||
|
||||
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
|
||||
string Serialize(CncProgram program);
|
||||
|
||||
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
|
||||
CncProgram Deserialize(string json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编排管理服务接口,负责矩阵布局的创建、编辑、序列化/反序列化和文件读写
|
||||
/// Matrix layout management service interface for creation, editing, serialization and file I/O
|
||||
/// </summary>
|
||||
public interface IMatrixService
|
||||
{
|
||||
/// <summary>创建矩阵布局 | Create a matrix layout</summary>
|
||||
MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing);
|
||||
|
||||
/// <summary>更新矩阵布局参数并重新计算单元格偏移 | Update layout parameters and recalculate cell offsets</summary>
|
||||
MatrixLayout UpdateLayout(MatrixLayout layout, int rows, int columns, double rowSpacing, double columnSpacing);
|
||||
|
||||
/// <summary>获取指定行列的单元格 | Get the cell at the specified row and column</summary>
|
||||
MatrixCell GetCell(MatrixLayout layout, int row, int column);
|
||||
|
||||
/// <summary>切换单元格启用/禁用状态 | Toggle cell enabled/disabled state</summary>
|
||||
MatrixLayout ToggleCellEnabled(MatrixLayout layout, int row, int column);
|
||||
|
||||
/// <summary>关联 CNC 程序到矩阵布局 | Associate a CNC program with the matrix layout</summary>
|
||||
MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program);
|
||||
|
||||
/// <summary>将矩阵布局保存到 JSON 文件 | Save matrix layout to JSON file</summary>
|
||||
Task SaveAsync(MatrixLayout layout, string filePath);
|
||||
|
||||
/// <summary>从 JSON 文件加载矩阵布局 | Load matrix layout from JSON file</summary>
|
||||
Task<MatrixLayout> LoadAsync(string filePath);
|
||||
|
||||
/// <summary>将矩阵布局序列化为 JSON 字符串 | Serialize matrix layout to JSON string</summary>
|
||||
string Serialize(MatrixLayout layout);
|
||||
|
||||
/// <summary>从 JSON 字符串反序列化矩阵布局 | Deserialize matrix layout from JSON string</summary>
|
||||
MatrixLayout Deserialize(string json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编排管理服务实现。
|
||||
/// 负责矩阵布局的创建、编辑、单元格偏移计算、JSON 序列化/反序列化和文件读写。
|
||||
/// Matrix layout management service implementation.
|
||||
/// Handles layout creation, editing, cell offset calculation, JSON serialization and file I/O.
|
||||
/// </summary>
|
||||
public class MatrixService : IMatrixService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// ── 序列化配置 | Serialization options ──
|
||||
private static readonly JsonSerializerOptions MatrixJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public MatrixService(ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_logger = logger.ForModule<MatrixService>();
|
||||
|
||||
_logger.Info("MatrixService 已初始化 | MatrixService initialized");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing)
|
||||
{
|
||||
ValidateDimensions(rows, columns);
|
||||
|
||||
var layout = new MatrixLayout(
|
||||
Id: Guid.NewGuid(),
|
||||
Rows: rows,
|
||||
Columns: columns,
|
||||
RowSpacing: rowSpacing,
|
||||
ColumnSpacing: columnSpacing,
|
||||
StartOffsetX: 0.0,
|
||||
StartOffsetY: 0.0,
|
||||
CncProgramPath: string.Empty,
|
||||
Cells: BuildCells(rows, columns, rowSpacing, columnSpacing, 0.0, 0.0));
|
||||
|
||||
_logger.Info("已创建矩阵布局 | Created matrix layout: Id={LayoutId}, Rows={Rows}, Columns={Columns}",
|
||||
layout.Id, rows, columns);
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout UpdateLayout(MatrixLayout layout, int rows, int columns, double rowSpacing, double columnSpacing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ValidateDimensions(rows, columns);
|
||||
|
||||
var updated = layout with
|
||||
{
|
||||
Rows = rows,
|
||||
Columns = columns,
|
||||
RowSpacing = rowSpacing,
|
||||
ColumnSpacing = columnSpacing,
|
||||
Cells = BuildCells(rows, columns, rowSpacing, columnSpacing, layout.StartOffsetX, layout.StartOffsetY)
|
||||
};
|
||||
|
||||
_logger.Info("已更新矩阵布局 | Updated matrix layout: Id={LayoutId}, Rows={Rows}, Columns={Columns}",
|
||||
layout.Id, rows, columns);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixCell GetCell(MatrixLayout layout, int row, int column)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ValidateCellCoordinates(layout, row, column);
|
||||
|
||||
int index = row * layout.Columns + column;
|
||||
return layout.Cells[index];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout ToggleCellEnabled(MatrixLayout layout, int row, int column)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ValidateCellCoordinates(layout, row, column);
|
||||
|
||||
int targetIndex = row * layout.Columns + column;
|
||||
var cells = new List<MatrixCell>(layout.Cells.Count);
|
||||
|
||||
for (int i = 0; i < layout.Cells.Count; i++)
|
||||
{
|
||||
if (i == targetIndex)
|
||||
{
|
||||
var cell = layout.Cells[i];
|
||||
cells.Add(cell with { IsEnabled = !cell.IsEnabled });
|
||||
}
|
||||
else
|
||||
{
|
||||
cells.Add(layout.Cells[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var updated = layout with { Cells = cells.AsReadOnly() };
|
||||
|
||||
_logger.Info("已切换单元格启用状态 | Toggled cell enabled state: Row={Row}, Column={Column}, IsEnabled={IsEnabled}",
|
||||
row, column, updated.Cells[targetIndex].IsEnabled);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
var updated = layout with { CncProgramPath = program.Name };
|
||||
|
||||
_logger.Info("已关联 CNC 程序到矩阵布局 | Associated CNC program with matrix layout: LayoutId={LayoutId}, Program={ProgramName}",
|
||||
layout.Id, program.Name);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(MatrixLayout layout, string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
|
||||
// 路径安全性验证:检查路径遍历 | Path safety: check for path traversal
|
||||
if (filePath.Contains("..", StringComparison.Ordinal))
|
||||
throw new UnauthorizedAccessException(
|
||||
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
|
||||
|
||||
var json = Serialize(layout);
|
||||
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("已保存矩阵布局到文件 | Saved matrix layout to file: {FilePath}, LayoutId={LayoutId}",
|
||||
filePath, layout.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MatrixLayout> LoadAsync(string filePath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
|
||||
// 路径安全性验证 | Path safety check
|
||||
if (filePath.Contains("..", StringComparison.Ordinal))
|
||||
throw new UnauthorizedAccessException(
|
||||
$"文件路径包含不安全的路径遍历字符 | File path contains unsafe path traversal characters: {filePath}");
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException(
|
||||
$"矩阵布局文件不存在 | Matrix layout file not found: {filePath}", filePath);
|
||||
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"无法读取矩阵布局文件 | Cannot read matrix layout file: {filePath}", ex);
|
||||
}
|
||||
|
||||
var layout = Deserialize(json);
|
||||
|
||||
_logger.Info("已加载矩阵布局 | Loaded matrix layout: {FilePath}, LayoutId={LayoutId}, Rows={Rows}, Columns={Columns}",
|
||||
filePath, layout.Id, layout.Rows, layout.Columns);
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Serialize(MatrixLayout layout)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
return JsonSerializer.Serialize(layout, MatrixJsonOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout Deserialize(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
throw new InvalidDataException("JSON 字符串为空 | JSON string is empty");
|
||||
|
||||
MatrixLayout layout;
|
||||
try
|
||||
{
|
||||
layout = JsonSerializer.Deserialize<MatrixLayout>(json, MatrixJsonOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"矩阵布局 JSON 格式无效 | Invalid matrix layout JSON format: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
if (layout is null)
|
||||
throw new InvalidDataException("反序列化结果为 null | Deserialization result is null");
|
||||
|
||||
if (layout.Rows <= 0)
|
||||
throw new InvalidDataException("矩阵布局数据不完整,行数无效 | Matrix layout data incomplete, invalid Rows");
|
||||
|
||||
if (layout.Columns <= 0)
|
||||
throw new InvalidDataException("矩阵布局数据不完整,列数无效 | Matrix layout data incomplete, invalid Columns");
|
||||
|
||||
if (layout.Cells is null)
|
||||
throw new InvalidDataException("矩阵布局数据不完整,缺少单元格列表 | Matrix layout data incomplete, missing Cells");
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
// ── 内部辅助方法 | Internal helper methods ──
|
||||
|
||||
/// <summary>
|
||||
/// 构建矩阵单元格列表,自动计算每个单元格的偏移坐标
|
||||
/// Build matrix cell list with auto-calculated offset coordinates
|
||||
/// OffsetX = StartOffsetX + col * ColumnSpacing
|
||||
/// OffsetY = StartOffsetY + row * RowSpacing
|
||||
/// </summary>
|
||||
internal static IReadOnlyList<MatrixCell> BuildCells(
|
||||
int rows, int columns,
|
||||
double rowSpacing, double columnSpacing,
|
||||
double startOffsetX, double startOffsetY)
|
||||
{
|
||||
var cells = new List<MatrixCell>(rows * columns);
|
||||
for (int row = 0; row < rows; row++)
|
||||
{
|
||||
for (int col = 0; col < columns; col++)
|
||||
{
|
||||
cells.Add(new MatrixCell(
|
||||
Row: row,
|
||||
Column: col,
|
||||
OffsetX: startOffsetX + col * columnSpacing,
|
||||
OffsetY: startOffsetY + row * rowSpacing,
|
||||
IsEnabled: true,
|
||||
Status: MatrixCellStatus.NotExecuted,
|
||||
ErrorMessage: string.Empty));
|
||||
}
|
||||
}
|
||||
return cells.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证行列数参数 | Validate row and column dimensions
|
||||
/// </summary>
|
||||
private static void ValidateDimensions(int rows, int columns)
|
||||
{
|
||||
if (rows <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(rows),
|
||||
$"行数必须大于 0 | Rows must be greater than 0: {rows}");
|
||||
|
||||
if (columns <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(columns),
|
||||
$"列数必须大于 0 | Columns must be greater than 0: {columns}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证单元格坐标是否在布局范围内 | Validate cell coordinates are within layout bounds
|
||||
/// </summary>
|
||||
private static void ValidateCellCoordinates(MatrixLayout layout, int row, int column)
|
||||
{
|
||||
if (row < 0 || row >= layout.Rows)
|
||||
throw new ArgumentOutOfRangeException(nameof(row),
|
||||
$"行索引超出范围 | Row index out of range: {row}, Rows={layout.Rows}");
|
||||
|
||||
if (column < 0 || column >= layout.Columns)
|
||||
throw new ArgumentOutOfRangeException(nameof(column),
|
||||
$"列索引超出范围 | Column index out of range: {column}, Columns={layout.Columns}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Measurement
|
||||
{
|
||||
/// <summary>
|
||||
/// 测量数据存储服务接口,负责测量记录的持久化、查询和统计
|
||||
/// Measurement data storage service interface for persistence, querying and statistics of measurement records
|
||||
/// </summary>
|
||||
public interface IMeasurementDataService
|
||||
{
|
||||
/// <summary>初始化数据库表和索引 | Initialize database tables and indexes</summary>
|
||||
Task InitializeAsync();
|
||||
|
||||
/// <summary>保存单条测量记录 | Save a single measurement record</summary>
|
||||
Task SaveMeasurementAsync(MeasurementRecord record);
|
||||
|
||||
/// <summary>按配方名称和时间范围查询测量记录 | Query measurement records by recipe name and time range</summary>
|
||||
Task<IReadOnlyList<MeasurementRecord>> QueryAsync(string recipeName, DateTime? from, DateTime? to);
|
||||
|
||||
/// <summary>按配方名称和时间范围获取统计数据 | Get statistics by recipe name and time range</summary>
|
||||
Task<MeasurementStatistics> GetStatisticsAsync(string recipeName, DateTime? from, DateTime? to);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Measurement
|
||||
{
|
||||
/// <summary>
|
||||
/// 测量数据存储服务实现,使用 SQLite(通过 IDbContext)持久化测量记录并提供统计查询。
|
||||
/// Measurement data storage service implementation using SQLite (via IDbContext) to persist records and provide statistics.
|
||||
/// </summary>
|
||||
public class MeasurementDataService : IMeasurementDataService
|
||||
{
|
||||
private readonly IDbContext _db;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// ── SQL 常量 | SQL constants ──
|
||||
|
||||
private const string CreateTableSql = @"
|
||||
CREATE TABLE IF NOT EXISTS measurement_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
recipe_name TEXT NOT NULL,
|
||||
step_index INTEGER NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
result_value REAL NOT NULL,
|
||||
is_pass INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_measurement_recipe ON measurement_data(recipe_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_measurement_timestamp ON measurement_data(timestamp);";
|
||||
|
||||
private const string InsertSql = @"
|
||||
INSERT INTO measurement_data (recipe_name, step_index, timestamp, result_value, is_pass)
|
||||
VALUES (@recipe_name, @step_index, @timestamp, @result_value, @is_pass)";
|
||||
|
||||
// ── 构造函数 | Constructor ──
|
||||
|
||||
public MeasurementDataService(IDbContext db, ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(db);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_db = db;
|
||||
_logger = logger.ForModule<MeasurementDataService>();
|
||||
|
||||
_logger.Info("MeasurementDataService 已初始化 | MeasurementDataService initialized");
|
||||
}
|
||||
|
||||
// ── 公共方法 | Public methods ──
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_logger.Info("正在初始化测量数据表 | Initializing measurement data table");
|
||||
|
||||
var result = await _db.ExecuteNonQueryAsync(CreateTableSql).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logger.Error(result.Exception!, "初始化测量数据表失败 | Failed to initialize measurement data table: {Message}", result.Message);
|
||||
throw new InvalidOperationException(
|
||||
$"初始化测量数据表失败 | Failed to initialize measurement data table: {result.Message}",
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
_logger.Info("测量数据表初始化完成 | Measurement data table initialized");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveMeasurementAsync(MeasurementRecord record)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["@recipe_name"] = record.RecipeName,
|
||||
["@step_index"] = record.StepIndex,
|
||||
["@timestamp"] = record.Timestamp.ToString("o"), // ISO 8601 往返格式 | ISO 8601 round-trip format
|
||||
["@result_value"] = record.ResultValue,
|
||||
["@is_pass"] = record.IsPass ? 1 : 0
|
||||
};
|
||||
|
||||
var result = await _db.ExecuteNonQueryAsync(InsertSql, parameters).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logger.Error(result.Exception!, "保存测量记录失败 | Failed to save measurement record: {Message}", result.Message);
|
||||
throw new InvalidOperationException(
|
||||
$"保存测量记录失败 | Failed to save measurement record: {result.Message}",
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
_logger.Debug("已保存测量记录 | Saved measurement record: Recipe={RecipeName}, Step={StepIndex}, IsPass={IsPass}",
|
||||
record.RecipeName, record.StepIndex, record.IsPass);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MeasurementRecord>> QueryAsync(string recipeName, DateTime? from, DateTime? to)
|
||||
{
|
||||
var (sql, parameters) = BuildFilteredQuery(
|
||||
"SELECT id, recipe_name, step_index, timestamp, result_value, is_pass FROM measurement_data",
|
||||
recipeName, from, to,
|
||||
orderBy: "ORDER BY timestamp ASC");
|
||||
|
||||
var (result, data) = await _db.QueryListAsync<MeasurementDataRow>(sql, parameters).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logger.Error(result.Exception!, "查询测量记录失败 | Failed to query measurement records: {Message}", result.Message);
|
||||
throw new InvalidOperationException(
|
||||
$"查询测量记录失败 | Failed to query measurement records: {result.Message}",
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
// 将数据库行映射为领域模型 | Map database rows to domain model
|
||||
var records = new List<MeasurementRecord>(data.Count);
|
||||
foreach (var row in data)
|
||||
{
|
||||
records.Add(MapRow(row));
|
||||
}
|
||||
|
||||
_logger.Debug("查询到 {Count} 条测量记录 | Queried {Count} measurement records", records.Count);
|
||||
return records.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MeasurementStatistics> GetStatisticsAsync(string recipeName, DateTime? from, DateTime? to)
|
||||
{
|
||||
var (sql, parameters) = BuildFilteredQuery(
|
||||
"SELECT COUNT(*) AS total_count, SUM(CASE WHEN is_pass = 1 THEN 1 ELSE 0 END) AS pass_count FROM measurement_data",
|
||||
recipeName, from, to);
|
||||
|
||||
var (result, data) = await _db.QueryListAsync<StatisticsRow>(sql, parameters).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
_logger.Error(result.Exception!, "获取测量统计失败 | Failed to get measurement statistics: {Message}", result.Message);
|
||||
throw new InvalidOperationException(
|
||||
$"获取测量统计失败 | Failed to get measurement statistics: {result.Message}",
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
// 无数据时返回全零统计 | Return zero statistics when no data
|
||||
if (data.Count == 0)
|
||||
return new MeasurementStatistics(0, 0, 0, 0.0);
|
||||
|
||||
var row = data[0];
|
||||
int total = row.total_count;
|
||||
int pass = row.pass_count;
|
||||
int fail = total - pass;
|
||||
double passRate = total > 0 ? (double)pass / total : 0.0;
|
||||
|
||||
_logger.Debug("统计结果 | Statistics: Total={Total}, Pass={Pass}, Fail={Fail}, PassRate={PassRate:P2}",
|
||||
total, pass, fail, passRate);
|
||||
|
||||
return new MeasurementStatistics(total, pass, fail, passRate);
|
||||
}
|
||||
|
||||
// ── 内部辅助方法 | Internal helper methods ──
|
||||
|
||||
/// <summary>
|
||||
/// 根据筛选条件动态构建 SQL 和参数字典
|
||||
/// Dynamically build SQL and parameter dictionary based on filter conditions
|
||||
/// </summary>
|
||||
private static (string Sql, Dictionary<string, object>? Parameters) BuildFilteredQuery(
|
||||
string selectClause,
|
||||
string? recipeName,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
string? orderBy = null)
|
||||
{
|
||||
var conditions = new List<string>();
|
||||
Dictionary<string, object>? parameters = null;
|
||||
|
||||
// 按配方名称筛选 | Filter by recipe name
|
||||
if (!string.IsNullOrWhiteSpace(recipeName))
|
||||
{
|
||||
conditions.Add("recipe_name = @recipe_name");
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
parameters["@recipe_name"] = recipeName;
|
||||
}
|
||||
|
||||
// 按起始时间筛选 | Filter by start time
|
||||
if (from.HasValue)
|
||||
{
|
||||
conditions.Add("timestamp >= @from");
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
parameters["@from"] = from.Value.ToString("o");
|
||||
}
|
||||
|
||||
// 按结束时间筛选 | Filter by end time
|
||||
if (to.HasValue)
|
||||
{
|
||||
conditions.Add("timestamp <= @to");
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
parameters["@to"] = to.Value.ToString("o");
|
||||
}
|
||||
|
||||
// 组装完整 SQL | Assemble complete SQL
|
||||
var sql = selectClause;
|
||||
if (conditions.Count > 0)
|
||||
{
|
||||
sql += " WHERE " + string.Join(" AND ", conditions);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(orderBy))
|
||||
{
|
||||
sql += " " + orderBy;
|
||||
}
|
||||
|
||||
return (sql, parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将数据库行 DTO 映射为领域模型
|
||||
/// Map database row DTO to domain model
|
||||
/// </summary>
|
||||
private static MeasurementRecord MapRow(MeasurementDataRow row)
|
||||
{
|
||||
return new MeasurementRecord
|
||||
{
|
||||
Id = row.id,
|
||||
RecipeName = row.recipe_name,
|
||||
StepIndex = row.step_index,
|
||||
Timestamp = DateTime.Parse(row.timestamp, null, System.Globalization.DateTimeStyles.RoundtripKind),
|
||||
ResultValue = row.result_value,
|
||||
IsPass = row.is_pass != 0
|
||||
};
|
||||
}
|
||||
|
||||
// ── 内部 DTO(用于 QueryListAsync 自动映射)| Internal DTOs for QueryListAsync auto-mapping ──
|
||||
|
||||
/// <summary>数据库行 DTO,属性名与 SQL 列名一致以支持自动映射</summary>
|
||||
internal class MeasurementDataRow
|
||||
{
|
||||
public long id { get; set; }
|
||||
public string recipe_name { get; set; } = string.Empty;
|
||||
public int step_index { get; set; }
|
||||
public string timestamp { get; set; } = string.Empty;
|
||||
public double result_value { get; set; }
|
||||
public int is_pass { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>统计查询 DTO</summary>
|
||||
internal class StatisticsRow
|
||||
{
|
||||
public int total_count { get; set; }
|
||||
public int pass_count { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Cnc;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作
|
||||
/// CNC editor ViewModel that manages the node list, editing operations and file operations
|
||||
/// </summary>
|
||||
public class CncEditorViewModel : BindableBase
|
||||
{
|
||||
private readonly ICncProgramService _cncProgramService;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// 当前 CNC 程序 | Current CNC program
|
||||
private CncProgram _currentProgram;
|
||||
|
||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||
private CncNodeViewModel _selectedNode;
|
||||
private bool _isModified;
|
||||
private string _programName;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public CncEditorViewModel(
|
||||
ICncProgramService cncProgramService,
|
||||
IAppStateService appStateService,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
|
||||
|
||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||
|
||||
// ── 节点插入命令 | Node insertion commands ──
|
||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
|
||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
|
||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
|
||||
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition));
|
||||
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule));
|
||||
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker));
|
||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog));
|
||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
|
||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
|
||||
|
||||
// ── 节点编辑命令 | Node editing commands ──
|
||||
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
||||
.ObservesProperty(() => SelectedNode);
|
||||
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
|
||||
|
||||
// ── 文件操作命令 | File operation commands ──
|
||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
||||
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
||||
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
||||
|
||||
_logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized");
|
||||
}
|
||||
|
||||
// ── 属性 | Properties ──────────────────────────────────────────
|
||||
|
||||
/// <summary>节点列表 | Node list</summary>
|
||||
public ObservableCollection<CncNodeViewModel> Nodes
|
||||
{
|
||||
get => _nodes;
|
||||
set => SetProperty(ref _nodes, value);
|
||||
}
|
||||
|
||||
/// <summary>当前选中的节点 | Currently selected node</summary>
|
||||
public CncNodeViewModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set => SetProperty(ref _selectedNode, value);
|
||||
}
|
||||
|
||||
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
|
||||
public bool IsModified
|
||||
{
|
||||
get => _isModified;
|
||||
set => SetProperty(ref _isModified, value);
|
||||
}
|
||||
|
||||
/// <summary>当前程序名称 | Current program name</summary>
|
||||
public string ProgramName
|
||||
{
|
||||
get => _programName;
|
||||
set => SetProperty(ref _programName, value);
|
||||
}
|
||||
|
||||
// ── 节点插入命令 | Node insertion commands ──────────────────────
|
||||
|
||||
/// <summary>插入参考点命令 | Insert reference point command</summary>
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
|
||||
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
|
||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||
|
||||
/// <summary>插入保存节点命令 | Insert save node command</summary>
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
|
||||
/// <summary>插入保存位置命令 | Insert save position command</summary>
|
||||
public DelegateCommand InsertSavePositionCommand { get; }
|
||||
|
||||
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
|
||||
public DelegateCommand InsertInspectionModuleCommand { get; }
|
||||
|
||||
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
|
||||
public DelegateCommand InsertInspectionMarkerCommand { get; }
|
||||
|
||||
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
|
||||
public DelegateCommand InsertPauseDialogCommand { get; }
|
||||
|
||||
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
|
||||
public DelegateCommand InsertWaitDelayCommand { get; }
|
||||
|
||||
/// <summary>插入完成程序命令 | Insert complete program command</summary>
|
||||
public DelegateCommand InsertCompleteProgramCommand { get; }
|
||||
|
||||
// ── 节点编辑命令 | Node editing commands ────────────────────────
|
||||
|
||||
/// <summary>删除选中节点命令 | Delete selected node command</summary>
|
||||
public DelegateCommand DeleteNodeCommand { get; }
|
||||
|
||||
/// <summary>上移节点命令 | Move node up command</summary>
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
|
||||
|
||||
/// <summary>下移节点命令 | Move node down command</summary>
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
|
||||
|
||||
// ── 文件操作命令 | File operation commands ──────────────────────
|
||||
|
||||
/// <summary>保存程序命令 | Save program command</summary>
|
||||
public DelegateCommand SaveProgramCommand { get; }
|
||||
|
||||
/// <summary>加载程序命令 | Load program command</summary>
|
||||
public DelegateCommand LoadProgramCommand { get; }
|
||||
|
||||
/// <summary>新建程序命令 | New program command</summary>
|
||||
public DelegateCommand NewProgramCommand { get; }
|
||||
|
||||
/// <summary>导出 CSV 命令 | Export CSV command</summary>
|
||||
public DelegateCommand ExportCsvCommand { get; }
|
||||
|
||||
// ── 命令执行方法 | Command execution methods ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 插入指定类型的节点到选中节点之后
|
||||
/// Insert a node of the specified type after the selected node
|
||||
/// </summary>
|
||||
private void ExecuteInsertNode(CncNodeType nodeType)
|
||||
{
|
||||
if (_currentProgram == null)
|
||||
{
|
||||
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var node = _cncProgramService.CreateNode(nodeType);
|
||||
int afterIndex = SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
|
||||
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
|
||||
|
||||
OnProgramEdited();
|
||||
_logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// 重复插入 CompleteProgram 等业务规则异常 | Business rule exceptions like duplicate CompleteProgram
|
||||
_logger.Warn("插入节点被阻止 | Node insertion blocked: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除选中节点 | Delete the selected node
|
||||
/// </summary>
|
||||
private void ExecuteDeleteNode()
|
||||
{
|
||||
if (_currentProgram == null || SelectedNode == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
|
||||
OnProgramEdited();
|
||||
_logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以删除节点(至少保留 1 个节点)
|
||||
/// Determines whether delete is allowed (at least 1 node must remain)
|
||||
/// </summary>
|
||||
private bool CanExecuteDeleteNode()
|
||||
{
|
||||
return SelectedNode != null
|
||||
&& _currentProgram != null
|
||||
&& _currentProgram.Nodes.Count > 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上移节点 | Move node up
|
||||
/// </summary>
|
||||
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
||||
{
|
||||
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
|
||||
OnProgramEdited();
|
||||
_logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下移节点 | Move node down
|
||||
/// </summary>
|
||||
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
||||
{
|
||||
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
|
||||
OnProgramEdited();
|
||||
_logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前程序到文件 | Save current program to file
|
||||
/// </summary>
|
||||
private async Task ExecuteSaveProgramAsync()
|
||||
{
|
||||
if (_currentProgram == null)
|
||||
{
|
||||
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 使用程序名称作为文件名 | Use program name as file name
|
||||
var filePath = $"{_currentProgram.Name}.xp";
|
||||
await _cncProgramService.SaveAsync(_currentProgram, filePath);
|
||||
IsModified = false;
|
||||
_logger.Info("程序已保存 | Program saved: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "保存程序失败 | Failed to save program");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载程序 | Load program from file
|
||||
/// </summary>
|
||||
private async Task ExecuteLoadProgramAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
|
||||
var filePath = $"{ProgramName}.xp";
|
||||
_currentProgram = await _cncProgramService.LoadAsync(filePath);
|
||||
ProgramName = _currentProgram.Name;
|
||||
IsModified = false;
|
||||
RefreshNodes();
|
||||
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "加载程序失败 | Failed to load program");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建新程序 | Create a new program
|
||||
/// </summary>
|
||||
private void ExecuteNewProgram()
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
|
||||
_currentProgram = _cncProgramService.CreateProgram(name);
|
||||
ProgramName = _currentProgram.Name;
|
||||
IsModified = false;
|
||||
RefreshNodes();
|
||||
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出 CSV(占位实现)| Export CSV (placeholder)
|
||||
/// </summary>
|
||||
private void ExecuteExportCsv()
|
||||
{
|
||||
// TODO: 实现 CSV 导出功能 | Implement CSV export functionality
|
||||
_logger.Info("CSV 导出功能尚未实现 | CSV export not yet implemented");
|
||||
}
|
||||
|
||||
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
|
||||
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
|
||||
/// </summary>
|
||||
private void OnProgramEdited()
|
||||
{
|
||||
IsModified = true;
|
||||
RefreshNodes();
|
||||
PublishProgramChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 _currentProgram.Nodes 重建 Nodes 集合
|
||||
/// Rebuild the Nodes collection from _currentProgram.Nodes
|
||||
/// </summary>
|
||||
private void RefreshNodes()
|
||||
{
|
||||
Nodes.Clear();
|
||||
|
||||
if (_currentProgram?.Nodes == null)
|
||||
return;
|
||||
|
||||
foreach (var node in _currentProgram.Nodes)
|
||||
{
|
||||
Nodes.Add(new CncNodeViewModel(node));
|
||||
}
|
||||
|
||||
// 尝试保持选中状态 | Try to preserve selection
|
||||
if (SelectedNode != null)
|
||||
{
|
||||
var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index);
|
||||
SelectedNode = match ?? Nodes.LastOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 IEventAggregator 发布 CNC 程序变更事件
|
||||
/// Publish CNC program changed event via IEventAggregator
|
||||
/// </summary>
|
||||
private void PublishProgramChanged()
|
||||
{
|
||||
_eventAggregator
|
||||
.GetEvent<CncProgramChangedEvent>()
|
||||
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Prism.Mvvm;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel
|
||||
/// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel
|
||||
/// </summary>
|
||||
public class CncNodeViewModel : BindableBase
|
||||
{
|
||||
private int _index;
|
||||
private string _name;
|
||||
private CncNodeType _nodeType;
|
||||
private string _icon;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,从 CncNode 模型初始化 ViewModel
|
||||
/// Constructor that initializes the ViewModel from a CncNode model
|
||||
/// </summary>
|
||||
public CncNodeViewModel(CncNode model)
|
||||
{
|
||||
Model = model;
|
||||
_index = model.Index;
|
||||
_name = model.Name;
|
||||
_nodeType = model.NodeType;
|
||||
_icon = GetIconForNodeType(model.NodeType);
|
||||
}
|
||||
|
||||
/// <summary>底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)</summary>
|
||||
public CncNode Model { get; }
|
||||
|
||||
/// <summary>节点在程序中的索引 | Node index in the program</summary>
|
||||
public int Index
|
||||
{
|
||||
get => _index;
|
||||
set => SetProperty(ref _index, value);
|
||||
}
|
||||
|
||||
/// <summary>节点显示名称 | Node display name</summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
/// <summary>节点类型 | Node type</summary>
|
||||
public CncNodeType NodeType
|
||||
{
|
||||
get => _nodeType;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _nodeType, value))
|
||||
{
|
||||
// 类型变更时自动更新图标 | Auto-update icon when type changes
|
||||
Icon = GetIconForNodeType(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>节点图标路径 | Node icon path</summary>
|
||||
public string Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetProperty(ref _icon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据节点类型返回对应的图标路径
|
||||
/// Returns the icon path for the given node type
|
||||
/// </summary>
|
||||
public static string GetIconForNodeType(CncNodeType nodeType)
|
||||
{
|
||||
return nodeType switch
|
||||
{
|
||||
CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png",
|
||||
CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png",
|
||||
CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png",
|
||||
CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png",
|
||||
CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png",
|
||||
CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png",
|
||||
CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png",
|
||||
CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png",
|
||||
CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png",
|
||||
_ => "/Resources/Icons/cnc_default.png",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Prism.Mvvm;
|
||||
using XplorePlane.Models;
|
||||
|
||||
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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Matrix;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编辑器 ViewModel,管理矩阵布局的配置、单元格网格和文件操作
|
||||
/// Matrix editor ViewModel that manages layout configuration, cell grid and file operations
|
||||
/// </summary>
|
||||
public class MatrixEditorViewModel : BindableBase
|
||||
{
|
||||
private readonly IMatrixService _matrixService;
|
||||
private readonly ICncProgramService _cncProgramService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// 当前矩阵布局 | Current matrix layout
|
||||
private MatrixLayout _currentLayout;
|
||||
|
||||
private int _rows = 1;
|
||||
private int _columns = 1;
|
||||
private double _rowSpacing;
|
||||
private double _columnSpacing;
|
||||
private ObservableCollection<MatrixCellViewModel> _cells;
|
||||
private MatrixCellViewModel _selectedCell;
|
||||
private string _associatedProgramPath;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public MatrixEditorViewModel(
|
||||
IMatrixService matrixService,
|
||||
ICncProgramService cncProgramService,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_matrixService = matrixService ?? throw new ArgumentNullException(nameof(matrixService));
|
||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MatrixEditorViewModel>();
|
||||
|
||||
_cells = new ObservableCollection<MatrixCellViewModel>();
|
||||
|
||||
// ── 命令初始化 | Command initialization ──
|
||||
UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout);
|
||||
ToggleCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteToggleCell);
|
||||
SelectCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteSelectCell);
|
||||
AssociateProgramCommand = new DelegateCommand(async () => await ExecuteAssociateProgramAsync());
|
||||
SaveLayoutCommand = new DelegateCommand(async () => await ExecuteSaveLayoutAsync());
|
||||
LoadLayoutCommand = new DelegateCommand(async () => await ExecuteLoadLayoutAsync());
|
||||
|
||||
_logger.Info("MatrixEditorViewModel 已初始化 | MatrixEditorViewModel initialized");
|
||||
}
|
||||
|
||||
// ── 属性 | Properties ──────────────────────────────────────────
|
||||
|
||||
/// <summary>矩阵行数 | Number of rows</summary>
|
||||
public int Rows
|
||||
{
|
||||
get => _rows;
|
||||
set => SetProperty(ref _rows, value);
|
||||
}
|
||||
|
||||
/// <summary>矩阵列数 | Number of columns</summary>
|
||||
public int Columns
|
||||
{
|
||||
get => _columns;
|
||||
set => SetProperty(ref _columns, value);
|
||||
}
|
||||
|
||||
/// <summary>行间距(mm)| Row spacing in mm</summary>
|
||||
public double RowSpacing
|
||||
{
|
||||
get => _rowSpacing;
|
||||
set => SetProperty(ref _rowSpacing, value);
|
||||
}
|
||||
|
||||
/// <summary>列间距(mm)| Column spacing in mm</summary>
|
||||
public double ColumnSpacing
|
||||
{
|
||||
get => _columnSpacing;
|
||||
set => SetProperty(ref _columnSpacing, value);
|
||||
}
|
||||
|
||||
/// <summary>矩阵单元格集合 | Matrix cell collection</summary>
|
||||
public ObservableCollection<MatrixCellViewModel> Cells
|
||||
{
|
||||
get => _cells;
|
||||
set => SetProperty(ref _cells, value);
|
||||
}
|
||||
|
||||
/// <summary>当前选中的单元格 | Currently selected cell</summary>
|
||||
public MatrixCellViewModel SelectedCell
|
||||
{
|
||||
get => _selectedCell;
|
||||
set => SetProperty(ref _selectedCell, value);
|
||||
}
|
||||
|
||||
/// <summary>关联的 CNC 程序路径 | Associated CNC program path</summary>
|
||||
public string AssociatedProgramPath
|
||||
{
|
||||
get => _associatedProgramPath;
|
||||
set => SetProperty(ref _associatedProgramPath, value);
|
||||
}
|
||||
|
||||
// ── 命令 | Commands ────────────────────────────────────────────
|
||||
|
||||
/// <summary>更新矩阵布局命令 | Update matrix layout command</summary>
|
||||
public DelegateCommand UpdateLayoutCommand { get; }
|
||||
|
||||
/// <summary>切换单元格启用状态命令 | Toggle cell enabled state command</summary>
|
||||
public DelegateCommand<MatrixCellViewModel> ToggleCellCommand { get; }
|
||||
|
||||
/// <summary>选中单元格命令 | Select cell command</summary>
|
||||
public DelegateCommand<MatrixCellViewModel> SelectCellCommand { get; }
|
||||
|
||||
/// <summary>关联 CNC 程序命令 | Associate CNC program command</summary>
|
||||
public DelegateCommand AssociateProgramCommand { get; }
|
||||
|
||||
/// <summary>保存矩阵布局命令 | Save matrix layout command</summary>
|
||||
public DelegateCommand SaveLayoutCommand { get; }
|
||||
|
||||
/// <summary>加载矩阵布局命令 | Load matrix layout command</summary>
|
||||
public DelegateCommand LoadLayoutCommand { get; }
|
||||
|
||||
// ── 命令执行方法 | Command execution methods ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 更新矩阵布局:如果当前无布局则创建,否则更新现有布局
|
||||
/// Update matrix layout: create if none exists, otherwise update existing
|
||||
/// </summary>
|
||||
private void ExecuteUpdateLayout()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentLayout == null)
|
||||
{
|
||||
_currentLayout = _matrixService.CreateLayout(Rows, Columns, RowSpacing, ColumnSpacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentLayout = _matrixService.UpdateLayout(_currentLayout, Rows, Columns, RowSpacing, ColumnSpacing);
|
||||
}
|
||||
|
||||
RefreshCells();
|
||||
_logger.Info("已更新矩阵布局 | Updated matrix layout: Rows={Rows}, Columns={Columns}", Rows, Columns);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中指定单元格,更新高亮状态
|
||||
/// Select the specified cell and update highlight state
|
||||
/// </summary>
|
||||
private void ExecuteSelectCell(MatrixCellViewModel cellVm)
|
||||
{
|
||||
if (cellVm == null)
|
||||
return;
|
||||
|
||||
// 取消之前选中的单元格 | Deselect previously selected cell
|
||||
if (SelectedCell != null)
|
||||
SelectedCell.IsSelected = false;
|
||||
|
||||
cellVm.IsSelected = true;
|
||||
SelectedCell = cellVm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换指定单元格的启用/禁用状态
|
||||
/// Toggle the enabled/disabled state of the specified cell
|
||||
/// </summary>
|
||||
private void ExecuteToggleCell(MatrixCellViewModel cellVm)
|
||||
{
|
||||
if (_currentLayout == null || cellVm == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_currentLayout = _matrixService.ToggleCellEnabled(_currentLayout, cellVm.Row, cellVm.Column);
|
||||
RefreshCells();
|
||||
_logger.Info("已切换单元格状态 | Toggled cell state: Row={Row}, Column={Column}", cellVm.Row, cellVm.Column);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "切换单元格状态失败 | Failed to toggle cell state");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关联 CNC 程序到当前矩阵布局(占位:从 AssociatedProgramPath 加载)
|
||||
/// Associate a CNC program with the current layout (placeholder: loads from AssociatedProgramPath)
|
||||
/// </summary>
|
||||
private async Task ExecuteAssociateProgramAsync()
|
||||
{
|
||||
if (_currentLayout == null)
|
||||
{
|
||||
_logger.Warn("无法关联程序:当前无矩阵布局 | Cannot associate program: no current layout");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AssociatedProgramPath))
|
||||
{
|
||||
_logger.Warn("无法关联程序:程序路径为空 | Cannot associate program: program path is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var program = await _cncProgramService.LoadAsync(AssociatedProgramPath);
|
||||
_currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program);
|
||||
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", AssociatedProgramPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前矩阵布局到文件 | Save current matrix layout to file
|
||||
/// </summary>
|
||||
private async Task ExecuteSaveLayoutAsync()
|
||||
{
|
||||
if (_currentLayout == null)
|
||||
{
|
||||
_logger.Warn("无法保存:当前无矩阵布局 | Cannot save: no current layout");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
|
||||
var filePath = $"matrix_{_currentLayout.Id}.json";
|
||||
await _matrixService.SaveAsync(_currentLayout, filePath);
|
||||
_logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "保存矩阵布局失败 | Failed to save matrix layout");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载矩阵布局 | Load matrix layout from file
|
||||
/// </summary>
|
||||
private async Task ExecuteLoadLayoutAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
|
||||
var filePath = $"matrix_layout.json";
|
||||
_currentLayout = await _matrixService.LoadAsync(filePath);
|
||||
|
||||
// 同步属性到 ViewModel | Sync properties to ViewModel
|
||||
Rows = _currentLayout.Rows;
|
||||
Columns = _currentLayout.Columns;
|
||||
RowSpacing = _currentLayout.RowSpacing;
|
||||
ColumnSpacing = _currentLayout.ColumnSpacing;
|
||||
AssociatedProgramPath = _currentLayout.CncProgramPath;
|
||||
|
||||
RefreshCells();
|
||||
_logger.Info("矩阵布局已加载 | Matrix layout loaded: Rows={Rows}, Columns={Columns}", Rows, Columns);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "加载矩阵布局失败 | Failed to load matrix layout");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 从 _currentLayout.Cells 重建 Cells 集合
|
||||
/// Rebuild the Cells collection from _currentLayout.Cells
|
||||
/// </summary>
|
||||
private void RefreshCells()
|
||||
{
|
||||
Cells.Clear();
|
||||
|
||||
if (_currentLayout?.Cells == null)
|
||||
return;
|
||||
|
||||
foreach (var cell in _currentLayout.Cells)
|
||||
{
|
||||
Cells.Add(new MatrixCellViewModel(cell));
|
||||
}
|
||||
|
||||
// 尝试保持选中状态 | Try to preserve selection
|
||||
if (SelectedCell != null)
|
||||
{
|
||||
var match = Cells.FirstOrDefault(c => c.Row == SelectedCell.Row && c.Column == SelectedCell.Column);
|
||||
SelectedCell = match ?? Cells.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand EditPropertiesCommand { get; set; }
|
||||
public DelegateCommand OpenImageProcessingCommand { get; set; }
|
||||
public DelegateCommand OpenPipelineEditorCommand { get; set; }
|
||||
public DelegateCommand OpenCncEditorCommand { get; set; }
|
||||
public DelegateCommand OpenMatrixEditorCommand { get; set; }
|
||||
|
||||
public MainViewModel(ILoggerService logger)
|
||||
{
|
||||
@@ -53,6 +55,20 @@ namespace XplorePlane.ViewModels
|
||||
_logger.Info("流水线编辑器窗口已打开");
|
||||
});
|
||||
|
||||
OpenCncEditorCommand = new DelegateCommand(() =>
|
||||
{
|
||||
var window = new Views.Cnc.CncEditorWindow();
|
||||
window.Show();
|
||||
_logger.Info("CNC 编辑器窗口已打开");
|
||||
});
|
||||
|
||||
OpenMatrixEditorCommand = new DelegateCommand(() =>
|
||||
{
|
||||
var window = new Views.Cnc.MatrixEditorWindow();
|
||||
window.Show();
|
||||
_logger.Info("矩阵编排窗口已打开");
|
||||
});
|
||||
|
||||
_logger.Info("MainViewModel 已初始化");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
|
||||
Title="CNC 编辑器"
|
||||
Width="1200"
|
||||
Height="750"
|
||||
MinWidth="900"
|
||||
MinHeight="550"
|
||||
ShowInTaskbar="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<cnc:CncPageView />
|
||||
</Window>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
public partial class CncEditorWindow : Window
|
||||
{
|
||||
public CncEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
<!-- CNC 编辑器主页面视图 | CNC editor main page view -->
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.Cnc.CncPageView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="900"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 面板背景和边框颜色 | Panel background and border colors -->
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 节点列表项样式 | Node list item style -->
|
||||
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式 | Toolbar button style -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="2,0" />
|
||||
<Setter Property="Padding" Value="6,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- Row 0: 工具栏 | Toolbar -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Row 1: 主内容区(左侧节点列表 + 右侧参数面板)| Main content (left: node list, right: parameter panel) -->
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ 工具栏:节点插入命令 + 文件操作命令 | Toolbar: node insert commands + file operation commands ═══ -->
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="6,4"
|
||||
Background="#F5F5F5"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<!-- 文件操作按钮 | File operation buttons -->
|
||||
<Button
|
||||
Command="{Binding NewProgramCommand}"
|
||||
Content="新建"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="新建程序 | New Program" />
|
||||
<Button
|
||||
Command="{Binding SaveProgramCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存程序 | Save Program" />
|
||||
<Button
|
||||
Command="{Binding LoadProgramCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载程序 | Load Program" />
|
||||
<Button
|
||||
Command="{Binding ExportCsvCommand}"
|
||||
Content="导出CSV"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="导出 CSV | Export CSV" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Width="1"
|
||||
Height="20"
|
||||
Margin="4,0"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 节点插入按钮(9 种节点类型)| Node insert buttons (9 node types) -->
|
||||
<Button
|
||||
Command="{Binding InsertReferencePointCommand}"
|
||||
Content="参考点"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入参考点 | Insert Reference Point" />
|
||||
<Button
|
||||
Command="{Binding InsertSaveNodeWithImageCommand}"
|
||||
Content="保存+图"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存节点并保存图片 | Save Node With Image" />
|
||||
<Button
|
||||
Command="{Binding InsertSaveNodeCommand}"
|
||||
Content="保存节点"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="仅保存节点 | Save Node" />
|
||||
<Button
|
||||
Command="{Binding InsertSavePositionCommand}"
|
||||
Content="保存位置"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存位置 | Save Position" />
|
||||
<Button
|
||||
Command="{Binding InsertInspectionModuleCommand}"
|
||||
Content="检测模块"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入检测模块 | Insert Inspection Module" />
|
||||
<Button
|
||||
Command="{Binding InsertInspectionMarkerCommand}"
|
||||
Content="检测标记"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入检测标记 | Insert Inspection Marker" />
|
||||
<Button
|
||||
Command="{Binding InsertPauseDialogCommand}"
|
||||
Content="停顿"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="插入停顿对话框 | Insert Pause Dialog" />
|
||||
<Button
|
||||
Command="{Binding InsertWaitDelayCommand}"
|
||||
Content="延时"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="设置等待延时 | Insert Wait Delay" />
|
||||
<Button
|
||||
Command="{Binding InsertCompleteProgramCommand}"
|
||||
Content="完成"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="完成程序 | Complete Program" />
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 主内容区:左侧节点列表 + 右侧参数面板 | Main content: left node list + right parameter panel ═══ -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧:节点列表 | Left: node list -->
|
||||
<ColumnDefinition Width="3*" MinWidth="200" />
|
||||
<!-- 分隔线 | Splitter -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- 右侧:参数面板 | Right: parameter panel -->
|
||||
<ColumnDefinition Width="2*" MinWidth="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ── 左侧:CNC 节点列表 | Left: CNC node list ── -->
|
||||
<ListBox
|
||||
x:Name="CncNodeListBox"
|
||||
Grid.Column="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{StaticResource CncNodeItemStyle}"
|
||||
ItemsSource="{Binding Nodes}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid x:Name="NodeRoot" MinHeight="40">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 图标列 | Icon column -->
|
||||
<ColumnDefinition Width="40" />
|
||||
<!-- 名称列 | Name column -->
|
||||
<ColumnDefinition Width="*" />
|
||||
<!-- 操作按钮列 | Action buttons column -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 节点图标 | Node icon -->
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<Image
|
||||
Width="20"
|
||||
Height="20"
|
||||
Source="{Binding Icon}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
|
||||
<!-- 节点序号和名称 | Node index and name -->
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Foreground="#888"
|
||||
Text="{Binding Index, StringFormat='[{0}] '}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 悬停操作按钮:上移 / 下移 / 删除 | Hover actions: MoveUp / MoveDown / Delete -->
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="▲"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移 | Move Up" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#cdcbcb"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="▼"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移 | Move Down" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#E05050"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
Content="✕"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除 | Delete" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<DataTemplate.Triggers>
|
||||
<!-- 鼠标悬停时显示操作按钮 | Show action buttons on mouse hover -->
|
||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- 垂直分隔线 | Vertical separator -->
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- ── 右侧:参数面板(根据节点类型动态渲染)| Right: parameter panel (dynamic rendering by node type) ── -->
|
||||
<ScrollViewer
|
||||
Grid.Column="2"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="8,6">
|
||||
<TextBlock
|
||||
Margin="0,0,0,4"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="#555"
|
||||
Text="参数配置 | Parameters" />
|
||||
|
||||
<!-- 动态参数内容区域(占位:根据 SelectedNode 类型渲染)| Dynamic parameter content area (placeholder for node-type-based rendering) -->
|
||||
<ContentControl Content="{Binding SelectedNode}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 编辑器主页面视图(MVVM 模式,逻辑在 ViewModel 中)
|
||||
/// CNC editor main page view (MVVM pattern, logic in ViewModel)
|
||||
/// </summary>
|
||||
public partial class CncPageView : UserControl
|
||||
{
|
||||
public CncPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.Cnc.MatrixEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
|
||||
Title="矩阵编排"
|
||||
Width="1200"
|
||||
Height="750"
|
||||
MinWidth="900"
|
||||
MinHeight="550"
|
||||
ShowInTaskbar="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<cnc:MatrixPageView />
|
||||
</Window>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
public partial class MatrixEditorWindow : Window
|
||||
{
|
||||
public MatrixEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<!-- 矩阵编排页面视图 | Matrix arrangement page view -->
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.Cnc.MatrixPageView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="900"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 面板背景和边框颜色(与 CncPageView 一致)| Panel background and border colors (consistent with CncPageView) -->
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 配置面板标签样式 | Configuration panel label style -->
|
||||
<Style x:Key="ConfigLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
</Style>
|
||||
|
||||
<!-- 配置面板输入框样式 | Configuration panel input style -->
|
||||
<Style x:Key="ConfigInput" TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="Padding" Value="4,2" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="2,4" />
|
||||
<Setter Property="Padding" Value="10,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧:配置面板 | Left: configuration panel -->
|
||||
<ColumnDefinition Width="220" />
|
||||
<!-- 分隔线 | Splitter -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- 右侧:矩阵网格 | Right: matrix grid -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ═══ 左侧:矩阵配置面板 | Left: matrix configuration panel ═══ -->
|
||||
<ScrollViewer
|
||||
Grid.Column="0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="10,8">
|
||||
<!-- 面板标题 | Panel title -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="#333"
|
||||
Text="矩阵配置 | Matrix Config" />
|
||||
|
||||
<!-- 行数 | Rows -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="行数 | Rows" />
|
||||
<TextBox
|
||||
Margin="0,2,0,8"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding Rows, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="矩阵行数 | Number of rows" />
|
||||
|
||||
<!-- 列数 | Columns -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="列数 | Columns" />
|
||||
<TextBox
|
||||
Margin="0,2,0,8"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding Columns, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="矩阵列数 | Number of columns" />
|
||||
|
||||
<!-- 行间距 | Row Spacing -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="行间距 (mm) | Row Spacing" />
|
||||
<TextBox
|
||||
Margin="0,2,0,8"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding RowSpacing, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="行间距(毫米)| Row spacing in mm" />
|
||||
|
||||
<!-- 列间距 | Column Spacing -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="列间距 (mm) | Col Spacing" />
|
||||
<TextBox
|
||||
Margin="0,2,0,8"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding ColumnSpacing, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="列间距(毫米)| Column spacing in mm" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,4,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 操作按钮 | Action buttons -->
|
||||
<Button
|
||||
Command="{Binding UpdateLayoutCommand}"
|
||||
Content="更新布局 | Update"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="根据当前参数更新矩阵网格 | Update matrix grid with current parameters" />
|
||||
<Button
|
||||
Command="{Binding AssociateProgramCommand}"
|
||||
Content="关联程序 | Associate"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="关联 CNC 程序到矩阵 | Associate CNC program to matrix" />
|
||||
<Button
|
||||
Command="{Binding SaveLayoutCommand}"
|
||||
Content="保存方案 | Save"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存矩阵方案 | Save matrix layout" />
|
||||
<Button
|
||||
Command="{Binding LoadLayoutCommand}"
|
||||
Content="加载方案 | Load"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载矩阵方案 | Load matrix layout" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,8,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 选中单元格详情 | Selected cell details -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,6"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="#555"
|
||||
Text="选中单元格 | Selected Cell" />
|
||||
<StackPanel DataContext="{Binding SelectedCell}">
|
||||
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
|
||||
<Run Foreground="#888" Text="位置 | Position: " />
|
||||
<Run Text="{Binding Row, Mode=OneWay, StringFormat='R{0}'}" />
|
||||
<Run Text=", " />
|
||||
<Run Text="{Binding Column, Mode=OneWay, StringFormat='C{0}'}" />
|
||||
</TextBlock>
|
||||
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
|
||||
<Run Foreground="#888" Text="偏移 X | Offset X: " />
|
||||
<Run Text="{Binding OffsetX, Mode=OneWay, StringFormat='{}{0:F2}'}" />
|
||||
</TextBlock>
|
||||
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
|
||||
<Run Foreground="#888" Text="偏移 Y | Offset Y: " />
|
||||
<Run Text="{Binding OffsetY, Mode=OneWay, StringFormat='{}{0:F2}'}" />
|
||||
</TextBlock>
|
||||
<TextBlock FontFamily="{StaticResource CsdFont}" FontSize="11">
|
||||
<Run Foreground="#888" Text="状态 | Enabled: " />
|
||||
<Run Text="{Binding IsEnabled, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 垂直分隔线 | Vertical separator -->
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- ═══ 右侧:矩阵网格视图 | Right: matrix grid view ═══ -->
|
||||
<ScrollViewer
|
||||
Grid.Column="2"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl
|
||||
Margin="8"
|
||||
ItemsSource="{Binding Cells}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<!-- 使用 UniformGrid 按行列排列单元格 | Use UniformGrid to arrange cells by rows and columns -->
|
||||
<UniformGrid Columns="{Binding Columns}" Rows="{Binding Rows}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- 单元格视觉模板 | Cell visual template -->
|
||||
<Border
|
||||
x:Name="CellBorder"
|
||||
MinWidth="80"
|
||||
MinHeight="64"
|
||||
Margin="2"
|
||||
Padding="4"
|
||||
Background="#F0F8FF"
|
||||
BorderBrush="#B0C4DE"
|
||||
BorderThickness="1"
|
||||
CornerRadius="3"
|
||||
Cursor="Hand">
|
||||
<Border.InputBindings>
|
||||
<!-- 左键点击选中单元格 | Left click to select cell -->
|
||||
<MouseBinding
|
||||
Command="{Binding DataContext.SelectCellCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
MouseAction="LeftClick" />
|
||||
</Border.InputBindings>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<!-- 行列索引 | Row/Column index -->
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333">
|
||||
<Run Text="{Binding Row, Mode=OneWay, StringFormat='R{0}'}" />
|
||||
<Run Text=" " />
|
||||
<Run Text="{Binding Column, Mode=OneWay, StringFormat='C{0}'}" />
|
||||
</TextBlock>
|
||||
<!-- 偏移坐标 | Offset coordinates -->
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="9"
|
||||
Foreground="#666">
|
||||
<Run Text="{Binding OffsetX, Mode=OneWay, StringFormat='X:{0:F1}'}" />
|
||||
<Run Text=" " />
|
||||
<Run Text="{Binding OffsetY, Mode=OneWay, StringFormat='Y:{0:F1}'}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<!-- 启用/禁用切换按钮 | Enable/Disable toggle button -->
|
||||
<Button
|
||||
x:Name="ToggleBtn"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,2,0,0"
|
||||
Padding="4,1"
|
||||
Background="Transparent"
|
||||
BorderBrush="#B0C4DE"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.ToggleCellCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="✓"
|
||||
Cursor="Hand"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="9"
|
||||
ToolTip="切换启用/禁用 | Toggle enabled/disabled" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<!-- 禁用状态:灰色背景 | Disabled state: gray background -->
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="#E8E8E8" />
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#CCCCCC" />
|
||||
<Setter TargetName="CellBorder" Property="Opacity" Value="0.6" />
|
||||
</DataTrigger>
|
||||
<!-- 选中状态:蓝色高亮 | Selected state: blue highlight -->
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="#E3F0FF" />
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#5B9BD5" />
|
||||
<Setter TargetName="CellBorder" Property="BorderThickness" Value="2" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编排页面视图(MVVM 模式,逻辑在 ViewModel 中)
|
||||
/// Matrix arrangement page view (MVVM pattern, logic in ViewModel)
|
||||
/// </summary>
|
||||
public partial class MatrixPageView : UserControl
|
||||
{
|
||||
public MatrixPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<!-- 测量统计视图 | Measurement statistics view -->
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.Cnc.MeasurementStatsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
d:DesignHeight="500"
|
||||
d:DesignWidth="600"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- 面板背景和边框颜色(与 CncPageView 一致)| Panel background and border colors (consistent with CncPageView) -->
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 标签样式 | Label style -->
|
||||
<Style x:Key="StatsLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="#888" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
</Style>
|
||||
|
||||
<!-- 统计数值样式 | Statistics value style -->
|
||||
<Style x:Key="StatsValue" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Foreground" Value="#333" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- 筛选输入框样式 | Filter input style -->
|
||||
<Style x:Key="FilterInput" TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="Padding" Value="4,2" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="2,4" />
|
||||
<Setter Property="Padding" Value="10,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="#cdcbcb" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<StackPanel Margin="16,12">
|
||||
<!-- ═══ 标题 | Title ═══ -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,12"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="14"
|
||||
FontWeight="Bold"
|
||||
Foreground="#333"
|
||||
Text="测量统计 | Measurement Statistics" />
|
||||
|
||||
<!-- ═══ 统计数据展示区域 | Statistics display area ═══ -->
|
||||
<Border
|
||||
Margin="0,0,0,12"
|
||||
Padding="12,10"
|
||||
Background="{StaticResource AccentBlue}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<UniformGrid Columns="4" Rows="1">
|
||||
<!-- 总检测数 | Total count -->
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource StatsLabel}" Text="总检测数 | Total" />
|
||||
<TextBlock Style="{StaticResource StatsValue}" Text="{Binding TotalCount}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 合格数 | Pass count -->
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource StatsLabel}" Text="合格数 | Pass" />
|
||||
<TextBlock Foreground="#2E7D32" Style="{StaticResource StatsValue}" Text="{Binding PassCount}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 不合格数 | Fail count -->
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource StatsLabel}" Text="不合格数 | Fail" />
|
||||
<TextBlock Foreground="#C62828" Style="{StaticResource StatsValue}" Text="{Binding FailCount}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 合格率 | Pass rate -->
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource StatsLabel}" Text="合格率 | Pass Rate" />
|
||||
<TextBlock Style="{StaticResource StatsValue}" Text="{Binding PassRate, StringFormat='{}{0:P1}'}" />
|
||||
</StackPanel>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,0,0,12"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- ═══ 筛选控件区域 | Filter controls area ═══ -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="12"
|
||||
FontWeight="Bold"
|
||||
Foreground="#555"
|
||||
Text="筛选条件 | Filters" />
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 配方名称 | Recipe name -->
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource StatsLabel}"
|
||||
Text="配方名称 | Recipe:" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource FilterInput}"
|
||||
Text="{Binding RecipeName, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="按配方名称筛选 | Filter by recipe name" />
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 起始日期 | From date -->
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource StatsLabel}"
|
||||
Text="起始日期 | From:" />
|
||||
<DatePicker
|
||||
Grid.Column="1"
|
||||
Margin="0,0,12,0"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
SelectedDate="{Binding FromDate, Mode=TwoWay}"
|
||||
ToolTip="筛选起始日期 | Filter start date" />
|
||||
|
||||
<!-- 结束日期 | To date -->
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Style="{StaticResource StatsLabel}"
|
||||
Text="结束日期 | To:" />
|
||||
<DatePicker
|
||||
Grid.Column="3"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
SelectedDate="{Binding ToDate, Mode=TwoWay}"
|
||||
ToolTip="筛选结束日期 | Filter end date" />
|
||||
</Grid>
|
||||
|
||||
<!-- 刷新按钮 | Refresh button -->
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Content="刷新 | Refresh"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="刷新统计数据 | Refresh statistics" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// 测量统计视图(MVVM 模式,逻辑在 ViewModel 中)
|
||||
/// Measurement statistics view (MVVM pattern, logic in ViewModel)
|
||||
/// </summary>
|
||||
public partial class MeasurementStatsView : UserControl
|
||||
{
|
||||
public MeasurementStatsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,64 +268,72 @@
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<!-- CNC 编辑器入口按钮 -->
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开 CNC 编辑器窗口,创建和编辑检测配方程序"
|
||||
telerik:ScreenTip.Title="CNC 编辑器"
|
||||
Command="{Binding OpenCncEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/cnc.png"
|
||||
Text="CNC 编辑器" />
|
||||
|
||||
<!-- 矩阵编排入口按钮 -->
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
|
||||
telerik:ScreenTip.Title="矩阵编排"
|
||||
Command="{Binding OpenMatrixEditorCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/matrix.png"
|
||||
Text="矩阵编排" />
|
||||
|
||||
<!-- CNC 节点快捷工具 -->
|
||||
<StackPanel>
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="参考点"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/reference.png"
|
||||
Text="参考点" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="添加位置"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/add-pos.png"
|
||||
Text="添加位置" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="完成"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/finish.png"
|
||||
Text="完成" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="标记"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/mark.png"
|
||||
Text="标记" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="模块"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/Module.png"
|
||||
Text="模块" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="全部保存"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/saveall.png"
|
||||
Text="全部保存" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="消息"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Text="消息" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Title="等待"
|
||||
Command="{Binding Path=SetStyle.Command}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/wait.png"
|
||||
Text="等待" />
|
||||
</StackPanel>
|
||||
<StackPanel />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
2、将telerik 升级到 2024.1.408.310;调整界面和主题;引入 硬件层依赖 √
|
||||
3、图像算子流程文件,保存文件后缀 .imw, image process workflow 缩写 √
|
||||
4、CNC保存文件后缀为.xp, 表示 XplorePlane CNC file 的缩写
|
||||
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式)
|
||||
5、硬件层射线源控件的初步集成(采用库层面的自定义控件方式) √
|
||||
PrismBootstrapper 的执行顺序是:RegisterTypes() → ConfigureModuleCatalog() → InitializeModules() → CreateShell()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user