- 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
179 lines
5.8 KiB
C#
179 lines
5.8 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.Linq;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using PersonalToolBox.Helpers;
|
||
using PersonalToolBox.Models;
|
||
using PersonalToolBox.Services;
|
||
using PersonalToolBox.Views;
|
||
|
||
namespace PersonalToolBox.ViewModels;
|
||
|
||
/// <summary>
|
||
/// 主窗口 ViewModel,管理 UI 状态、数据绑定和用户交互逻辑
|
||
/// </summary>
|
||
public partial class MainViewModel : ObservableObject
|
||
{
|
||
private readonly ILogService _logService;
|
||
private readonly IDataService _dataService;
|
||
private readonly IProcessExecutionService _processService;
|
||
private readonly IServiceProvider _serviceProvider;
|
||
|
||
/// <summary>
|
||
/// 全部分类的虚拟对象
|
||
/// </summary>
|
||
private static readonly Category AllCategory = new() { Id = "", Name = "全部" };
|
||
|
||
public MainViewModel(
|
||
ILogService logService,
|
||
IDataService dataService,
|
||
IProcessExecutionService processService,
|
||
IServiceProvider serviceProvider)
|
||
{
|
||
_logService = logService;
|
||
_dataService = dataService;
|
||
_processService = processService;
|
||
_serviceProvider = serviceProvider;
|
||
|
||
LoadData();
|
||
}
|
||
|
||
// ───────────────────────────── 可观察属性 ─────────────────────────────
|
||
|
||
[ObservableProperty]
|
||
private string _searchText = string.Empty;
|
||
|
||
[ObservableProperty]
|
||
private Category? _selectedCategory;
|
||
|
||
public ObservableCollection<LogEntry> Logs => _logService.Logs;
|
||
|
||
public ObservableCollection<Category> Categories { get; } = new();
|
||
|
||
public ObservableCollection<ToolItem> Tools { get; } = new();
|
||
|
||
[ObservableProperty]
|
||
private ObservableCollection<ToolItem> _filteredTools = new();
|
||
|
||
[ObservableProperty]
|
||
private string _currentTheme = "Dark";
|
||
|
||
// ───────────────────────────── 命令 ─────────────────────────────
|
||
|
||
[RelayCommand]
|
||
private void ClearLogs() => _logService.Clear();
|
||
|
||
[RelayCommand]
|
||
private void ToggleTheme()
|
||
{
|
||
CurrentTheme = CurrentTheme == "Dark" ? "Light" : "Dark";
|
||
ThemeHelper.ApplyTheme(CurrentTheme);
|
||
_dataService.Config.Theme = CurrentTheme;
|
||
_dataService.Save();
|
||
_logService.Info($"已切换到{(CurrentTheme == "Dark" ? "暗黑" : "明亮")}主题");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开添加工具弹窗
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void AddTool()
|
||
{
|
||
var vm = _serviceProvider.GetRequiredService<ToolEditViewModel>();
|
||
var window = new ToolEditWindow(vm);
|
||
window.ShowDialog();
|
||
|
||
if (vm.Saved)
|
||
{
|
||
RefreshData();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开编辑工具弹窗
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void EditTool(ToolItem tool)
|
||
{
|
||
if (tool == null) return;
|
||
|
||
// 创建编辑 ViewModel(需要新建实例,DI 容器无法区分参数)
|
||
var dataService = _serviceProvider.GetRequiredService<IDataService>();
|
||
var logService = _serviceProvider.GetRequiredService<ILogService>();
|
||
var editVm = new ToolEditViewModel(dataService, logService, tool);
|
||
var window = new ToolEditWindow(editVm);
|
||
window.ShowDialog();
|
||
|
||
if (editVm.Saved)
|
||
{
|
||
RefreshData();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行工具(双击卡片或右键菜单)
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void ExecuteTool(ToolItem? tool)
|
||
{
|
||
if (tool == null) return;
|
||
_processService.Execute(tool);
|
||
}
|
||
|
||
// ───────────────────────────── 数据刷新 ─────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 从配置重新加载数据并刷新 UI
|
||
/// </summary>
|
||
public void RefreshData()
|
||
{
|
||
var previousCategoryId = SelectedCategory?.Id;
|
||
|
||
Categories.Clear();
|
||
Categories.Add(AllCategory);
|
||
|
||
foreach (var cat in _dataService.Config.Categories)
|
||
Categories.Add(cat);
|
||
|
||
Tools.Clear();
|
||
foreach (var tool in _dataService.Config.Tools)
|
||
Tools.Add(tool);
|
||
|
||
// 恢复之前选中的分类(若仍存在),否则选中"全部"
|
||
SelectedCategory = Categories.FirstOrDefault(c => c.Id == previousCategoryId) ?? AllCategory;
|
||
ApplyFilter();
|
||
}
|
||
|
||
// ───────────────────────────── 初始化 ─────────────────────────────
|
||
|
||
private void LoadData()
|
||
{
|
||
RefreshData();
|
||
|
||
SelectedCategory = AllCategory;
|
||
CurrentTheme = _dataService.Config.Theme;
|
||
ThemeHelper.ApplyTheme(CurrentTheme);
|
||
}
|
||
|
||
// ───────────────────────────── 过滤逻辑 ─────────────────────────────
|
||
|
||
partial void OnSearchTextChanged(string value) => ApplyFilter();
|
||
|
||
partial void OnSelectedCategoryChanged(Category? value) => ApplyFilter();
|
||
|
||
private void ApplyFilter()
|
||
{
|
||
var filtered = Tools.AsEnumerable();
|
||
|
||
if (SelectedCategory != null && !string.IsNullOrEmpty(SelectedCategory.Id))
|
||
filtered = filtered.Where(t => t.CategoryId == SelectedCategory.Id);
|
||
|
||
if (!string.IsNullOrWhiteSpace(SearchText))
|
||
filtered = filtered.Where(t =>
|
||
t.Name.Contains(SearchText, System.StringComparison.OrdinalIgnoreCase));
|
||
|
||
FilteredTools = new ObservableCollection<ToolItem>(filtered);
|
||
}
|
||
}
|