Files
personal-toolbox/PersonalToolBox/ViewModels/MainViewModel.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

179 lines
5.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.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);
}
}