From 8500f8b5ed7b66e7ac1955be209cbf185ed517b5 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Thu, 7 May 2026 13:31:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DCNC=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=EF=BC=8C=EF=BC=8B=E5=8F=B7=E8=AE=A2=E9=98=85=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=EF=BC=9B=E4=BF=AE=E5=A4=8DCNC=E5=92=8C=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E6=A8=A1=E5=BC=8F=E7=9A=84=E5=88=87=E6=8D=A2=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XplorePlane/Assets/Icons/eye.png | Bin 0 -> 6890 bytes .../ViewModels/Cnc/CncEditorViewModel.cs | 3 ++ .../CncInspectionModulePipelineViewModel.cs | 6 +++ .../PipelineEditorViewModel.cs | 21 ++++++++ XplorePlane/ViewModels/Main/MainViewModel.cs | 45 +++++++++++++++++- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 XplorePlane/Assets/Icons/eye.png diff --git a/XplorePlane/Assets/Icons/eye.png b/XplorePlane/Assets/Icons/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..800e01ee45724a732eab695b85906e9c4e6be5bb GIT binary patch literal 6890 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供外部直接 await 的保存方法 + public Task SaveAsync() => ExecuteSaveProgramAsync(); + private async Task ExecuteSaveProgramAsync() { if (_currentProgram == null) diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs index be00d31..bf7c2d7 100644 --- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -77,6 +77,9 @@ namespace XplorePlane.ViewModels.Cnc _editorViewModel.PropertyChanged += OnEditorPropertyChanged; RefreshFromSelection(); + + _eventAggregator?.GetEvent() + .Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); }); } public ObservableCollection PipelineNodes { get; } @@ -174,6 +177,9 @@ namespace XplorePlane.ViewModels.Cnc RaiseCommandCanExecuteChanged(); } + private bool CanAddOperator(string operatorKey) => + HasActiveModule && !string.IsNullOrWhiteSpace(operatorKey); + private void AddOperator(string operatorKey) { if (!HasActiveModule || string.IsNullOrWhiteSpace(operatorKey)) diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index f0233c0..e21d72f 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -42,6 +42,7 @@ namespace XplorePlane.ViewModels private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName; private string _currentFilePath; private PipelineNodeViewModel _executionEndNode; + private bool _isModified; private CancellationTokenSource _executionCts; private CancellationTokenSource _debounceCts; @@ -172,6 +173,13 @@ namespace XplorePlane.ViewModels private set => SetProperty(ref _pipelineFileDisplayName, value); } + /// 流水线是否有未保存的修改 + public bool IsModified + { + get => _isModified; + private set => SetProperty(ref _isModified, value); + } + public PipelineNodeViewModel ExecutionEndNode { get => _executionEndNode; @@ -266,6 +274,7 @@ namespace XplorePlane.ViewModels _logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}", operatorKey, displayName, PipelineNodes.Count); SetInfoStatus($"已添加算子:{displayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -283,6 +292,7 @@ namespace XplorePlane.ViewModels SelectNeighborAfterRemoval(removedIndex); SetInfoStatus($"已移除算子:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -294,6 +304,7 @@ namespace XplorePlane.ViewModels PipelineNodes.Move(index, index - 1); RenumberNodes(); UpdateExecutionRangeState(); + IsModified = true; TriggerDebouncedExecution(); } @@ -305,6 +316,7 @@ namespace XplorePlane.ViewModels PipelineNodes.Move(index, index + 1); RenumberNodes(); UpdateExecutionRangeState(); + IsModified = true; TriggerDebouncedExecution(); } @@ -325,6 +337,7 @@ namespace XplorePlane.ViewModels UpdateExecutionRangeState(); SelectedNode = node; SetInfoStatus($"已调整算子顺序:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -337,6 +350,7 @@ namespace XplorePlane.ViewModels SetInfoStatus(node.IsEnabled ? $"已启用算子:{node.DisplayName}" : $"已停用算子:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -406,6 +420,7 @@ namespace XplorePlane.ViewModels { if (e.PropertyName == nameof(ProcessorParameterVM.Value)) { + IsModified = true; if (TryReportInvalidParameters()) return; @@ -623,9 +638,13 @@ namespace XplorePlane.ViewModels PreviewImage = null; _currentFilePath = null; PipelineFileDisplayName = DefaultPipelineFileDisplayName; + IsModified = false; SetInfoStatus("已新建流水线"); } + /// 供外部直接 await 的保存方法 + public Task SaveAsync() => SavePipelineAsync(); + private async Task SavePipelineAsync() { if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100) @@ -674,6 +693,7 @@ namespace XplorePlane.ViewModels var model = BuildPipelineModel(); await _persistenceService.SaveAsync(model, filePath); PipelineFileDisplayName = FormatPipelinePath(filePath); + IsModified = false; SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}"); } catch (IOException ex) @@ -750,6 +770,7 @@ namespace XplorePlane.ViewModels UpdateExecutionRangeState(); _logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count); + IsModified = false; SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)"); } catch (Exception ex) diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 26e6403..9e9deb8 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels private readonly IXpDataPathService _xpDataPathService; private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; + private readonly PipelineEditorViewModel _pipelineEditorViewModel; + private readonly PipelineEditorView _pipelineEditorView; public string LicenseInfo { @@ -212,6 +214,8 @@ namespace XplorePlane.ViewModels _xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService)); _cncEditorViewModel = _containerProvider.Resolve(); _cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; + _pipelineEditorViewModel = _containerProvider.Resolve(); + _pipelineEditorView = new PipelineEditorView { DataContext = _pipelineEditorViewModel }; _mainViewportService.StateChanged += OnMainViewportStateChanged; _cncEditorViewModel.PropertyChanged += (s, e) => @@ -310,7 +314,7 @@ namespace XplorePlane.ViewModels OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer); UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource); - ImagePanelContent = new PipelineEditorView(); + ImagePanelContent = _pipelineEditorView; ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); DataRootPath = _xpDataPathService.RootPath; @@ -352,10 +356,31 @@ namespace XplorePlane.ViewModels } private void ExecuteOpenCncEditor() + { + _ = ExecuteOpenCncEditorAsync(); + } + + private async Task ExecuteOpenCncEditorAsync() { if (_isCncEditorMode) { - ImagePanelContent = new PipelineEditorView(); + // CNC → 普通模式:检查 CNC 程序是否有未保存修改 + if (_cncEditorViewModel.IsModified) + { + var result = MessageBox.Show( + "CNC 程序有未保存的修改,是否保存?", + "未保存的修改", + MessageBoxButton.YesNoCancel, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Cancel) + return; + + if (result == MessageBoxResult.Yes) + await _cncEditorViewModel.SaveAsync(); + } + + ImagePanelContent = _pipelineEditorView; ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); _isCncEditorMode = false; @@ -363,6 +388,22 @@ namespace XplorePlane.ViewModels return; } + // 普通 → CNC 模式:检查流水线是否有未保存修改 + if (_pipelineEditorViewModel.IsModified) + { + var result = MessageBox.Show( + "图像处理流水线有未保存的修改,是否保存?", + "未保存的修改", + MessageBoxButton.YesNoCancel, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Cancel) + return; + + if (result == MessageBoxResult.Yes) + await _pipelineEditorViewModel.SaveAsync(); + } + ShowCncEditor(); }