Files
personal-toolbox/PersonalToolBox/ViewModels/ToolEditViewModel.cs
home-PC 71be5da54b 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
2026-05-09 21:52:31 +08:00

192 lines
6.0 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.IO;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
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;
/// <summary>
/// 分类下拉列表
/// </summary>
public ObservableCollection<Category> Categories { get; } = new();
/// <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;
// 加载分类列表
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);
}
}
// ───────────────────────────── 命令 ─────────────────────────────
/// <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.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(),
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);
}
}