diff --git a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs index 9716bde..0573dda 100644 --- a/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelineEditorViewModelTests.cs @@ -6,6 +6,7 @@ using System.Windows.Media.Imaging; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services; +using XplorePlane.Services.Storage; using XplorePlane.Tests.Helpers; using XplorePlane.ViewModels; using Xunit; @@ -22,6 +23,7 @@ namespace XplorePlane.Tests.Pipeline private readonly Mock _mockExecSvc; private readonly Mock _mockPersistSvc; private readonly Mock _mockLogger; + private readonly Mock _mockDataPathService; public PipelineEditorViewModelTests() { @@ -29,11 +31,19 @@ namespace XplorePlane.Tests.Pipeline _mockExecSvc = new Mock(); _mockPersistSvc = new Mock(); _mockLogger = new Mock(); + _mockDataPathService = new Mock(); _mockLogger.Setup(l => l.ForModule()).Returns(_mockLogger.Object); + _mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath()); } private PipelineEditorViewModel CreateVm() => - new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object); + new PipelineEditorViewModel( + _mockImageSvc.Object, + _mockExecSvc.Object, + _mockPersistSvc.Object, + new EventAggregator(), + _mockLogger.Object, + _mockDataPathService.Object); // ── 6.1 AddOperatorCommand ──────────────────────────────────── diff --git a/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs b/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs index f274e0b..7a6be26 100644 --- a/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Threading.Tasks; +using Moq; using XplorePlane.Models; using XplorePlane.Services; +using XplorePlane.Services.Storage; using XplorePlane.Tests.Helpers; using Xunit; @@ -199,5 +201,21 @@ namespace XplorePlane.Tests.Pipeline Assert.Equal(2, result.Count); } + + [Fact] + public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService() + { + var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" }); + var mockDataPathSvc = new Mock(); + mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir); + + var service = new PipelinePersistenceService(mockImageSvc.Object, mockDataPathSvc.Object); + await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm")); + + var result = await service.LoadAllAsync(null); + + Assert.Single(result); + Assert.Equal("P3", result[0].Name); + } } -} \ No newline at end of file +} diff --git a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs index 060b14f..8558cc1 100644 --- a/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs +++ b/XplorePlane.Tests/Pipeline/PipelinePropertyTests.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services; +using XplorePlane.Services.Storage; using XplorePlane.Tests.Helpers; using XplorePlane.ViewModels; @@ -31,8 +32,16 @@ namespace XplorePlane.Tests.Pipeline var mockExecSvc = new Mock(); var mockPersistSvc = new Mock(); var mockLogger = new Mock(); + var mockDataPathService = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); - return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object); + mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath()); + return new PipelineEditorViewModel( + mockImageSvc.Object, + mockExecSvc.Object, + mockPersistSvc.Object, + new EventAggregator(), + mockLogger.Object, + mockDataPathService.Object); } /// diff --git a/XplorePlane.Tests/Services/InspectionResultStoreTests.cs b/XplorePlane.Tests/Services/InspectionResultStoreTests.cs index 640eeea..21ffa5e 100644 --- a/XplorePlane.Tests/Services/InspectionResultStoreTests.cs +++ b/XplorePlane.Tests/Services/InspectionResultStoreTests.cs @@ -11,6 +11,7 @@ using XP.Common.Database.Interfaces; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.InspectionResults; +using XplorePlane.Services.Storage; using Xunit; namespace XplorePlane.Tests.Services @@ -294,6 +295,33 @@ namespace XplorePlane.Tests.Services Assert.Equal(originalHash, snapshot.PipelineHash); } + [Fact] + public async Task Constructor_WithDataPathService_WritesIntoDataInspectionResultsDirectory() + { + var mockDataPathService = new Mock(); + var dataRoot = Path.Combine(_tempRoot, "xpdata", "Data"); + mockDataPathService.SetupGet(s => s.DataPath).Returns(dataRoot); + + var store = new InspectionResultStore(_dbContext, _mockLogger.Object, mockDataPathService.Object); + var run = new InspectionRunRecord + { + ProgramName = "Program-By-Service", + WorkpieceId = "Part-03", + SerialNumber = "SN-DATA" + }; + + await store.BeginRunAsync(run); + await store.CompleteRunAsync(run.RunId); + + var manifestPath = Path.Combine( + dataRoot, + "InspectionResults", + run.ResultRootPath.Replace('/', Path.DirectorySeparatorChar), + "manifest.json"); + + Assert.True(File.Exists(manifestPath)); + } + public void Dispose() { _dbContext.Dispose(); diff --git a/XplorePlane.Tests/ViewModels/CncEditorViewModelTests.cs b/XplorePlane.Tests/ViewModels/CncEditorViewModelTests.cs index af6c14d..47791c7 100644 --- a/XplorePlane.Tests/ViewModels/CncEditorViewModelTests.cs +++ b/XplorePlane.Tests/ViewModels/CncEditorViewModelTests.cs @@ -16,6 +16,7 @@ using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Services.AppState; using XplorePlane.Services.Cnc; +using XplorePlane.Services.Storage; using XplorePlane.ViewModels.Cnc; using Xunit; @@ -32,7 +33,9 @@ namespace XplorePlane.Tests.ViewModels var mockCncProgramSvc = new Mock(); var mockAppState = new Mock(); var mockLogger = new Mock(); + var mockDataPathService = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); + mockDataPathService.SetupGet(s => s.PlanPath).Returns(System.IO.Path.GetTempPath()); mockExecSvc ??= new Mock(); @@ -52,7 +55,8 @@ namespace XplorePlane.Tests.ViewModels mockAppState.Object, new EventAggregator(), mockLogger.Object, - mockExecSvc.Object); + mockExecSvc.Object, + mockDataPathService.Object); if (initialProgram != null) { diff --git a/XplorePlane/App.config b/XplorePlane/App.config index 7465d52..5fd6b21 100644 --- a/XplorePlane/App.config +++ b/XplorePlane/App.config @@ -7,6 +7,7 @@ + diff --git a/XplorePlane/App.xaml.cs b/XplorePlane/App.xaml.cs index 66d730f..0091c4e 100644 --- a/XplorePlane/App.xaml.cs +++ b/XplorePlane/App.xaml.cs @@ -40,6 +40,7 @@ using XplorePlane.Services.MainViewport; using XplorePlane.Services.Matrix; using XplorePlane.Services.Measurement; using XplorePlane.Services.Recipe; +using XplorePlane.Services.Storage; using XplorePlane.ViewModels; using XplorePlane.ViewModels.Cnc; using XplorePlane.Views; @@ -377,6 +378,7 @@ namespace XplorePlane // 注册全局状态服务(单例) containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); // 注册检测配方服务(单例) containerRegistry.RegisterSingleton(); diff --git a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs index 88c046f..e4237d0 100644 --- a/XplorePlane/Services/InspectionResults/InspectionResultStore.cs +++ b/XplorePlane/Services/InspectionResults/InspectionResultStore.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using XP.Common.Database.Interfaces; using XP.Common.Logging.Interfaces; using XplorePlane.Models; +using XplorePlane.Services.Storage; namespace XplorePlane.Services.InspectionResults { @@ -160,6 +161,14 @@ WHERE run_id = @run_id"; "InspectionResults"); } + public InspectionResultStore(IDbContext db, ILoggerService logger, IXpDataPathService dataPathService) + : this( + db, + logger, + Path.Combine(dataPathService?.DataPath ?? throw new ArgumentNullException(nameof(dataPathService)), "InspectionResults")) + { + } + public async Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null) { ArgumentNullException.ThrowIfNull(runRecord); diff --git a/XplorePlane/Services/Pipeline/PipelinePersistenceService.cs b/XplorePlane/Services/Pipeline/PipelinePersistenceService.cs index 5ffaa18..136a291 100644 --- a/XplorePlane/Services/Pipeline/PipelinePersistenceService.cs +++ b/XplorePlane/Services/Pipeline/PipelinePersistenceService.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using XplorePlane.Models; +using XplorePlane.Services.Storage; namespace XplorePlane.Services { @@ -29,6 +30,11 @@ namespace XplorePlane.Services "XplorePlane", "Pipelines"); } + public PipelinePersistenceService(IImageProcessingService imageProcessingService, IXpDataPathService dataPathService) + : this(imageProcessingService, dataPathService?.ToolsPath) + { + } + public async Task SaveAsync(PipelineModel pipeline, string filePath) { if (pipeline == null) throw new ArgumentNullException(nameof(pipeline)); diff --git a/XplorePlane/Services/Storage/IXpDataPathService.cs b/XplorePlane/Services/Storage/IXpDataPathService.cs new file mode 100644 index 0000000..c4bd2d4 --- /dev/null +++ b/XplorePlane/Services/Storage/IXpDataPathService.cs @@ -0,0 +1,23 @@ +using System; + +namespace XplorePlane.Services.Storage +{ + public interface IXpDataPathService + { + string DefaultRootPath { get; } + + string RootPath { get; } + + string PlanPath { get; } + + string ToolsPath { get; } + + string DataPath { get; } + + string ReportPath { get; } + + string LegacyInspectionDataPath { get; } + + void SaveRootPath(string rootPath); + } +} diff --git a/XplorePlane/Services/Storage/XpDataPathService.cs b/XplorePlane/Services/Storage/XpDataPathService.cs new file mode 100644 index 0000000..4d261c2 --- /dev/null +++ b/XplorePlane/Services/Storage/XpDataPathService.cs @@ -0,0 +1,145 @@ +using System; +using System.Configuration; +using System.IO; +using XP.Common.Logging.Interfaces; + +namespace XplorePlane.Services.Storage +{ + public class XpDataPathService : IXpDataPathService + { + internal const string RootPathSettingKey = "XpData:RootPath"; + private const string DefaultRoot = @"D:\XPData"; + + private readonly ILoggerService _logger; + private readonly object _syncRoot = new(); + private string _rootPath; + + public XpDataPathService(ILoggerService logger) + { + _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule(); + _rootPath = LoadConfiguredRootPath(); + EnsureManagedDirectories(_rootPath); + } + + public string DefaultRootPath => DefaultRoot; + + public string RootPath + { + get + { + lock (_syncRoot) + { + return _rootPath; + } + } + } + + public string PlanPath => EnsureSubdirectory("Plan"); + + public string ToolsPath => EnsureSubdirectory("Tools"); + + public string DataPath => EnsureSubdirectory("Data"); + + public string ReportPath => EnsureSubdirectory("Report"); + + public string LegacyInspectionDataPath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "XplorePlane", + "InspectionResults"); + + public void SaveRootPath(string rootPath) + { + var normalizedRootPath = NormalizeRootPath(rootPath); + EnsureManagedDirectories(normalizedRootPath); + + try + { + var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); + var appSettings = config.AppSettings.Settings; + + if (appSettings[RootPathSettingKey] == null) + { + appSettings.Add(RootPathSettingKey, normalizedRootPath); + } + else + { + appSettings[RootPathSettingKey].Value = normalizedRootPath; + } + + config.Save(ConfigurationSaveMode.Modified); + ConfigurationManager.RefreshSection("appSettings"); + } + catch (ConfigurationErrorsException ex) + { + _logger.Error(ex, "保存 XP 数据根目录失败:配置文件写入异常"); + throw new InvalidOperationException($"保存数据根目录失败:{ex.Message}", ex); + } + catch (UnauthorizedAccessException ex) + { + _logger.Error(ex, "保存 XP 数据根目录失败:无权写入配置文件"); + throw new InvalidOperationException("保存数据根目录失败:没有配置文件写入权限。", ex); + } + + lock (_syncRoot) + { + _rootPath = normalizedRootPath; + } + } + + private string EnsureSubdirectory(string directoryName) + { + string rootPath; + lock (_syncRoot) + { + rootPath = _rootPath; + } + + EnsureManagedDirectories(rootPath); + return Path.Combine(rootPath, directoryName); + } + + private string LoadConfiguredRootPath() + { + try + { + return NormalizeRootPath(ConfigurationManager.AppSettings[RootPathSettingKey]); + } + catch (Exception ex) + { + _logger.Warn("读取 XP 数据根目录失败,回退默认目录:{Message}", ex.Message); + return NormalizeRootPath(null); + } + } + + private static string NormalizeRootPath(string rootPath) + { + var candidate = string.IsNullOrWhiteSpace(rootPath) ? DefaultRoot : rootPath.Trim(); + + try + { + var normalized = Path.GetFullPath(candidate) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + if (!Path.IsPathRooted(normalized)) + { + return DefaultRoot; + } + + return normalized; + } + catch + { + return DefaultRoot; + } + } + + private static void EnsureManagedDirectories(string rootPath) + { + Directory.CreateDirectory(rootPath); + Directory.CreateDirectory(Path.Combine(rootPath, "Plan")); + Directory.CreateDirectory(Path.Combine(rootPath, "Tools")); + Directory.CreateDirectory(Path.Combine(rootPath, "Data")); + Directory.CreateDirectory(Path.Combine(rootPath, "Report")); + } + } +} diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index fd6804b..5b0e31f 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -16,6 +16,7 @@ using XplorePlane.Events; using XplorePlane.Models; using XplorePlane.Services.AppState; using XplorePlane.Services.Cnc; +using XplorePlane.Services.Storage; namespace XplorePlane.ViewModels.Cnc { @@ -28,6 +29,7 @@ namespace XplorePlane.ViewModels.Cnc private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; private readonly ICncExecutionService _cncExecutionService; + private readonly IXpDataPathService _dataPathService; private CncProgram _currentProgram; private ObservableCollection _nodes; @@ -51,13 +53,15 @@ namespace XplorePlane.ViewModels.Cnc IAppStateService appStateService, IEventAggregator eventAggregator, ILoggerService logger, - ICncExecutionService cncExecutionService) + ICncExecutionService cncExecutionService, + IXpDataPathService dataPathService) { _cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService)); ArgumentNullException.ThrowIfNull(appStateService); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule(); _cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService)); + _dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService)); _nodes = new ObservableCollection(); _treeNodes = new ObservableCollection(); @@ -331,7 +335,8 @@ namespace XplorePlane.ViewModels.Cnc Title = "保存 CNC 程序", Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*", DefaultExt = ".xp", - FileName = _currentProgram.Name + FileName = _currentProgram.Name, + InitialDirectory = GetPlanDirectory() }; if (dlg.ShowDialog() != true) @@ -355,7 +360,8 @@ namespace XplorePlane.ViewModels.Cnc { Title = "加载 CNC 程序", Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*", - DefaultExt = ".xp" + DefaultExt = ".xp", + InitialDirectory = GetPlanDirectory() }; if (dlg.ShowDialog() != true) @@ -438,6 +444,13 @@ namespace XplorePlane.ViewModels.Cnc } } + private string GetPlanDirectory() + { + var directory = _dataPathService.PlanPath; + Directory.CreateDirectory(directory); + return directory; + } + private static string Esc(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index 2c6b3fe..c79ffe1 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -14,6 +14,7 @@ using XP.Common.Logging.Interfaces; using XplorePlane.Events; using XplorePlane.Models; using XplorePlane.Services; +using XplorePlane.Services.Storage; namespace XplorePlane.ViewModels { @@ -28,6 +29,7 @@ namespace XplorePlane.ViewModels private readonly IPipelinePersistenceService _persistenceService; private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; + private readonly IXpDataPathService _dataPathService; private PipelineNodeViewModel _selectedNode; private BitmapSource _sourceImage; @@ -49,13 +51,15 @@ namespace XplorePlane.ViewModels IPipelineExecutionService executionService, IPipelinePersistenceService persistenceService, IEventAggregator eventAggregator, - ILoggerService logger) + ILoggerService logger, + IXpDataPathService dataPathService) { _imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService)); _executionService = executionService ?? throw new ArgumentNullException(nameof(executionService)); _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + _dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService)); PipelineNodes = new ObservableCollection(); AvailableDevices = new ObservableCollection(); @@ -808,11 +812,9 @@ namespace XplorePlane.ViewModels return $"已执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)"; } - private static string GetPipelineDirectory() + private string GetPipelineDirectory() { - var dir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "XplorePlane", "Pipelines"); + var dir = _dataPathService.ToolsPath; Directory.CreateDirectory(dir); return dir; } diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index b6ff57e..eeeac48 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -11,6 +11,7 @@ using System.Windows; using System.Windows.Media.Imaging; using XplorePlane.Events; using XplorePlane.Services.MainViewport; +using XplorePlane.Services.Storage; using XplorePlane.ViewModels.Cnc; using XplorePlane.Views; using XplorePlane.Views.Cnc; @@ -29,6 +30,7 @@ namespace XplorePlane.ViewModels private readonly IContainerProvider _containerProvider; private readonly IEventAggregator _eventAggregator; private readonly IMainViewportService _mainViewportService; + private readonly IXpDataPathService _xpDataPathService; private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; @@ -66,6 +68,9 @@ namespace XplorePlane.ViewModels public DelegateCommand OpenUserManualCommand { get; } public DelegateCommand OpenCameraSettingsCommand { get; } public DelegateCommand OpenSettingsCommand { get; } + public DelegateCommand BrowseDataRootPathCommand { get; } + public DelegateCommand ResetDataRootPathCommand { get; } + public DelegateCommand SaveDataRootPathCommand { get; } public DelegateCommand NewCncProgramCommand { get; } public DelegateCommand SaveCncProgramCommand { get; } public DelegateCommand LoadCncProgramCommand { get; } @@ -119,6 +124,20 @@ namespace XplorePlane.ViewModels public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector; + public string DataRootPath + { + get => _dataRootPath; + set => SetProperty(ref _dataRootPath, value); + } + + public string PlanRootPath => _xpDataPathService.PlanPath; + + public string ToolsRootPath => _xpDataPathService.ToolsPath; + + public string ResultsRootPath => _xpDataPathService.DataPath; + + public string ReportRootPath => _xpDataPathService.ReportPath; + /// 右侧图像区域内容 | Right-side image panel content public object ImagePanelContent { @@ -155,16 +174,20 @@ namespace XplorePlane.ViewModels private bool _isCncEditorMode; private string _licenseInfo = "当前时间"; + private string _dataRootPath = string.Empty; + public MainViewModel( ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator, - IMainViewportService mainViewportService) + IMainViewportService mainViewportService, + IXpDataPathService xpDataPathService) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService)); + _xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService)); _cncEditorViewModel = _containerProvider.Resolve(); _cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; @@ -215,6 +238,9 @@ namespace XplorePlane.ViewModels OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual); OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings); OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings); + BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath); + ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath); + SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath); NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute())); SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute())); LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute())); @@ -259,6 +285,7 @@ namespace XplorePlane.ViewModels ImagePanelContent = new PipelineEditorView(); ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); + DataRootPath = _xpDataPathService.RootPath; _logger.Info("MainViewModel 已初始化"); } @@ -395,6 +422,56 @@ namespace XplorePlane.ViewModels } } + private void ExecuteBrowseDataRootPath() + { + try + { + var dialog = new OpenFolderDialog + { + Title = "选择 XP 数据根目录", + InitialDirectory = Directory.Exists(DataRootPath) ? DataRootPath : _xpDataPathService.RootPath + }; + + if (dialog.ShowDialog() == true) + { + DataRootPath = dialog.FolderName; + } + } + catch (Exception ex) + { + _logger.Error(ex, "浏览 XP 数据根目录失败"); + MessageBox.Show($"浏览数据根目录失败:{ex.Message}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void ExecuteResetDataRootPath() + { + DataRootPath = _xpDataPathService.DefaultRootPath; + } + + private void ExecuteSaveDataRootPath() + { + try + { + _xpDataPathService.SaveRootPath(DataRootPath); + DataRootPath = _xpDataPathService.RootPath; + RaisePropertyChanged(nameof(PlanRootPath)); + RaisePropertyChanged(nameof(ToolsRootPath)); + RaisePropertyChanged(nameof(ResultsRootPath)); + RaisePropertyChanged(nameof(ReportRootPath)); + + MessageBox.Show("XP 数据根目录已保存。新的保存/加载对话框会立即使用新路径。", + "提示", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + _logger.Error(ex, "保存 XP 数据根目录失败"); + MessageBox.Show($"保存数据根目录失败:{ex.Message}", + "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + private void ExecuteAxisReset() { var result = MessageBox.Show("确认执行轴复位操作?", "轴复位", diff --git a/XplorePlane/Views/Setting/SettingsWindow.xaml b/XplorePlane/Views/Setting/SettingsWindow.xaml index bce239f..43ffba2 100644 --- a/XplorePlane/Views/Setting/SettingsWindow.xaml +++ b/XplorePlane/Views/Setting/SettingsWindow.xaml @@ -28,20 +28,61 @@ - + + + + + + + + + + + + + + + +