Phase 7: 自定义图标系统 + 组合嵌套 + 删除功能 + UI 优化

- 安装 FontAwesome.Sharp v6.6.0,新增 Helpers/IconProvider.cs(190+ 图标,11 分类)
- ToolEditWindow/GroupEditWindow: 添加图标选择 ComboBox,按分类分组,带图标预览
- MainWindow 卡片模板: 使用 fa:IconBlock 渲染图标,IconCode 为空时回退首字母
- 组合角标从 emoji 改为 FontAwesome Cubes 图标,放置于卡片右上角
- ComboBox 样式修复: TextBlock→ContentPresenter,支持 ItemTemplate+DisplayMemberPath
- ComboBox 滚轮修复: CanContentScroll=False 解决分组模式下滑轮跳组问题
- 编辑弹窗: 所有输入控件添加 VerticalContentAlignment=Center
- 组合可包含其他组合: GetAncestorIds 递归排除自身及祖先防止循环引用
- ProcessExecutionService: 支持嵌套组合递归执行,visited 集合防死循环
- MainWindow 右键菜单新增删除功能,工具和组合均支持
- 移除侧边栏顶部个人工具箱标题文字
- 更新测试: 适配组合嵌套逻辑,新增祖先排除测试 (83/83 通过)
This commit is contained in:
2026-05-10 02:19:32 +08:00
parent 2c985e8d63
commit 85919381b1
13 changed files with 695 additions and 89 deletions

View File

@@ -1,9 +1,11 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using PersonalToolBox.Helpers;
using PersonalToolBox.Models;
using PersonalToolBox.Services;
@@ -38,11 +40,19 @@ public partial class ToolEditViewModel : ObservableObject
[ObservableProperty]
private Category? _selectedCategory;
[ObservableProperty]
private IconProvider.IconOption? _selectedIcon;
/// <summary>
/// 分类下拉列表
/// </summary>
public ObservableCollection<Category> Categories { get; } = new();
/// <summary>
/// 按分类分组的可用图标列表
/// </summary>
public ListCollectionView AvailableIconsView { get; }
/// <summary>
/// 窗口关闭回调(由 View 层设置)
/// </summary>
@@ -61,6 +71,9 @@ public partial class ToolEditViewModel : ObservableObject
_logService = logService;
_editingTool = toolToEdit;
AvailableIconsView = new ListCollectionView(IconProvider.AvailableIcons);
AvailableIconsView.GroupDescriptions.Add(new PropertyGroupDescription("Category"));
// 加载分类列表
foreach (var cat in _dataService.Config.Categories)
Categories.Add(cat);
@@ -74,6 +87,11 @@ public partial class ToolEditViewModel : ObservableObject
Arguments = toolToEdit.Arguments;
HotKey = toolToEdit.HotKey;
SelectedCategory = Categories.FirstOrDefault(c => c.Id == toolToEdit.CategoryId);
if (!string.IsNullOrEmpty(toolToEdit.IconCode))
{
SelectedIcon = IconProvider.AvailableIcons.FirstOrDefault(i => i.Icon.ToString() == toolToEdit.IconCode);
}
}
}
@@ -125,6 +143,7 @@ public partial class ToolEditViewModel : ObservableObject
if (_editingTool != null)
{
_editingTool.Name = Name.Trim();
_editingTool.IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty;
_editingTool.ExecutablePath = ExecutablePath.Trim();
_editingTool.Arguments = Arguments.Trim();
_editingTool.HotKey = HotKey.Trim();
@@ -139,6 +158,7 @@ public partial class ToolEditViewModel : ObservableObject
var newTool = new ToolItem
{
Name = Name.Trim(),
IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty,
ExecutablePath = ExecutablePath.Trim(),
Arguments = Arguments.Trim(),
HotKey = HotKey.Trim(),