Compare commits
10 Commits
84c1c5f16d
...
6684143dc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 6684143dc9 | |||
| dd62d04124 | |||
| dee9359c5c | |||
| df50000e6a | |||
| 4be032918d | |||
| 741874e85d | |||
| acbed526f6 | |||
| ad1fdb0230 | |||
| 5a11af9ab1 | |||
| 4301f8a5f7 |
@@ -176,6 +176,20 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
control.UpdateAdorner();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许滚轮缩放(默认允许)。
|
||||
/// 实时图像显示过程中可设为 false 以禁用滚轮缩放。
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsWheelZoomEnabledProperty =
|
||||
DependencyProperty.Register(nameof(IsWheelZoomEnabled), typeof(bool), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(true));
|
||||
|
||||
public bool IsWheelZoomEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsWheelZoomEnabledProperty);
|
||||
set => SetValue(IsWheelZoomEnabledProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PanOffsetXProperty =
|
||||
DependencyProperty.Register(nameof(PanOffsetX), typeof(double), typeof(PolygonRoiCanvas),
|
||||
new PropertyMetadata(0.0, OnPanOffsetChanged));
|
||||
@@ -2209,6 +2223,13 @@ namespace XP.ImageProcessing.RoiControl.Controls
|
||||
|
||||
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
// 实时显示过程中禁用滚轮缩放
|
||||
if (!IsWheelZoomEnabled)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double oldZoom = ZoomScale;
|
||||
double newZoom = e.Delta > 0 ? oldZoom * ZoomStep : oldZoom / ZoomStep;
|
||||
newZoom = Math.Max(0.1, Math.Min(10.0, newZoom));
|
||||
|
||||
@@ -2,8 +2,10 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using Xunit;
|
||||
@@ -24,7 +26,11 @@ namespace XplorePlane.Tests.Pipeline
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
||||
_svc = new PipelinePersistenceService(mockImageSvc.Object, _tempDir);
|
||||
var mockPermissionSvc = new Mock<IPermissionService>();
|
||||
mockPermissionSvc.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||||
var mockLogger = new Mock<ILoggerService>();
|
||||
mockLogger.Setup(l => l.ForModule<PipelinePersistenceService>()).Returns(mockLogger.Object);
|
||||
_svc = new PipelinePersistenceService(mockImageSvc.Object, mockPermissionSvc.Object, mockLogger.Object, _tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -206,10 +212,14 @@ namespace XplorePlane.Tests.Pipeline
|
||||
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" });
|
||||
var mockPermissionSvc = new Mock<IPermissionService>();
|
||||
mockPermissionSvc.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||||
var mockLogger = new Mock<ILoggerService>();
|
||||
mockLogger.Setup(l => l.ForModule<PipelinePersistenceService>()).Returns(mockLogger.Object);
|
||||
var mockDataPathSvc = new Mock<IXpDataPathService>();
|
||||
mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir);
|
||||
|
||||
var service = new PipelinePersistenceService(mockImageSvc.Object, mockDataPathSvc.Object);
|
||||
var service = new PipelinePersistenceService(mockImageSvc.Object, mockPermissionSvc.Object, mockLogger.Object, mockDataPathSvc.Object);
|
||||
await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm"));
|
||||
|
||||
var result = await service.LoadAllAsync(null);
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.Tests.Helpers;
|
||||
using XplorePlane.ViewModels;
|
||||
@@ -181,7 +182,11 @@ namespace XplorePlane.Tests.Pipeline
|
||||
try
|
||||
{
|
||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
||||
var svc = new PipelinePersistenceService(mockImageSvc.Object, tempDir);
|
||||
var mockPermissionSvc = new Mock<IPermissionService>();
|
||||
mockPermissionSvc.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||||
var mockLogger = new Mock<ILoggerService>();
|
||||
mockLogger.Setup(l => l.ForModule<PipelinePersistenceService>()).Returns(mockLogger.Object);
|
||||
var svc = new PipelinePersistenceService(mockImageSvc.Object, mockPermissionSvc.Object, mockLogger.Object, tempDir);
|
||||
|
||||
var model = new PipelineModel { Name = $"Pipeline_{nodeCount}" };
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
|
||||
@@ -6,6 +6,7 @@ using XP.Hardware.RaySource.Services;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Permission;
|
||||
using Xunit;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
@@ -24,7 +25,10 @@ namespace XplorePlane.Tests.Services
|
||||
var logger = new Mock<ILoggerService>();
|
||||
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
|
||||
|
||||
var service = new CncProgramService(appState.Object, raySource.Object, logger.Object);
|
||||
var permissionService = new Mock<IPermissionService>();
|
||||
permissionService.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
|
||||
|
||||
var service = new CncProgramService(appState.Object, raySource.Object, logger.Object, permissionService.Object);
|
||||
var program = new CncProgram(
|
||||
Guid.NewGuid(),
|
||||
"Program",
|
||||
|
||||
@@ -18,6 +18,7 @@ using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using Xunit;
|
||||
|
||||
@@ -36,10 +37,14 @@ namespace XplorePlane.Tests.ViewModels
|
||||
var mockAppState = new Mock<IAppStateService>();
|
||||
var mockLogger = new Mock<ILoggerService>();
|
||||
var mockDataPathService = new Mock<IXpDataPathService>();
|
||||
var mockPermissionService = new Mock<IPermissionService>();
|
||||
mockPipelinePersistenceService ??= new Mock<IPipelinePersistenceService>();
|
||||
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
|
||||
mockDataPathService.SetupGet(s => s.PlanPath).Returns(System.IO.Path.GetTempPath());
|
||||
|
||||
// Default: grant all permissions (Admin/SuperAdmin behavior)
|
||||
mockPermissionService.Setup(p => p.HasPermission(It.IsAny<XplorePlane.Models.Permission>())).Returns(true);
|
||||
|
||||
mockExecSvc ??= new Mock<ICncExecutionService>();
|
||||
|
||||
// Setup CreateProgram so ExecuteNewProgram works
|
||||
@@ -95,7 +100,8 @@ namespace XplorePlane.Tests.ViewModels
|
||||
mockLogger.Object,
|
||||
mockExecSvc.Object,
|
||||
mockDataPathService.Object,
|
||||
mockPipelinePersistenceService.Object);
|
||||
mockPipelinePersistenceService.Object,
|
||||
mockPermissionService.Object);
|
||||
|
||||
if (initialProgram != null)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<Application x:Class="XplorePlane.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:XplorePlane.Converters">
|
||||
xmlns:converters="clr-namespace:XplorePlane.Converters"
|
||||
ShutdownMode="OnMainWindowClose">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
|
||||
+28
-6
@@ -42,6 +42,7 @@ using XP.Hardware.RaySource.Services;
|
||||
using XP.ReportEngine;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Services.Camera;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Debug;
|
||||
@@ -56,6 +57,7 @@ using XplorePlane.ViewModels;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
using XplorePlane.ViewModels.ImageProcessing;
|
||||
using XplorePlane.ViewModels.Main;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XplorePlane.Views.Debug;
|
||||
@@ -340,11 +342,17 @@ namespace XplorePlane
|
||||
}
|
||||
|
||||
// 执行授权检查 | Perform license authorization check
|
||||
if (!PerformLicenseCheck())
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
return null;
|
||||
}
|
||||
//if (!PerformLicenseCheck())
|
||||
//{
|
||||
// Application.Current.Shutdown();
|
||||
// return null;
|
||||
//}
|
||||
|
||||
// ── 登录认证:跳过登录对话框,默认以管理员角色登录 ──
|
||||
var permissionService = Container.Resolve<IPermissionService>();
|
||||
permissionService.Authenticate("xpuser"); // 默认登录为管理员 (Admin)
|
||||
|
||||
Log.Information("默认登录为管理员角色,跳过登录对话框");
|
||||
|
||||
var shell = Container.Resolve<MainWindow>();
|
||||
|
||||
@@ -624,6 +632,8 @@ namespace XplorePlane
|
||||
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
|
||||
containerRegistry.RegisterSingleton<IImagePersistenceService, ImagePersistenceService>();
|
||||
containerRegistry.RegisterSingleton<ICncExecutionService, CncExecutionService>();
|
||||
containerRegistry.RegisterSingleton<IMatrixOrchestrationService, MatrixOrchestrationService>();
|
||||
containerRegistry.RegisterSingleton<MatrixSummaryWriter>();
|
||||
|
||||
// ── 主界面实时图像 / 探测器双队列服务(单例)──
|
||||
containerRegistry.RegisterSingleton<IMainViewportService, MainViewportService>();
|
||||
@@ -637,7 +647,7 @@ namespace XplorePlane
|
||||
|
||||
// ── CNC / 矩阵导航视图 ──
|
||||
containerRegistry.RegisterForNavigation<CncPageView>();
|
||||
containerRegistry.RegisterForNavigation<MatrixPageView>();
|
||||
containerRegistry.RegisterForNavigation<MatrixPageView, MatrixEditorViewModel>();
|
||||
containerRegistry.Register<InspectionReportViewerWindow>();
|
||||
|
||||
// ── 导航相机服务(单例)──
|
||||
@@ -668,6 +678,18 @@ namespace XplorePlane
|
||||
// ── 录制服务(单例)──
|
||||
containerRegistry.RegisterSingleton<IViewportRecordingService, ViewportRecordingService>();
|
||||
|
||||
// ── 权限管理服务(单例)──
|
||||
containerRegistry.RegisterSingleton<IPermissionService, PermissionService>();
|
||||
|
||||
// ── 登录对话框 ViewModel(瞬态)──
|
||||
containerRegistry.Register<LoginDialogViewModel>();
|
||||
|
||||
// ── Ribbon 右侧状态区域 ViewModel(单例,跟随 MainViewModel 生命周期)──
|
||||
containerRegistry.RegisterSingleton<RibbonStatusAreaViewModel>();
|
||||
|
||||
// ── 密码管理 ViewModel(瞬态,每次打开对话框创建新实例)──
|
||||
containerRegistry.Register<ViewModels.Setting.PasswordManagementViewModel>();
|
||||
|
||||
Log.Information("依赖注入容器配置完成");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace XplorePlane.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// 布尔值取反转换器,用于将 true 转为 false、false 转为 true
|
||||
/// Inverse boolean converter: converts true to false and false to true
|
||||
/// </summary>
|
||||
[ValueConversion(typeof(bool), typeof(bool))]
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is bool b ? !b : true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is bool b ? !b : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Prism.Events;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色变更事件,通过 Prism EventAggregator 发布。
|
||||
/// 订阅方:所有需要根据角色刷新 UI 状态的 ViewModel。
|
||||
/// </summary>
|
||||
public class RoleChangedEvent : PubSubEvent<RoleChangedPayload>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>角色变更载荷。</summary>
|
||||
/// <param name="OldRole">变更前角色(首次登录时为 null)。</param>
|
||||
/// <param name="NewRole">变更后角色。</param>
|
||||
public record RoleChangedPayload(UserRole? OldRole, UserRole NewRole);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace XplorePlane.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限提示附加属性。当控件因权限不足被禁用时,显示提示信息。
|
||||
/// 用法:helpers:PermissionTooltipHelper.IsPermissionRestricted="True"
|
||||
/// 当 IsEnabled=False 时自动显示 "当前角色无权访问此功能" 的 ToolTip。
|
||||
/// </summary>
|
||||
public static class PermissionTooltipHelper
|
||||
{
|
||||
private const string PermissionDeniedTooltip = "当前角色无权访问此功能";
|
||||
|
||||
#region IsPermissionRestricted Attached Property
|
||||
|
||||
public static readonly DependencyProperty IsPermissionRestrictedProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"IsPermissionRestricted",
|
||||
typeof(bool),
|
||||
typeof(PermissionTooltipHelper),
|
||||
new PropertyMetadata(false, OnIsPermissionRestrictedChanged));
|
||||
|
||||
public static bool GetIsPermissionRestricted(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(IsPermissionRestrictedProperty);
|
||||
}
|
||||
|
||||
public static void SetIsPermissionRestricted(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(IsPermissionRestrictedProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region OriginalTooltip (internal storage)
|
||||
|
||||
private static readonly DependencyProperty OriginalTooltipProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"OriginalTooltip",
|
||||
typeof(object),
|
||||
typeof(PermissionTooltipHelper),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
private static object GetOriginalTooltip(DependencyObject obj)
|
||||
{
|
||||
return obj.GetValue(OriginalTooltipProperty);
|
||||
}
|
||||
|
||||
private static void SetOriginalTooltip(DependencyObject obj, object value)
|
||||
{
|
||||
obj.SetValue(OriginalTooltipProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HasStoredOriginal (internal flag)
|
||||
|
||||
private static readonly DependencyProperty HasStoredOriginalProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"HasStoredOriginal",
|
||||
typeof(bool),
|
||||
typeof(PermissionTooltipHelper),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
private static bool GetHasStoredOriginal(DependencyObject obj)
|
||||
{
|
||||
return (bool)obj.GetValue(HasStoredOriginalProperty);
|
||||
}
|
||||
|
||||
private static void SetHasStoredOriginal(DependencyObject obj, bool value)
|
||||
{
|
||||
obj.SetValue(HasStoredOriginalProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static void OnIsPermissionRestrictedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not FrameworkElement element)
|
||||
return;
|
||||
|
||||
var isRestricted = (bool)e.NewValue;
|
||||
|
||||
if (isRestricted)
|
||||
{
|
||||
// Subscribe to IsEnabledChanged to update tooltip dynamically
|
||||
element.IsEnabledChanged += OnIsEnabledChanged;
|
||||
|
||||
// Enable showing tooltip on disabled controls
|
||||
ToolTipService.SetShowOnDisabled(element, true);
|
||||
|
||||
// Apply tooltip based on current state
|
||||
UpdateTooltip(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unsubscribe
|
||||
element.IsEnabledChanged -= OnIsEnabledChanged;
|
||||
|
||||
// Restore original tooltip
|
||||
RestoreOriginalTooltip(element);
|
||||
|
||||
// Reset ShowOnDisabled
|
||||
ToolTipService.SetShowOnDisabled(element, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
UpdateTooltip(element);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateTooltip(FrameworkElement element)
|
||||
{
|
||||
if (!element.IsEnabled)
|
||||
{
|
||||
// Store original tooltip if not already stored
|
||||
if (!GetHasStoredOriginal(element))
|
||||
{
|
||||
SetOriginalTooltip(element, element.ToolTip);
|
||||
SetHasStoredOriginal(element, true);
|
||||
}
|
||||
|
||||
// Set permission denied tooltip
|
||||
element.ToolTip = PermissionDeniedTooltip;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore original tooltip when enabled
|
||||
RestoreOriginalTooltip(element);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreOriginalTooltip(FrameworkElement element)
|
||||
{
|
||||
if (GetHasStoredOriginal(element))
|
||||
{
|
||||
element.ToolTip = GetOriginalTooltip(element);
|
||||
SetHasStoredOriginal(element, false);
|
||||
SetOriginalTooltip(element, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public record AuthenticationResult(bool Success, UserRole? Role, string ErrorMessage = null);
|
||||
}
|
||||
@@ -16,9 +16,12 @@ namespace XplorePlane.Models
|
||||
private bool _isHighlighted;
|
||||
private string _highlightColor;
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
/// <summary>字段名称(英文标识,用于内部匹配)| Field name (English key for matching)</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>显示名称(中文)| Display name (Chinese for UI)</summary>
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>字段值(格式化字符串)| Field value (formatted string)</summary>
|
||||
public string Value
|
||||
{
|
||||
@@ -56,10 +59,10 @@ namespace XplorePlane.Models
|
||||
/// <summary>时间戳 | Timestamp</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>事件类型(MotionStateChanged, RaySourceStateChanged 等)| Event type</summary>
|
||||
/// <summary>事件类型(MotionStateChanged 等,英文标识)| Event type</summary>
|
||||
public string EventType { get; set; }
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
/// <summary>字段名称(英文标识)| Field name</summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>旧值 | Old value</summary>
|
||||
@@ -68,8 +71,20 @@ namespace XplorePlane.Models
|
||||
/// <summary>新值 | New value</summary>
|
||||
public string NewValue { get; set; }
|
||||
|
||||
/// <summary>状态类别(用于过滤)| State category (for filtering)</summary>
|
||||
/// <summary>状态类别(用于过滤,英文标识)| State category (for filtering)</summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>事件类型中文显示名 | Event type Chinese display name</summary>
|
||||
public string EventTypeDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.EventType(EventType);
|
||||
|
||||
/// <summary>类别中文显示名 | Category Chinese display name</summary>
|
||||
public string CategoryDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.Category(Category);
|
||||
|
||||
/// <summary>字段中文显示名 | Field Chinese display name</summary>
|
||||
public string FieldNameDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.Field(FieldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,10 +130,10 @@ namespace XplorePlane.Models
|
||||
/// </summary>
|
||||
public class StateDifference
|
||||
{
|
||||
/// <summary>状态类别 | State category</summary>
|
||||
/// <summary>状态类别(英文标识)| State category</summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
/// <summary>字段名称(英文标识)| Field name</summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>快照1的值 | Value from snapshot 1</summary>
|
||||
@@ -126,6 +141,14 @@ namespace XplorePlane.Models
|
||||
|
||||
/// <summary>快照2的值 | Value from snapshot 2</summary>
|
||||
public string Value2 { get; set; }
|
||||
|
||||
/// <summary>类别中文显示名 | Category Chinese display name</summary>
|
||||
public string CategoryDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.Category(Category);
|
||||
|
||||
/// <summary>字段中文显示名 | Field Chinese display name</summary>
|
||||
public string FieldNameDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.Field(FieldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -137,9 +160,13 @@ namespace XplorePlane.Models
|
||||
private double _eventsPerSecond;
|
||||
private double _averageLatency;
|
||||
|
||||
/// <summary>状态类型 | State type</summary>
|
||||
/// <summary>状态类型(英文标识)| State type</summary>
|
||||
public string StateType { get; set; }
|
||||
|
||||
/// <summary>状态类型中文显示名 | State type Chinese display name</summary>
|
||||
public string StateTypeDisplay =>
|
||||
XplorePlane.ViewModels.Debug.DebugPanelLocalization.Category(StateType);
|
||||
|
||||
/// <summary>事件频率(事件/秒)| Events per second</summary>
|
||||
public double EventsPerSecond
|
||||
{
|
||||
|
||||
@@ -42,4 +42,39 @@ namespace XplorePlane.Models
|
||||
string CncProgramPath,
|
||||
IReadOnlyList<MatrixCell> Cells
|
||||
);
|
||||
|
||||
// ── 矩阵执行摘要模型(用于 matrix_summary.json 序列化)────────────
|
||||
|
||||
/// <summary>矩阵执行摘要文件根对象 | Matrix execution summary file root object</summary>
|
||||
public class MatrixSummaryFile
|
||||
{
|
||||
public MatrixSummaryConfig Config { get; set; }
|
||||
public string ProgramName { get; set; }
|
||||
public string StartTime { get; set; } // ISO 8601
|
||||
public double DurationSeconds { get; set; }
|
||||
public int TotalCells { get; set; }
|
||||
public int EnabledCells { get; set; }
|
||||
public int CompletedCells { get; set; }
|
||||
public int FailedCells { get; set; }
|
||||
public List<MatrixCellSummaryEntry> Cells { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>矩阵配置信息 | Matrix configuration</summary>
|
||||
public class MatrixSummaryConfig
|
||||
{
|
||||
public int Rows { get; set; }
|
||||
public int Columns { get; set; }
|
||||
public double RowSpacing { get; set; }
|
||||
public double ColumnSpacing { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>矩阵单元格摘要条目 | Matrix cell summary entry</summary>
|
||||
public class MatrixCellSummaryEntry
|
||||
{
|
||||
public string RunId { get; set; }
|
||||
public int Row { get; set; }
|
||||
public int Column { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool? Pass { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public record PasswordChangeResult(bool Success, string ErrorMessage = null);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public class PasswordStorageModel
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public Dictionary<string, string> Passwords { get; set; } = new();
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
/// <summary>系统权限标识,每个可控功能对应一个枚举值。</summary>
|
||||
public enum Permission
|
||||
{
|
||||
// ── 厂家级设置 ──
|
||||
AccessPlcSettings,
|
||||
AccessHardwareSettings,
|
||||
AccessMotionControlSettings,
|
||||
AccessDetectorSettings,
|
||||
AccessRaySourceSettings,
|
||||
|
||||
// ── 报告设定 ──
|
||||
AccessReportSettings,
|
||||
|
||||
// ── CNC 程序编辑 ──
|
||||
CncInsertNode,
|
||||
CncDeleteNode,
|
||||
CncRenameNode,
|
||||
CncReorderNode,
|
||||
CncNewProgram,
|
||||
CncSaveProgram,
|
||||
CncDeleteProgramFile,
|
||||
|
||||
// ── 检测模块编辑 ──
|
||||
InspectionAddOperator,
|
||||
InspectionRemoveOperator,
|
||||
InspectionReorderOperator,
|
||||
InspectionToggleOperator,
|
||||
InspectionEditParameters,
|
||||
|
||||
// ── CNC 程序查看与运行(所有角色)──
|
||||
CncViewProgram,
|
||||
CncRunProgram,
|
||||
CncStopProgram,
|
||||
ViewInspectionResults,
|
||||
|
||||
// ── 密码管理 ──
|
||||
ManagePasswords,
|
||||
|
||||
// ── 用户切换 ──
|
||||
SwitchUser
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色变更事件参数(供不使用 EventAggregator 的组件直接订阅)。
|
||||
/// </summary>
|
||||
public class RoleChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>变更前角色(首次登录时为 null)。</summary>
|
||||
public UserRole? OldRole { get; }
|
||||
|
||||
/// <summary>变更后角色。</summary>
|
||||
public UserRole NewRole { get; }
|
||||
|
||||
public RoleChangedEventArgs(UserRole? oldRole, UserRole newRole)
|
||||
{
|
||||
OldRole = oldRole;
|
||||
NewRole = newRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
/// <summary>系统角色定义,层级从高到低。</summary>
|
||||
public enum UserRole
|
||||
{
|
||||
SuperAdmin = 0, // 超级管理员
|
||||
Admin = 1, // 管理员
|
||||
User = 2 // 普通用户
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Services;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Permission;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.Services.Cnc
|
||||
{
|
||||
@@ -23,6 +25,7 @@ namespace XplorePlane.Services.Cnc
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly IRaySourceService _raySourceService;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
// ── 序列化配置 | Serialization options ──
|
||||
private static readonly JsonSerializerOptions CncJsonOptions = new()
|
||||
@@ -35,15 +38,18 @@ namespace XplorePlane.Services.Cnc
|
||||
public CncProgramService(
|
||||
IAppStateService appStateService,
|
||||
IRaySourceService raySourceService,
|
||||
ILoggerService logger)
|
||||
ILoggerService logger,
|
||||
IPermissionService permissionService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appStateService);
|
||||
ArgumentNullException.ThrowIfNull(raySourceService);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(permissionService);
|
||||
|
||||
_appStateService = appStateService;
|
||||
_raySourceService = raySourceService;
|
||||
_logger = logger.ForModule<CncProgramService>();
|
||||
_permissionService = permissionService;
|
||||
|
||||
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
||||
}
|
||||
@@ -51,6 +57,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public CncProgram CreateProgram(string name)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncNewProgram))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 CreateProgram,当前角色无权限 | Permission denied: CreateProgram, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
|
||||
|
||||
@@ -122,6 +135,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncInsertNode))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 InsertNode,当前角色无权限 | Permission denied: InsertNode, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return program;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
@@ -156,6 +176,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public CncProgram RemoveNode(CncProgram program, int index)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncDeleteNode))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 RemoveNode,当前角色无权限 | Permission denied: RemoveNode, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return program;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
if (index < 0 || index >= program.Nodes.Count)
|
||||
@@ -179,6 +206,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncReorderNode))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 MoveNode,当前角色无权限 | Permission denied: MoveNode, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return program;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
|
||||
@@ -208,6 +242,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncRenameNode))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 UpdateNode,当前角色无权限 | Permission denied: UpdateNode, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return program;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
@@ -234,6 +275,13 @@ namespace XplorePlane.Services.Cnc
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(CncProgram program, string filePath)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.CncSaveProgram))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 SaveAsync,当前角色无权限 | Permission denied: SaveAsync, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编排执行服务接口。
|
||||
/// 按行优先顺序展开矩阵并执行每个启用单元格。
|
||||
/// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
|
||||
/// </summary>
|
||||
public interface IMatrixOrchestrationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 按行优先顺序展开矩阵并执行每个启用单元格。
|
||||
/// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
|
||||
/// </summary>
|
||||
Task ExecuteAsync(
|
||||
MatrixLayout layout,
|
||||
CncProgram templateProgram,
|
||||
IProgress<MatrixCellExecutionProgress> progress,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace XplorePlane.Services.Matrix
|
||||
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);
|
||||
MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program, string filePath = null);
|
||||
|
||||
/// <summary>将矩阵布局保存到 JSON 文件 | Save matrix layout to JSON file</summary>
|
||||
Task SaveAsync(MatrixLayout layout, string filePath);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵单元格执行进度报告。
|
||||
/// </summary>
|
||||
public record MatrixCellExecutionProgress(
|
||||
int Row,
|
||||
int Column,
|
||||
MatrixCellStatus Status,
|
||||
int CurrentIndex, // 当前启用单元格序号(从 1 开始)
|
||||
int TotalEnabled, // 总启用单元格数
|
||||
string ErrorMessage = null
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.InspectionResults;
|
||||
using XplorePlane.Services.Storage;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵编排执行服务实现。
|
||||
/// 按行优先顺序展开矩阵并执行每个启用单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
|
||||
/// Matrix orchestration execution service implementation.
|
||||
/// Expands matrix in row-major order and executes each enabled cell: deep clone template → apply offset → call ICncExecutionService.
|
||||
/// </summary>
|
||||
public class MatrixOrchestrationService : IMatrixOrchestrationService
|
||||
{
|
||||
private readonly ICncExecutionService _cncExecutionService;
|
||||
private readonly IInspectionResultStore _inspectionResultStore;
|
||||
private readonly MatrixSummaryWriter _matrixSummaryWriter;
|
||||
private readonly IXpDataPathService _dataPathService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public MatrixOrchestrationService(
|
||||
ICncExecutionService cncExecutionService,
|
||||
IInspectionResultStore inspectionResultStore,
|
||||
MatrixSummaryWriter matrixSummaryWriter,
|
||||
IXpDataPathService dataPathService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cncExecutionService);
|
||||
ArgumentNullException.ThrowIfNull(inspectionResultStore);
|
||||
ArgumentNullException.ThrowIfNull(matrixSummaryWriter);
|
||||
ArgumentNullException.ThrowIfNull(dataPathService);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_cncExecutionService = cncExecutionService;
|
||||
_inspectionResultStore = inspectionResultStore;
|
||||
_matrixSummaryWriter = matrixSummaryWriter;
|
||||
_dataPathService = dataPathService;
|
||||
_logger = logger.ForModule<MatrixOrchestrationService>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(
|
||||
MatrixLayout layout,
|
||||
CncProgram templateProgram,
|
||||
IProgress<MatrixCellExecutionProgress> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 筛选启用的单元格,按行优先(Row 升序,Column 升序)排列
|
||||
var enabledCells = layout.Cells
|
||||
.Where(c => c.IsEnabled)
|
||||
.OrderBy(c => c.Row)
|
||||
.ThenBy(c => c.Column)
|
||||
.ToList();
|
||||
|
||||
if (enabledCells.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("没有已启用的位置,请至少启用一个单元格");
|
||||
}
|
||||
|
||||
var totalEnabled = enabledCells.Count;
|
||||
var startTime = DateTime.UtcNow;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
int completedCount = 0;
|
||||
int failedCount = 0;
|
||||
var cellEntries = new List<MatrixCellSummaryEntry>();
|
||||
|
||||
_logger.Info(
|
||||
"矩阵执行开始 | Matrix execution started: Program={ProgramName}, EnabledCells={EnabledCount}, TotalCells={TotalCells}",
|
||||
templateProgram.Name, totalEnabled, layout.Rows * layout.Columns);
|
||||
|
||||
for (int i = 0; i < enabledCells.Count; i++)
|
||||
{
|
||||
var cell = enabledCells[i];
|
||||
var currentIndex = i + 1;
|
||||
|
||||
// 检查取消请求
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// 报告 Executing 状态
|
||||
progress?.Report(new MatrixCellExecutionProgress(
|
||||
cell.Row,
|
||||
cell.Column,
|
||||
MatrixCellStatus.Executing,
|
||||
currentIndex,
|
||||
totalEnabled));
|
||||
|
||||
// 生成单元格专用程序
|
||||
var cellProgram = ApplyOffset(templateProgram, cell);
|
||||
|
||||
try
|
||||
{
|
||||
// 调用 CNC 执行服务(内部已处理 BeginRunAsync 归档)
|
||||
await _cncExecutionService.ExecuteAsync(
|
||||
cellProgram,
|
||||
new Progress<CncNodeExecutionProgress>(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 成功完成
|
||||
completedCount++;
|
||||
progress?.Report(new MatrixCellExecutionProgress(
|
||||
cell.Row,
|
||||
cell.Column,
|
||||
MatrixCellStatus.Completed,
|
||||
currentIndex,
|
||||
totalEnabled));
|
||||
|
||||
cellEntries.Add(new MatrixCellSummaryEntry
|
||||
{
|
||||
RunId = cellProgram.Id.ToString(),
|
||||
Row = cell.Row,
|
||||
Column = cell.Column,
|
||||
Status = nameof(MatrixCellStatus.Completed),
|
||||
Pass = true
|
||||
});
|
||||
|
||||
_logger.Info(
|
||||
"单元格执行完成 | Cell completed: R{Row}C{Col} ({Current}/{Total})",
|
||||
cell.Row, cell.Column, currentIndex, totalEnabled);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 探测器断连或用户取消 — 标记当前单元格为 Error,传播异常退出循环
|
||||
failedCount++;
|
||||
progress?.Report(new MatrixCellExecutionProgress(
|
||||
cell.Row,
|
||||
cell.Column,
|
||||
MatrixCellStatus.Error,
|
||||
currentIndex,
|
||||
totalEnabled,
|
||||
"操作已取消(探测器断连或用户停止)"));
|
||||
|
||||
cellEntries.Add(new MatrixCellSummaryEntry
|
||||
{
|
||||
RunId = cellProgram.Id.ToString(),
|
||||
Row = cell.Row,
|
||||
Column = cell.Column,
|
||||
Status = nameof(MatrixCellStatus.Error),
|
||||
Pass = false
|
||||
});
|
||||
|
||||
_logger.Info(
|
||||
"矩阵执行被取消 | Matrix execution cancelled at R{Row}C{Col} ({Current}/{Total})",
|
||||
cell.Row, cell.Column, currentIndex, totalEnabled);
|
||||
|
||||
// 写入摘要(即使被取消也尝试写入已有结果)
|
||||
stopwatch.Stop();
|
||||
await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false);
|
||||
LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed);
|
||||
|
||||
throw; // 传播 OperationCanceledException
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 普通异常 — 标记 Error,继续下一个单元格
|
||||
failedCount++;
|
||||
var errorMessage = ex.Message;
|
||||
|
||||
progress?.Report(new MatrixCellExecutionProgress(
|
||||
cell.Row,
|
||||
cell.Column,
|
||||
MatrixCellStatus.Error,
|
||||
currentIndex,
|
||||
totalEnabled,
|
||||
errorMessage));
|
||||
|
||||
cellEntries.Add(new MatrixCellSummaryEntry
|
||||
{
|
||||
RunId = cellProgram.Id.ToString(),
|
||||
Row = cell.Row,
|
||||
Column = cell.Column,
|
||||
Status = nameof(MatrixCellStatus.Error),
|
||||
Pass = false
|
||||
});
|
||||
|
||||
_logger.Error(ex,
|
||||
"单元格执行失败 | Cell execution failed: R{Row}C{Col} ({Current}/{Total})",
|
||||
cell.Row, cell.Column, currentIndex, totalEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
// 全部完成后写入摘要
|
||||
stopwatch.Stop();
|
||||
await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false);
|
||||
LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入矩阵执行摘要文件。
|
||||
/// </summary>
|
||||
private async Task WriteSummaryAsync(
|
||||
MatrixLayout layout,
|
||||
string templateProgramName,
|
||||
DateTime startTime,
|
||||
IReadOnlyList<MatrixCellSummaryEntry> cellEntries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var outputDirectory = System.IO.Path.Combine(_dataPathService.DataPath, "MatrixResults");
|
||||
await _matrixSummaryWriter.WriteAsync(
|
||||
layout,
|
||||
templateProgramName,
|
||||
startTime,
|
||||
cellEntries,
|
||||
outputDirectory,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录汇总日志。
|
||||
/// </summary>
|
||||
private void LogSummary(string programName, int totalCells, int completed, int failed, TimeSpan duration)
|
||||
{
|
||||
_logger.Info(
|
||||
"矩阵执行汇总 | Matrix execution summary: Program={ProgramName}, TotalCells={TotalCells}, Completed={Completed}, Failed={Failed}, Duration={Duration:F1}s",
|
||||
programName, totalCells, completed, failed, duration.TotalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将模板程序按单元格偏移量生成该单元格专用的 CNC 程序副本。
|
||||
/// 使用 record with 表达式深度克隆,对每个 SavePositionNode 叠加偏移量(μm),
|
||||
/// 生成新 Id 和新程序名,原模板对象不被修改。
|
||||
///
|
||||
/// Generates a cell-specific CNC program copy by applying cell offset to the template.
|
||||
/// Uses record with expression for deep clone, adds offset (μm) to each SavePositionNode,
|
||||
/// generates new Id and program name. Original template is NOT modified.
|
||||
/// </summary>
|
||||
internal static CncProgram ApplyOffset(CncProgram template, MatrixCell cell)
|
||||
{
|
||||
var offsetXum = cell.OffsetX * 1000.0;
|
||||
var offsetYum = cell.OffsetY * 1000.0;
|
||||
|
||||
var newNodes = template.Nodes
|
||||
.Select(node => node is SavePositionNode sp
|
||||
? sp with
|
||||
{
|
||||
MotionState = sp.MotionState with
|
||||
{
|
||||
StageX = sp.MotionState.StageX + offsetXum,
|
||||
StageY = sp.MotionState.StageY + offsetYum
|
||||
}
|
||||
}
|
||||
: node)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
return template with
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = $"{template.Name}_R{cell.Row}C{cell.Column}",
|
||||
Nodes = newNodes
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
@@ -24,6 +25,7 @@ namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
@@ -40,6 +42,8 @@ namespace XplorePlane.Services.Matrix
|
||||
public MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing)
|
||||
{
|
||||
ValidateDimensions(rows, columns);
|
||||
ValidateSpacing(rowSpacing);
|
||||
ValidateSpacing(columnSpacing);
|
||||
|
||||
var layout = new MatrixLayout(
|
||||
Id: Guid.NewGuid(),
|
||||
@@ -62,6 +66,8 @@ namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ValidateDimensions(rows, columns);
|
||||
ValidateSpacing(rowSpacing);
|
||||
ValidateSpacing(columnSpacing);
|
||||
|
||||
var updated = layout with
|
||||
{
|
||||
@@ -117,15 +123,17 @@ namespace XplorePlane.Services.Matrix
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program)
|
||||
public MatrixLayout AssociateCncProgram(MatrixLayout layout, CncProgram program, string filePath = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
var updated = layout with { CncProgramPath = program.Name };
|
||||
// 优先使用传入的文件路径(绝对路径),否则回退到程序名
|
||||
var programPath = !string.IsNullOrWhiteSpace(filePath) ? filePath : program.Name;
|
||||
var updated = layout with { CncProgramPath = programPath };
|
||||
|
||||
_logger.Info("已关联 CNC 程序到矩阵布局 | Associated CNC program with matrix layout: LayoutId={LayoutId}, Program={ProgramName}",
|
||||
layout.Id, program.Name);
|
||||
_logger.Info("已关联 CNC 程序到矩阵布局 | Associated CNC program with matrix layout: LayoutId={LayoutId}, Program={ProgramPath}",
|
||||
layout.Id, programPath);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -249,8 +257,24 @@ namespace XplorePlane.Services.Matrix
|
||||
return cells.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证间距参数 | Validate spacing parameter
|
||||
/// Spacing must be in [0.0, 1000.0].
|
||||
/// </summary>
|
||||
private static void ValidateSpacing(double spacing)
|
||||
{
|
||||
if (spacing < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(spacing),
|
||||
$"间距不能为负数 | Spacing must not be negative: {spacing}");
|
||||
|
||||
if (spacing > 1000.0)
|
||||
throw new ArgumentOutOfRangeException(nameof(spacing),
|
||||
$"间距不能超过 1000mm | Spacing must not exceed 1000mm: {spacing}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证行列数参数 | Validate row and column dimensions
|
||||
/// Rows and Columns must be in [1, 50].
|
||||
/// </summary>
|
||||
private static void ValidateDimensions(int rows, int columns)
|
||||
{
|
||||
@@ -258,9 +282,17 @@ namespace XplorePlane.Services.Matrix
|
||||
throw new ArgumentOutOfRangeException(nameof(rows),
|
||||
$"行数必须大于 0 | Rows must be greater than 0: {rows}");
|
||||
|
||||
if (rows > 50)
|
||||
throw new ArgumentOutOfRangeException(nameof(rows),
|
||||
$"行数不能超过 50 | Rows must not exceed 50: {rows}");
|
||||
|
||||
if (columns <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(columns),
|
||||
$"列数必须大于 0 | Columns must be greater than 0: {columns}");
|
||||
|
||||
if (columns > 50)
|
||||
throw new ArgumentOutOfRangeException(nameof(columns),
|
||||
$"列数不能超过 50 | Columns must not exceed 50: {columns}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Matrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 矩阵执行摘要写入器,负责将矩阵执行结果序列化为 matrix_summary.json 文件。
|
||||
/// Matrix execution summary writer, responsible for serializing matrix execution results to matrix_summary.json.
|
||||
/// </summary>
|
||||
public class MatrixSummaryWriter
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions SummaryJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public MatrixSummaryWriter(ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger.ForModule<MatrixSummaryWriter>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将矩阵执行摘要写入 JSON 文件。写入失败时记录错误日志,不重新抛出异常。
|
||||
/// Write matrix execution summary to JSON file. Logs error on failure without rethrowing.
|
||||
/// </summary>
|
||||
/// <param name="layout">矩阵布局 | Matrix layout</param>
|
||||
/// <param name="templateProgramName">模板程序名称 | Template program name</param>
|
||||
/// <param name="startTime">执行开始时间 | Execution start time</param>
|
||||
/// <param name="cellEntries">各单元格摘要条目 | Cell summary entries</param>
|
||||
/// <param name="outputDirectory">输出目录 | Output directory</param>
|
||||
/// <param name="ct">取消令牌 | Cancellation token</param>
|
||||
public async Task WriteAsync(
|
||||
MatrixLayout layout,
|
||||
string templateProgramName,
|
||||
DateTime startTime,
|
||||
IReadOnlyList<MatrixCellSummaryEntry> cellEntries,
|
||||
string outputDirectory,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var duration = DateTime.UtcNow - startTime;
|
||||
|
||||
var summaryFile = new MatrixSummaryFile
|
||||
{
|
||||
Config = new MatrixSummaryConfig
|
||||
{
|
||||
Rows = layout.Rows,
|
||||
Columns = layout.Columns,
|
||||
RowSpacing = layout.RowSpacing,
|
||||
ColumnSpacing = layout.ColumnSpacing
|
||||
},
|
||||
ProgramName = templateProgramName,
|
||||
StartTime = startTime.ToString("o"),
|
||||
DurationSeconds = Math.Round(duration.TotalSeconds, 2),
|
||||
TotalCells = layout.Rows * layout.Columns,
|
||||
EnabledCells = layout.Cells.Count(c => c.IsEnabled),
|
||||
CompletedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Completed)),
|
||||
FailedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Error)),
|
||||
Cells = cellEntries.ToList()
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(summaryFile, SummaryJsonOptions);
|
||||
var filePath = Path.Combine(outputDirectory, "matrix_summary.json");
|
||||
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
await File.WriteAllTextAsync(filePath, json, ct).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("已写入矩阵执行摘要 | Matrix summary written: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "写入矩阵执行摘要失败 | Failed to write matrix summary to {OutputDirectory}", outputDirectory);
|
||||
// 不重新抛出,确保不影响已完成的单元格检测结果归档
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace XplorePlane.Services.Permission
|
||||
{
|
||||
/// <summary>
|
||||
/// 工厂默认密码(编译时常量)。
|
||||
/// 当密码文件缺失或损坏时,系统回退到这些默认值。
|
||||
/// </summary>
|
||||
internal static class DefaultPasswords
|
||||
{
|
||||
public const string SuperAdmin = "xpadmin";
|
||||
public const string Admin = "xpuser";
|
||||
public const string User = "1234";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using XplorePlane.Models;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.Services.Permission
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限管理服务接口。单例注册,管理用户认证、角色状态和权限校验。
|
||||
/// </summary>
|
||||
public interface IPermissionService
|
||||
{
|
||||
/// <summary>当前登录角色。未认证时为 null。</summary>
|
||||
UserRole? CurrentRole { get; }
|
||||
|
||||
/// <summary>角色变更事件(供不使用 EventAggregator 的组件直接订阅)。</summary>
|
||||
event EventHandler<RoleChangedEventArgs> RoleChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 验证密码并设置当前角色。
|
||||
/// </summary>
|
||||
/// <param name="password">用户输入的密码。</param>
|
||||
/// <returns>认证结果,包含是否成功和匹配的角色。</returns>
|
||||
AuthenticationResult Authenticate(string password);
|
||||
|
||||
/// <summary>
|
||||
/// 检查当前角色是否拥有指定权限。
|
||||
/// </summary>
|
||||
bool HasPermission(PermissionEnum permission);
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定角色是否拥有指定权限(用于测试和预判)。
|
||||
/// </summary>
|
||||
bool HasPermission(UserRole role, PermissionEnum permission);
|
||||
|
||||
/// <summary>
|
||||
/// 修改指定角色的密码。仅 Super_Admin 可调用。
|
||||
/// </summary>
|
||||
PasswordChangeResult ChangePassword(UserRole targetRole, string newPassword);
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否允许切换用户(CNC 执行中不允许)。
|
||||
/// </summary>
|
||||
bool CanSwitchUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 注销当前用户(切换用户前调用)。
|
||||
/// </summary>
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using XplorePlane.Models;
|
||||
|
||||
namespace XplorePlane.Services.Permission
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色-权限静态映射表。层级关系:SuperAdmin ⊇ Admin ⊇ User。
|
||||
/// </summary>
|
||||
internal static class PermissionMatrix
|
||||
{
|
||||
/// <summary>
|
||||
/// 每个角色独有的权限集合(不含继承)。
|
||||
/// 层级检查通过 HasPermission 方法从当前角色向下累积实现。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<UserRole, HashSet<Models.Permission>> RolePermissions = new()
|
||||
{
|
||||
[UserRole.User] = new HashSet<Models.Permission>
|
||||
{
|
||||
Models.Permission.CncViewProgram,
|
||||
Models.Permission.CncRunProgram,
|
||||
Models.Permission.CncStopProgram,
|
||||
Models.Permission.ViewInspectionResults,
|
||||
Models.Permission.SwitchUser
|
||||
},
|
||||
[UserRole.Admin] = new HashSet<Models.Permission>
|
||||
{
|
||||
// 继承 User 所有权限 +
|
||||
Models.Permission.CncInsertNode,
|
||||
Models.Permission.CncDeleteNode,
|
||||
Models.Permission.CncRenameNode,
|
||||
Models.Permission.CncReorderNode,
|
||||
Models.Permission.CncNewProgram,
|
||||
Models.Permission.CncSaveProgram,
|
||||
Models.Permission.CncDeleteProgramFile,
|
||||
Models.Permission.InspectionAddOperator,
|
||||
Models.Permission.InspectionRemoveOperator,
|
||||
Models.Permission.InspectionReorderOperator,
|
||||
Models.Permission.InspectionToggleOperator,
|
||||
Models.Permission.InspectionEditParameters,
|
||||
Models.Permission.AccessReportSettings
|
||||
},
|
||||
[UserRole.SuperAdmin] = new HashSet<Models.Permission>
|
||||
{
|
||||
// 继承 Admin 所有权限 +
|
||||
Models.Permission.AccessPlcSettings,
|
||||
Models.Permission.AccessHardwareSettings,
|
||||
Models.Permission.AccessMotionControlSettings,
|
||||
Models.Permission.AccessDetectorSettings,
|
||||
Models.Permission.AccessRaySourceSettings,
|
||||
Models.Permission.ManagePasswords
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定角色是否拥有指定权限(含层级继承)。
|
||||
/// 从当前角色向下(User 方向)逐级检查,任一级别包含该权限即返回 true。
|
||||
/// </summary>
|
||||
/// <param name="role">要检查的角色。</param>
|
||||
/// <param name="permission">要检查的权限。</param>
|
||||
/// <returns>如果角色拥有该权限则返回 true,否则返回 false。</returns>
|
||||
public static bool HasPermission(UserRole role, Models.Permission permission)
|
||||
{
|
||||
// 层级检查:从当前角色向下(User)累积权限
|
||||
for (var r = role; r <= UserRole.User; r++)
|
||||
{
|
||||
if (RolePermissions.TryGetValue(r, out var perms) && perms.Contains(permission))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Prism.Events;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.Services.Permission
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限管理服务实现。单例注册,管理用户认证、角色状态和权限校验。
|
||||
/// </summary>
|
||||
public class PermissionService : IPermissionService
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private Dictionary<string, UserRole> _passwordToRoleMap;
|
||||
|
||||
/// <summary>密码文件路径。</summary>
|
||||
private static readonly string PasswordFilePath = Path.Combine(
|
||||
AppDomain.CurrentDomain.BaseDirectory, "passwords.json");
|
||||
|
||||
/// <inheritdoc />
|
||||
public UserRole? CurrentRole { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<RoleChangedEventArgs> RoleChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanSwitchUser
|
||||
{
|
||||
get
|
||||
{
|
||||
var systemState = _appStateService.SystemState;
|
||||
return systemState.OperationMode == OperationMode.Idle;
|
||||
}
|
||||
}
|
||||
|
||||
public PermissionService(
|
||||
IEventAggregator eventAggregator,
|
||||
IAppStateService appStateService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(eventAggregator);
|
||||
ArgumentNullException.ThrowIfNull(appStateService);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_eventAggregator = eventAggregator;
|
||||
_appStateService = appStateService;
|
||||
_logger = logger.ForModule<PermissionService>();
|
||||
|
||||
LoadPasswords();
|
||||
|
||||
_logger.Info("PermissionService 已初始化");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AuthenticationResult Authenticate(string password)
|
||||
{
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
_logger.Warn("认证失败:密码为空");
|
||||
return new AuthenticationResult(false, null, "密码不能为空");
|
||||
}
|
||||
|
||||
if (_passwordToRoleMap.TryGetValue(password, out var role))
|
||||
{
|
||||
var oldRole = CurrentRole;
|
||||
CurrentRole = role;
|
||||
|
||||
_logger.Info("用户以 {Role} 角色登录", role);
|
||||
|
||||
// 发布 Prism EventAggregator 事件
|
||||
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||
.Publish(new RoleChangedPayload(oldRole, role));
|
||||
|
||||
// 发布直接事件
|
||||
RoleChanged?.Invoke(this, new RoleChangedEventArgs(oldRole, role));
|
||||
|
||||
return new AuthenticationResult(true, role);
|
||||
}
|
||||
|
||||
_logger.Warn("认证失败,密码不匹配");
|
||||
return new AuthenticationResult(false, null, "认证失败");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPermission(PermissionEnum permission)
|
||||
{
|
||||
if (CurrentRole == null)
|
||||
{
|
||||
_logger.Warn("权限拒绝: 未认证用户尝试访问 Permission={Permission}", permission);
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = PermissionMatrix.HasPermission(CurrentRole.Value, permission);
|
||||
if (!result)
|
||||
{
|
||||
_logger.Warn("权限拒绝: Role={Role}, Permission={Permission}", CurrentRole.Value, permission);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPermission(UserRole role, PermissionEnum permission)
|
||||
{
|
||||
return PermissionMatrix.HasPermission(role, permission);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PasswordChangeResult ChangePassword(UserRole targetRole, string newPassword)
|
||||
{
|
||||
// 权限校验:仅 SuperAdmin 可修改密码
|
||||
if (CurrentRole != UserRole.SuperAdmin)
|
||||
{
|
||||
_logger.Warn("密码修改被拒绝: 当前角色 {Role} 无权修改密码", CurrentRole);
|
||||
return new PasswordChangeResult(false, "仅超级管理员可修改密码");
|
||||
}
|
||||
|
||||
// 密码长度校验
|
||||
if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 4 || newPassword.Length > 32)
|
||||
{
|
||||
return new PasswordChangeResult(false, "密码长度必须在 4-32 个字符之间");
|
||||
}
|
||||
|
||||
// 更新内存中的密码映射
|
||||
// 先移除旧密码映射
|
||||
string oldPassword = null;
|
||||
foreach (var kvp in _passwordToRoleMap)
|
||||
{
|
||||
if (kvp.Value == targetRole)
|
||||
{
|
||||
oldPassword = kvp.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldPassword != null)
|
||||
{
|
||||
_passwordToRoleMap.Remove(oldPassword);
|
||||
}
|
||||
|
||||
_passwordToRoleMap[newPassword] = targetRole;
|
||||
|
||||
// 持久化到文件
|
||||
if (!SavePasswords())
|
||||
{
|
||||
// 回滚内存状态
|
||||
_passwordToRoleMap.Remove(newPassword);
|
||||
if (oldPassword != null)
|
||||
{
|
||||
_passwordToRoleMap[oldPassword] = targetRole;
|
||||
}
|
||||
return new PasswordChangeResult(false, "密码文件写入失败");
|
||||
}
|
||||
|
||||
_logger.Info("角色 {Role} 密码已更新", targetRole);
|
||||
return new PasswordChangeResult(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Logout()
|
||||
{
|
||||
var oldRole = CurrentRole;
|
||||
CurrentRole = null;
|
||||
|
||||
if (oldRole != null)
|
||||
{
|
||||
_logger.Info("用户已注销,原角色: {Role}", oldRole);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从密码文件加载密码,失败时回退到工厂默认密码。
|
||||
/// </summary>
|
||||
private void LoadPasswords()
|
||||
{
|
||||
_passwordToRoleMap = new Dictionary<string, UserRole>();
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(PasswordFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(PasswordFilePath);
|
||||
var storage = JsonConvert.DeserializeObject<PasswordStorageModel>(json);
|
||||
|
||||
if (storage?.Passwords != null && storage.Passwords.Count > 0)
|
||||
{
|
||||
foreach (var kvp in storage.Passwords)
|
||||
{
|
||||
if (Enum.TryParse<UserRole>(kvp.Key, out var role))
|
||||
{
|
||||
_passwordToRoleMap[kvp.Value] = role;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证三个角色都有密码映射
|
||||
if (_passwordToRoleMap.Count >= 3)
|
||||
{
|
||||
_logger.Info("密码文件加载成功: {Path}", PasswordFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Warn("密码文件不完整,使用默认密码");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("密码文件内容无效,使用默认密码");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("密码文件不存在,使用默认密码: {Path}", PasswordFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("密码文件读取失败,使用默认密码: {Exception}", ex.Message);
|
||||
}
|
||||
|
||||
// 回退到工厂默认密码
|
||||
LoadDefaultPasswords();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载工厂默认密码。
|
||||
/// </summary>
|
||||
private void LoadDefaultPasswords()
|
||||
{
|
||||
_passwordToRoleMap = new Dictionary<string, UserRole>
|
||||
{
|
||||
[DefaultPasswords.SuperAdmin] = UserRole.SuperAdmin,
|
||||
[DefaultPasswords.Admin] = UserRole.Admin,
|
||||
[DefaultPasswords.User] = UserRole.User
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前密码保存到文件。
|
||||
/// </summary>
|
||||
/// <returns>是否保存成功。</returns>
|
||||
private bool SavePasswords()
|
||||
{
|
||||
try
|
||||
{
|
||||
var passwords = new Dictionary<string, string>();
|
||||
foreach (var kvp in _passwordToRoleMap)
|
||||
{
|
||||
passwords[kvp.Value.ToString()] = kvp.Key;
|
||||
}
|
||||
|
||||
var storage = new PasswordStorageModel
|
||||
{
|
||||
Version = 1,
|
||||
Passwords = passwords,
|
||||
LastModified = DateTime.Now
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(storage, Formatting.Indented);
|
||||
File.WriteAllText(PasswordFilePath, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "密码文件写入失败: {Path}", PasswordFilePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,19 @@ 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.Permission;
|
||||
using XplorePlane.Services.Storage;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.Services
|
||||
{
|
||||
public class PipelinePersistenceService : IPipelinePersistenceService
|
||||
{
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly string _baseDirectory;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@@ -23,21 +28,38 @@ namespace XplorePlane.Services
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public PipelinePersistenceService(IImageProcessingService imageProcessingService, string baseDirectory = null)
|
||||
public PipelinePersistenceService(
|
||||
IImageProcessingService imageProcessingService,
|
||||
IPermissionService permissionService,
|
||||
ILoggerService logger,
|
||||
string baseDirectory = null)
|
||||
{
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<PipelinePersistenceService>();
|
||||
_baseDirectory = baseDirectory ?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"XplorePlane", "Pipelines");
|
||||
}
|
||||
|
||||
public PipelinePersistenceService(IImageProcessingService imageProcessingService, IXpDataPathService dataPathService)
|
||||
: this(imageProcessingService, dataPathService?.ToolsPath)
|
||||
public PipelinePersistenceService(
|
||||
IImageProcessingService imageProcessingService,
|
||||
IPermissionService permissionService,
|
||||
ILoggerService logger,
|
||||
IXpDataPathService dataPathService)
|
||||
: this(imageProcessingService, permissionService, logger, dataPathService?.ToolsPath)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PipelineModel pipeline, string filePath)
|
||||
{
|
||||
if (!_permissionService.HasPermission(PermissionEnum.InspectionEditParameters))
|
||||
{
|
||||
_logger.Warn("权限拒绝: 尝试执行 SaveAsync (Pipeline),当前角色无权限 | Permission denied: Pipeline SaveAsync, Role={Role}",
|
||||
_permissionService.CurrentRole);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
|
||||
ValidatePath(filePath);
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Services.Storage;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
@@ -33,6 +35,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private readonly IXpDataPathService _dataPathService;
|
||||
private readonly IPipelinePersistenceService _pipelinePersistenceService;
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
private CncProgram _currentProgram;
|
||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||
@@ -59,6 +62,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
ICncExecutionService cncExecutionService,
|
||||
IXpDataPathService dataPathService,
|
||||
IPipelinePersistenceService pipelinePersistenceService,
|
||||
IPermissionService permissionService,
|
||||
IImageProcessingService imageProcessingService = null)
|
||||
{
|
||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||
@@ -68,6 +72,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
|
||||
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
||||
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
|
||||
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||
_imageProcessingService = imageProcessingService; // optional — used for pipeline step display names
|
||||
|
||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||
@@ -77,15 +82,15 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
|
||||
};
|
||||
|
||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint), () => !IsRunning);
|
||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage), () => !IsRunning);
|
||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode), () => !IsRunning);
|
||||
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition), () => !IsRunning);
|
||||
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule), () => !IsRunning);
|
||||
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker), () => !IsRunning);
|
||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog), () => !IsRunning);
|
||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay), () => !IsRunning);
|
||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram), () => !IsRunning);
|
||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint), () => !IsRunning && CanEditCncProgram);
|
||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage), () => !IsRunning && CanEditCncProgram);
|
||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode), () => !IsRunning && CanEditCncProgram);
|
||||
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition), () => !IsRunning && CanEditCncProgram);
|
||||
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule), () => !IsRunning && CanEditCncProgram);
|
||||
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker), () => !IsRunning && CanEditCncProgram);
|
||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog), () => !IsRunning && CanEditCncProgram);
|
||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay), () => !IsRunning && CanEditCncProgram);
|
||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram), () => !IsRunning && CanEditCncProgram);
|
||||
|
||||
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
||||
.ObservesProperty(() => SelectedNode);
|
||||
@@ -94,14 +99,17 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
|
||||
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
|
||||
|
||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync(), () => CanEditCncProgram);
|
||||
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram, () => CanEditCncProgram);
|
||||
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
||||
|
||||
RunCncCommand = new DelegateCommand(async () => await ExecuteRunAsync(), CanExecuteRun);
|
||||
StopCncCommand = new DelegateCommand(ExecuteStop, CanExecuteStop);
|
||||
|
||||
// Subscribe to role changes to refresh permission-dependent properties
|
||||
_eventAggregator.GetEvent<RoleChangedEvent>().Subscribe(OnRoleChanged);
|
||||
|
||||
_logger.Info("CncEditorViewModel initialized");
|
||||
}
|
||||
|
||||
@@ -191,6 +199,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private set => SetProperty(ref _hasExecutionError, value);
|
||||
}
|
||||
|
||||
/// <summary>当前角色是否允许编辑 CNC 程序(插入、删除、重命名、重排序、新建、保存、删除文件)。</summary>
|
||||
public bool CanEditCncProgram => _permissionService.HasPermission(PermissionEnum.CncInsertNode);
|
||||
|
||||
/// <summary>当前角色是否允许编辑检测模块(添加、删除、重排序、启用/禁用、编辑参数)。</summary>
|
||||
public bool CanEditInspectionModule => _permissionService.HasPermission(PermissionEnum.InspectionAddOperator);
|
||||
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
@@ -274,6 +288,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private bool CanExecuteDeleteNode()
|
||||
{
|
||||
return !IsRunning
|
||||
&& CanEditCncProgram
|
||||
&& SelectedNode != null
|
||||
&& _currentProgram != null
|
||||
&& _currentProgram.Nodes.Count > 1;
|
||||
@@ -281,7 +296,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
||||
{
|
||||
if (IsRunning || _currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
|
||||
if (IsRunning || !CanEditCncProgram || _currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -305,7 +320,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
||||
{
|
||||
if (IsRunning || _currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
||||
if (IsRunning || !CanEditCncProgram || _currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -615,10 +630,19 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
DeleteNodeCommand.RaiseCanExecuteChanged();
|
||||
MoveNodeUpCommand.RaiseCanExecuteChanged();
|
||||
MoveNodeDownCommand.RaiseCanExecuteChanged();
|
||||
SaveProgramCommand.RaiseCanExecuteChanged();
|
||||
NewProgramCommand.RaiseCanExecuteChanged();
|
||||
RunCncCommand.RaiseCanExecuteChanged();
|
||||
StopCncCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnRoleChanged(RoleChangedPayload payload)
|
||||
{
|
||||
RaisePropertyChanged(nameof(CanEditCncProgram));
|
||||
RaisePropertyChanged(nameof(CanEditInspectionModule));
|
||||
RaiseEditCommandsCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnProgramEdited()
|
||||
{
|
||||
IsModified = true;
|
||||
|
||||
@@ -17,8 +17,10 @@ using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.MainViewport;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using Prism.Events;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
@@ -30,6 +32,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private readonly IPipelineExecutionService _executionService;
|
||||
private readonly IMainViewportService _mainViewportService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private CncNodeViewModel _activeModuleNode;
|
||||
@@ -50,7 +53,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
ILoggerService logger,
|
||||
IPipelineExecutionService executionService = null,
|
||||
IMainViewportService mainViewportService = null,
|
||||
IEventAggregator eventAggregator = null)
|
||||
IEventAggregator eventAggregator = null,
|
||||
IPermissionService permissionService = null)
|
||||
{
|
||||
_editorViewModel = editorViewModel ?? throw new ArgumentNullException(nameof(editorViewModel));
|
||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||
@@ -59,6 +63,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
_executionService = executionService;
|
||||
_mainViewportService = mainViewportService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_permissionService = permissionService;
|
||||
|
||||
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
||||
|
||||
@@ -84,6 +89,9 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
_eventAggregator?.GetEvent<AddOperatorRequestedEvent>()
|
||||
.Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); });
|
||||
|
||||
_eventAggregator?.GetEvent<RoleChangedEvent>()
|
||||
.Subscribe(OnRoleChanged, ThreadOption.UIThread);
|
||||
}
|
||||
|
||||
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
|
||||
@@ -150,6 +158,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
public Visibility EmptyStateVisibility => HasActiveModule ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
/// <summary>
|
||||
/// 当前角色是否有权编辑检测模块流水线(添加/删除/排序/启停算子、编辑参数)。
|
||||
/// </summary>
|
||||
public bool CanEditPipeline =>
|
||||
_permissionService?.HasPermission(PermissionEnum.InspectionAddOperator) ?? true;
|
||||
|
||||
public ICommand AddOperatorCommand { get; }
|
||||
|
||||
public ICommand RemoveOperatorCommand { get; }
|
||||
@@ -573,11 +587,38 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
OperatorKey = node.OperatorKey,
|
||||
Order = index,
|
||||
IsEnabled = node.IsEnabled,
|
||||
Parameters = node.Parameters.ToDictionary(parameter => parameter.Name, parameter => parameter.Value)
|
||||
Parameters = BuildNodeParameters(node)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建节点参数字典,过滤掉超出 PolyCount 的多余坐标参数。
|
||||
/// 只保存实际有效的 PolyX/Y 值,避免写入大量无意义的 0。
|
||||
/// </summary>
|
||||
private static Dictionary<string, object> BuildNodeParameters(PipelineNodeViewModel node)
|
||||
{
|
||||
// 获取实际多边形点数
|
||||
var polyCountParam = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount");
|
||||
int polyCount = 0;
|
||||
if (polyCountParam?.Value != null)
|
||||
int.TryParse(polyCountParam.Value.ToString(), out polyCount);
|
||||
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (var param in node.Parameters)
|
||||
{
|
||||
// 过滤超出实际点数的 PolyX/Y 参数
|
||||
if (param.Name.StartsWith("PolyX") || param.Name.StartsWith("PolyY"))
|
||||
{
|
||||
var indexStr = param.Name.StartsWith("PolyX") ? param.Name[5..] : param.Name[5..];
|
||||
if (int.TryParse(indexStr, out int idx) && idx >= polyCount)
|
||||
continue; // 跳过超出实际点数的坐标
|
||||
}
|
||||
result[param.Name] = param.Value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<PipelineNodeViewModel> GetNodesInExecutionScope()
|
||||
{
|
||||
var orderedNodes = PipelineNodes.OrderBy(node => node.Order);
|
||||
@@ -659,6 +700,11 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
(AddOperatorCommand as DelegateCommand<string>)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnRoleChanged(RoleChangedPayload payload)
|
||||
{
|
||||
RaisePropertyChanged(nameof(CanEditPipeline));
|
||||
}
|
||||
|
||||
private static object ConvertSavedValue(object savedValue, Type targetType)
|
||||
{
|
||||
if (savedValue is not JsonElement jsonElement)
|
||||
@@ -875,17 +921,27 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
polyCountParam.Value = count;
|
||||
}
|
||||
|
||||
// 更新坐标(最多 32 个点)
|
||||
for (int i = 0; i < 32; i++)
|
||||
// 更新坐标:只写入实际点数,不写多余的 0 值
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var px = node.Parameters.FirstOrDefault(p => p.Name == $"PolyX{i}");
|
||||
var py = node.Parameters.FirstOrDefault(p => p.Name == $"PolyY{i}");
|
||||
double x = (points != null && i < points.Count) ? points[i].X : 0;
|
||||
double y = (points != null && i < points.Count) ? points[i].Y : 0;
|
||||
double x = points[i].X;
|
||||
double y = points[i].Y;
|
||||
if (px != null) px.Value = (int)x;
|
||||
if (py != null) py.Value = (int)y;
|
||||
}
|
||||
|
||||
// 清除超出实际点数的旧坐标值(防止残留)
|
||||
for (int i = count; i < 32; i++)
|
||||
{
|
||||
var px = node.Parameters.FirstOrDefault(p => p.Name == $"PolyX{i}");
|
||||
var py = node.Parameters.FirstOrDefault(p => p.Name == $"PolyY{i}");
|
||||
if (px == null && py == null) break; // 参数不存在则停止
|
||||
if (px != null) px.Value = 0;
|
||||
if (py != null) py.Value = 0;
|
||||
}
|
||||
|
||||
// 写入后验证
|
||||
var verifyRoiMode = node.Parameters.FirstOrDefault(p => p.Name == "RoiMode")?.Value;
|
||||
var verifyPolyCount = node.Parameters.FirstOrDefault(p => p.Name == "PolyCount")?.Value;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Cnc;
|
||||
@@ -20,12 +24,16 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
private readonly IMatrixService _matrixService;
|
||||
private readonly ICncProgramService _cncProgramService;
|
||||
private readonly IMatrixOrchestrationService _orchestrationService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// 当前矩阵布局 | Current matrix layout
|
||||
private MatrixLayout _currentLayout;
|
||||
|
||||
// 执行取消令牌源 | Execution cancellation token source
|
||||
private CancellationTokenSource _executionCts;
|
||||
|
||||
private int _rows = 1;
|
||||
private int _columns = 1;
|
||||
private double _rowSpacing;
|
||||
@@ -34,29 +42,51 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private MatrixCellViewModel _selectedCell;
|
||||
private string _associatedProgramPath;
|
||||
|
||||
// ── 验证错误属性 | Validation error properties ──
|
||||
private string _rowsError;
|
||||
private string _columnsError;
|
||||
private string _rowSpacingError;
|
||||
private string _colSpacingError;
|
||||
private bool _canUpdateLayout = true;
|
||||
|
||||
// ── 显示与状态属性 | Display and state properties ──
|
||||
private string _associatedProgramName;
|
||||
private int _enabledCount;
|
||||
private int _totalCount;
|
||||
private bool _isExecuting;
|
||||
private string _statusText;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public MatrixEditorViewModel(
|
||||
IMatrixService matrixService,
|
||||
ICncProgramService cncProgramService,
|
||||
IMatrixOrchestrationService orchestrationService,
|
||||
IEventAggregator eventAggregator,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_matrixService = matrixService ?? throw new ArgumentNullException(nameof(matrixService));
|
||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||
_orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
|
||||
_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);
|
||||
UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout, () => CanUpdateLayout)
|
||||
.ObservesProperty(() => CanUpdateLayout);
|
||||
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());
|
||||
RunMatrixCommand = new DelegateCommand(async () => await ExecuteRunMatrixAsync(), CanExecuteRunMatrix)
|
||||
.ObservesProperty(() => IsExecuting)
|
||||
.ObservesProperty(() => AssociatedProgramPath);
|
||||
StopCommand = new DelegateCommand(ExecuteStop, () => IsExecuting)
|
||||
.ObservesProperty(() => IsExecuting);
|
||||
|
||||
_logger.Info("MatrixEditorViewModel 已初始化 | MatrixEditorViewModel initialized");
|
||||
}
|
||||
@@ -109,7 +139,89 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
public string AssociatedProgramPath
|
||||
{
|
||||
get => _associatedProgramPath;
|
||||
set => SetProperty(ref _associatedProgramPath, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _associatedProgramPath, value))
|
||||
{
|
||||
AssociatedProgramName = string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
: Path.GetFileName(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 验证错误属性 | Validation error properties ──────────────────
|
||||
|
||||
/// <summary>行数输入错误提示 | Rows input error message</summary>
|
||||
public string RowsError
|
||||
{
|
||||
get => _rowsError;
|
||||
set => SetProperty(ref _rowsError, value);
|
||||
}
|
||||
|
||||
/// <summary>列数输入错误提示 | Columns input error message</summary>
|
||||
public string ColumnsError
|
||||
{
|
||||
get => _columnsError;
|
||||
set => SetProperty(ref _columnsError, value);
|
||||
}
|
||||
|
||||
/// <summary>行间距输入错误提示 | Row spacing input error message</summary>
|
||||
public string RowSpacingError
|
||||
{
|
||||
get => _rowSpacingError;
|
||||
set => SetProperty(ref _rowSpacingError, value);
|
||||
}
|
||||
|
||||
/// <summary>列间距输入错误提示 | Column spacing input error message</summary>
|
||||
public string ColSpacingError
|
||||
{
|
||||
get => _colSpacingError;
|
||||
set => SetProperty(ref _colSpacingError, value);
|
||||
}
|
||||
|
||||
/// <summary>是否可以更新布局(输入合法时为 true)| Whether layout can be updated (true when inputs are valid)</summary>
|
||||
public bool CanUpdateLayout
|
||||
{
|
||||
get => _canUpdateLayout;
|
||||
set => SetProperty(ref _canUpdateLayout, value);
|
||||
}
|
||||
|
||||
// ── 显示与状态属性 | Display and state properties ────────────────
|
||||
|
||||
/// <summary>关联程序的文件名(不含路径)| Associated program filename (without path)</summary>
|
||||
public string AssociatedProgramName
|
||||
{
|
||||
get => _associatedProgramName;
|
||||
set => SetProperty(ref _associatedProgramName, value);
|
||||
}
|
||||
|
||||
/// <summary>已启用的单元格数量 | Number of enabled cells</summary>
|
||||
public int EnabledCount
|
||||
{
|
||||
get => _enabledCount;
|
||||
set => SetProperty(ref _enabledCount, value);
|
||||
}
|
||||
|
||||
/// <summary>单元格总数 | Total number of cells</summary>
|
||||
public int TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
set => SetProperty(ref _totalCount, value);
|
||||
}
|
||||
|
||||
/// <summary>是否正在执行矩阵 | Whether matrix execution is in progress</summary>
|
||||
public bool IsExecuting
|
||||
{
|
||||
get => _isExecuting;
|
||||
set => SetProperty(ref _isExecuting, value);
|
||||
}
|
||||
|
||||
/// <summary>底部状态栏文本 | Bottom status bar text</summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
// ── 命令 | Commands ────────────────────────────────────────────
|
||||
@@ -132,6 +244,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
/// <summary>加载矩阵布局命令 | Load matrix layout command</summary>
|
||||
public DelegateCommand LoadLayoutCommand { get; }
|
||||
|
||||
/// <summary>运行矩阵执行命令 | Run matrix execution command</summary>
|
||||
public DelegateCommand RunMatrixCommand { get; }
|
||||
|
||||
/// <summary>停止执行命令 | Stop execution command</summary>
|
||||
public DelegateCommand StopCommand { get; }
|
||||
|
||||
// ── 命令执行方法 | Command execution methods ────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -140,6 +258,13 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
/// </summary>
|
||||
private void ExecuteUpdateLayout()
|
||||
{
|
||||
// 清除之前的错误 | Clear previous errors
|
||||
RowsError = null;
|
||||
ColumnsError = null;
|
||||
RowSpacingError = null;
|
||||
ColSpacingError = null;
|
||||
CanUpdateLayout = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_currentLayout == null)
|
||||
@@ -154,12 +279,56 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
RefreshCells();
|
||||
_logger.Info("已更新矩阵布局 | Updated matrix layout: Rows={Rows}, Columns={Columns}", Rows, Columns);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
HandleValidationError(ex);
|
||||
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理参数验证异常,设置对应的错误属性
|
||||
/// Handle parameter validation exception and set corresponding error properties
|
||||
/// </summary>
|
||||
private void HandleValidationError(ArgumentOutOfRangeException ex)
|
||||
{
|
||||
CanUpdateLayout = false;
|
||||
var paramName = ex.ParamName?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
if (paramName.Contains("row") && paramName.Contains("spacing"))
|
||||
{
|
||||
RowSpacingError = "间距不能为负数";
|
||||
}
|
||||
else if (paramName.Contains("col") && paramName.Contains("spacing"))
|
||||
{
|
||||
ColSpacingError = "间距不能为负数";
|
||||
}
|
||||
else if (paramName.Contains("spacing"))
|
||||
{
|
||||
// Generic spacing error — set both
|
||||
RowSpacingError = "间距不能为负数";
|
||||
ColSpacingError = "间距不能为负数";
|
||||
}
|
||||
else if (paramName.Contains("row"))
|
||||
{
|
||||
RowsError = "行数/列数必须在 1 到 50 之间";
|
||||
}
|
||||
else if (paramName.Contains("col"))
|
||||
{
|
||||
ColumnsError = "行数/列数必须在 1 到 50 之间";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: try to determine from message or set both
|
||||
RowsError = "行数/列数必须在 1 到 50 之间";
|
||||
ColumnsError = "行数/列数必须在 1 到 50 之间";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选中指定单元格,更新高亮状态
|
||||
/// Select the specified cell and update highlight state
|
||||
@@ -199,8 +368,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关联 CNC 程序到当前矩阵布局(占位:从 AssociatedProgramPath 加载)
|
||||
/// Associate a CNC program with the current layout (placeholder: loads from AssociatedProgramPath)
|
||||
/// 关联 CNC 程序:使用 OpenFileDialog 选择 .xp 文件,验证文件有效性
|
||||
/// Associate CNC program: use OpenFileDialog to select .xp file, validate file
|
||||
/// </summary>
|
||||
private async Task ExecuteAssociateProgramAsync()
|
||||
{
|
||||
@@ -210,26 +379,50 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AssociatedProgramPath))
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
_logger.Warn("无法关联程序:程序路径为空 | Cannot associate program: program path is empty");
|
||||
Title = "选择 CNC 模板程序",
|
||||
Filter = "CNC 程序文件 (*.xp)|*.xp",
|
||||
CheckFileExists = true
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = dialog.FileName;
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
var program = await _cncProgramService.LoadAsync(AssociatedProgramPath);
|
||||
_currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program);
|
||||
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", AssociatedProgramPath);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var program = await _cncProgramService.LoadAsync(filePath);
|
||||
|
||||
// 验证程序包含至少一个 SavePositionNode
|
||||
if (!program.Nodes.OfType<SavePositionNode>().Any())
|
||||
{
|
||||
MessageBox.Show("所选程序不包含位置节点,无法用作矩阵模板", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program, filePath);
|
||||
AssociatedProgramPath = filePath;
|
||||
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program");
|
||||
MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program: {FileName}", fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前矩阵布局到文件 | Save current matrix layout to file
|
||||
/// 保存当前矩阵布局:使用 SaveFileDialog 选择保存路径
|
||||
/// Save current matrix layout: use SaveFileDialog to select save path
|
||||
/// </summary>
|
||||
private async Task ExecuteSaveLayoutAsync()
|
||||
{
|
||||
@@ -239,12 +432,21 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "保存矩阵方案",
|
||||
Filter = "JSON 文件 (*.json)|*.json",
|
||||
DefaultExt = ".json",
|
||||
FileName = $"matrix_{_currentLayout.Id}"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
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);
|
||||
await _matrixService.SaveAsync(_currentLayout, dialog.FileName);
|
||||
_logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", dialog.FileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -253,15 +455,24 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载矩阵布局 | Load matrix layout from file
|
||||
/// 从文件加载矩阵布局:使用 OpenFileDialog 选择文件
|
||||
/// Load matrix layout from file: use OpenFileDialog to select file
|
||||
/// </summary>
|
||||
private async Task ExecuteLoadLayoutAsync()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "加载矩阵方案",
|
||||
Filter = "JSON 文件 (*.json)|*.json",
|
||||
CheckFileExists = true
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
|
||||
var filePath = $"matrix_layout.json";
|
||||
_currentLayout = await _matrixService.LoadAsync(filePath);
|
||||
_currentLayout = await _matrixService.LoadAsync(dialog.FileName);
|
||||
|
||||
// 同步属性到 ViewModel | Sync properties to ViewModel
|
||||
Rows = _currentLayout.Rows;
|
||||
@@ -271,14 +482,132 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
AssociatedProgramPath = _currentLayout.CncProgramPath;
|
||||
|
||||
RefreshCells();
|
||||
|
||||
// 检查关联的 CNC 程序文件是否存在 | Check if associated CNC program file exists
|
||||
if (!string.IsNullOrWhiteSpace(_currentLayout.CncProgramPath)
|
||||
&& !File.Exists(_currentLayout.CncProgramPath))
|
||||
{
|
||||
MessageBox.Show("关联的 CNC 程序文件已移动或删除,请重新关联", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
_logger.Info("矩阵布局已加载 | Matrix layout loaded: Rows={Rows}, Columns={Columns}", Rows, Columns);
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
MessageBox.Show($"方案文件格式无效:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
_logger.Error(ex, "加载矩阵布局失败:格式无效 | Failed to load matrix layout: invalid format");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "加载矩阵布局失败 | Failed to load matrix layout");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断 RunMatrixCommand 是否可执行
|
||||
/// Determine if RunMatrixCommand can execute
|
||||
/// </summary>
|
||||
private bool CanExecuteRunMatrix()
|
||||
{
|
||||
return !IsExecuting && !string.IsNullOrWhiteSpace(AssociatedProgramPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行矩阵运行:加载模板程序 → 重置状态 → 执行 → 更新 UI
|
||||
/// Execute matrix run: load template → reset states → execute → update UI
|
||||
/// </summary>
|
||||
private async Task ExecuteRunMatrixAsync()
|
||||
{
|
||||
if (_currentLayout == null || string.IsNullOrWhiteSpace(AssociatedProgramPath))
|
||||
return;
|
||||
|
||||
CncProgram templateProgram;
|
||||
try
|
||||
{
|
||||
templateProgram = await _cncProgramService.LoadAsync(AssociatedProgramPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"无法加载程序文件:{AssociatedProgramName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
_logger.Error(ex, "运行矩阵失败:无法加载模板程序 | Run matrix failed: cannot load template program");
|
||||
return;
|
||||
}
|
||||
|
||||
IsExecuting = true;
|
||||
|
||||
// 重置所有单元格状态为 NotExecuted | Reset all cell statuses to NotExecuted
|
||||
foreach (var cell in Cells)
|
||||
{
|
||||
cell.Status = MatrixCellStatus.NotExecuted;
|
||||
cell.ErrorMessage = null;
|
||||
}
|
||||
|
||||
StatusText = null;
|
||||
_executionCts = new CancellationTokenSource();
|
||||
|
||||
var progress = new Progress<MatrixCellExecutionProgress>(p =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
// 更新对应单元格的状态 | Update the corresponding cell's status
|
||||
var cellVm = Cells.FirstOrDefault(c => c.Row == p.Row && c.Column == p.Column);
|
||||
if (cellVm != null)
|
||||
{
|
||||
cellVm.Status = p.Status;
|
||||
if (!string.IsNullOrEmpty(p.ErrorMessage))
|
||||
cellVm.ErrorMessage = p.ErrorMessage;
|
||||
}
|
||||
|
||||
// 更新状态文本 | Update status text
|
||||
if (p.Status == MatrixCellStatus.Executing)
|
||||
{
|
||||
StatusText = $"正在执行:{AssociatedProgramName} | 当前位置:R{p.Row}C{p.Column}({p.CurrentIndex}/{p.TotalEnabled})";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _orchestrationService.ExecuteAsync(_currentLayout, templateProgram, progress, _executionCts.Token);
|
||||
|
||||
// 执行完成 | Execution completed
|
||||
var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed);
|
||||
var failedCount = Cells.Count(c => c.Status == MatrixCellStatus.Error);
|
||||
var totalEnabled = Cells.Count(c => c.IsEnabled);
|
||||
StatusText = $"执行完成:共 {totalEnabled} 个位置,{completedCount} 成功,{failedCount} 失败";
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 用户停止或外部取消 | User stopped or external cancellation
|
||||
var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed || c.Status == MatrixCellStatus.Error);
|
||||
var notExecutedCount = Cells.Count(c => c.IsEnabled && c.Status == MatrixCellStatus.NotExecuted);
|
||||
StatusText = $"执行已停止:已完成 {completedCount} 个位置,{notExecutedCount} 个位置未执行";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
_logger.Error(ex, "运行矩阵失败 | Run matrix failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "矩阵执行异常 | Matrix execution exception");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsExecuting = false;
|
||||
_executionCts?.Dispose();
|
||||
_executionCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止矩阵执行 | Stop matrix execution
|
||||
/// </summary>
|
||||
private void ExecuteStop()
|
||||
{
|
||||
_executionCts?.Cancel();
|
||||
}
|
||||
|
||||
// ── 辅助方法 | Helper methods ───────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -290,13 +619,21 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
Cells.Clear();
|
||||
|
||||
if (_currentLayout?.Cells == null)
|
||||
{
|
||||
EnabledCount = 0;
|
||||
TotalCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var cell in _currentLayout.Cells)
|
||||
{
|
||||
Cells.Add(new MatrixCellViewModel(cell));
|
||||
}
|
||||
|
||||
// 更新统计 | Update statistics
|
||||
TotalCount = Cells.Count;
|
||||
EnabledCount = Cells.Count(c => c.IsEnabled);
|
||||
|
||||
// 尝试保持选中状态 | Try to preserve selection
|
||||
if (SelectedCell != null)
|
||||
{
|
||||
@@ -305,4 +642,4 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XplorePlane.ViewModels.Debug
|
||||
{
|
||||
/// <summary>
|
||||
/// 调试面板本地化映射:将状态类别、事件类型、字段名等英文标识翻译为中文显示名。
|
||||
/// 显示用中文,内部匹配/过滤仍使用英文标识,二者解耦。
|
||||
/// </summary>
|
||||
internal static class DebugPanelLocalization
|
||||
{
|
||||
/// <summary>状态类别中文名(MotionState → 运动状态)</summary>
|
||||
public static readonly IReadOnlyDictionary<string, string> Categories = new Dictionary<string, string>
|
||||
{
|
||||
["MotionState"] = "运动状态",
|
||||
["RaySourceState"] = "射线源状态",
|
||||
["DetectorState"] = "探测器状态",
|
||||
["SystemState"] = "系统状态",
|
||||
["CameraState"] = "相机状态",
|
||||
["LinkedViewState"] = "联动视图状态",
|
||||
["RecipeExecutionState"] = "配方执行状态",
|
||||
["CalibrationMatrix"] = "标定矩阵"
|
||||
};
|
||||
|
||||
/// <summary>事件类型中文名(MotionStateChanged → 运动状态变化)</summary>
|
||||
public static readonly IReadOnlyDictionary<string, string> EventTypes = new Dictionary<string, string>
|
||||
{
|
||||
["MotionStateChanged"] = "运动状态变化",
|
||||
["RaySourceStateChanged"] = "射线源状态变化",
|
||||
["DetectorStateChanged"] = "探测器状态变化",
|
||||
["SystemStateChanged"] = "系统状态变化",
|
||||
["CameraStateChanged"] = "相机状态变化",
|
||||
["LinkedViewStateChanged"] = "联动视图状态变化",
|
||||
["RecipeExecutionStateChanged"] = "配方执行状态变化"
|
||||
};
|
||||
|
||||
/// <summary>状态字段中文名(StageX → 载物台X位置 等)</summary>
|
||||
public static readonly IReadOnlyDictionary<string, string> Fields = new Dictionary<string, string>
|
||||
{
|
||||
// MotionState
|
||||
["StageX"] = "载物台X (μm)",
|
||||
["StageY"] = "载物台Y (μm)",
|
||||
["SourceZ"] = "射线源Z (μm)",
|
||||
["DetectorZ"] = "探测器Z (μm)",
|
||||
["DetectorSwing"] = "探测器摆角 (°)",
|
||||
["FDD"] = "焦点-探测器距离 (μm)",
|
||||
["StageXSpeed"] = "载物台X速度 (μm/s)",
|
||||
["StageYSpeed"] = "载物台Y速度 (μm/s)",
|
||||
["SourceZSpeed"] = "射线源Z速度 (μm/s)",
|
||||
["DetectorZSpeed"] = "探测器Z速度 (μm/s)",
|
||||
["DetectorSwingSpeed"] = "摆角速度 (°/s)",
|
||||
["FDDSpeed"] = "FDD速度 (μm/s)",
|
||||
["StageRotation"] = "载台旋转 (°)",
|
||||
["FixtureRotation"] = "夹具旋转 (°)",
|
||||
["FOD"] = "焦点-物体距离 (μm)",
|
||||
["Magnification"] = "放大倍率",
|
||||
["StageRotationSpeed"] = "载台旋转速度 (°/s)",
|
||||
["FixtureRotationSpeed"] = "夹具旋转速度 (°/s)",
|
||||
|
||||
// RaySourceState
|
||||
["IsOn"] = "是否开启",
|
||||
["Voltage"] = "电压 (kV)",
|
||||
["Power"] = "功率 (W)",
|
||||
|
||||
// DetectorState
|
||||
["IsConnected"] = "是否已连接",
|
||||
["IsAcquiring"] = "是否正在采集",
|
||||
["FrameRate"] = "帧率 (fps)",
|
||||
["Resolution"] = "分辨率",
|
||||
|
||||
// SystemState
|
||||
["OperationMode"] = "操作模式",
|
||||
["HasError"] = "是否存在错误",
|
||||
["ErrorMessage"] = "错误信息",
|
||||
|
||||
// CameraState
|
||||
["IsStreaming"] = "是否正在推流",
|
||||
["CurrentFrame"] = "当前帧",
|
||||
["Width"] = "图像宽度 (px)",
|
||||
["Height"] = "图像高度 (px)",
|
||||
|
||||
// LinkedViewState
|
||||
["TargetPosition"] = "目标物理位置",
|
||||
["IsExecuting"] = "是否正在执行",
|
||||
["LastRequestTime"] = "最近请求时间",
|
||||
|
||||
// CalibrationMatrix
|
||||
["M11"] = "M11", ["M12"] = "M12", ["M13"] = "M13",
|
||||
["M21"] = "M21", ["M22"] = "M22", ["M23"] = "M23",
|
||||
["M31"] = "M31", ["M32"] = "M32", ["M33"] = "M33",
|
||||
|
||||
// 通用
|
||||
["(No changes)"] = "(无变化)"
|
||||
};
|
||||
|
||||
/// <summary>获取类别中文名,未匹配时返回原值</summary>
|
||||
public static string Category(string key) =>
|
||||
key != null && Categories.TryGetValue(key, out var v) ? v : key;
|
||||
|
||||
/// <summary>获取事件类型中文名,未匹配时返回原值</summary>
|
||||
public static string EventType(string key) =>
|
||||
key != null && EventTypes.TryGetValue(key, out var v) ? v : key;
|
||||
|
||||
/// <summary>获取字段中文名,未匹配时返回原值</summary>
|
||||
public static string Field(string key) =>
|
||||
key != null && Fields.TryGetValue(key, out var v) ? v : key;
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = PromptSavePath("Export event log as JSON", "JSON files (*.json)|*.json");
|
||||
var path = PromptSavePath("导出事件日志为 JSON", "JSON 文件 (*.json)|*.json");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
@@ -292,12 +292,12 @@ namespace XplorePlane.ViewModels.Debug
|
||||
};
|
||||
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(payload, _jsonOptions), Encoding.UTF8);
|
||||
MessageBox.Show("Event log exported successfully.", "Export", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
MessageBox.Show("事件日志已成功导出。", "导出", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "导出事件日志 JSON 失败 | Failed to export event log as JSON");
|
||||
MessageBox.Show($"Failed to export JSON: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show($"导出 JSON 失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = PromptSavePath("Export event log as CSV", "CSV files (*.csv)|*.csv");
|
||||
var path = PromptSavePath("导出事件日志为 CSV", "CSV 文件 (*.csv)|*.csv");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
@@ -328,12 +328,12 @@ namespace XplorePlane.ViewModels.Debug
|
||||
}
|
||||
|
||||
File.WriteAllText(path, builder.ToString(), Encoding.UTF8);
|
||||
MessageBox.Show("Event log exported successfully.", "Export", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
MessageBox.Show("事件日志已成功导出。", "导出", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "导出事件日志 CSV 失败 | Failed to export event log as CSV");
|
||||
MessageBox.Show($"Failed to export CSV: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show($"导出 CSV 失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace XplorePlane.ViewModels.Debug
|
||||
private bool _disposed;
|
||||
|
||||
public ObservableCollection<PerformanceMetric> Metrics { get; } = new();
|
||||
public ObservableCollection<TrendDataPoint> TrendData { get; } = new();
|
||||
|
||||
public double MaxLatency
|
||||
{
|
||||
@@ -63,7 +62,6 @@ namespace XplorePlane.ViewModels.Debug
|
||||
_timer.Tick += (_, _) =>
|
||||
{
|
||||
UpdateFrequencyMetrics();
|
||||
UpdateTrendData();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +93,6 @@ namespace XplorePlane.ViewModels.Debug
|
||||
|
||||
_timer.Stop();
|
||||
Metrics.Clear();
|
||||
TrendData.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
@@ -150,19 +147,5 @@ namespace XplorePlane.ViewModels.Debug
|
||||
MaxLatency = Metrics.Count == 0 ? 0 : Metrics.Max(m => m.AverageLatency);
|
||||
IsLatencyWarning = MaxLatency > 500;
|
||||
}
|
||||
|
||||
private void UpdateTrendData()
|
||||
{
|
||||
TrendData.Add(new TrendDataPoint
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
Value = Metrics.Sum(m => m.EventsPerSecond)
|
||||
});
|
||||
|
||||
while (TrendData.Count > 60)
|
||||
{
|
||||
TrendData.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
var selected = Snapshots.Where(s => s.IsSelected).Take(2).ToList();
|
||||
if (selected.Count != 2)
|
||||
{
|
||||
MessageBox.Show("Please select exactly two snapshots to compare.", "Compare Snapshots", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
MessageBox.Show("请勾选恰好两个快照进行对比。", "快照对比", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "比较快照失败 | Failed to compare snapshots");
|
||||
MessageBox.Show($"Failed to compare snapshots: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show($"对比快照失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,14 +169,14 @@ namespace XplorePlane.ViewModels.Debug
|
||||
{
|
||||
if (SelectedSnapshot?.Snapshot == null)
|
||||
{
|
||||
MessageBox.Show("Please select a snapshot first.", "Export Snapshot", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
MessageBox.Show("请先选择一个快照。", "导出快照", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "Export Snapshot",
|
||||
Filter = "JSON files (*.json)|*.json",
|
||||
Title = "导出快照",
|
||||
Filter = "JSON 文件 (*.json)|*.json",
|
||||
FileName = $"snapshot-{SelectedSnapshot.Snapshot.Timestamp:yyyyMMdd-HHmmss}.json"
|
||||
};
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "导出快照失败 | Failed to export snapshot");
|
||||
MessageBox.Show($"Failed to export snapshot: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show($"导出快照失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show("Delete selected snapshot?", "Delete Snapshot", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
var result = MessageBox.Show("确定删除选中的快照?", "删除快照", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
@@ -244,6 +244,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
var root = new StateNodeViewModel
|
||||
{
|
||||
Name = category,
|
||||
DisplayName = DebugPanelLocalization.Category(category),
|
||||
Category = category
|
||||
};
|
||||
|
||||
@@ -254,6 +255,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
root.Children.Add(new StateNodeViewModel
|
||||
{
|
||||
Name = property.Name,
|
||||
DisplayName = DebugPanelLocalization.Field(property.Name),
|
||||
Category = category,
|
||||
Value = DebugPanelStateFormatter.FormatValue(property.GetValue(value))
|
||||
});
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
var root = new StateNodeViewModel
|
||||
{
|
||||
Name = category,
|
||||
DisplayName = DebugPanelLocalization.Category(category),
|
||||
Category = category
|
||||
};
|
||||
|
||||
@@ -102,6 +103,7 @@ namespace XplorePlane.ViewModels.Debug
|
||||
root.Children.Add(new StateNodeViewModel
|
||||
{
|
||||
Name = property.Name,
|
||||
DisplayName = DebugPanelLocalization.Field(property.Name),
|
||||
Category = category,
|
||||
Value = string.Empty
|
||||
});
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
string PipelineFileDisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前角色是否有权编辑流水线。为 false 时禁用所有编辑操作。
|
||||
/// </summary>
|
||||
bool CanEditPipeline { get; }
|
||||
|
||||
ICommand AddOperatorCommand { get; }
|
||||
|
||||
ICommand RemoveOperatorCommand { get; }
|
||||
|
||||
@@ -243,6 +243,9 @@ namespace XplorePlane.ViewModels
|
||||
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
|
||||
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
|
||||
|
||||
/// <summary>独立流水线编辑器始终允许编辑(不受角色限制)。</summary>
|
||||
public bool CanEditPipeline => true;
|
||||
|
||||
// ── Command Implementations ───────────────────────────────────
|
||||
|
||||
private bool CanAddOperator(string operatorKey) =>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XplorePlane.Services.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录对话框 ViewModel。
|
||||
/// 负责密码输入验证和调用 IPermissionService 进行认证。
|
||||
/// </summary>
|
||||
public class LoginDialogViewModel : BindableBase
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
private string _password = string.Empty;
|
||||
private string _errorMessage = string.Empty;
|
||||
private bool _hasError;
|
||||
private bool? _dialogResult;
|
||||
|
||||
public LoginDialogViewModel(IPermissionService permissionService)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
|
||||
LoginCommand = new DelegateCommand(ExecuteLogin);
|
||||
CancelCommand = new DelegateCommand(ExecuteCancel);
|
||||
}
|
||||
|
||||
/// <summary>密码输入(通过 attached behavior 绑定 PasswordBox)。</summary>
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => SetProperty(ref _password, value);
|
||||
}
|
||||
|
||||
/// <summary>错误消息,认证失败或空密码时显示。</summary>
|
||||
public string ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _errorMessage, value))
|
||||
{
|
||||
HasError = !string.IsNullOrEmpty(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>是否存在错误(ErrorMessage 非空时为 true)。</summary>
|
||||
public bool HasError
|
||||
{
|
||||
get => _hasError;
|
||||
private set => SetProperty(ref _hasError, value);
|
||||
}
|
||||
|
||||
/// <summary>对话框结果。设置后关闭对话框(true=认证成功,false=取消)。</summary>
|
||||
public bool? DialogResult
|
||||
{
|
||||
get => _dialogResult;
|
||||
set => SetProperty(ref _dialogResult, value);
|
||||
}
|
||||
|
||||
/// <summary>登录命令:校验非空 → 调用认证 → 成功关闭/失败显示错误。</summary>
|
||||
public DelegateCommand LoginCommand { get; }
|
||||
|
||||
/// <summary>取消命令:关闭对话框。</summary>
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
private void ExecuteLogin()
|
||||
{
|
||||
// 空密码前端拦截,不调用服务
|
||||
if (string.IsNullOrEmpty(Password))
|
||||
{
|
||||
ErrorMessage = "请输入密码";
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _permissionService.Authenticate(Password);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
ErrorMessage = string.Empty;
|
||||
DialogResult = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = result.ErrorMessage ?? "认证失败";
|
||||
Password = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCancel()
|
||||
{
|
||||
DialogResult = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,34 +15,29 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using XP.Camera.Calibration;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Services;
|
||||
using XP.ImageProcessing.Processors;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.MainViewport;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Services.Recording;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
using XplorePlane.ViewModels.ImageProcessing;
|
||||
using XplorePlane.ViewModels.Main;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XplorePlane.Views.Debug;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Services;
|
||||
using System.Windows.Threading;
|
||||
using XP.ImageProcessing.Processors;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.ViewModels.ImageProcessing;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XplorePlane.Views.ImageProcessing;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels
|
||||
{
|
||||
@@ -54,6 +49,7 @@ namespace XplorePlane.ViewModels
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IMainViewportService _mainViewportService;
|
||||
private readonly IXpDataPathService _xpDataPathService;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly CncEditorViewModel _cncEditorViewModel;
|
||||
private readonly CncPageView _cncPageView;
|
||||
private readonly PipelineEditorViewModel _pipelineEditorViewModel;
|
||||
@@ -199,6 +195,30 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||
public DelegateCommand UseLiveDetectorSourceCommand { get; }
|
||||
public DelegateCommand OpenPasswordManagementCommand { get; }
|
||||
|
||||
// ── 权限相关设置可见性属性 ──
|
||||
|
||||
private bool _isFactorySettingsVisible = true;
|
||||
/// <summary>
|
||||
/// 厂家级设置导航项是否可见。仅 SuperAdmin 可见。
|
||||
/// 包括:射线源、探测器、运动控制、相机设置、PLC 地址等硬件设置。
|
||||
/// </summary>
|
||||
public bool IsFactorySettingsVisible
|
||||
{
|
||||
get => _isFactorySettingsVisible;
|
||||
private set => SetProperty(ref _isFactorySettingsVisible, value);
|
||||
}
|
||||
|
||||
private bool _isReportSettingsVisible = true;
|
||||
/// <summary>
|
||||
/// 报告设定导航项是否可见。SuperAdmin 和 Admin 可见,User 不可见。
|
||||
/// </summary>
|
||||
public bool IsReportSettingsVisible
|
||||
{
|
||||
get => _isReportSettingsVisible;
|
||||
private set => SetProperty(ref _isReportSettingsVisible, value);
|
||||
}
|
||||
|
||||
public bool IsMainViewportRealtimeEnabled
|
||||
{
|
||||
@@ -303,6 +323,7 @@ namespace XplorePlane.ViewModels
|
||||
IEventAggregator eventAggregator,
|
||||
IMainViewportService mainViewportService,
|
||||
IXpDataPathService xpDataPathService,
|
||||
IPermissionService permissionService,
|
||||
IViewportRecordingService recordingService)
|
||||
{
|
||||
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
@@ -310,6 +331,7 @@ namespace XplorePlane.ViewModels
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
|
||||
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
|
||||
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
||||
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
||||
_pipelineEditorViewModel = _containerProvider.Resolve<PipelineEditorViewModel>();
|
||||
@@ -431,6 +453,7 @@ namespace XplorePlane.ViewModels
|
||||
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
||||
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
||||
UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource);
|
||||
OpenPasswordManagementCommand = new DelegateCommand(ExecuteOpenPasswordManagement);
|
||||
|
||||
ImagePanelContent = _pipelineEditorView;
|
||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
||||
@@ -445,9 +468,22 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
ToggleRecordingCommand = new DelegateCommand(ExecuteToggleRecording, CanExecuteToggleRecording);
|
||||
|
||||
// 订阅角色变更事件,刷新设置导航可见性
|
||||
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||
.Subscribe(OnRoleChangedForSettings, ThreadOption.UIThread);
|
||||
|
||||
// 初始化设置导航可见性
|
||||
RefreshSettingsVisibility();
|
||||
|
||||
// 初始化 Ribbon 右侧状态区域 ViewModel
|
||||
RibbonStatusArea = _containerProvider.Resolve<RibbonStatusAreaViewModel>();
|
||||
|
||||
_logger.Info("MainViewModel 已初始化");
|
||||
}
|
||||
|
||||
/// <summary>Ribbon 右侧状态区域 ViewModel(角色显示 + 切换用户按钮)。</summary>
|
||||
public RibbonStatusAreaViewModel RibbonStatusArea { get; private set; }
|
||||
|
||||
public string CncStatusMessage => _cncEditorViewModel.StatusMessage;
|
||||
public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError;
|
||||
|
||||
@@ -622,6 +658,9 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
private void ExecuteOpenCameraSettings()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("CameraSettings"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var vm = _containerProvider.Resolve<NavigationPropertyPanelViewModel>();
|
||||
@@ -909,6 +948,9 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
private void ExecuteOpenDetectorConfig()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("DetectorConfig"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w,
|
||||
@@ -924,24 +966,36 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
private void ExecuteOpenMotionDebug()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("MotionControl"))
|
||||
return;
|
||||
|
||||
ShowOrActivate(_motionDebugWindow, w => _motionDebugWindow = w,
|
||||
() => new XP.Hardware.MotionControl.Views.MotionDebugWindow(), "运动调试");
|
||||
}
|
||||
|
||||
private void ExecuteOpenPlcAddrConfig()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("PlcAddrConfig"))
|
||||
return;
|
||||
|
||||
ShowOrActivate(_plcAddrConfigWindow, w => _plcAddrConfigWindow = w,
|
||||
() => _containerProvider.Resolve<XP.Hardware.PLC.Views.PlcAddrConfigEditorWindow>(), "PLC 地址配置");
|
||||
}
|
||||
|
||||
private void ExecuteOpenRaySourceConfig()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("RaySourceConfig"))
|
||||
return;
|
||||
|
||||
ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w,
|
||||
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
|
||||
}
|
||||
|
||||
private void ExecuteOpenReportConfig()
|
||||
{
|
||||
if (!ConfirmReportSettingsNavigation("ReportConfig"))
|
||||
return;
|
||||
|
||||
ShowOrActivate(_reportConfigWindow, w => _reportConfigWindow = w,
|
||||
() =>
|
||||
{
|
||||
@@ -950,6 +1004,28 @@ namespace XplorePlane.ViewModels
|
||||
}, "报告配置");
|
||||
}
|
||||
|
||||
private void ExecuteOpenPasswordManagement()
|
||||
{
|
||||
if (!ConfirmFactorySettingsNavigation("PasswordManagement"))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var viewModel = _containerProvider.Resolve<ViewModels.Setting.PasswordManagementViewModel>();
|
||||
var window = new Views.Setting.PasswordManagementView(viewModel)
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
window.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to open password management window");
|
||||
MessageBox.Show($"打开密码管理窗口失败: {ex.Message}",
|
||||
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteLoadImage()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
@@ -1632,5 +1708,74 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
|
||||
#endregion 测量命令实现
|
||||
|
||||
#region 设置导航权限控制
|
||||
|
||||
/// <summary>
|
||||
/// 角色变更时刷新设置导航可见性。
|
||||
/// </summary>
|
||||
private void OnRoleChangedForSettings(RoleChangedPayload payload)
|
||||
{
|
||||
RefreshSettingsVisibility();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前角色刷新设置导航项的可见性。
|
||||
/// Factory_Settings: 仅 SuperAdmin 可见。
|
||||
/// Report_Settings: SuperAdmin 和 Admin 可见,User 不可见。
|
||||
/// </summary>
|
||||
private void RefreshSettingsVisibility()
|
||||
{
|
||||
IsFactorySettingsVisible = _permissionService.HasPermission(PermissionEnum.AccessPlcSettings);
|
||||
IsReportSettingsVisible = _permissionService.HasPermission(PermissionEnum.AccessReportSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否允许导航到厂家级设置页面。
|
||||
/// 如果权限不足,记录日志并显示通知。
|
||||
/// </summary>
|
||||
/// <param name="targetPage">目标页面标识。</param>
|
||||
/// <returns>是否允许导航。</returns>
|
||||
private bool ConfirmFactorySettingsNavigation(string targetPage)
|
||||
{
|
||||
if (_permissionService.HasPermission(PermissionEnum.AccessPlcSettings))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var role = _permissionService.CurrentRole?.ToString() ?? "未认证";
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
_logger.Warn("权限拒绝导航: Role={Role}, Target={Target}, Timestamp={Timestamp}",
|
||||
role, targetPage, timestamp);
|
||||
|
||||
MessageBox.Show("当前角色无权访问此功能", "权限不足",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否允许导航到报告设定页面。
|
||||
/// 如果权限不足,记录日志并显示通知。
|
||||
/// </summary>
|
||||
/// <param name="targetPage">目标页面标识。</param>
|
||||
/// <returns>是否允许导航。</returns>
|
||||
private bool ConfirmReportSettingsNavigation(string targetPage)
|
||||
{
|
||||
if (_permissionService.HasPermission(PermissionEnum.AccessReportSettings))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var role = _permissionService.CurrentRole?.ToString() ?? "未认证";
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
_logger.Warn("权限拒绝导航: Role={Role}, Target={Target}, Timestamp={Timestamp}",
|
||||
role, targetPage, timestamp);
|
||||
|
||||
MessageBox.Show("当前角色无权访问此功能", "权限不足",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion 设置导航权限控制
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
using Serilog;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Permission;
|
||||
using XplorePlane.Views;
|
||||
|
||||
namespace XplorePlane.ViewModels.Main
|
||||
{
|
||||
/// <summary>
|
||||
/// Ribbon 右侧状态区域 ViewModel。
|
||||
/// 显示当前角色中文名称标签和"切换用户"按钮。
|
||||
/// 位置:Ribbon 右侧区域(类似 Office 账户区域),认证成功后始终可见。
|
||||
/// </summary>
|
||||
public class RibbonStatusAreaViewModel : BindableBase
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly IContainerProvider _containerProvider;
|
||||
|
||||
private string _currentRoleDisplayName = string.Empty;
|
||||
private bool _canSwitchUser;
|
||||
|
||||
/// <summary>当前角色中文显示名(超级管理员 / 管理员 / 用户)。</summary>
|
||||
public string CurrentRoleDisplayName
|
||||
{
|
||||
get => _currentRoleDisplayName;
|
||||
set => SetProperty(ref _currentRoleDisplayName, value);
|
||||
}
|
||||
|
||||
/// <summary>是否可切换用户(绑定按钮 IsEnabled)。</summary>
|
||||
public bool CanSwitchUser
|
||||
{
|
||||
get => _canSwitchUser;
|
||||
set => SetProperty(ref _canSwitchUser, value);
|
||||
}
|
||||
|
||||
/// <summary>切换用户命令。CNC 执行中禁用。</summary>
|
||||
public DelegateCommand SwitchUserCommand { get; }
|
||||
|
||||
public RibbonStatusAreaViewModel(
|
||||
IPermissionService permissionService,
|
||||
IEventAggregator eventAggregator,
|
||||
IAppStateService appStateService,
|
||||
IContainerProvider containerProvider)
|
||||
{
|
||||
_permissionService = permissionService ?? throw new System.ArgumentNullException(nameof(permissionService));
|
||||
_eventAggregator = eventAggregator ?? throw new System.ArgumentNullException(nameof(eventAggregator));
|
||||
_appStateService = appStateService ?? throw new System.ArgumentNullException(nameof(appStateService));
|
||||
_containerProvider = containerProvider ?? throw new System.ArgumentNullException(nameof(containerProvider));
|
||||
|
||||
SwitchUserCommand = new DelegateCommand(ExecuteSwitchUser, () => CanSwitchUser);
|
||||
|
||||
// Subscribe to RoleChangedEvent to update CurrentRoleDisplayName
|
||||
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||
.Subscribe(OnRoleChanged, ThreadOption.UIThread);
|
||||
|
||||
// Subscribe to SystemState changes to update CanSwitchUser
|
||||
_appStateService.SystemStateChanged += OnSystemStateChanged;
|
||||
|
||||
// Initialize from current state
|
||||
InitializeFromCurrentState();
|
||||
}
|
||||
|
||||
private void InitializeFromCurrentState()
|
||||
{
|
||||
if (_permissionService.CurrentRole.HasValue)
|
||||
{
|
||||
CurrentRoleDisplayName = MapRoleToDisplayName(_permissionService.CurrentRole.Value);
|
||||
}
|
||||
|
||||
CanSwitchUser = _permissionService.CanSwitchUser;
|
||||
}
|
||||
|
||||
private void OnRoleChanged(RoleChangedPayload payload)
|
||||
{
|
||||
CurrentRoleDisplayName = MapRoleToDisplayName(payload.NewRole);
|
||||
}
|
||||
|
||||
private void OnSystemStateChanged(object sender, StateChangedEventArgs<SystemState> e)
|
||||
{
|
||||
CanSwitchUser = _permissionService.CanSwitchUser;
|
||||
SwitchUserCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void ExecuteSwitchUser()
|
||||
{
|
||||
// 检查是否允许切换用户(CNC 执行中不允许)
|
||||
if (!_permissionService.CanSwitchUser)
|
||||
{
|
||||
MessageBox.Show("CNC 执行中不允许切换用户", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示登录对话框进行重新认证
|
||||
// 不先调用 Logout(),这样取消时旧角色自动保留(满足需求 6.4)
|
||||
var loginViewModel = _containerProvider.Resolve<LoginDialogViewModel>();
|
||||
var loginDialog = new LoginDialog(loginViewModel);
|
||||
var result = loginDialog.ShowDialog();
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
// 认证成功:Authenticate() 已更新 CurrentRole 并发布 RoleChangedEvent,
|
||||
// UI 会自动刷新(满足需求 6.3)
|
||||
Log.Information("用户切换成功,新角色: {Role}", _permissionService.CurrentRole);
|
||||
}
|
||||
// 如果取消(result == false 或 null),什么都不做,旧角色保持不变(满足需求 6.4)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UserRole 枚举映射为中文显示名。
|
||||
/// </summary>
|
||||
private static string MapRoleToDisplayName(UserRole role)
|
||||
{
|
||||
return role switch
|
||||
{
|
||||
UserRole.SuperAdmin => "超级管理员",
|
||||
UserRole.Admin => "管理员",
|
||||
UserRole.User => "用户",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,10 +94,18 @@ namespace XplorePlane.ViewModels
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isRealtimeEnabled, value))
|
||||
{
|
||||
_mainViewportService.SetRealtimeDisplayEnabled(value);
|
||||
RaisePropertyChanged(nameof(IsWheelZoomEnabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许滚轮缩放:实时显示过程中禁用,停止实时后可缩放。
|
||||
/// </summary>
|
||||
public bool IsWheelZoomEnabled => !_isRealtimeEnabled;
|
||||
|
||||
// Task 5.3: IsDetectorConnected property (read-only, private setter)
|
||||
public bool IsDetectorConnected
|
||||
{
|
||||
@@ -227,6 +235,7 @@ namespace XplorePlane.ViewModels
|
||||
// Task 5.8: Sync IsRealtimeEnabled from service
|
||||
_isRealtimeEnabled = _mainViewportService.IsRealtimeDisplayEnabled;
|
||||
RaisePropertyChanged(nameof(IsRealtimeEnabled));
|
||||
RaisePropertyChanged(nameof(IsWheelZoomEnabled));
|
||||
|
||||
// Task 5.8: Sync _isCncRunning from service and raise IsAnimatedSwitchEnabled
|
||||
_isCncRunning = _mainViewportService.IsCncRunning;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels.Setting
|
||||
{
|
||||
/// <summary>
|
||||
/// 密码管理 ViewModel。
|
||||
/// 对应 UI 元素:"密码管理" 按钮位于 "全局设置" (Global Settings) Ribbon Group 中。
|
||||
/// 可见性规则:仅 Super_Admin 可见,Admin/User 时隐藏(与 Factory_Settings 相同规则)。
|
||||
/// </summary>
|
||||
public class PasswordManagementViewModel : BindableBase
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private const int MinPasswordLength = 4;
|
||||
private const int MaxPasswordLength = 32;
|
||||
|
||||
public PasswordManagementViewModel(IPermissionService permissionService, ILoggerService logger)
|
||||
{
|
||||
_permissionService = permissionService ?? throw new System.ArgumentNullException(nameof(permissionService));
|
||||
_logger = logger?.ForModule<PasswordManagementViewModel>() ?? throw new System.ArgumentNullException(nameof(logger));
|
||||
|
||||
ChangeSuperAdminPasswordCommand = new DelegateCommand(ExecuteChangeSuperAdminPassword);
|
||||
ChangeAdminPasswordCommand = new DelegateCommand(ExecuteChangeAdminPassword);
|
||||
ChangeUserPasswordCommand = new DelegateCommand(ExecuteChangeUserPassword);
|
||||
}
|
||||
|
||||
// ── 密码输入属性 ──
|
||||
|
||||
private string _superAdminPassword = string.Empty;
|
||||
public string SuperAdminPassword
|
||||
{
|
||||
get => _superAdminPassword;
|
||||
set => SetProperty(ref _superAdminPassword, value);
|
||||
}
|
||||
|
||||
private string _adminPassword = string.Empty;
|
||||
public string AdminPassword
|
||||
{
|
||||
get => _adminPassword;
|
||||
set => SetProperty(ref _adminPassword, value);
|
||||
}
|
||||
|
||||
private string _userPassword = string.Empty;
|
||||
public string UserPassword
|
||||
{
|
||||
get => _userPassword;
|
||||
set => SetProperty(ref _userPassword, value);
|
||||
}
|
||||
|
||||
// ── 反馈消息属性 ──
|
||||
|
||||
private string _superAdminMessage = string.Empty;
|
||||
public string SuperAdminMessage
|
||||
{
|
||||
get => _superAdminMessage;
|
||||
set => SetProperty(ref _superAdminMessage, value);
|
||||
}
|
||||
|
||||
private bool _superAdminMessageIsError;
|
||||
public bool SuperAdminMessageIsError
|
||||
{
|
||||
get => _superAdminMessageIsError;
|
||||
set => SetProperty(ref _superAdminMessageIsError, value);
|
||||
}
|
||||
|
||||
private string _adminMessage = string.Empty;
|
||||
public string AdminMessage
|
||||
{
|
||||
get => _adminMessage;
|
||||
set => SetProperty(ref _adminMessage, value);
|
||||
}
|
||||
|
||||
private bool _adminMessageIsError;
|
||||
public bool AdminMessageIsError
|
||||
{
|
||||
get => _adminMessageIsError;
|
||||
set => SetProperty(ref _adminMessageIsError, value);
|
||||
}
|
||||
|
||||
private string _userMessage = string.Empty;
|
||||
public string UserMessage
|
||||
{
|
||||
get => _userMessage;
|
||||
set => SetProperty(ref _userMessage, value);
|
||||
}
|
||||
|
||||
private bool _userMessageIsError;
|
||||
public bool UserMessageIsError
|
||||
{
|
||||
get => _userMessageIsError;
|
||||
set => SetProperty(ref _userMessageIsError, value);
|
||||
}
|
||||
|
||||
// ── 命令 ──
|
||||
|
||||
public DelegateCommand ChangeSuperAdminPasswordCommand { get; }
|
||||
public DelegateCommand ChangeAdminPasswordCommand { get; }
|
||||
public DelegateCommand ChangeUserPasswordCommand { get; }
|
||||
|
||||
// ── 命令执行 ──
|
||||
|
||||
private void ExecuteChangeSuperAdminPassword()
|
||||
{
|
||||
var result = ValidateAndChangePassword(UserRole.SuperAdmin, SuperAdminPassword);
|
||||
SuperAdminMessageIsError = !result.Success;
|
||||
SuperAdminMessage = result.Success ? "密码修改成功" : result.ErrorMessage;
|
||||
if (result.Success)
|
||||
SuperAdminPassword = string.Empty;
|
||||
}
|
||||
|
||||
private void ExecuteChangeAdminPassword()
|
||||
{
|
||||
var result = ValidateAndChangePassword(UserRole.Admin, AdminPassword);
|
||||
AdminMessageIsError = !result.Success;
|
||||
AdminMessage = result.Success ? "密码修改成功" : result.ErrorMessage;
|
||||
if (result.Success)
|
||||
AdminPassword = string.Empty;
|
||||
}
|
||||
|
||||
private void ExecuteChangeUserPassword()
|
||||
{
|
||||
var result = ValidateAndChangePassword(UserRole.User, UserPassword);
|
||||
UserMessageIsError = !result.Success;
|
||||
UserMessage = result.Success ? "密码修改成功" : result.ErrorMessage;
|
||||
if (result.Success)
|
||||
UserPassword = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证密码长度并调用服务修改密码。
|
||||
/// </summary>
|
||||
private PasswordChangeResult ValidateAndChangePassword(UserRole targetRole, string newPassword)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newPassword))
|
||||
{
|
||||
return new PasswordChangeResult(false, "请输入新密码");
|
||||
}
|
||||
|
||||
if (newPassword.Length < MinPasswordLength || newPassword.Length > MaxPasswordLength)
|
||||
{
|
||||
return new PasswordChangeResult(false, $"密码长度必须在 {MinPasswordLength}-{MaxPasswordLength} 个字符之间");
|
||||
}
|
||||
|
||||
var result = _permissionService.ChangePassword(targetRole, newPassword);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.Info("角色 {Role} 密码已更新", targetRole);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("角色 {Role} 密码修改失败: {Error}", targetRole, result.ErrorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,44 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Windows;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Events;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Permission;
|
||||
using PermissionEnum = XplorePlane.Models.Permission;
|
||||
|
||||
namespace XplorePlane.ViewModels.Setting
|
||||
{
|
||||
public class SettingsViewModel : BindableBase
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
public DelegateCommand ResetToDefaultCommand { get; }
|
||||
|
||||
public SettingsViewModel(ILoggerService logger)
|
||||
// ── 权限相关设置可见性属性 ──
|
||||
|
||||
private bool _isFactorySettingsVisible = true;
|
||||
/// <summary>
|
||||
/// 厂家级设置标签页是否可见(射线源、探测器、PLC)。仅 SuperAdmin 可见。
|
||||
/// </summary>
|
||||
public bool IsFactorySettingsVisible
|
||||
{
|
||||
get => _isFactorySettingsVisible;
|
||||
private set => SetProperty(ref _isFactorySettingsVisible, value);
|
||||
}
|
||||
|
||||
public SettingsViewModel(ILoggerService logger, IPermissionService permissionService, IEventAggregator eventAggregator)
|
||||
{
|
||||
_logger = logger?.ForModule<SettingsViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
|
||||
_logger.Info("SettingsViewModel 构造函数被调用 | SettingsViewModel constructor called");
|
||||
|
||||
@@ -28,6 +49,13 @@ namespace XplorePlane.ViewModels.Setting
|
||||
_logger.Debug("Commands initialized: SaveCommand={SaveCommand}, CancelCommand={CancelCommand}, ResetToDefaultCommand={ResetToDefaultCommand}",
|
||||
SaveCommand != null, CancelCommand != null, ResetToDefaultCommand != null);
|
||||
|
||||
// 订阅角色变更事件
|
||||
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||
.Subscribe(OnRoleChanged, ThreadOption.UIThread);
|
||||
|
||||
// 初始化可见性
|
||||
RefreshSettingsVisibility();
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
@@ -439,5 +467,19 @@ namespace XplorePlane.ViewModels.Setting
|
||||
config.AppSettings.Settings[key].Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoleChanged(RoleChangedPayload payload)
|
||||
{
|
||||
RefreshSettingsVisibility();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前角色刷新设置标签页的可见性。
|
||||
/// 厂家级设置(射线源、探测器、PLC)仅 SuperAdmin 可见。
|
||||
/// </summary>
|
||||
private void RefreshSettingsVisibility()
|
||||
{
|
||||
IsFactorySettingsVisible = _permissionService.HasPermission(PermissionEnum.AccessPlcSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:behaviors="clr-namespace:XplorePlane.Controls"
|
||||
xmlns:helpers="clr-namespace:XplorePlane.Helpers"
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
||||
d:DesignHeight="760"
|
||||
@@ -269,7 +270,8 @@
|
||||
Content="完成"
|
||||
Style="{StaticResource TreeToolbarButtonCompact}" />
|
||||
</WrapPanel>
|
||||
<WrapPanel Margin="0,4,0,0">
|
||||
<WrapPanel Margin="0,4,0,0" IsEnabled="{Binding CanEditCncProgram}"
|
||||
helpers:PermissionTooltipHelper.IsPermissionRestricted="True">
|
||||
<WrapPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace XplorePlane.Views.Cnc
|
||||
var executionService = ContainerLocator.Current.Resolve<IPipelineExecutionService>();
|
||||
var mainViewportService = ContainerLocator.Current.Resolve<XplorePlane.Services.MainViewport.IMainViewportService>();
|
||||
var eventAggregator = ContainerLocator.Current.Resolve<Prism.Events.IEventAggregator>();
|
||||
var permissionService = ContainerLocator.Current.Resolve<XplorePlane.Services.Permission.IPermissionService>();
|
||||
|
||||
_inspectionModulePipelineViewModel = new CncInspectionModulePipelineViewModel(
|
||||
editorViewModel,
|
||||
@@ -65,7 +66,8 @@ namespace XplorePlane.Views.Cnc
|
||||
logger,
|
||||
executionService,
|
||||
mainViewportService,
|
||||
eventAggregator);
|
||||
eventAggregator,
|
||||
permissionService);
|
||||
|
||||
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:models="clr-namespace:XplorePlane.Models"
|
||||
xmlns:converters="clr-namespace:XplorePlane.Converters"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="900"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
@@ -19,6 +21,10 @@
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<!-- 转换器 | Converters -->
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
|
||||
|
||||
<!-- 配置面板标签样式 | Configuration panel label style -->
|
||||
<Style x:Key="ConfigLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
@@ -38,6 +44,15 @@
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- 错误提示文本样式 | Error message text style -->
|
||||
<Style x:Key="ErrorText" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="Foreground" Value="#E53935" />
|
||||
<Setter Property="Margin" Value="0,0,0,4" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
|
||||
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
<Setter Property="Height" Value="28" />
|
||||
@@ -58,236 +73,401 @@
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧:配置面板 | Left: configuration panel -->
|
||||
<ColumnDefinition Width="220" />
|
||||
<!-- 分隔线 | Splitter -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- 右侧:矩阵网格 | Right: matrix grid -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 主内容区域 | Main content area -->
|
||||
<RowDefinition Height="*" />
|
||||
<!-- 底部状态栏 | Bottom status bar -->
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ 左侧:矩阵配置面板 | 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" />
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧:配置面板 | Left: configuration panel -->
|
||||
<ColumnDefinition Width="220" />
|
||||
<!-- 分隔线 | Splitter -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- 右侧:矩阵网格 | Right: matrix grid -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 行数 | 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" />
|
||||
<!-- ═══ 左侧:矩阵配置面板 | 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" />
|
||||
|
||||
<!-- 列数 | 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" />
|
||||
<!-- 行数 | Rows -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="行数 | Rows" />
|
||||
<TextBox
|
||||
Margin="0,2,0,2"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding Rows, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="矩阵行数 | Number of rows" />
|
||||
<!-- 行数错误提示 | Rows error message -->
|
||||
<TextBlock
|
||||
Style="{StaticResource ErrorText}"
|
||||
Text="{Binding RowsError}" />
|
||||
|
||||
<!-- 行间距 | 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" />
|
||||
<!-- 列数 | Columns -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="列数 | Columns" />
|
||||
<TextBox
|
||||
Margin="0,2,0,2"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding Columns, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="矩阵列数 | Number of columns" />
|
||||
<!-- 列数错误提示 | Columns error message -->
|
||||
<TextBlock
|
||||
Style="{StaticResource ErrorText}"
|
||||
Text="{Binding ColumnsError}" />
|
||||
|
||||
<!-- 列间距 | 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" />
|
||||
<!-- 行间距 | Row Spacing -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="行间距 (mm) | Row Spacing" />
|
||||
<TextBox
|
||||
Margin="0,2,0,2"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding RowSpacing, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="行间距(毫米)| Row spacing in mm" />
|
||||
<!-- 行间距错误提示 | Row spacing error message -->
|
||||
<TextBlock
|
||||
Style="{StaticResource ErrorText}"
|
||||
Text="{Binding RowSpacingError}" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,4,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
<!-- 列间距 | Column Spacing -->
|
||||
<TextBlock Style="{StaticResource ConfigLabel}" Text="列间距 (mm) | Col Spacing" />
|
||||
<TextBox
|
||||
Margin="0,2,0,2"
|
||||
Style="{StaticResource ConfigInput}"
|
||||
Text="{Binding ColumnSpacing, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="列间距(毫米)| Column spacing in mm" />
|
||||
<!-- 列间距错误提示 | Column spacing error message -->
|
||||
<TextBlock
|
||||
Style="{StaticResource ErrorText}"
|
||||
Text="{Binding ColSpacingError}" />
|
||||
|
||||
<!-- 操作按钮 | 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,4,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,8,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
<!-- 操作按钮 | Action buttons -->
|
||||
<Button
|
||||
Command="{Binding UpdateLayoutCommand}"
|
||||
Content="更新布局 | Update"
|
||||
IsEnabled="{Binding IsExecuting, Converter={StaticResource InverseBoolConverter}}"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="根据当前参数更新矩阵网格 | Update matrix grid with current parameters" />
|
||||
<Button
|
||||
Command="{Binding AssociateProgramCommand}"
|
||||
Content="关联程序 | Associate"
|
||||
IsEnabled="{Binding IsExecuting, Converter={StaticResource InverseBoolConverter}}"
|
||||
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" />
|
||||
|
||||
<!-- 选中单元格详情 | 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}'}" />
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,4,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 运行/停止按钮 | Run/Stop buttons -->
|
||||
<Button
|
||||
Command="{Binding RunMatrixCommand}"
|
||||
Content="▶ 运行矩阵 | Run"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="开始执行矩阵检测 | Start matrix execution" />
|
||||
<Button
|
||||
Command="{Binding StopCommand}"
|
||||
Content="■ 停止执行 | Stop"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="停止当前矩阵执行 | Stop current matrix execution"
|
||||
Visibility="{Binding IsExecuting, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,8,0,8"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<!-- 关联程序显示区域 | Associated program display area -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,4"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Foreground="#555"
|
||||
Text="关联程序 | Program" />
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
Margin="0,0,0,8">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="{Binding AssociatedProgramName}" />
|
||||
<Setter Property="Foreground" Value="#333" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AssociatedProgramName}" Value="{x:Null}">
|
||||
<Setter Property="Text" Value="请先关联 CNC 程序" />
|
||||
<Setter Property="Foreground" Value="#999" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding AssociatedProgramName}" Value="">
|
||||
<Setter Property="Text" Value="请先关联 CNC 程序" />
|
||||
<Setter Property="Foreground" Value="#999" />
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</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}" />
|
||||
|
||||
<!-- 分隔线 | Separator -->
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,0,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 ═══ -->
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 网格标题与统计 | Grid title and statistics -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 矩阵网格 | Matrix grid -->
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 矩阵网格标题和统计标签 | Matrix grid title and statistics label -->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="8,6,8,2"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="12"
|
||||
FontWeight="Bold"
|
||||
Foreground="#333"
|
||||
VerticalAlignment="Center"
|
||||
Text="矩阵网格" />
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
Foreground="#666"
|
||||
VerticalAlignment="Center">
|
||||
<Run Text="已启用:" /><Run Text="{Binding EnabledCount, Mode=OneWay}" /><Run Text=" / 共 " /><Run Text="{Binding TotalCount, Mode=OneWay}" /><Run Text=" 个位置" />
|
||||
</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"
|
||||
<!-- 矩阵网格内容 | Matrix grid content -->
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
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="White"
|
||||
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>
|
||||
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
|
||||
x:Name="CellIndexText"
|
||||
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
|
||||
x:Name="CellOffsetText"
|
||||
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>
|
||||
<!-- ═══ 执行状态颜色样式 | Execution status color styles ═══ -->
|
||||
|
||||
<!-- 未执行状态:默认白色背景 | NotExecuted: default white background -->
|
||||
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.NotExecuted}">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="White" />
|
||||
</DataTrigger>
|
||||
|
||||
<!-- 执行中状态:蓝色高亮 | Executing: blue highlight -->
|
||||
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Executing}">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="#2196F3" />
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#1565C0" />
|
||||
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
|
||||
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#E3F2FD" />
|
||||
</DataTrigger>
|
||||
|
||||
<!-- 已完成状态:绿色背景 | Completed: green background -->
|
||||
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Completed}">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="#4CAF50" />
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#2E7D32" />
|
||||
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
|
||||
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#E8F5E9" />
|
||||
</DataTrigger>
|
||||
|
||||
<!-- 错误状态:红色背景 + ToolTip 显示错误信息 | Error: red background + ToolTip with error message -->
|
||||
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Error}">
|
||||
<Setter TargetName="CellBorder" Property="Background" Value="#F44336" />
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#C62828" />
|
||||
<Setter TargetName="CellBorder" Property="ToolTip" Value="{Binding ErrorMessage}" />
|
||||
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
|
||||
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#FFEBEE" />
|
||||
</DataTrigger>
|
||||
|
||||
<!-- 已禁用状态:灰色半透明 | Disabled: gray semi-transparent -->
|
||||
<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 border -->
|
||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#5B9BD5" />
|
||||
<Setter TargetName="CellBorder" Property="BorderThickness" Value="2" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- ═══ 底部状态栏 | Bottom status bar ═══ -->
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
BorderBrush="{StaticResource SeparatorBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="8,4">
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
Foreground="#555"
|
||||
Text="{Binding StatusText}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
||||
@@ -20,15 +20,9 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<ToolBarTray Grid.Row="0" Background="#F5F5F5">
|
||||
<ToolBar Background="Transparent">
|
||||
<Button Content="💾 保存布局" Command="{Binding SaveLayoutCommand}" Padding="8,4" />
|
||||
<Separator />
|
||||
<Button Content="🔄 重置布局" Command="{Binding ResetLayoutCommand}" Padding="8,4" />
|
||||
<Separator />
|
||||
<Button Content="📤 导出全部" Command="{Binding ExportAllCommand}" Padding="8,4" />
|
||||
</ToolBar>
|
||||
</ToolBarTray>
|
||||
<Border Grid.Row="0" Background="#F5F5F5" BorderBrush="#E0E0E0" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Margin="4"/>
|
||||
</Border>
|
||||
|
||||
<!-- 主内容区:使用 Grid 布局精确控制各面板的位置和大小 -->
|
||||
<Grid Grid.Row="1" Margin="4">
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
IsReadOnly="True">
|
||||
<telerik:RadGridView.Columns>
|
||||
<telerik:GridViewDataColumn Header="时间" DataMemberBinding="{Binding Timestamp, Converter={StaticResource TimestampFormatConverter}}" Width="120" />
|
||||
<telerik:GridViewDataColumn Header="事件" DataMemberBinding="{Binding EventType}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="类别" DataMemberBinding="{Binding Category}" Width="140" />
|
||||
<telerik:GridViewDataColumn Header="字段" DataMemberBinding="{Binding FieldName}" Width="150" />
|
||||
<telerik:GridViewDataColumn Header="事件" DataMemberBinding="{Binding EventTypeDisplay}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="类别" DataMemberBinding="{Binding CategoryDisplay}" Width="140" />
|
||||
<telerik:GridViewDataColumn Header="字段" DataMemberBinding="{Binding FieldNameDisplay}" Width="150" />
|
||||
<telerik:GridViewDataColumn Header="旧值" DataMemberBinding="{Binding OldValue}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="新值" DataMemberBinding="{Binding NewValue}" Width="*" />
|
||||
</telerik:RadGridView.Columns>
|
||||
|
||||
@@ -21,51 +21,21 @@
|
||||
|
||||
<Grid Margin="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*" />
|
||||
<RowDefinition Height="3*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="{Binding MaxLatency, StringFormat=最大延迟: {0:F2} ms}" FontWeight="SemiBold" Margin="0,0,0,8" />
|
||||
|
||||
<TextBlock Text="{Binding MaxLatency, StringFormat=最大延迟: {0:F2} ms}" FontWeight="SemiBold" />
|
||||
|
||||
<telerik:RadGridView Grid.Row="1"
|
||||
Margin="0,8,0,0"
|
||||
ItemsSource="{Binding Metrics}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True">
|
||||
<telerik:RadGridView.Columns>
|
||||
<telerik:GridViewDataColumn Header="状态类型" DataMemberBinding="{Binding StateType}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="频率(Hz)" DataMemberBinding="{Binding EventsPerSecond}" Width="120" />
|
||||
<telerik:GridViewDataColumn Header="延迟(ms)" DataMemberBinding="{Binding AverageLatency}" Width="140" />
|
||||
</telerik:RadGridView.Columns>
|
||||
</telerik:RadGridView>
|
||||
</Grid>
|
||||
|
||||
<telerik:RadChart Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding TrendData}">
|
||||
<telerik:RadChart.DefaultView>
|
||||
<telerik:ChartDefaultView>
|
||||
<telerik:ChartDefaultView.ChartArea>
|
||||
<telerik:ChartArea />
|
||||
</telerik:ChartDefaultView.ChartArea>
|
||||
<telerik:ChartDefaultView.ChartLegend>
|
||||
<telerik:ChartLegend />
|
||||
</telerik:ChartDefaultView.ChartLegend>
|
||||
</telerik:ChartDefaultView>
|
||||
</telerik:RadChart.DefaultView>
|
||||
<telerik:RadChart.SeriesMappings>
|
||||
<telerik:SeriesMapping CollectionIndex="0">
|
||||
<telerik:SeriesMapping.SeriesDefinition>
|
||||
<telerik:LineSeriesDefinition />
|
||||
</telerik:SeriesMapping.SeriesDefinition>
|
||||
<telerik:ItemMapping DataPointMember="XValue" FieldName="Timestamp" />
|
||||
<telerik:ItemMapping DataPointMember="YValue" FieldName="Value" />
|
||||
</telerik:SeriesMapping>
|
||||
</telerik:RadChart.SeriesMappings>
|
||||
</telerik:RadChart>
|
||||
<telerik:RadGridView Grid.Row="1"
|
||||
ItemsSource="{Binding Metrics}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True">
|
||||
<telerik:RadGridView.Columns>
|
||||
<telerik:GridViewDataColumn Header="状态类型" DataMemberBinding="{Binding StateTypeDisplay}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="频率(Hz)" DataMemberBinding="{Binding EventsPerSecond}" Width="120" />
|
||||
<telerik:GridViewDataColumn Header="延迟(ms)" DataMemberBinding="{Binding AverageLatency}" Width="140" />
|
||||
</telerik:RadGridView.Columns>
|
||||
</telerik:RadGridView>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
mc:Ignorable="d"
|
||||
Title="Snapshot Differences"
|
||||
Title="快照差异对比"
|
||||
Width="900"
|
||||
Height="600"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
@@ -17,15 +17,15 @@
|
||||
|
||||
<telerik:RadGridView ItemsSource="{Binding Differences}" AutoGenerateColumns="False" IsReadOnly="True">
|
||||
<telerik:RadGridView.Columns>
|
||||
<telerik:GridViewDataColumn Header="Category" DataMemberBinding="{Binding Category}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="Field" DataMemberBinding="{Binding FieldName}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="Snapshot 1" DataMemberBinding="{Binding Value1}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="Snapshot 2" DataMemberBinding="{Binding Value2}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="类别" DataMemberBinding="{Binding CategoryDisplay}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="字段" DataMemberBinding="{Binding FieldNameDisplay}" Width="180" />
|
||||
<telerik:GridViewDataColumn Header="快照1" DataMemberBinding="{Binding Value1}" Width="*" />
|
||||
<telerik:GridViewDataColumn Header="快照2" DataMemberBinding="{Binding Value2}" Width="*" />
|
||||
</telerik:RadGridView.Columns>
|
||||
</telerik:RadGridView>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||
<Button Content="Export" Command="{Binding ExportDifferencesCommand}" Padding="12,4" />
|
||||
<Button Content="导出" Command="{Binding ExportDifferencesCommand}" Padding="12,4" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding Name}"
|
||||
Text="{Binding DisplayName}"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Name}"
|
||||
ToolTip="{Binding DisplayName}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Value}"
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding Name}"
|
||||
Text="{Binding DisplayName}"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding Name}"
|
||||
ToolTip="{Binding DisplayName}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Value}"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
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:helpers="clr-namespace:XplorePlane.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="300"
|
||||
@@ -80,6 +81,8 @@
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
IsEnabled="{Binding CanEditPipeline}"
|
||||
helpers:PermissionTooltipHelper.IsPermissionRestricted="True"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding NewPipelineCommand}"
|
||||
@@ -287,6 +290,8 @@
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="3"
|
||||
IsEnabled="{Binding CanEditPipeline}"
|
||||
helpers:PermissionTooltipHelper.IsPermissionRestricted="True"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="8,6">
|
||||
|
||||
@@ -153,6 +153,9 @@ namespace XplorePlane.Views
|
||||
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
|
||||
return;
|
||||
|
||||
if (!vm.CanEditPipeline)
|
||||
return;
|
||||
|
||||
// 双击切换启用/禁用
|
||||
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
|
||||
e.Handled = true;
|
||||
@@ -163,6 +166,9 @@ namespace XplorePlane.Views
|
||||
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
|
||||
return;
|
||||
|
||||
if (!vm.CanEditPipeline)
|
||||
return;
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -215,11 +221,17 @@ namespace XplorePlane.Views
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vm.CanEditPipeline)
|
||||
return;
|
||||
|
||||
vm.AddOperatorCommand.Execute(operatorKey);
|
||||
}
|
||||
|
||||
private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
|
||||
{
|
||||
if (!vm.CanEditPipeline)
|
||||
return;
|
||||
|
||||
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|
||||
|| !vm.PipelineNodes.Contains(draggedNode))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<Window x:Class="XplorePlane.Views.LoginDialog"
|
||||
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"
|
||||
mc:Ignorable="d"
|
||||
Title="用户登录"
|
||||
SizeToContent="WidthAndHeight"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="13"
|
||||
Background="#F5F5F5">
|
||||
|
||||
<Grid Margin="32,24,32,24" MinWidth="300">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="请输入密码"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<PasswordBox x:Name="PasswordInput"
|
||||
Grid.Row="1"
|
||||
Height="32"
|
||||
Padding="6,4"
|
||||
PasswordChanged="PasswordInput_PasswordChanged"
|
||||
KeyDown="PasswordInput_KeyDown"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- 错误消息 -->
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding ErrorMessage}"
|
||||
Foreground="#D32F2F"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,0,0,16">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasError}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<StackPanel Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right">
|
||||
<Button Content="登录"
|
||||
Width="80"
|
||||
Height="30"
|
||||
Command="{Binding LoginCommand}"
|
||||
IsDefault="True"
|
||||
Margin="0,0,8,0" />
|
||||
<Button Content="取消"
|
||||
Width="80"
|
||||
Height="30"
|
||||
Command="{Binding CancelCommand}"
|
||||
IsCancel="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// LoginDialog.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LoginDialog : Window
|
||||
{
|
||||
public LoginDialog(LoginDialogViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
|
||||
// 订阅 ViewModel 的 PropertyChanged,当 DialogResult 变化时关闭窗口
|
||||
viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
// 窗口加载后自动聚焦密码框
|
||||
Loaded += (_, _) => PasswordInput.Focus();
|
||||
}
|
||||
|
||||
private LoginDialogViewModel ViewModel => DataContext as LoginDialogViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 处理 PasswordBox 的 PasswordChanged 事件,将密码同步到 ViewModel。
|
||||
/// WPF 的 PasswordBox 不支持数据绑定,需要通过代码后置处理。
|
||||
/// </summary>
|
||||
private void PasswordInput_PasswordChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.Password = PasswordInput.Password;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理回车键快捷登录
|
||||
/// </summary>
|
||||
private void PasswordInput_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && ViewModel?.LoginCommand?.CanExecute() == true)
|
||||
{
|
||||
ViewModel.LoginCommand.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听 ViewModel 的 DialogResult 属性变化,设置窗口的 DialogResult 以关闭对话框。
|
||||
/// </summary>
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(LoginDialogViewModel.DialogResult))
|
||||
{
|
||||
var result = ViewModel?.DialogResult;
|
||||
if (result.HasValue)
|
||||
{
|
||||
DialogResult = result.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
xmlns:spreadsheetControls="clr-namespace:Telerik.Windows.Controls.Spreadsheet.Controls;assembly=Telerik.Windows.Controls.Spreadsheet"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
xmlns:mainViews="clr-namespace:XplorePlane.Views.Main"
|
||||
xmlns:views1="clr-namespace:XP.Hardware.RaySource.Views;assembly=XP.Hardware.RaySource"
|
||||
xmlns:mcViews="clr-namespace:XP.Hardware.MotionControl.Views;assembly=XP.Hardware.MotionControl"
|
||||
x:Name="ParentWindow"
|
||||
@@ -444,6 +445,12 @@
|
||||
SmallImage="/Assets/Icons/setting.png"
|
||||
Command="{Binding OpenSettingsCommand}"
|
||||
Text="设置" />
|
||||
<telerik:RadRibbonButton
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenPasswordManagementCommand}"
|
||||
Text="密码管理"
|
||||
Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup
|
||||
@@ -452,7 +459,8 @@
|
||||
DialogLauncherCommandParameter="Alignment"
|
||||
DialogLauncherVisibility="Collapsed"
|
||||
Header="硬件"
|
||||
IsEnabled="True">
|
||||
IsEnabled="True"
|
||||
Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
@@ -593,7 +601,8 @@
|
||||
Command="{Binding OpenReportConfigCommand}"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Text="报告生成" />
|
||||
Text="报告生成"
|
||||
Visibility="{Binding IsReportSettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</telerik:RadRibbonGroup>
|
||||
</telerik:RadRibbonTab>
|
||||
<telerik:RadRibbonTab Header="帮助">
|
||||
@@ -616,6 +625,14 @@
|
||||
</telerik:RadRibbonTab>
|
||||
</telerik:RadRibbonView>
|
||||
|
||||
<!-- Ribbon 右侧状态区域:显示当前角色和切换用户按钮 -->
|
||||
<mainViews:RibbonStatusAreaView
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,6,12,0"
|
||||
DataContext="{Binding RibbonStatusArea}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<UserControl
|
||||
x:Class="XplorePlane.Views.Main.RibbonStatusAreaView"
|
||||
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"
|
||||
d:DesignHeight="30"
|
||||
d:DesignWidth="200"
|
||||
mc:Ignorable="d">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0">
|
||||
<TextBlock
|
||||
Text="{Binding CurrentRoleDisplayName}"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"
|
||||
Margin="0,0,8,0" />
|
||||
<Button
|
||||
Content="切换用户"
|
||||
Command="{Binding SwitchUserCommand}"
|
||||
IsEnabled="{Binding CanSwitchUser}"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
Padding="8,3"
|
||||
MinWidth="60" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace XplorePlane.Views.Main
|
||||
{
|
||||
/// <summary>
|
||||
/// Ribbon 右侧状态区域视图。
|
||||
/// 显示当前角色名称和"切换用户"按钮。
|
||||
/// </summary>
|
||||
public partial class RibbonStatusAreaView : UserControl
|
||||
{
|
||||
public RibbonStatusAreaView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
x:Name="RoiCanvas"
|
||||
Background="White"
|
||||
ImageSource="{Binding ImageSource}"
|
||||
IsWheelZoomEnabled="{Binding IsWheelZoomEnabled}"
|
||||
ScaleBarMmPerPixel="0.139"
|
||||
ShowScaleBar="{Binding DataContext.IsScaleBarVisible, RelativeSource={RelativeSource AncestorType=Window}}" />
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<Window
|
||||
x:Class="XplorePlane.Views.Setting.PasswordManagementView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="密码管理"
|
||||
Width="480"
|
||||
Height="360"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="13">
|
||||
<Window.Resources>
|
||||
<Style x:Key="MessageStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Margin" Value="0,2,0,0" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource Self}}" Value="True">
|
||||
<Setter Property="Foreground" Value="#CC0000" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource Self}}" Value="False">
|
||||
<Setter Property="Foreground" Value="#008000" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="24,20,24,20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 超级管理员 -->
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock Text="超级管理员密码" FontWeight="SemiBold" Margin="0,0,0,6" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Height="28"
|
||||
VerticalContentAlignment="Center"
|
||||
Padding="6,0"
|
||||
Text="{Binding SuperAdminPassword, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="修改"
|
||||
Width="60"
|
||||
Height="28"
|
||||
Command="{Binding ChangeSuperAdminPasswordCommand}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="{Binding SuperAdminMessage}"
|
||||
Tag="{Binding SuperAdminMessageIsError}"
|
||||
Style="{StaticResource MessageStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 管理员 -->
|
||||
<StackPanel Grid.Row="2">
|
||||
<TextBlock Text="管理员密码" FontWeight="SemiBold" Margin="0,0,0,6" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Height="28"
|
||||
VerticalContentAlignment="Center"
|
||||
Padding="6,0"
|
||||
Text="{Binding AdminPassword, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="修改"
|
||||
Width="60"
|
||||
Height="28"
|
||||
Command="{Binding ChangeAdminPasswordCommand}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="{Binding AdminMessage}"
|
||||
Tag="{Binding AdminMessageIsError}"
|
||||
Style="{StaticResource MessageStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 普通用户 -->
|
||||
<StackPanel Grid.Row="4">
|
||||
<TextBlock Text="用户密码" FontWeight="SemiBold" Margin="0,0,0,6" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Height="28"
|
||||
VerticalContentAlignment="Center"
|
||||
Padding="6,0"
|
||||
Text="{Binding UserPassword, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="修改"
|
||||
Width="60"
|
||||
Height="28"
|
||||
Command="{Binding ChangeUserPasswordCommand}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Text="{Binding UserMessage}"
|
||||
Tag="{Binding UserMessageIsError}"
|
||||
Style="{StaticResource MessageStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<Button
|
||||
Grid.Row="6"
|
||||
Content="关闭"
|
||||
Width="80"
|
||||
Height="30"
|
||||
HorizontalAlignment="Right"
|
||||
Click="CloseButton_Click" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace XplorePlane.Views.Setting
|
||||
{
|
||||
public partial class PasswordManagementView : Window
|
||||
{
|
||||
public PasswordManagementView(object viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,16 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="系统设置"
|
||||
Width="1000"
|
||||
Width="800"
|
||||
Height="700"
|
||||
MinWidth="900"
|
||||
MinWidth="800"
|
||||
MinHeight="600"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False"
|
||||
Background="#F5F5F5">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
@@ -294,7 +296,7 @@
|
||||
</TabItem>
|
||||
|
||||
<!-- 射线源设置 -->
|
||||
<TabItem Header="射线源">
|
||||
<TabItem Header="射线源" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
@@ -368,7 +370,7 @@
|
||||
</TabItem>
|
||||
|
||||
<!-- 探测器设置 -->
|
||||
<TabItem Header="探测器">
|
||||
<TabItem Header="探测器" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
@@ -452,7 +454,7 @@
|
||||
</TabItem>
|
||||
|
||||
<!-- PLC设置 -->
|
||||
<TabItem Header="PLC">
|
||||
<TabItem Header="PLC" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
|
||||
Reference in New Issue
Block a user