Files
personal-toolbox/PersonalToolBox/ViewModels/ToolEditViewModel.cs
home-PC 85919381b1 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 通过)
2026-05-10 02:19:32 +08:00

212 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
namespace PersonalToolBox.ViewModels;
/// <summary>
/// 工具编辑窗口 ViewModel管理添加/编辑工具的交互逻辑
/// </summary>
public partial class ToolEditViewModel : ObservableObject
{
private readonly IDataService _dataService;
private readonly ILogService _logService;
private readonly ToolItem? _editingTool;
// ───────────────────────────── 可观察属性 ─────────────────────────────
[ObservableProperty]
private string _windowTitle = "添加工具";
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private string _executablePath = string.Empty;
[ObservableProperty]
private string _arguments = string.Empty;
[ObservableProperty]
private string _hotKey = string.Empty;
[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>
public Action<bool?>? CloseAction { get; set; }
/// <summary>
/// 是否保存成功(供调用方判断是否需要刷新)
/// </summary>
public bool Saved { get; private set; }
// ───────────────────────────── 构造函数 ─────────────────────────────
public ToolEditViewModel(IDataService dataService, ILogService logService, ToolItem? toolToEdit = null)
{
_dataService = dataService;
_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);
// 编辑模式
if (toolToEdit != null)
{
WindowTitle = "编辑工具";
Name = toolToEdit.Name;
ExecutablePath = toolToEdit.ExecutablePath;
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);
}
}
}
// ───────────────────────────── 命令 ─────────────────────────────
/// <summary>
/// 浏览本地文件
/// </summary>
[RelayCommand]
private void BrowseFile()
{
var dialog = new OpenFileDialog
{
Title = "选择可执行文件或脚本",
Filter = "所有文件|*.*|可执行文件|*.exe|脚本文件|*.bat;*.cmd;*.ps1;*.py|快捷方式|*.lnk"
};
if (dialog.ShowDialog() == true)
{
ExecutablePath = dialog.FileName;
if (string.IsNullOrWhiteSpace(Name))
{
Name = Path.GetFileNameWithoutExtension(dialog.FileName);
}
}
}
/// <summary>
/// 保存工具
/// </summary>
[RelayCommand]
private void Save()
{
try
{
if (string.IsNullOrWhiteSpace(Name))
{
_logService.Warning("工具名称不能为空");
return;
}
if (string.IsNullOrWhiteSpace(ExecutablePath))
{
_logService.Warning($"工具 \"{Name}\" 路径不能为空");
return;
}
// 编辑模式:更新已有工具
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();
_editingTool.CategoryId = SelectedCategory?.Id ?? string.Empty;
_editingTool.IsValid = IsExecutablePathValid(ExecutablePath.Trim());
_logService.Info($"已更新工具: {Name.Trim()}");
}
// 添加模式:创建新工具
else
{
var newTool = new ToolItem
{
Name = Name.Trim(),
IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty,
ExecutablePath = ExecutablePath.Trim(),
Arguments = Arguments.Trim(),
HotKey = HotKey.Trim(),
CategoryId = SelectedCategory?.Id ?? string.Empty,
IsValid = IsExecutablePathValid(ExecutablePath.Trim())
};
_dataService.Config.Tools.Add(newTool);
_logService.Info($"已添加工具: {Name.Trim()}");
}
_dataService.Save();
Saved = true;
CloseAction?.Invoke(true);
}
catch (Exception ex)
{
_logService.Error($"保存工具失败: {ex.Message}");
}
}
/// <summary>
/// 取消编辑
/// </summary>
[RelayCommand]
private void Cancel()
{
CloseAction?.Invoke(false);
}
/// <summary>
/// 验证可执行路径是否有效
/// </summary>
private static bool IsExecutablePathValid(string path)
{
if (string.IsNullOrWhiteSpace(path))
return false;
// URL 格式https://...)直接通过
if (Uri.TryCreate(path, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
return true;
// 纯文件名(无路径分隔符)可能位于系统 PATH 中
if (!path.Contains('\\') && !path.Contains('/'))
return true;
return File.Exists(path);
}
}