Phase 6: 一键多开 (工具组合) 功能
- 数据模型: ToolItem 新增 IsGroup(bool) + SubToolIds(List<string>) 字段
- 执行逻辑: ProcessExecutionService 改为 ExecuteAsync, 组合卡片遍历子工具逐一启动(500ms延迟), 孤儿ID跳过并打印警告
- 组合编辑: GroupEditViewModel + GroupEditWindow, 复选框列表勾选非组合工具
- 主界面: 标题栏新增 '+添加组合' 按钮(蓝色), 组合卡片右下角显示 📦 角标
- 右键菜单: 区分 '编辑工具' (普通) 和 '编辑组合' (IsGroup=true)
- 快捷键: HotKeyManager 适配 ExecuteAsync 异步调用
- 测试: 82 tests total (ProcessExecution 4->6, GroupEdit 5 new)
This commit is contained in:
@@ -7,19 +7,26 @@ namespace PersonalToolBox.Tests.Services;
|
|||||||
public class ProcessExecutionServiceTests
|
public class ProcessExecutionServiceTests
|
||||||
{
|
{
|
||||||
private readonly Mock<ILogService> _logServiceMock = new();
|
private readonly Mock<ILogService> _logServiceMock = new();
|
||||||
|
private readonly Mock<IDataService> _dataServiceMock = new();
|
||||||
|
private readonly AppConfig _config = new();
|
||||||
|
|
||||||
|
public ProcessExecutionServiceTests()
|
||||||
|
{
|
||||||
|
_dataServiceMock.Setup(d => d.Config).Returns(_config);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Execute_NullTool_LogsError()
|
public async Task Execute_NullTool_LogsError()
|
||||||
{
|
{
|
||||||
var service = new ProcessExecutionService(_logServiceMock.Object);
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
service.Execute(null!);
|
await service.ExecuteAsync(null!);
|
||||||
|
|
||||||
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("空工具项"))), Times.Once);
|
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("空工具项"))), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Execute_InvalidTool_LogsWarning()
|
public async Task Execute_InvalidTool_LogsWarning()
|
||||||
{
|
{
|
||||||
var tool = new ToolItem
|
var tool = new ToolItem
|
||||||
{
|
{
|
||||||
@@ -27,15 +34,15 @@ public class ProcessExecutionServiceTests
|
|||||||
ExecutablePath = @"C:\nonexistent.exe",
|
ExecutablePath = @"C:\nonexistent.exe",
|
||||||
IsValid = false
|
IsValid = false
|
||||||
};
|
};
|
||||||
var service = new ProcessExecutionService(_logServiceMock.Object);
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
service.Execute(tool);
|
await service.ExecuteAsync(tool);
|
||||||
|
|
||||||
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("无法运行") && s.Contains("BadTool"))), Times.Once);
|
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("无法运行") && s.Contains("BadTool"))), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Execute_ProcessFails_LogsError()
|
public async Task Execute_ProcessFails_LogsError()
|
||||||
{
|
{
|
||||||
var tool = new ToolItem
|
var tool = new ToolItem
|
||||||
{
|
{
|
||||||
@@ -43,28 +50,71 @@ public class ProcessExecutionServiceTests
|
|||||||
ExecutablePath = @"Z:\impossible_path\not_really.exe",
|
ExecutablePath = @"Z:\impossible_path\not_really.exe",
|
||||||
IsValid = true
|
IsValid = true
|
||||||
};
|
};
|
||||||
var service = new ProcessExecutionService(_logServiceMock.Object);
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
service.Execute(tool);
|
await service.ExecuteAsync(tool);
|
||||||
|
|
||||||
// Opening a non-existent file should throw and be caught
|
|
||||||
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("FailTool") && s.Contains("失败"))), Times.Once);
|
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("FailTool") && s.Contains("失败"))), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Execute_Success_LogsInfo()
|
public async Task Execute_Success_LogsInfo()
|
||||||
{
|
{
|
||||||
// Use a command that definitely exists on Windows
|
|
||||||
var tool = new ToolItem
|
var tool = new ToolItem
|
||||||
{
|
{
|
||||||
Name = "记事本",
|
Name = "记事本",
|
||||||
ExecutablePath = "notepad.exe",
|
ExecutablePath = "notepad.exe",
|
||||||
IsValid = true
|
IsValid = true
|
||||||
};
|
};
|
||||||
var service = new ProcessExecutionService(_logServiceMock.Object);
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
service.Execute(tool);
|
await service.ExecuteAsync(tool);
|
||||||
|
|
||||||
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("记事本"))), Times.Once);
|
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("记事本"))), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_Group_BatchExecutesSubTools()
|
||||||
|
{
|
||||||
|
var sub1 = new ToolItem { Id = "s1", Name = "Sub1", ExecutablePath = "cmd.exe", IsValid = true };
|
||||||
|
var sub2 = new ToolItem { Id = "s2", Name = "Sub2", ExecutablePath = "cmd.exe", IsValid = true };
|
||||||
|
_config.Tools.Add(sub1);
|
||||||
|
_config.Tools.Add(sub2);
|
||||||
|
|
||||||
|
var group = new ToolItem
|
||||||
|
{
|
||||||
|
Name = "MyGroup",
|
||||||
|
IsGroup = true,
|
||||||
|
SubToolIds = new List<string> { "s1", "s2" },
|
||||||
|
IsValid = true
|
||||||
|
};
|
||||||
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
|
await service.ExecuteAsync(group);
|
||||||
|
|
||||||
|
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("开始启动组合"))), Times.Once);
|
||||||
|
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("组合") && s.Contains("启动完成"))), Times.Once);
|
||||||
|
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("Sub1"))), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Execute_Group_OrphanSubIds_Skipped()
|
||||||
|
{
|
||||||
|
var sub1 = new ToolItem { Id = "s1", Name = "Sub1", ExecutablePath = "cmd.exe", IsValid = true };
|
||||||
|
_config.Tools.Add(sub1);
|
||||||
|
|
||||||
|
var group = new ToolItem
|
||||||
|
{
|
||||||
|
Name = "Group",
|
||||||
|
IsGroup = true,
|
||||||
|
SubToolIds = new List<string> { "s1", "missing_id" },
|
||||||
|
IsValid = true
|
||||||
|
};
|
||||||
|
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
|
||||||
|
|
||||||
|
await service.ExecuteAsync(group);
|
||||||
|
|
||||||
|
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("找不到 ID") && s.Contains("missing_id"))), Times.Once);
|
||||||
|
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("Sub1"))), Times.Once);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs
Normal file
97
PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Moq;
|
||||||
|
using PersonalToolBox.Models;
|
||||||
|
using PersonalToolBox.Services;
|
||||||
|
using PersonalToolBox.ViewModels;
|
||||||
|
|
||||||
|
namespace PersonalToolBox.Tests.ViewModels;
|
||||||
|
|
||||||
|
public class GroupEditViewModelTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IDataService> _dataServiceMock = new();
|
||||||
|
private readonly Mock<ILogService> _logServiceMock = new();
|
||||||
|
private readonly AppConfig _config;
|
||||||
|
|
||||||
|
public GroupEditViewModelTests()
|
||||||
|
{
|
||||||
|
_config = new AppConfig
|
||||||
|
{
|
||||||
|
Tools =
|
||||||
|
[
|
||||||
|
new() { Id = "t1", Name = "VS Code", IsValid = true },
|
||||||
|
new() { Id = "t2", Name = "Postman", IsValid = true },
|
||||||
|
new() { Id = "t3", Name = "SomeGroup", IsGroup = true, SubToolIds = new List<string> { "t1" }, IsValid = true }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
_dataServiceMock.Setup(d => d.Config).Returns(_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_AddMode_LoadsOnlyNonGroupTools()
|
||||||
|
{
|
||||||
|
var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||||
|
|
||||||
|
Assert.Equal("添加组合", vm.WindowTitle);
|
||||||
|
Assert.Equal(2, vm.AvailableTools.Count); // t1, t2 only (t3 is a group)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_EditMode_PreselectedTools()
|
||||||
|
{
|
||||||
|
var group = new ToolItem
|
||||||
|
{
|
||||||
|
Name = "ExistingGroup",
|
||||||
|
IsGroup = true,
|
||||||
|
SubToolIds = new List<string> { "t1" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, group);
|
||||||
|
|
||||||
|
Assert.Equal("编辑组合", vm.WindowTitle);
|
||||||
|
Assert.Equal("ExistingGroup", vm.Name);
|
||||||
|
Assert.True(vm.AvailableTools.First(t => t.Tool.Id == "t1").IsSelected);
|
||||||
|
Assert.False(vm.AvailableTools.First(t => t.Tool.Id == "t2").IsSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_EmptyName_LogsWarning()
|
||||||
|
{
|
||||||
|
var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("名称不能为空"))), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_AddMode_CreatesGroupAndCloses()
|
||||||
|
{
|
||||||
|
var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object)
|
||||||
|
{
|
||||||
|
Name = "NewGroup"
|
||||||
|
};
|
||||||
|
vm.AvailableTools.First(t => t.Tool.Id == "t1").IsSelected = true;
|
||||||
|
bool? closeResult = null;
|
||||||
|
vm.CloseAction = (r) => closeResult = r;
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.True(vm.Saved);
|
||||||
|
Assert.True(closeResult);
|
||||||
|
Assert.Single(_config.Tools.Where(t => t.IsGroup && t.Name == "NewGroup"));
|
||||||
|
Assert.Contains("t1", _config.Tools.First(t => t.Name == "NewGroup").SubToolIds);
|
||||||
|
_dataServiceMock.Verify(x => x.Save(), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cancel_ClosesWithoutSaving()
|
||||||
|
{
|
||||||
|
var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||||
|
bool? closeResult = null;
|
||||||
|
vm.CloseAction = (r) => closeResult = r;
|
||||||
|
|
||||||
|
vm.CancelCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.False(vm.Saved);
|
||||||
|
Assert.False(closeResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,7 +165,7 @@ public class MainViewModelTests
|
|||||||
|
|
||||||
vm.ExecuteToolCommand.Execute(tool);
|
vm.ExecuteToolCommand.Execute(tool);
|
||||||
|
|
||||||
_processServiceMock.Verify(x => x.Execute(tool), Times.Once);
|
_processServiceMock.Verify(x => x.ExecuteAsync(tool), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -173,7 +173,7 @@ public class MainViewModelTests
|
|||||||
{
|
{
|
||||||
var vm = CreateViewModel();
|
var vm = CreateViewModel();
|
||||||
vm.ExecuteToolCommand.Execute(null);
|
vm.ExecuteToolCommand.Execute(null);
|
||||||
_processServiceMock.Verify(x => x.Execute(It.IsAny<ToolItem>()), Times.Never);
|
_processServiceMock.Verify(x => x.ExecuteAsync(It.IsAny<ToolItem>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public partial class App : System.Windows.Application
|
|||||||
services.AddSingleton<ViewModels.MainViewModel>();
|
services.AddSingleton<ViewModels.MainViewModel>();
|
||||||
services.AddTransient<ViewModels.ToolEditViewModel>();
|
services.AddTransient<ViewModels.ToolEditViewModel>();
|
||||||
services.AddTransient<ViewModels.CategoryEditViewModel>();
|
services.AddTransient<ViewModels.CategoryEditViewModel>();
|
||||||
|
services.AddTransient<ViewModels.GroupEditViewModel>();
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public class HotKeyManager
|
|||||||
if (_hotkeyMap.TryGetValue(id, out var tool))
|
if (_hotkeyMap.TryGetValue(id, out var tool))
|
||||||
{
|
{
|
||||||
_logService.Info($"通过快捷键启动: {tool.Name}");
|
_logService.Info($"通过快捷键启动: {tool.Name}");
|
||||||
_processService.Execute(tool);
|
_ = _processService.ExecuteAsync(tool);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,14 @@ public class ToolItem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsValid { get; set; } = true;
|
public bool IsValid { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为组合卡片(一键多开)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsGroup { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当 IsGroup 为 true 时,存储需批量启动的子工具 ID 列表
|
||||||
|
/// </summary>
|
||||||
|
public List<string> SubToolIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ namespace PersonalToolBox.Services;
|
|||||||
public interface IProcessExecutionService
|
public interface IProcessExecutionService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行指定的工具项
|
/// 执行指定的工具项(含组合批量启动)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tool">要执行的工具项</param>
|
Task ExecuteAsync(ToolItem tool);
|
||||||
void Execute(ToolItem tool);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ namespace PersonalToolBox.Services;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 进程执行服务,负责启动外部工具进程并处理异常
|
/// 进程执行服务,负责启动外部工具进程并处理异常
|
||||||
|
/// 支持一键多开:当 IsGroup 为 true 时批量启动子工具
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProcessExecutionService : IProcessExecutionService
|
public class ProcessExecutionService : IProcessExecutionService
|
||||||
{
|
{
|
||||||
private readonly ILogService _logService;
|
private readonly ILogService _logService;
|
||||||
|
private readonly IDataService _dataService;
|
||||||
|
|
||||||
public ProcessExecutionService(ILogService logService)
|
public ProcessExecutionService(ILogService logService, IDataService dataService)
|
||||||
{
|
{
|
||||||
_logService = logService;
|
_logService = logService;
|
||||||
|
_dataService = dataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(ToolItem tool)
|
public async Task ExecuteAsync(ToolItem tool)
|
||||||
{
|
{
|
||||||
if (tool == null)
|
if (tool == null)
|
||||||
{
|
{
|
||||||
@@ -23,12 +26,63 @@ public class ProcessExecutionService : IProcessExecutionService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组合卡片:遍历子工具列表逐一启动
|
||||||
|
if (tool.IsGroup)
|
||||||
|
{
|
||||||
|
await ExecuteGroupAsync(tool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通工具:直接启动
|
||||||
if (!tool.IsValid)
|
if (!tool.IsValid)
|
||||||
{
|
{
|
||||||
_logService.Warning($"无法运行工具 \"{tool.Name}\",路径失效: {tool.ExecutablePath}");
|
_logService.Warning($"无法运行工具 \"{tool.Name}\",路径失效: {tool.ExecutablePath}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchSingleTool(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量启动组合中的所有子工具,每次间隔 500ms 防止系统卡顿
|
||||||
|
/// </summary>
|
||||||
|
private async Task ExecuteGroupAsync(ToolItem group)
|
||||||
|
{
|
||||||
|
var subTools = new List<ToolItem>();
|
||||||
|
|
||||||
|
foreach (var subId in group.SubToolIds)
|
||||||
|
{
|
||||||
|
var subTool = _dataService.Config.Tools.FirstOrDefault(t => t.Id == subId);
|
||||||
|
if (subTool == null)
|
||||||
|
{
|
||||||
|
_logService.Warning($"组合启动跳过:找不到 ID 为 {subId} 的工具");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
subTools.Add(subTool);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logService.Info($"开始启动组合 \"{group.Name}\",共 {subTools.Count} 个工具");
|
||||||
|
|
||||||
|
foreach (var subTool in subTools)
|
||||||
|
{
|
||||||
|
if (!subTool.IsValid)
|
||||||
|
{
|
||||||
|
_logService.Warning($"组合启动跳过 \"{subTool.Name}\",路径失效: {subTool.ExecutablePath}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchSingleTool(subTool);
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logService.Info($"组合 \"{group.Name}\" 启动完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动单个工具进程
|
||||||
|
/// </summary>
|
||||||
|
private void LaunchSingleTool(ToolItem tool)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var startInfo = new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
|
|||||||
144
PersonalToolBox/ViewModels/GroupEditViewModel.cs
Normal file
144
PersonalToolBox/ViewModels/GroupEditViewModel.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using PersonalToolBox.Models;
|
||||||
|
using PersonalToolBox.Services;
|
||||||
|
|
||||||
|
namespace PersonalToolBox.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组合编辑窗口 ViewModel,管理一键多开组合的创建与编辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class GroupEditViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IDataService _dataService;
|
||||||
|
private readonly ILogService _logService;
|
||||||
|
private readonly ToolItem? _editingGroup;
|
||||||
|
|
||||||
|
// ──────── 可观察属性 ────────
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _windowTitle = "添加组合";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _name = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _hotKey = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private Category? _selectedCategory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可供勾选的普通工具列表(IsGroup == false)
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<SelectableTool> AvailableTools { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类下拉列表
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<Category> Categories { get; } = new();
|
||||||
|
|
||||||
|
public Action<bool?>? CloseAction { get; set; }
|
||||||
|
public bool Saved { get; private set; }
|
||||||
|
|
||||||
|
// ──────── 构造 ────────
|
||||||
|
|
||||||
|
public GroupEditViewModel(IDataService dataService, ILogService logService, ToolItem? groupToEdit = null)
|
||||||
|
{
|
||||||
|
_dataService = dataService;
|
||||||
|
_logService = logService;
|
||||||
|
_editingGroup = groupToEdit;
|
||||||
|
|
||||||
|
foreach (var cat in _dataService.Config.Categories)
|
||||||
|
Categories.Add(cat);
|
||||||
|
|
||||||
|
// 加载所有普通工具(非组合)
|
||||||
|
var selectedIds = _editingGroup?.SubToolIds ?? new List<string>();
|
||||||
|
foreach (var tool in _dataService.Config.Tools.Where(t => !t.IsGroup))
|
||||||
|
{
|
||||||
|
AvailableTools.Add(new SelectableTool
|
||||||
|
{
|
||||||
|
Tool = tool,
|
||||||
|
IsSelected = selectedIds.Contains(tool.Id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupToEdit != null)
|
||||||
|
{
|
||||||
|
WindowTitle = "编辑组合";
|
||||||
|
Name = groupToEdit.Name;
|
||||||
|
HotKey = groupToEdit.HotKey;
|
||||||
|
SelectedCategory = Categories.FirstOrDefault(c => c.Id == groupToEdit.CategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────── 命令 ────────
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
_logService.Warning("组合名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedIds = AvailableTools
|
||||||
|
.Where(t => t.IsSelected)
|
||||||
|
.Select(t => t.Tool.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (_editingGroup != null)
|
||||||
|
{
|
||||||
|
_editingGroup.Name = Name.Trim();
|
||||||
|
_editingGroup.HotKey = HotKey.Trim();
|
||||||
|
_editingGroup.CategoryId = SelectedCategory?.Id ?? string.Empty;
|
||||||
|
_editingGroup.SubToolIds = selectedIds;
|
||||||
|
_editingGroup.IsValid = true;
|
||||||
|
_logService.Info($"已更新组合: {Name.Trim()}(包含 {selectedIds.Count} 个工具)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dataService.Config.Tools.Add(new ToolItem
|
||||||
|
{
|
||||||
|
Name = Name.Trim(),
|
||||||
|
HotKey = HotKey.Trim(),
|
||||||
|
CategoryId = SelectedCategory?.Id ?? string.Empty,
|
||||||
|
IsGroup = true,
|
||||||
|
SubToolIds = selectedIds,
|
||||||
|
IsValid = true
|
||||||
|
});
|
||||||
|
_logService.Info($"已添加组合: {Name.Trim()}(包含 {selectedIds.Count} 个工具)");
|
||||||
|
}
|
||||||
|
|
||||||
|
_dataService.Save();
|
||||||
|
Saved = true;
|
||||||
|
CloseAction?.Invoke(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logService.Error($"保存组合失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
CloseAction?.Invoke(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可勾选的工具项(用于 CheckBox 列表绑定)
|
||||||
|
/// </summary>
|
||||||
|
public partial class SelectableTool : ObservableObject
|
||||||
|
{
|
||||||
|
public ToolItem Tool { get; set; } = null!;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isSelected;
|
||||||
|
}
|
||||||
@@ -143,10 +143,10 @@ public partial class MainViewModel : ObservableObject
|
|||||||
/// 执行工具(双击卡片或右键菜单)
|
/// 执行工具(双击卡片或右键菜单)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ExecuteTool(ToolItem? tool)
|
private async Task ExecuteTool(ToolItem? tool)
|
||||||
{
|
{
|
||||||
if (tool == null) return;
|
if (tool == null) return;
|
||||||
_processService.Execute(tool);
|
await _processService.ExecuteAsync(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────── 分类管理命令 ─────────────────────────────
|
// ───────────────────────────── 分类管理命令 ─────────────────────────────
|
||||||
@@ -206,6 +206,38 @@ public partial class MainViewModel : ObservableObject
|
|||||||
RefreshData();
|
RefreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────── 组合管理命令 ─────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加组合
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddGroup()
|
||||||
|
{
|
||||||
|
var vm = _serviceProvider.GetRequiredService<GroupEditViewModel>();
|
||||||
|
var window = new GroupEditWindow(vm);
|
||||||
|
window.ShowDialog();
|
||||||
|
|
||||||
|
if (vm.Saved) RefreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 编辑组合
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void EditGroup(ToolItem tool)
|
||||||
|
{
|
||||||
|
if (tool == null || !tool.IsGroup) return;
|
||||||
|
|
||||||
|
var dataService = _serviceProvider.GetRequiredService<IDataService>();
|
||||||
|
var logService = _serviceProvider.GetRequiredService<ILogService>();
|
||||||
|
var editVm = new GroupEditViewModel(dataService, logService, tool);
|
||||||
|
var window = new GroupEditWindow(editVm);
|
||||||
|
window.ShowDialog();
|
||||||
|
|
||||||
|
if (editVm.Saved) RefreshData();
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────────────── 数据刷新 ─────────────────────────────
|
// ───────────────────────────── 数据刷新 ─────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
100
PersonalToolBox/Views/GroupEditWindow.xaml
Normal file
100
PersonalToolBox/Views/GroupEditWindow.xaml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<Window x:Class="PersonalToolBox.Views.GroupEditWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="{Binding WindowTitle}" Height="500" Width="500"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
Background="{DynamicResource Theme.Background}">
|
||||||
|
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="70"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 组合名称 -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
|
Text="名称:" Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1"
|
||||||
|
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="{DynamicResource Theme.InputBackground}"
|
||||||
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
BorderBrush="{DynamicResource Theme.InputBorder}"
|
||||||
|
Height="28" Margin="0,0,0,10" Padding="6,0"/>
|
||||||
|
|
||||||
|
<!-- 所属分类 -->
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||||
|
Text="分类:" Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<ComboBox Grid.Row="1" Grid.Column="1"
|
||||||
|
ItemsSource="{Binding Categories}"
|
||||||
|
SelectedItem="{Binding SelectedCategory}"
|
||||||
|
DisplayMemberPath="Name"
|
||||||
|
Height="28" Margin="0,0,0,10" Padding="4,0"/>
|
||||||
|
|
||||||
|
<!-- 快捷键 -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||||
|
Text="快捷键:" Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1"
|
||||||
|
Text="{Binding HotKey, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="{DynamicResource Theme.InputBackground}"
|
||||||
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
BorderBrush="{DynamicResource Theme.InputBorder}"
|
||||||
|
Height="28" Margin="0,0,0,10" Padding="6,0"/>
|
||||||
|
|
||||||
|
<!-- 子工具选择 -->
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0"
|
||||||
|
Text="包含工具:" Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
VerticalAlignment="Top" Margin="0,4,0,0"/>
|
||||||
|
<Border Grid.Row="3" Grid.Column="1"
|
||||||
|
Background="{DynamicResource Theme.InputBackground}"
|
||||||
|
BorderBrush="{DynamicResource Theme.InputBorder}"
|
||||||
|
BorderThickness="1"
|
||||||
|
Margin="0,0,0,10">
|
||||||
|
<ListBox ItemsSource="{Binding AvailableTools}"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
MaxHeight="240">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<CheckBox IsChecked="{Binding IsSelected}"
|
||||||
|
Content="{Binding Tool.Name}"
|
||||||
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
Margin="4,2"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<StackPanel Grid.Row="5" Grid.Column="1"
|
||||||
|
Orientation="Horizontal" HorizontalAlignment="Right"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
<Button Content="保存"
|
||||||
|
Command="{Binding SaveCommand}"
|
||||||
|
Background="{DynamicResource Theme.ButtonBackground}"
|
||||||
|
Foreground="{DynamicResource Theme.ButtonForeground}"
|
||||||
|
BorderThickness="0"
|
||||||
|
Width="80" Height="30" FontSize="13"
|
||||||
|
Cursor="Hand" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="取消"
|
||||||
|
Command="{Binding CancelCommand}"
|
||||||
|
Background="{DynamicResource Theme.CardBackground}"
|
||||||
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
|
BorderBrush="{DynamicResource Theme.CardBorder}"
|
||||||
|
BorderThickness="1"
|
||||||
|
Width="80" Height="30" FontSize="13"
|
||||||
|
Cursor="Hand"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
17
PersonalToolBox/Views/GroupEditWindow.xaml.cs
Normal file
17
PersonalToolBox/Views/GroupEditWindow.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using PersonalToolBox.ViewModels;
|
||||||
|
|
||||||
|
namespace PersonalToolBox.Views;
|
||||||
|
|
||||||
|
public partial class GroupEditWindow : Window
|
||||||
|
{
|
||||||
|
public GroupEditWindow(GroupEditViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
viewModel.CloseAction = (result) => { DialogResult = result; };
|
||||||
|
|
||||||
|
Owner = Application.Current.MainWindow;
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,17 +38,59 @@
|
|||||||
<MenuItem Header="运行"
|
<MenuItem Header="运行"
|
||||||
Command="{Binding PlacementTarget.Tag.ExecuteToolCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
Command="{Binding PlacementTarget.Tag.ExecuteToolCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
<MenuItem Header="编辑"
|
<MenuItem Header="编辑工具"
|
||||||
Command="{Binding PlacementTarget.Tag.EditToolCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
Command="{Binding PlacementTarget.Tag.EditToolCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}">
|
||||||
|
<MenuItem.Style>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsGroup}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</MenuItem.Style>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="编辑组合"
|
||||||
|
Command="{Binding PlacementTarget.Tag.EditGroupCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<MenuItem.Style>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsGroup}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</MenuItem.Style>
|
||||||
|
</MenuItem>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Border.ContextMenu>
|
</Border.ContextMenu>
|
||||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<Grid HorizontalAlignment="Center" Margin="0,0,0,4">
|
||||||
<TextBlock Text="{Binding Name, Converter={StaticResource FirstCharConverter}}"
|
<TextBlock Text="{Binding Name, Converter={StaticResource FirstCharConverter}}"
|
||||||
FontSize="28"
|
FontSize="28"
|
||||||
Foreground="{DynamicResource Theme.Accent}"
|
Foreground="{DynamicResource Theme.Accent}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"/>
|
||||||
Margin="0,0,0,6"/>
|
<!-- 组合角标 -->
|
||||||
|
<TextBlock Text="📦" FontSize="11"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Margin="0,0,-12,-4">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsGroup}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
<TextBlock Text="{Binding Name}"
|
<TextBlock Text="{Binding Name}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
Foreground="{DynamicResource Theme.Foreground}"
|
Foreground="{DynamicResource Theme.Foreground}"
|
||||||
@@ -214,6 +256,7 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="100"/>
|
<ColumnDefinition Width="100"/>
|
||||||
|
<ColumnDefinition Width="100"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBox Grid.Column="0"
|
<TextBox Grid.Column="0"
|
||||||
@@ -236,6 +279,23 @@
|
|||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
Height="32"
|
Height="32"
|
||||||
|
Cursor="Hand"
|
||||||
|
Margin="0,0,6,0">
|
||||||
|
<Button.Resources>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="CornerRadius" Value="4"/>
|
||||||
|
</Style>
|
||||||
|
</Button.Resources>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Content="+ 添加组合"
|
||||||
|
Command="{Binding AddGroupCommand}"
|
||||||
|
Background="{DynamicResource Theme.Accent}"
|
||||||
|
Foreground="{DynamicResource Theme.ButtonForeground}"
|
||||||
|
BorderThickness="0"
|
||||||
|
FontSize="12"
|
||||||
|
Height="32"
|
||||||
Cursor="Hand">
|
Cursor="Hand">
|
||||||
<Button.Resources>
|
<Button.Resources>
|
||||||
<Style TargetType="Border">
|
<Style TargetType="Border">
|
||||||
|
|||||||
73
补充需求文档-01.md
Normal file
73
补充需求文档-01.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 个人工具箱 (Personal ToolBox) - 补充需求文档
|
||||||
|
**特性模块:** 一键多开 / 工具组合 (Batch Execution / Tool Groups)
|
||||||
|
**文档版本:** V1.1 增量补充包
|
||||||
|
|
||||||
|
## 一、 需求分析与交互设计
|
||||||
|
1. **核心概念提取**:引入“**组合卡片 (Group Card)**”的概念。它在 UI 上的表现形式与普通应用卡片一致(同属一个网格布局,可被搜索、可绑定快捷键),但它的动作不是启动单一路径,而是批量触发其他已有工具的启动事件。
|
||||||
|
2. **UI/UX 变更**:
|
||||||
|
* **主界面**:在原本的“添加工具”按钮旁,新增“**添加组合**”按钮。
|
||||||
|
* **卡片视觉区分**:如果是组合卡片,UI 上需有视觉提示(例如:图标右下角有一个叠层的小角标,或者副标题显示“包含 3 个应用”)。
|
||||||
|
* **组合编辑弹窗**:新增一个弹窗,用户可输入组合名称、选择图标,并通过**复选框列表 (Checkboxes)** 勾选当前工具箱中已存在的其他工具。
|
||||||
|
|
||||||
|
## 二、 数据模型与架构变更 (SDD)
|
||||||
|
为了最小化对现有代码的破坏,我们**不创建新的主模型**,而是直接扩展现有的 `ToolItem` 模型。
|
||||||
|
|
||||||
|
### 2.1 数据模型扩展 (`ToolItem.cs`)
|
||||||
|
在原有属性基础上,增加两个字段:
|
||||||
|
* `IsGroup` (bool): 标记该卡片是否为组合卡片。默认为 `false`。
|
||||||
|
* `SubToolIds` (List<string>): 当 `IsGroup` 为 `true` 时,存储需要批量启动的子工具的 `Id` 列表。
|
||||||
|
|
||||||
|
**变更后的 `config.json` 示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Tools": [
|
||||||
|
{
|
||||||
|
"Id": "uuid-001",
|
||||||
|
"Name": "VS Code",
|
||||||
|
"Path": "C:\\vscode.exe",
|
||||||
|
"IsGroup": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "uuid-002",
|
||||||
|
"Name": "晨间工作流 (一键多开)",
|
||||||
|
"IconCode": "f0e8",
|
||||||
|
"IsGroup": true,
|
||||||
|
"SubToolIds": ["uuid-001", "uuid-another-id"],
|
||||||
|
"CategoryId": "1",
|
||||||
|
"HotKey": "Ctrl+Shift+W"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 执行逻辑演进 (`ProcessExecutionService.cs`)
|
||||||
|
执行服务需要加入递归或循环逻辑:
|
||||||
|
1. 接收到 `ToolItem` 后,判断 `IsGroup`。
|
||||||
|
2. 若为 `false`,走原有的单进程启动逻辑。
|
||||||
|
3. 若为 `true`,遍历 `SubToolIds`,从全局数据中查找到对应的 `ToolItem`。
|
||||||
|
4. **防卡顿机制**:通过 `async/await` 和 `Task.Delay(500)`,在每次启动应用之间加入 500 毫秒的延迟,防止瞬间拉起大量进程导致系统假死。
|
||||||
|
5. **孤儿数据容错**:如果子工具被删除了(通过 ID 找不到),直接跳过并打印一条日志,不中断整个多开流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> "我们现在要补充一个非常关键的需求:**Phase 6: 一键多开 (工具组合) 功能**。
|
||||||
|
> 用户希望创建一个特殊的卡片,点击它时可以同时打开多个已配置的工具。为了保证与之前架构兼容,请帮我按以下步骤修改:
|
||||||
|
>
|
||||||
|
> **1. 更新数据模型 (`ToolItem.cs`):**
|
||||||
|
> - 新增属性 `public bool IsGroup { get; set; }`
|
||||||
|
> - 新增属性 `public ObservableCollection<string> SubToolIds { get; set; } = new();`
|
||||||
|
>
|
||||||
|
> **2. 更新执行逻辑 (`ProcessExecutionService.cs`):**
|
||||||
|
> - 将 `Execute` 方法改为异步方法 `public async Task ExecuteAsync(ToolItem item)`。
|
||||||
|
> - 在方法内部增加逻辑:如果 `item.IsGroup == true`,则遍历 `item.SubToolIds`,通过 `IDataService` 获取对应的子 `ToolItem`。
|
||||||
|
> - 使用 `foreach` 循环执行子工具。为了防止系统卡顿,每次拉起进程后加入 `await Task.Delay(500)`。
|
||||||
|
> - 如果通过 ID 找不到子工具(可能被删除了),调用 `ILogService` 打印警告:“组合启动跳过:找不到 ID 为 xxx 的工具”,并继续执行下一个。
|
||||||
|
>
|
||||||
|
> **3. UI 与交互更新 (`MainWindow.xaml` & `MainViewModel.cs`):**
|
||||||
|
> - 在主界面“添加工具”按钮旁边,新增一个“添加组合 (Add Group)”按钮。
|
||||||
|
> - 为组合卡片在 UI 上增加视觉区分:在 `ItemsControl` 的卡片 DataTemplate 中,利用 `DataTrigger` 绑定 `IsGroup` 属性。如果为 true,显示一个形如“📦 组合”的 Badge 或角标文字。
|
||||||
|
>
|
||||||
|
> **4. 新增组合编辑弹窗 (`GroupEditWindow.xaml`):**
|
||||||
|
> - 创建一个新的弹窗用于编辑组合。
|
||||||
|
> - 包含:组合名称、图标、所属分类、快捷键。
|
||||||
|
> - 核心:一个包含当前所有非组合工具(`IsGroup == false`)的列表,每项前面带有一个 `CheckBox`。用户勾选后,保存时将选中的工具 ID 存入 `SubToolIds`。
|
||||||
Reference in New Issue
Block a user