基于角色的权限控制

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:
zhengxuan.zhang
2026-06-01 17:15:59 +08:00
parent acbed526f6
commit 741874e85d
41 changed files with 1953 additions and 43 deletions
@@ -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)
{ {
+30
View File
@@ -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("依赖注入容器配置完成");
} }
+18
View File
@@ -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; }
}
}
+44
View File
@@ -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;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace XplorePlane.Models
{
/// <summary>系统角色定义,层级从高到低。</summary>
public enum UserRole
{
SuperAdmin = 0, // 超级管理员
Admin = 1, // 管理员
User = 2 // 普通用户
}
}
+49 -1
View File
@@ -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;
}
}
}
+157 -12
View File
@@ -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);
}
} }
} }
+3 -1
View File
@@ -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" />
+3 -1
View File
@@ -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))
{ {
+76
View File
@@ -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>
+65
View File
@@ -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;
}
}
}
}
}
+19 -2
View File
@@ -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}"