- 安装 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 通过)
191 lines
6.0 KiB
C#
191 lines
6.0 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.ComponentModel;
|
||
using System.Linq;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using PersonalToolBox.Helpers;
|
||
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;
|
||
|
||
[ObservableProperty]
|
||
private IconProvider.IconOption? _selectedIcon;
|
||
|
||
/// <summary>
|
||
/// 可供勾选的普通工具列表(IsGroup == false)
|
||
/// </summary>
|
||
public ObservableCollection<SelectableTool> AvailableTools { get; } = new();
|
||
|
||
/// <summary>
|
||
/// 分类下拉列表
|
||
/// </summary>
|
||
public ObservableCollection<Category> Categories { get; } = new();
|
||
|
||
/// <summary>
|
||
/// 按分类分组的可用图标列表
|
||
/// </summary>
|
||
public ListCollectionView AvailableIconsView { get; }
|
||
|
||
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;
|
||
|
||
AvailableIconsView = new ListCollectionView(IconProvider.AvailableIcons);
|
||
AvailableIconsView.GroupDescriptions.Add(new PropertyGroupDescription("Category"));
|
||
|
||
foreach (var cat in _dataService.Config.Categories)
|
||
Categories.Add(cat);
|
||
|
||
// 加载所有工具(排除自身及祖先组合,防止循环引用)
|
||
var excludeIds = GetAncestorIds();
|
||
var selectedIds = _editingGroup?.SubToolIds ?? new List<string>();
|
||
foreach (var tool in _dataService.Config.Tools.Where(t => !excludeIds.Contains(t.Id)))
|
||
{
|
||
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);
|
||
|
||
if (!string.IsNullOrEmpty(groupToEdit.IconCode))
|
||
{
|
||
SelectedIcon = IconProvider.AvailableIcons.FirstOrDefault(i => i.Icon.ToString() == groupToEdit.IconCode);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ──────── 命令 ────────
|
||
|
||
[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.IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty;
|
||
_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(),
|
||
IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty,
|
||
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>
|
||
/// 获取需要排除的工具 ID 集合(自身 + 所有祖先组合),防止循环引用
|
||
/// </summary>
|
||
private System.Collections.Generic.HashSet<string> GetAncestorIds()
|
||
{
|
||
var ancestors = new System.Collections.Generic.HashSet<string>();
|
||
if (_editingGroup != null)
|
||
{
|
||
ancestors.Add(_editingGroup.Id);
|
||
CollectAncestors(_editingGroup.Id, ancestors);
|
||
}
|
||
return ancestors;
|
||
}
|
||
|
||
private void CollectAncestors(string groupId, System.Collections.Generic.HashSet<string> ancestors)
|
||
{
|
||
foreach (var tool in _dataService.Config.Tools.Where(t => t.IsGroup))
|
||
{
|
||
if (tool.SubToolIds.Contains(groupId) && ancestors.Add(tool.Id))
|
||
{
|
||
CollectAncestors(tool.Id, ancestors);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 可勾选的工具项(用于 CheckBox 列表绑定)
|
||
/// </summary>
|
||
public partial class SelectableTool : ObservableObject
|
||
{
|
||
public ToolItem Tool { get; set; } = null!;
|
||
|
||
[ObservableProperty]
|
||
private bool _isSelected;
|
||
}
|