diff --git a/PersonalToolBox.Tests/ViewModels/CategoryEditViewModelTests.cs b/PersonalToolBox.Tests/ViewModels/CategoryEditViewModelTests.cs new file mode 100644 index 0000000..fe00357 --- /dev/null +++ b/PersonalToolBox.Tests/ViewModels/CategoryEditViewModelTests.cs @@ -0,0 +1,94 @@ +using Moq; +using PersonalToolBox.Models; +using PersonalToolBox.Services; +using PersonalToolBox.ViewModels; + +namespace PersonalToolBox.Tests.ViewModels; + +public class CategoryEditViewModelTests +{ + private readonly Mock _dataServiceMock = new(); + private readonly Mock _logServiceMock = new(); + private readonly AppConfig _config; + + public CategoryEditViewModelTests() + { + _config = new AppConfig(); + _dataServiceMock.Setup(d => d.Config).Returns(_config); + } + + [Fact] + public void Constructor_AddMode_SetsTitle() + { + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object); + + Assert.Equal("添加分类", vm.WindowTitle); + Assert.Empty(vm.Name); + } + + [Fact] + public void Constructor_EditMode_SetsTitleAndName() + { + var cat = new Category { Name = "开发工具" }; + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, cat); + + Assert.Equal("编辑分类", vm.WindowTitle); + Assert.Equal("开发工具", vm.Name); + } + + [Fact] + public void Save_EmptyName_LogsWarning() + { + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object); + + vm.SaveCommand.Execute(null); + + _logServiceMock.Verify(x => x.Warning(It.Is(s => s.Contains("名称不能为空"))), Times.Once); + } + + [Fact] + public void Save_AddMode_AddsCategoryAndCloses() + { + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object) { Name = "新分类" }; + bool? closeResult = null; + vm.CloseAction = (r) => closeResult = r; + + vm.SaveCommand.Execute(null); + + Assert.True(vm.Saved); + Assert.True(closeResult); + Assert.Single(_config.Categories); + Assert.Equal("新分类", _config.Categories[0].Name); + _dataServiceMock.Verify(x => x.Save(), Times.Once); + } + + [Fact] + public void Save_EditMode_UpdatesCategoryAndCloses() + { + var cat = new Category { Name = "旧名称" }; + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, cat) { Name = "新名称" }; + bool? closeResult = null; + vm.CloseAction = (r) => closeResult = r; + + vm.SaveCommand.Execute(null); + + Assert.True(vm.Saved); + Assert.True(closeResult); + Assert.Equal("新名称", cat.Name); + _dataServiceMock.Verify(x => x.Save(), Times.Once); + } + + [Fact] + public void Cancel_ClosesWithoutSaving() + { + var vm = new CategoryEditViewModel(_dataServiceMock.Object, _logServiceMock.Object); + bool? closeResult = null; + vm.CloseAction = (r) => closeResult = r; + + vm.CancelCommand.Execute(null); + + Assert.False(vm.Saved); + Assert.False(closeResult); + _dataServiceMock.Verify(x => x.Save(), Times.Never); + } +} diff --git a/PersonalToolBox/App.xaml.cs b/PersonalToolBox/App.xaml.cs index 08f2458..b70e649 100644 --- a/PersonalToolBox/App.xaml.cs +++ b/PersonalToolBox/App.xaml.cs @@ -70,6 +70,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); } diff --git a/PersonalToolBox/ViewModels/CategoryEditViewModel.cs b/PersonalToolBox/ViewModels/CategoryEditViewModel.cs new file mode 100644 index 0000000..6e8db66 --- /dev/null +++ b/PersonalToolBox/ViewModels/CategoryEditViewModel.cs @@ -0,0 +1,80 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PersonalToolBox.Models; +using PersonalToolBox.Services; + +namespace PersonalToolBox.ViewModels; + +/// +/// 分类编辑窗口 ViewModel,管理添加/编辑分类的交互逻辑 +/// +public partial class CategoryEditViewModel : ObservableObject +{ + private readonly IDataService _dataService; + private readonly ILogService _logService; + private readonly Category? _editingCategory; + + [ObservableProperty] + private string _windowTitle = "添加分类"; + + [ObservableProperty] + private string _name = string.Empty; + + public Action? CloseAction { get; set; } + public bool Saved { get; private set; } + + public CategoryEditViewModel(IDataService dataService, ILogService logService, Category? categoryToEdit = null) + { + _dataService = dataService; + _logService = logService; + _editingCategory = categoryToEdit; + + if (categoryToEdit != null) + { + WindowTitle = "编辑分类"; + Name = categoryToEdit.Name; + } + } + + [RelayCommand] + private void Save() + { + try + { + if (string.IsNullOrWhiteSpace(Name)) + { + _logService.Warning("分类名称不能为空"); + return; + } + + if (_editingCategory != null) + { + _editingCategory.Name = Name.Trim(); + _logService.Info($"已更新分类: {Name.Trim()}"); + } + else + { + _dataService.Config.Categories.Add(new Category + { + Id = Guid.NewGuid().ToString(), + Name = Name.Trim() + }); + _logService.Info($"已添加分类: {Name.Trim()}"); + } + + _dataService.Save(); + Saved = true; + CloseAction?.Invoke(true); + } + catch (Exception ex) + { + _logService.Error($"保存分类失败: {ex.Message}"); + } + } + + [RelayCommand] + private void Cancel() + { + CloseAction?.Invoke(false); + } +} diff --git a/PersonalToolBox/ViewModels/MainViewModel.cs b/PersonalToolBox/ViewModels/MainViewModel.cs index 7539af3..c9760e6 100644 --- a/PersonalToolBox/ViewModels/MainViewModel.cs +++ b/PersonalToolBox/ViewModels/MainViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Linq; +using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; @@ -121,6 +122,63 @@ public partial class MainViewModel : ObservableObject _processService.Execute(tool); } + // ───────────────────────────── 分类管理命令 ───────────────────────────── + + /// + /// 添加分类 + /// + [RelayCommand] + private void AddCategory() + { + var vm = _serviceProvider.GetRequiredService(); + var window = new CategoryEditWindow(vm); + window.ShowDialog(); + + if (vm.Saved) RefreshData(); + } + + /// + /// 编辑分类(右键菜单触发) + /// + [RelayCommand] + private void EditCategory(Category? category) + { + if (category == null || string.IsNullOrEmpty(category.Id)) return; + + var dataService = _serviceProvider.GetRequiredService(); + var logService = _serviceProvider.GetRequiredService(); + var editVm = new CategoryEditViewModel(dataService, logService, category); + var window = new CategoryEditWindow(editVm); + window.ShowDialog(); + + if (editVm.Saved) RefreshData(); + } + + /// + /// 删除分类,其下所有工具移入"全部" + /// + [RelayCommand] + private void DeleteCategory(Category? category) + { + if (category == null || string.IsNullOrEmpty(category.Id)) return; + + if (MessageBox.Show( + $"确定删除分类 \"{category.Name}\" 吗?\n该分类下的所有工具将移入「全部」。", + "确认删除", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) + return; + + // 将属于该分类的工具设为"全部" + foreach (var tool in _dataService.Config.Tools.Where(t => t.CategoryId == category.Id)) + tool.CategoryId = string.Empty; + + // 从配置中移除分类 + _dataService.Config.Categories.Remove(category); + _dataService.Save(); + + _logService.Info($"已删除分类: {category.Name}"); + RefreshData(); + } + // ───────────────────────────── 数据刷新 ───────────────────────────── /// diff --git a/PersonalToolBox/Views/CategoryEditWindow.xaml b/PersonalToolBox/Views/CategoryEditWindow.xaml new file mode 100644 index 0000000..e144e65 --- /dev/null +++ b/PersonalToolBox/Views/CategoryEditWindow.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + +