基于角色的权限控制
1、用户角色枚举、权限枚举、结果记录和密码存储模型 IPermissionService 接口及包含认证、权限检查、密码管理和登出功能的 PermissionService 单例 2、支持层级化角色-权限映射的权限矩阵(SuperAdmin ⊇ Admin ⊇ User) 密码持久化至 passwords.json 文件,并提供工厂默认值回退机制 3、UI 层 LoginDialog — 启动时弹出模态登录对话框,支持密码掩码输入、错误提示以及取消退出功能 RibbonStatusAreaView — 在Ribbon右侧区域始终显示角色标签和“切换用户”按钮 权限感知的CncEditorViewModel — 用户角色无法使用CNC编辑控件 权限感知的CncInspectionModulePipelineViewModel — 用户角色无法进行流程编辑 设置导航可见性 — Admin/User角色隐藏Factory_Settings,User角色隐藏Report_Settings PasswordManagementView — 仅SuperAdmin可访问的修改角色密码对话框 PermissionTooltipHelper — 附加属性,在禁用控件上显示“当前角色无权访问此功能”提示
This commit is contained in:
@@ -2,8 +2,10 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
using XplorePlane.Tests.Helpers;
|
using XplorePlane.Tests.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -24,7 +26,11 @@ namespace XplorePlane.Tests.Pipeline
|
|||||||
Directory.CreateDirectory(_tempDir);
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
|
||||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
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()
|
public void Dispose()
|
||||||
@@ -206,10 +212,14 @@ namespace XplorePlane.Tests.Pipeline
|
|||||||
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
|
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
|
||||||
{
|
{
|
||||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" });
|
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>();
|
var mockDataPathSvc = new Mock<IXpDataPathService>();
|
||||||
mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir);
|
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"));
|
await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm"));
|
||||||
|
|
||||||
var result = await service.LoadAllAsync(null);
|
var result = await service.LoadAllAsync(null);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
|||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
using XplorePlane.Tests.Helpers;
|
using XplorePlane.Tests.Helpers;
|
||||||
using XplorePlane.ViewModels;
|
using XplorePlane.ViewModels;
|
||||||
@@ -181,7 +182,11 @@ namespace XplorePlane.Tests.Pipeline
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
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}" };
|
var model = new PipelineModel { Name = $"Pipeline_{nodeCount}" };
|
||||||
for (int i = 0; i < nodeCount; i++)
|
for (int i = 0; i < nodeCount; i++)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using XP.Hardware.RaySource.Services;
|
|||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
using XplorePlane.Services.Cnc;
|
using XplorePlane.Services.Cnc;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace XplorePlane.Tests.Services
|
namespace XplorePlane.Tests.Services
|
||||||
@@ -24,7 +25,10 @@ namespace XplorePlane.Tests.Services
|
|||||||
var logger = new Mock<ILoggerService>();
|
var logger = new Mock<ILoggerService>();
|
||||||
logger.Setup(l => l.ForModule<CncProgramService>()).Returns(logger.Object);
|
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(
|
var program = new CncProgram(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
"Program",
|
"Program",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using XplorePlane.Services.AppState;
|
|||||||
using XplorePlane.Services.Cnc;
|
using XplorePlane.Services.Cnc;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.ViewModels.Cnc;
|
using XplorePlane.ViewModels.Cnc;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -36,10 +37,14 @@ namespace XplorePlane.Tests.ViewModels
|
|||||||
var mockAppState = new Mock<IAppStateService>();
|
var mockAppState = new Mock<IAppStateService>();
|
||||||
var mockLogger = new Mock<ILoggerService>();
|
var mockLogger = new Mock<ILoggerService>();
|
||||||
var mockDataPathService = new Mock<IXpDataPathService>();
|
var mockDataPathService = new Mock<IXpDataPathService>();
|
||||||
|
var mockPermissionService = new Mock<IPermissionService>();
|
||||||
mockPipelinePersistenceService ??= new Mock<IPipelinePersistenceService>();
|
mockPipelinePersistenceService ??= new Mock<IPipelinePersistenceService>();
|
||||||
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
|
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
|
||||||
mockDataPathService.SetupGet(s => s.PlanPath).Returns(System.IO.Path.GetTempPath());
|
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>();
|
mockExecSvc ??= new Mock<ICncExecutionService>();
|
||||||
|
|
||||||
// Setup CreateProgram so ExecuteNewProgram works
|
// Setup CreateProgram so ExecuteNewProgram works
|
||||||
@@ -95,7 +100,8 @@ namespace XplorePlane.Tests.ViewModels
|
|||||||
mockLogger.Object,
|
mockLogger.Object,
|
||||||
mockExecSvc.Object,
|
mockExecSvc.Object,
|
||||||
mockDataPathService.Object,
|
mockDataPathService.Object,
|
||||||
mockPipelinePersistenceService.Object);
|
mockPipelinePersistenceService.Object,
|
||||||
|
mockPermissionService.Object);
|
||||||
|
|
||||||
if (initialProgram != null)
|
if (initialProgram != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ using XP.Hardware.RaySource.Services;
|
|||||||
using XP.ReportEngine;
|
using XP.ReportEngine;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Camera;
|
using XplorePlane.Services.Camera;
|
||||||
using XplorePlane.Services.Cnc;
|
using XplorePlane.Services.Cnc;
|
||||||
using XplorePlane.Services.Debug;
|
using XplorePlane.Services.Debug;
|
||||||
@@ -56,6 +57,7 @@ using XplorePlane.ViewModels;
|
|||||||
using XplorePlane.ViewModels.Cnc;
|
using XplorePlane.ViewModels.Cnc;
|
||||||
using XplorePlane.ViewModels.Debug;
|
using XplorePlane.ViewModels.Debug;
|
||||||
using XplorePlane.ViewModels.ImageProcessing;
|
using XplorePlane.ViewModels.ImageProcessing;
|
||||||
|
using XplorePlane.ViewModels.Main;
|
||||||
using XplorePlane.Views;
|
using XplorePlane.Views;
|
||||||
using XplorePlane.Views.Cnc;
|
using XplorePlane.Views.Cnc;
|
||||||
using XplorePlane.Views.Debug;
|
using XplorePlane.Views.Debug;
|
||||||
@@ -346,6 +348,22 @@ namespace XplorePlane
|
|||||||
// return null;
|
// return null;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
// ── 登录认证:在显示主窗口前弹出登录对话框 ──
|
||||||
|
var loginViewModel = Container.Resolve<LoginDialogViewModel>();
|
||||||
|
var loginDialog = new LoginDialog(loginViewModel);
|
||||||
|
var loginResult = loginDialog.ShowDialog();
|
||||||
|
|
||||||
|
if (loginResult != true)
|
||||||
|
{
|
||||||
|
// 用户取消登录,退出应用
|
||||||
|
Log.Information("用户取消登录,应用程序退出");
|
||||||
|
Application.Current.Shutdown();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("登录认证成功,角色: {Role}",
|
||||||
|
Container.Resolve<IPermissionService>().CurrentRole);
|
||||||
|
|
||||||
var shell = Container.Resolve<MainWindow>();
|
var shell = Container.Resolve<MainWindow>();
|
||||||
|
|
||||||
// 主窗体加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
// 主窗体加载完成后再连接相机,确保所有模块和原生 DLL 已完成初始化
|
||||||
@@ -668,6 +686,18 @@ namespace XplorePlane
|
|||||||
// ── 录制服务(单例)──
|
// ── 录制服务(单例)──
|
||||||
containerRegistry.RegisterSingleton<IViewportRecordingService, ViewportRecordingService>();
|
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("依赖注入容器配置完成");
|
Log.Information("依赖注入容器配置完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 XP.Hardware.RaySource.Services;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.Services.Cnc
|
namespace XplorePlane.Services.Cnc
|
||||||
{
|
{
|
||||||
@@ -23,6 +25,7 @@ namespace XplorePlane.Services.Cnc
|
|||||||
private readonly IAppStateService _appStateService;
|
private readonly IAppStateService _appStateService;
|
||||||
private readonly IRaySourceService _raySourceService;
|
private readonly IRaySourceService _raySourceService;
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
|
|
||||||
// ── 序列化配置 | Serialization options ──
|
// ── 序列化配置 | Serialization options ──
|
||||||
private static readonly JsonSerializerOptions CncJsonOptions = new()
|
private static readonly JsonSerializerOptions CncJsonOptions = new()
|
||||||
@@ -35,15 +38,18 @@ namespace XplorePlane.Services.Cnc
|
|||||||
public CncProgramService(
|
public CncProgramService(
|
||||||
IAppStateService appStateService,
|
IAppStateService appStateService,
|
||||||
IRaySourceService raySourceService,
|
IRaySourceService raySourceService,
|
||||||
ILoggerService logger)
|
ILoggerService logger,
|
||||||
|
IPermissionService permissionService)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(appStateService);
|
ArgumentNullException.ThrowIfNull(appStateService);
|
||||||
ArgumentNullException.ThrowIfNull(raySourceService);
|
ArgumentNullException.ThrowIfNull(raySourceService);
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
ArgumentNullException.ThrowIfNull(permissionService);
|
||||||
|
|
||||||
_appStateService = appStateService;
|
_appStateService = appStateService;
|
||||||
_raySourceService = raySourceService;
|
_raySourceService = raySourceService;
|
||||||
_logger = logger.ForModule<CncProgramService>();
|
_logger = logger.ForModule<CncProgramService>();
|
||||||
|
_permissionService = permissionService;
|
||||||
|
|
||||||
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
|
||||||
}
|
}
|
||||||
@@ -51,6 +57,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CncProgram CreateProgram(string name)
|
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))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
|
throw new ArgumentException("程序名称不能为空 | Program name cannot be empty", nameof(name));
|
||||||
|
|
||||||
@@ -122,6 +135,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node)
|
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(program);
|
||||||
ArgumentNullException.ThrowIfNull(node);
|
ArgumentNullException.ThrowIfNull(node);
|
||||||
|
|
||||||
@@ -156,6 +176,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CncProgram RemoveNode(CncProgram program, int index)
|
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);
|
ArgumentNullException.ThrowIfNull(program);
|
||||||
|
|
||||||
if (index < 0 || index >= program.Nodes.Count)
|
if (index < 0 || index >= program.Nodes.Count)
|
||||||
@@ -179,6 +206,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex)
|
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);
|
ArgumentNullException.ThrowIfNull(program);
|
||||||
|
|
||||||
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
|
if (oldIndex < 0 || oldIndex >= program.Nodes.Count)
|
||||||
@@ -208,6 +242,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
|
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(program);
|
||||||
ArgumentNullException.ThrowIfNull(node);
|
ArgumentNullException.ThrowIfNull(node);
|
||||||
|
|
||||||
@@ -234,6 +275,13 @@ namespace XplorePlane.Services.Cnc
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SaveAsync(CncProgram program, string filePath)
|
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(program);
|
||||||
ArgumentNullException.ThrowIfNull(filePath);
|
ArgumentNullException.ThrowIfNull(filePath);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.Services
|
namespace XplorePlane.Services
|
||||||
{
|
{
|
||||||
public class PipelinePersistenceService : IPipelinePersistenceService
|
public class PipelinePersistenceService : IPipelinePersistenceService
|
||||||
{
|
{
|
||||||
private readonly IImageProcessingService _imageProcessingService;
|
private readonly IImageProcessingService _imageProcessingService;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
private readonly string _baseDirectory;
|
private readonly string _baseDirectory;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
@@ -23,21 +28,38 @@ namespace XplorePlane.Services
|
|||||||
Converters = { new JsonStringEnumConverter() }
|
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));
|
_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(
|
_baseDirectory = baseDirectory ?? Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
"XplorePlane", "Pipelines");
|
"XplorePlane", "Pipelines");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PipelinePersistenceService(IImageProcessingService imageProcessingService, IXpDataPathService dataPathService)
|
public PipelinePersistenceService(
|
||||||
: this(imageProcessingService, dataPathService?.ToolsPath)
|
IImageProcessingService imageProcessingService,
|
||||||
|
IPermissionService permissionService,
|
||||||
|
ILoggerService logger,
|
||||||
|
IXpDataPathService dataPathService)
|
||||||
|
: this(imageProcessingService, permissionService, logger, dataPathService?.ToolsPath)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(PipelineModel pipeline, string filePath)
|
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));
|
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
|
||||||
ValidatePath(filePath);
|
ValidatePath(filePath);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ using XplorePlane.Models;
|
|||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
using XplorePlane.Services.Cnc;
|
using XplorePlane.Services.Cnc;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.ViewModels.Cnc
|
namespace XplorePlane.ViewModels.Cnc
|
||||||
{
|
{
|
||||||
@@ -33,6 +35,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
private readonly IXpDataPathService _dataPathService;
|
private readonly IXpDataPathService _dataPathService;
|
||||||
private readonly IPipelinePersistenceService _pipelinePersistenceService;
|
private readonly IPipelinePersistenceService _pipelinePersistenceService;
|
||||||
private readonly IImageProcessingService _imageProcessingService;
|
private readonly IImageProcessingService _imageProcessingService;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
|
|
||||||
private CncProgram _currentProgram;
|
private CncProgram _currentProgram;
|
||||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||||
@@ -59,6 +62,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
ICncExecutionService cncExecutionService,
|
ICncExecutionService cncExecutionService,
|
||||||
IXpDataPathService dataPathService,
|
IXpDataPathService dataPathService,
|
||||||
IPipelinePersistenceService pipelinePersistenceService,
|
IPipelinePersistenceService pipelinePersistenceService,
|
||||||
|
IPermissionService permissionService,
|
||||||
IImageProcessingService imageProcessingService = null)
|
IImageProcessingService imageProcessingService = null)
|
||||||
{
|
{
|
||||||
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
|
||||||
@@ -68,6 +72,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
|
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
|
||||||
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
|
||||||
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
|
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
|
||||||
|
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||||
_imageProcessingService = imageProcessingService; // optional — used for pipeline step display names
|
_imageProcessingService = imageProcessingService; // optional — used for pipeline step display names
|
||||||
|
|
||||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||||
@@ -77,15 +82,15 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
|
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
|
||||||
};
|
};
|
||||||
|
|
||||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint), () => !IsRunning);
|
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage), () => !IsRunning);
|
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode), () => !IsRunning);
|
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition), () => !IsRunning);
|
InsertSavePositionCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SavePosition), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule), () => !IsRunning);
|
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionModule), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker), () => !IsRunning);
|
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.InspectionMarker), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog), () => !IsRunning);
|
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.PauseDialog), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay), () => !IsRunning);
|
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay), () => !IsRunning && CanEditCncProgram);
|
||||||
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram), () => !IsRunning);
|
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram), () => !IsRunning && CanEditCncProgram);
|
||||||
|
|
||||||
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
|
||||||
.ObservesProperty(() => SelectedNode);
|
.ObservesProperty(() => SelectedNode);
|
||||||
@@ -94,14 +99,17 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
|
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
|
||||||
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
|
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());
|
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||||
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
|
NewProgramCommand = new DelegateCommand(ExecuteNewProgram, () => CanEditCncProgram);
|
||||||
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
|
||||||
|
|
||||||
RunCncCommand = new DelegateCommand(async () => await ExecuteRunAsync(), CanExecuteRun);
|
RunCncCommand = new DelegateCommand(async () => await ExecuteRunAsync(), CanExecuteRun);
|
||||||
StopCncCommand = new DelegateCommand(ExecuteStop, CanExecuteStop);
|
StopCncCommand = new DelegateCommand(ExecuteStop, CanExecuteStop);
|
||||||
|
|
||||||
|
// Subscribe to role changes to refresh permission-dependent properties
|
||||||
|
_eventAggregator.GetEvent<RoleChangedEvent>().Subscribe(OnRoleChanged);
|
||||||
|
|
||||||
_logger.Info("CncEditorViewModel initialized");
|
_logger.Info("CncEditorViewModel initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +199,12 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
private set => SetProperty(ref _hasExecutionError, value);
|
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 InsertReferencePointCommand { get; }
|
||||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||||
@@ -274,6 +288,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
private bool CanExecuteDeleteNode()
|
private bool CanExecuteDeleteNode()
|
||||||
{
|
{
|
||||||
return !IsRunning
|
return !IsRunning
|
||||||
|
&& CanEditCncProgram
|
||||||
&& SelectedNode != null
|
&& SelectedNode != null
|
||||||
&& _currentProgram != null
|
&& _currentProgram != null
|
||||||
&& _currentProgram.Nodes.Count > 1;
|
&& _currentProgram.Nodes.Count > 1;
|
||||||
@@ -281,7 +296,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
|
|
||||||
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
|
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;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -305,7 +320,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
|
|
||||||
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
|
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;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -615,10 +630,19 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
DeleteNodeCommand.RaiseCanExecuteChanged();
|
DeleteNodeCommand.RaiseCanExecuteChanged();
|
||||||
MoveNodeUpCommand.RaiseCanExecuteChanged();
|
MoveNodeUpCommand.RaiseCanExecuteChanged();
|
||||||
MoveNodeDownCommand.RaiseCanExecuteChanged();
|
MoveNodeDownCommand.RaiseCanExecuteChanged();
|
||||||
|
SaveProgramCommand.RaiseCanExecuteChanged();
|
||||||
|
NewProgramCommand.RaiseCanExecuteChanged();
|
||||||
RunCncCommand.RaiseCanExecuteChanged();
|
RunCncCommand.RaiseCanExecuteChanged();
|
||||||
StopCncCommand.RaiseCanExecuteChanged();
|
StopCncCommand.RaiseCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnRoleChanged(RoleChangedPayload payload)
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(CanEditCncProgram));
|
||||||
|
RaisePropertyChanged(nameof(CanEditInspectionModule));
|
||||||
|
RaiseEditCommandsCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnProgramEdited()
|
private void OnProgramEdited()
|
||||||
{
|
{
|
||||||
IsModified = true;
|
IsModified = true;
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ using XplorePlane.Events;
|
|||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services;
|
using XplorePlane.Services;
|
||||||
using XplorePlane.Services.MainViewport;
|
using XplorePlane.Services.MainViewport;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.ViewModels.Cnc
|
namespace XplorePlane.ViewModels.Cnc
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
private readonly IPipelineExecutionService _executionService;
|
private readonly IPipelineExecutionService _executionService;
|
||||||
private readonly IMainViewportService _mainViewportService;
|
private readonly IMainViewportService _mainViewportService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
private CncNodeViewModel _activeModuleNode;
|
private CncNodeViewModel _activeModuleNode;
|
||||||
@@ -50,7 +53,8 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
ILoggerService logger,
|
ILoggerService logger,
|
||||||
IPipelineExecutionService executionService = null,
|
IPipelineExecutionService executionService = null,
|
||||||
IMainViewportService mainViewportService = null,
|
IMainViewportService mainViewportService = null,
|
||||||
IEventAggregator eventAggregator = null)
|
IEventAggregator eventAggregator = null,
|
||||||
|
IPermissionService permissionService = null)
|
||||||
{
|
{
|
||||||
_editorViewModel = editorViewModel ?? throw new ArgumentNullException(nameof(editorViewModel));
|
_editorViewModel = editorViewModel ?? throw new ArgumentNullException(nameof(editorViewModel));
|
||||||
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
|
||||||
@@ -59,6 +63,7 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
_executionService = executionService;
|
_executionService = executionService;
|
||||||
_mainViewportService = mainViewportService;
|
_mainViewportService = mainViewportService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
|
_permissionService = permissionService;
|
||||||
|
|
||||||
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
|
||||||
|
|
||||||
@@ -84,6 +89,9 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
|
|
||||||
_eventAggregator?.GetEvent<AddOperatorRequestedEvent>()
|
_eventAggregator?.GetEvent<AddOperatorRequestedEvent>()
|
||||||
.Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); });
|
.Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); });
|
||||||
|
|
||||||
|
_eventAggregator?.GetEvent<RoleChangedEvent>()
|
||||||
|
.Subscribe(OnRoleChanged, ThreadOption.UIThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
|
public ObservableCollection<PipelineNodeViewModel> PipelineNodes { get; }
|
||||||
@@ -150,6 +158,12 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
|
|
||||||
public Visibility EmptyStateVisibility => HasActiveModule ? Visibility.Collapsed : Visibility.Visible;
|
public Visibility EmptyStateVisibility => HasActiveModule ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前角色是否有权编辑检测模块流水线(添加/删除/排序/启停算子、编辑参数)。
|
||||||
|
/// </summary>
|
||||||
|
public bool CanEditPipeline =>
|
||||||
|
_permissionService?.HasPermission(PermissionEnum.InspectionAddOperator) ?? true;
|
||||||
|
|
||||||
public ICommand AddOperatorCommand { get; }
|
public ICommand AddOperatorCommand { get; }
|
||||||
|
|
||||||
public ICommand RemoveOperatorCommand { get; }
|
public ICommand RemoveOperatorCommand { get; }
|
||||||
@@ -686,6 +700,11 @@ namespace XplorePlane.ViewModels.Cnc
|
|||||||
(AddOperatorCommand as DelegateCommand<string>)?.RaiseCanExecuteChanged();
|
(AddOperatorCommand as DelegateCommand<string>)?.RaiseCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnRoleChanged(RoleChangedPayload payload)
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(CanEditPipeline));
|
||||||
|
}
|
||||||
|
|
||||||
private static object ConvertSavedValue(object savedValue, Type targetType)
|
private static object ConvertSavedValue(object savedValue, Type targetType)
|
||||||
{
|
{
|
||||||
if (savedValue is not JsonElement jsonElement)
|
if (savedValue is not JsonElement jsonElement)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,11 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
string PipelineFileDisplayName { get; }
|
string PipelineFileDisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前角色是否有权编辑流水线。为 false 时禁用所有编辑操作。
|
||||||
|
/// </summary>
|
||||||
|
bool CanEditPipeline { get; }
|
||||||
|
|
||||||
ICommand AddOperatorCommand { get; }
|
ICommand AddOperatorCommand { get; }
|
||||||
|
|
||||||
ICommand RemoveOperatorCommand { get; }
|
ICommand RemoveOperatorCommand { get; }
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ namespace XplorePlane.ViewModels
|
|||||||
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
|
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
|
||||||
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
|
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
|
||||||
|
|
||||||
|
/// <summary>独立流水线编辑器始终允许编辑(不受角色限制)。</summary>
|
||||||
|
public bool CanEditPipeline => true;
|
||||||
|
|
||||||
// ── Command Implementations ───────────────────────────────────
|
// ── Command Implementations ───────────────────────────────────
|
||||||
|
|
||||||
private bool CanAddOperator(string operatorKey) =>
|
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;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Threading;
|
||||||
using XP.Camera.Calibration;
|
using XP.Camera.Calibration;
|
||||||
using XP.Common.GeneralForm.Views;
|
using XP.Common.GeneralForm.Views;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
using XP.Common.PdfViewer.Interfaces;
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
using XP.Hardware.MotionControl.Abstractions;
|
using XP.Hardware.MotionControl.Abstractions;
|
||||||
using XP.Hardware.MotionControl.Services;
|
using XP.Hardware.MotionControl.Services;
|
||||||
|
using XP.ImageProcessing.Processors;
|
||||||
using XplorePlane.Events;
|
using XplorePlane.Events;
|
||||||
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services.MainViewport;
|
using XplorePlane.Services.MainViewport;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
using XplorePlane.Services.Recording;
|
using XplorePlane.Services.Recording;
|
||||||
using XplorePlane.Services.Storage;
|
using XplorePlane.Services.Storage;
|
||||||
using XplorePlane.ViewModels.Cnc;
|
using XplorePlane.ViewModels.Cnc;
|
||||||
using XplorePlane.ViewModels.Debug;
|
using XplorePlane.ViewModels.Debug;
|
||||||
|
using XplorePlane.ViewModels.ImageProcessing;
|
||||||
|
using XplorePlane.ViewModels.Main;
|
||||||
using XplorePlane.Views;
|
using XplorePlane.Views;
|
||||||
using XplorePlane.Views.Cnc;
|
using XplorePlane.Views.Cnc;
|
||||||
using XplorePlane.Views.Debug;
|
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 XplorePlane.Views.ImageProcessing;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.ViewModels
|
namespace XplorePlane.ViewModels
|
||||||
{
|
{
|
||||||
@@ -54,6 +49,7 @@ namespace XplorePlane.ViewModels
|
|||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly IMainViewportService _mainViewportService;
|
private readonly IMainViewportService _mainViewportService;
|
||||||
private readonly IXpDataPathService _xpDataPathService;
|
private readonly IXpDataPathService _xpDataPathService;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
private readonly CncEditorViewModel _cncEditorViewModel;
|
private readonly CncEditorViewModel _cncEditorViewModel;
|
||||||
private readonly CncPageView _cncPageView;
|
private readonly CncPageView _cncPageView;
|
||||||
private readonly PipelineEditorViewModel _pipelineEditorViewModel;
|
private readonly PipelineEditorViewModel _pipelineEditorViewModel;
|
||||||
@@ -199,6 +195,30 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
public DelegateCommand OpenRealTimeLogViewerCommand { get; }
|
||||||
public DelegateCommand UseLiveDetectorSourceCommand { 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
|
public bool IsMainViewportRealtimeEnabled
|
||||||
{
|
{
|
||||||
@@ -303,6 +323,7 @@ namespace XplorePlane.ViewModels
|
|||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
IMainViewportService mainViewportService,
|
IMainViewportService mainViewportService,
|
||||||
IXpDataPathService xpDataPathService,
|
IXpDataPathService xpDataPathService,
|
||||||
|
IPermissionService permissionService,
|
||||||
IViewportRecordingService recordingService)
|
IViewportRecordingService recordingService)
|
||||||
{
|
{
|
||||||
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
@@ -310,6 +331,7 @@ namespace XplorePlane.ViewModels
|
|||||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||||
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
|
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
|
||||||
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
|
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
|
||||||
|
_permissionService = permissionService ?? throw new ArgumentNullException(nameof(permissionService));
|
||||||
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
|
||||||
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
|
||||||
_pipelineEditorViewModel = _containerProvider.Resolve<PipelineEditorViewModel>();
|
_pipelineEditorViewModel = _containerProvider.Resolve<PipelineEditorViewModel>();
|
||||||
@@ -431,6 +453,7 @@ namespace XplorePlane.ViewModels
|
|||||||
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
||||||
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
|
||||||
UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource);
|
UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource);
|
||||||
|
OpenPasswordManagementCommand = new DelegateCommand(ExecuteOpenPasswordManagement);
|
||||||
|
|
||||||
ImagePanelContent = _pipelineEditorView;
|
ImagePanelContent = _pipelineEditorView;
|
||||||
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
|
||||||
@@ -445,9 +468,22 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
ToggleRecordingCommand = new DelegateCommand(ExecuteToggleRecording, CanExecuteToggleRecording);
|
ToggleRecordingCommand = new DelegateCommand(ExecuteToggleRecording, CanExecuteToggleRecording);
|
||||||
|
|
||||||
|
// 订阅角色变更事件,刷新设置导航可见性
|
||||||
|
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||||
|
.Subscribe(OnRoleChangedForSettings, ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 初始化设置导航可见性
|
||||||
|
RefreshSettingsVisibility();
|
||||||
|
|
||||||
|
// 初始化 Ribbon 右侧状态区域 ViewModel
|
||||||
|
RibbonStatusArea = _containerProvider.Resolve<RibbonStatusAreaViewModel>();
|
||||||
|
|
||||||
_logger.Info("MainViewModel 已初始化");
|
_logger.Info("MainViewModel 已初始化");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Ribbon 右侧状态区域 ViewModel(角色显示 + 切换用户按钮)。</summary>
|
||||||
|
public RibbonStatusAreaViewModel RibbonStatusArea { get; private set; }
|
||||||
|
|
||||||
public string CncStatusMessage => _cncEditorViewModel.StatusMessage;
|
public string CncStatusMessage => _cncEditorViewModel.StatusMessage;
|
||||||
public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError;
|
public bool CncHasExecutionError => _cncEditorViewModel.HasExecutionError;
|
||||||
|
|
||||||
@@ -622,6 +658,9 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
private void ExecuteOpenCameraSettings()
|
private void ExecuteOpenCameraSettings()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmFactorySettingsNavigation("CameraSettings"))
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vm = _containerProvider.Resolve<NavigationPropertyPanelViewModel>();
|
var vm = _containerProvider.Resolve<NavigationPropertyPanelViewModel>();
|
||||||
@@ -909,6 +948,9 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
private void ExecuteOpenDetectorConfig()
|
private void ExecuteOpenDetectorConfig()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmFactorySettingsNavigation("DetectorConfig"))
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w,
|
ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w,
|
||||||
@@ -924,24 +966,36 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
private void ExecuteOpenMotionDebug()
|
private void ExecuteOpenMotionDebug()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmFactorySettingsNavigation("MotionControl"))
|
||||||
|
return;
|
||||||
|
|
||||||
ShowOrActivate(_motionDebugWindow, w => _motionDebugWindow = w,
|
ShowOrActivate(_motionDebugWindow, w => _motionDebugWindow = w,
|
||||||
() => new XP.Hardware.MotionControl.Views.MotionDebugWindow(), "运动调试");
|
() => new XP.Hardware.MotionControl.Views.MotionDebugWindow(), "运动调试");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenPlcAddrConfig()
|
private void ExecuteOpenPlcAddrConfig()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmFactorySettingsNavigation("PlcAddrConfig"))
|
||||||
|
return;
|
||||||
|
|
||||||
ShowOrActivate(_plcAddrConfigWindow, w => _plcAddrConfigWindow = w,
|
ShowOrActivate(_plcAddrConfigWindow, w => _plcAddrConfigWindow = w,
|
||||||
() => _containerProvider.Resolve<XP.Hardware.PLC.Views.PlcAddrConfigEditorWindow>(), "PLC 地址配置");
|
() => _containerProvider.Resolve<XP.Hardware.PLC.Views.PlcAddrConfigEditorWindow>(), "PLC 地址配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenRaySourceConfig()
|
private void ExecuteOpenRaySourceConfig()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmFactorySettingsNavigation("RaySourceConfig"))
|
||||||
|
return;
|
||||||
|
|
||||||
ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w,
|
ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w,
|
||||||
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
|
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOpenReportConfig()
|
private void ExecuteOpenReportConfig()
|
||||||
{
|
{
|
||||||
|
if (!ConfirmReportSettingsNavigation("ReportConfig"))
|
||||||
|
return;
|
||||||
|
|
||||||
ShowOrActivate(_reportConfigWindow, w => _reportConfigWindow = w,
|
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()
|
private void ExecuteLoadImage()
|
||||||
{
|
{
|
||||||
var dialog = new OpenFileDialog
|
var dialog = new OpenFileDialog
|
||||||
@@ -1632,5 +1708,74 @@ namespace XplorePlane.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion 测量命令实现
|
#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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Commands;
|
||||||
|
using Prism.Events;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XplorePlane.Events;
|
||||||
|
using XplorePlane.Models;
|
||||||
|
using XplorePlane.Services.Permission;
|
||||||
|
using PermissionEnum = XplorePlane.Models.Permission;
|
||||||
|
|
||||||
namespace XplorePlane.ViewModels.Setting
|
namespace XplorePlane.ViewModels.Setting
|
||||||
{
|
{
|
||||||
public class SettingsViewModel : BindableBase
|
public class SettingsViewModel : BindableBase
|
||||||
{
|
{
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly IPermissionService _permissionService;
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
|
||||||
public DelegateCommand SaveCommand { get; }
|
public DelegateCommand SaveCommand { get; }
|
||||||
public DelegateCommand CancelCommand { get; }
|
public DelegateCommand CancelCommand { get; }
|
||||||
public DelegateCommand ResetToDefaultCommand { 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));
|
_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");
|
_logger.Info("SettingsViewModel 构造函数被调用 | SettingsViewModel constructor called");
|
||||||
|
|
||||||
@@ -28,6 +49,13 @@ namespace XplorePlane.ViewModels.Setting
|
|||||||
_logger.Debug("Commands initialized: SaveCommand={SaveCommand}, CancelCommand={CancelCommand}, ResetToDefaultCommand={ResetToDefaultCommand}",
|
_logger.Debug("Commands initialized: SaveCommand={SaveCommand}, CancelCommand={CancelCommand}, ResetToDefaultCommand={ResetToDefaultCommand}",
|
||||||
SaveCommand != null, CancelCommand != null, ResetToDefaultCommand != null);
|
SaveCommand != null, CancelCommand != null, ResetToDefaultCommand != null);
|
||||||
|
|
||||||
|
// 订阅角色变更事件
|
||||||
|
_eventAggregator.GetEvent<RoleChangedEvent>()
|
||||||
|
.Subscribe(OnRoleChanged, ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 初始化可见性
|
||||||
|
RefreshSettingsVisibility();
|
||||||
|
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,5 +467,19 @@ namespace XplorePlane.ViewModels.Setting
|
|||||||
config.AppSettings.Settings[key].Value = value;
|
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:behaviors="clr-namespace:XplorePlane.Controls"
|
xmlns:behaviors="clr-namespace:XplorePlane.Controls"
|
||||||
|
xmlns:helpers="clr-namespace:XplorePlane.Helpers"
|
||||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||||
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
||||||
d:DesignHeight="760"
|
d:DesignHeight="760"
|
||||||
@@ -269,7 +270,8 @@
|
|||||||
Content="完成"
|
Content="完成"
|
||||||
Style="{StaticResource TreeToolbarButtonCompact}" />
|
Style="{StaticResource TreeToolbarButtonCompact}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,4,0,0">
|
<WrapPanel Margin="0,4,0,0" IsEnabled="{Binding CanEditCncProgram}"
|
||||||
|
helpers:PermissionTooltipHelper.IsPermissionRestricted="True">
|
||||||
<WrapPanel.Resources>
|
<WrapPanel.Resources>
|
||||||
<Style TargetType="TextBlock">
|
<Style TargetType="TextBlock">
|
||||||
<Setter Property="Visibility" Value="Collapsed" />
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace XplorePlane.Views.Cnc
|
|||||||
var executionService = ContainerLocator.Current.Resolve<IPipelineExecutionService>();
|
var executionService = ContainerLocator.Current.Resolve<IPipelineExecutionService>();
|
||||||
var mainViewportService = ContainerLocator.Current.Resolve<XplorePlane.Services.MainViewport.IMainViewportService>();
|
var mainViewportService = ContainerLocator.Current.Resolve<XplorePlane.Services.MainViewport.IMainViewportService>();
|
||||||
var eventAggregator = ContainerLocator.Current.Resolve<Prism.Events.IEventAggregator>();
|
var eventAggregator = ContainerLocator.Current.Resolve<Prism.Events.IEventAggregator>();
|
||||||
|
var permissionService = ContainerLocator.Current.Resolve<XplorePlane.Services.Permission.IPermissionService>();
|
||||||
|
|
||||||
_inspectionModulePipelineViewModel = new CncInspectionModulePipelineViewModel(
|
_inspectionModulePipelineViewModel = new CncInspectionModulePipelineViewModel(
|
||||||
editorViewModel,
|
editorViewModel,
|
||||||
@@ -65,7 +66,8 @@ namespace XplorePlane.Views.Cnc
|
|||||||
logger,
|
logger,
|
||||||
executionService,
|
executionService,
|
||||||
mainViewportService,
|
mainViewportService,
|
||||||
eventAggregator);
|
eventAggregator,
|
||||||
|
permissionService);
|
||||||
|
|
||||||
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
|
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:helpers="clr-namespace:XplorePlane.Helpers"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
d:DesignHeight="700"
|
d:DesignHeight="700"
|
||||||
d:DesignWidth="300"
|
d:DesignWidth="300"
|
||||||
@@ -80,6 +81,8 @@
|
|||||||
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
|
IsEnabled="{Binding CanEditPipeline}"
|
||||||
|
helpers:PermissionTooltipHelper.IsPermissionRestricted="True"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
Command="{Binding NewPipelineCommand}"
|
Command="{Binding NewPipelineCommand}"
|
||||||
@@ -287,6 +290,8 @@
|
|||||||
|
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
|
IsEnabled="{Binding CanEditPipeline}"
|
||||||
|
helpers:PermissionTooltipHelper.IsPermissionRestricted="True"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="8,6">
|
<StackPanel Margin="8,6">
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ namespace XplorePlane.Views
|
|||||||
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
|
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!vm.CanEditPipeline)
|
||||||
|
return;
|
||||||
|
|
||||||
// 双击切换启用/禁用
|
// 双击切换启用/禁用
|
||||||
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
|
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
@@ -163,6 +166,9 @@ namespace XplorePlane.Views
|
|||||||
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
|
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!vm.CanEditPipeline)
|
||||||
|
return;
|
||||||
|
|
||||||
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
|
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
@@ -215,11 +221,17 @@ namespace XplorePlane.Views
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!vm.CanEditPipeline)
|
||||||
|
return;
|
||||||
|
|
||||||
vm.AddOperatorCommand.Execute(operatorKey);
|
vm.AddOperatorCommand.Execute(operatorKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
|
private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (!vm.CanEditPipeline)
|
||||||
|
return;
|
||||||
|
|
||||||
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|
||||||
|| !vm.PipelineNodes.Contains(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:spreadsheetControls="clr-namespace:Telerik.Windows.Controls.Spreadsheet.Controls;assembly=Telerik.Windows.Controls.Spreadsheet"
|
||||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
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:views1="clr-namespace:XP.Hardware.RaySource.Views;assembly=XP.Hardware.RaySource"
|
||||||
xmlns:mcViews="clr-namespace:XP.Hardware.MotionControl.Views;assembly=XP.Hardware.MotionControl"
|
xmlns:mcViews="clr-namespace:XP.Hardware.MotionControl.Views;assembly=XP.Hardware.MotionControl"
|
||||||
x:Name="ParentWindow"
|
x:Name="ParentWindow"
|
||||||
@@ -444,6 +445,12 @@
|
|||||||
SmallImage="/Assets/Icons/setting.png"
|
SmallImage="/Assets/Icons/setting.png"
|
||||||
Command="{Binding OpenSettingsCommand}"
|
Command="{Binding OpenSettingsCommand}"
|
||||||
Text="设置" />
|
Text="设置" />
|
||||||
|
<telerik:RadRibbonButton
|
||||||
|
Size="Large"
|
||||||
|
SmallImage="/Assets/Icons/tools.png"
|
||||||
|
Command="{Binding OpenPasswordManagementCommand}"
|
||||||
|
Text="密码管理"
|
||||||
|
Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
|
|
||||||
<telerik:RadRibbonGroup
|
<telerik:RadRibbonGroup
|
||||||
@@ -452,7 +459,8 @@
|
|||||||
DialogLauncherCommandParameter="Alignment"
|
DialogLauncherCommandParameter="Alignment"
|
||||||
DialogLauncherVisibility="Collapsed"
|
DialogLauncherVisibility="Collapsed"
|
||||||
Header="硬件"
|
Header="硬件"
|
||||||
IsEnabled="True">
|
IsEnabled="True"
|
||||||
|
Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<telerik:RadRibbonGroup.Variants>
|
<telerik:RadRibbonGroup.Variants>
|
||||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||||
</telerik:RadRibbonGroup.Variants>
|
</telerik:RadRibbonGroup.Variants>
|
||||||
@@ -593,7 +601,8 @@
|
|||||||
Command="{Binding OpenReportConfigCommand}"
|
Command="{Binding OpenReportConfigCommand}"
|
||||||
Size="Large"
|
Size="Large"
|
||||||
SmallImage="/Assets/Icons/message.png"
|
SmallImage="/Assets/Icons/message.png"
|
||||||
Text="报告生成" />
|
Text="报告生成"
|
||||||
|
Visibility="{Binding IsReportSettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
</telerik:RadRibbonGroup>
|
</telerik:RadRibbonGroup>
|
||||||
</telerik:RadRibbonTab>
|
</telerik:RadRibbonTab>
|
||||||
<telerik:RadRibbonTab Header="帮助">
|
<telerik:RadRibbonTab Header="帮助">
|
||||||
@@ -616,6 +625,14 @@
|
|||||||
</telerik:RadRibbonTab>
|
</telerik:RadRibbonTab>
|
||||||
</telerik:RadRibbonView>
|
</telerik:RadRibbonView>
|
||||||
|
|
||||||
|
<!-- Ribbon 右侧状态区域:显示当前角色和切换用户按钮 -->
|
||||||
|
<mainViews:RibbonStatusAreaView
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,6,12,0"
|
||||||
|
DataContext="{Binding RibbonStatusArea}" />
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.ColumnSpan="3"
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
ShowInTaskbar="False"
|
ShowInTaskbar="False"
|
||||||
Background="#F5F5F5">
|
Background="#F5F5F5">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||||
|
|
||||||
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
||||||
<Setter Property="FontSize" Value="16" />
|
<Setter Property="FontSize" Value="16" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
@@ -294,7 +296,7 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<!-- 射线源设置 -->
|
<!-- 射线源设置 -->
|
||||||
<TabItem Header="射线源">
|
<TabItem Header="射线源" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="16">
|
<StackPanel Margin="16">
|
||||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||||
@@ -368,7 +370,7 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<!-- 探测器设置 -->
|
<!-- 探测器设置 -->
|
||||||
<TabItem Header="探测器">
|
<TabItem Header="探测器" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="16">
|
<StackPanel Margin="16">
|
||||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||||
@@ -452,7 +454,7 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<!-- PLC设置 -->
|
<!-- PLC设置 -->
|
||||||
<TabItem Header="PLC">
|
<TabItem Header="PLC" Visibility="{Binding IsFactorySettingsVisible, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="16">
|
<StackPanel Margin="16">
|
||||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||||
|
|||||||
Reference in New Issue
Block a user