Phase 2-3: UI layout, theme switching, CRUD tools, process execution

- Phase 2: MainWindow 3-section layout (sidebar/content/log bar), Dark/Light theme with ThemeHelper, MainViewModel with ObservableProperty/RelayCommand, tool card filtering by search + category

- Phase 3: ToolEditWindow for add/edit tools, ProcessExecutionService (Process.Start + error handling), double-click + right-click context menu (run/edit), path browse dialog

- Bugfix: ContextMenu commands now use PlacementTarget.Tag binding (ContextMenu in separate visual tree)

- Bugfix: StaticResource converters moved to XAML before DataTemplate to fix XamlParseException on tool card render

- Bugfix: Pure filenames (no path separators) treated as PATH commands, not marked invalid

- Bugfix: RefreshData preserves SelectedCategory; Load() catches all exceptions; Save() wrapped in try-catch; auto-scroll log to newest entry

- Tests: xUnit project with 55 tests covering models, services, converters, and view models
This commit is contained in:
2026-05-09 21:52:31 +08:00
parent 752f09a7e4
commit 71be5da54b
22 changed files with 1991 additions and 22 deletions

View File

@@ -0,0 +1,15 @@
using PersonalToolBox.Models;
namespace PersonalToolBox.Services;
/// <summary>
/// 进程执行服务接口,负责启动外部工具进程
/// </summary>
public interface IProcessExecutionService
{
/// <summary>
/// 执行指定的工具项
/// </summary>
/// <param name="tool">要执行的工具项</param>
void Execute(ToolItem tool);
}

View File

@@ -65,11 +65,22 @@ public class JsonDataService : IDataService
continue;
}
// 纯文件名(无路径分隔符)可能位于系统 PATH 中,不标记为失效
if (!tool.ExecutablePath.Contains('\\') && !tool.ExecutablePath.Contains('/'))
{
tool.IsValid = true;
continue;
}
if (!File.Exists(tool.ExecutablePath))
{
tool.IsValid = false;
_logService.Warning($"工具 \"{tool.Name}\" 路径失效,找不到文件: {tool.ExecutablePath}");
}
else
{
tool.IsValid = true;
}
}
_logService.Info($"配置加载完成: {Config.Categories.Count} 个分类, {Config.Tools.Count} 个工具");
@@ -79,6 +90,11 @@ public class JsonDataService : IDataService
_logService.Error($"配置文件 JSON 解析失败: {ex.Message}");
Config = new AppConfig();
}
catch (Exception ex)
{
_logService.Error($"配置文件加载失败: {ex.Message}");
Config = new AppConfig();
}
}
public void Save()

View File

@@ -0,0 +1,49 @@
using System.Diagnostics;
using PersonalToolBox.Models;
namespace PersonalToolBox.Services;
/// <summary>
/// 进程执行服务,负责启动外部工具进程并处理异常
/// </summary>
public class ProcessExecutionService : IProcessExecutionService
{
private readonly ILogService _logService;
public ProcessExecutionService(ILogService logService)
{
_logService = logService;
}
public void Execute(ToolItem tool)
{
if (tool == null)
{
_logService.Error("尝试执行空工具项");
return;
}
if (!tool.IsValid)
{
_logService.Warning($"无法运行工具 \"{tool.Name}\",路径失效: {tool.ExecutablePath}");
return;
}
try
{
var startInfo = new ProcessStartInfo
{
FileName = tool.ExecutablePath,
Arguments = tool.Arguments ?? string.Empty,
UseShellExecute = true
};
Process.Start(startInfo);
_logService.Info($"成功启动: {tool.Name}");
}
catch (Exception ex)
{
_logService.Error($"启动工具 \"{tool.Name}\" 失败: {ex.Message}");
}
}
}