From 85919381b1108a3b957afb500c11be0528b57b75 Mon Sep 17 00:00:00 2001 From: home-PC Date: Sun, 10 May 2026 02:19:32 +0800 Subject: [PATCH] =?UTF-8?q?Phase=207:=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=B3=BB=E7=BB=9F=20+=20=E7=BB=84=E5=90=88?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=20+=20=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20+=20UI=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 安装 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 通过) --- .../ViewModels/GroupEditViewModelTests.cs | 24 +- PersonalToolBox/Helpers/IconProvider.cs | 238 ++++++++++++++++++ PersonalToolBox/Models/ToolItem.cs | 2 +- PersonalToolBox/PersonalToolBox.csproj | 1 + .../Services/ProcessExecutionService.cs | 36 ++- .../ViewModels/GroupEditViewModel.cs | 50 +++- PersonalToolBox/ViewModels/MainViewModel.cs | 19 ++ .../ViewModels/ToolEditViewModel.cs | 20 ++ PersonalToolBox/Views/CategoryEditWindow.xaml | 1 + PersonalToolBox/Views/GroupEditWindow.xaml | 189 ++++++++++++-- PersonalToolBox/Views/MainWindow.xaml | 71 ++++-- PersonalToolBox/Views/MainWindow.xaml.cs | 17 ++ PersonalToolBox/Views/ToolEditWindow.xaml | 116 ++++++--- 13 files changed, 695 insertions(+), 89 deletions(-) create mode 100644 PersonalToolBox/Helpers/IconProvider.cs diff --git a/PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs b/PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs index 0aa5c1c..ba5f759 100644 --- a/PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs +++ b/PersonalToolBox.Tests/ViewModels/GroupEditViewModelTests.cs @@ -26,12 +26,32 @@ public class GroupEditViewModelTests } [Fact] - public void Constructor_AddMode_LoadsOnlyNonGroupTools() + public void Constructor_AddMode_LoadsAllToolsIncludingGroups() { var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object); Assert.Equal("添加组合", vm.WindowTitle); - Assert.Equal(2, vm.AvailableTools.Count); // t1, t2 only (t3 is a group) + Assert.Equal(3, vm.AvailableTools.Count); // t1, t2, t3 (new groups can include other groups) + } + + [Fact] + public void Constructor_EditMode_ExcludesSelfAndAncestors() + { + // Group A (t3) contains t1 -> Group B being edited should exclude itself + var group = new ToolItem + { + Id = "t4", + Name = "GroupB", + IsGroup = true, + SubToolIds = new List() + }; + // t3 contains t4, so t3 is an ancestor of t4 + _config.Tools.First(t => t.Id == "t3").SubToolIds.Add("t4"); + + var vm = new GroupEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, group); + + // t4 (self) and t3 (ancestor) should be excluded + Assert.Equal(2, vm.AvailableTools.Count); // t1, t2 only } [Fact] diff --git a/PersonalToolBox/Helpers/IconProvider.cs b/PersonalToolBox/Helpers/IconProvider.cs new file mode 100644 index 0000000..bd2377a --- /dev/null +++ b/PersonalToolBox/Helpers/IconProvider.cs @@ -0,0 +1,238 @@ +using System.Collections.Generic; +using FontAwesome.Sharp; + +namespace PersonalToolBox.Helpers; + +public static class IconProvider +{ + public record IconOption(string Name, IconChar Icon, string Category); + + public static List AvailableIcons { get; } = new() + { + // ── 开发工具 ── + new("代码", IconChar.Code, "开发"), + new("终端", IconChar.Terminal, "开发"), + new("Bug", IconChar.Bug, "开发"), + new("Git", IconChar.GitAlt, "开发"), + new("GitHub", IconChar.Github, "开发"), + new("数据库", IconChar.Database, "开发"), + new("服务器", IconChar.Server, "开发"), + new("云服务", IconChar.Cloud, "开发"), + new("代码分支", IconChar.CodeBranch, "开发"), + new("键盘", IconChar.Keyboard, "开发"), + new("Docker", IconChar.Docker, "开发"), + new("Python", IconChar.Python, "开发"), + new("HTML5", IconChar.Html5, "开发"), + new("CSS3", IconChar.Css3Alt, "开发"), + new("JS", IconChar.Js, "开发"), + new("Markdown", IconChar.Markdown, "开发"), + new("NPM", IconChar.Npm, "开发"), + new("Node", IconChar.NodeJs, "开发"), + new("Rust", IconChar.Rust, "开发"), + + // ── 文件与文件夹 ── + new("文件夹", IconChar.Folder, "文件"), + new("文件夹打开", IconChar.FolderOpen, "文件"), + new("文件夹树", IconChar.FolderTree, "文件"), + new("文件", IconChar.File, "文件"), + new("文件代码", IconChar.FileCode, "文件"), + new("文件图片", IconChar.FileImage, "文件"), + new("文件PDF", IconChar.FilePdf, "文件"), + new("文件Word", IconChar.FileWord, "文件"), + new("文件Excel", IconChar.FileExcel, "文件"), + new("文件存档", IconChar.FileArchive, "文件"), + new("文件音频", IconChar.FileAudio, "文件"), + new("文件视频", IconChar.FileVideo, "文件"), + new("文件签名", IconChar.FileSignature, "文件"), + new("下载", IconChar.Download, "文件"), + new("上传", IconChar.Upload, "文件"), + new("保存", IconChar.Save, "文件"), + new("复制", IconChar.Copy, "文件"), + new("粘贴", IconChar.Paste, "文件"), + new("剪切", IconChar.Cut, "文件"), + new("打印", IconChar.Print, "文件"), + new("剪贴板", IconChar.Clipboard, "文件"), + new("回收站", IconChar.Trash, "文件"), + new("撤销", IconChar.Undo, "文件"), + new("重做", IconChar.Redo, "文件"), + + // ── 网页与网络 ── + new("地球", IconChar.Globe, "网络"), + new("WiFi", IconChar.Wifi, "网络"), + new("链接", IconChar.Link, "网络"), + new("RSS", IconChar.Rss, "网络"), + new("网络", IconChar.NetworkWired, "网络"), + new("蓝牙", IconChar.Bluetooth, "网络"), + new("信号", IconChar.Signal, "网络"), + new("卫星", IconChar.Satellite, "网络"), + new("上传云", IconChar.CloudUploadAlt, "网络"), + new("下载云", IconChar.CloudDownloadAlt, "网络"), + new("共享", IconChar.ShareAlt, "网络"), + new("同步", IconChar.Sync, "网络"), + new("邮件发送", IconChar.PaperPlane, "网络"), + new("以太网", IconChar.Ethernet, "网络"), + + // ── 设置与工具 ── + new("齿轮", IconChar.Cog, "设置"), + new("扳手", IconChar.Wrench, "设置"), + new("工具", IconChar.Tools, "设置"), + new("搜索", IconChar.Search, "设置"), + new("锁定", IconChar.Lock, "设置"), + new("解锁", IconChar.Unlock, "设置"), + new("盾牌", IconChar.ShieldAlt, "设置"), + new("钥匙", IconChar.Key, "设置"), + new("放大镜+", IconChar.SearchPlus, "设置"), + new("放大镜-", IconChar.SearchMinus, "设置"), + new("过滤器", IconChar.Filter, "设置"), + new("排序", IconChar.Sort, "设置"), + new("垃圾桶", IconChar.TrashAlt, "设置"), + new("编辑", IconChar.Edit, "设置"), + new("笔", IconChar.Pen, "设置"), + new("铅笔", IconChar.PencilAlt, "设置"), + new("放大", IconChar.Expand, "设置"), + new("缩小", IconChar.Compress, "设置"), + new("二维码", IconChar.Qrcode, "设置"), + new("条形码", IconChar.Barcode, "设置"), + new("指纹", IconChar.Fingerprint, "设置"), + new("拼图", IconChar.PuzzlePiece, "设置"), + + // ── 媒体与娱乐 ── + new("播放", IconChar.Play, "媒体"), + new("暂停", IconChar.Pause, "媒体"), + new("停止", IconChar.Stop, "媒体"), + new("音乐", IconChar.Music, "媒体"), + new("图片", IconChar.Image, "媒体"), + new("视频", IconChar.Video, "媒体"), + new("相机", IconChar.Camera, "媒体"), + new("游戏手柄", IconChar.Gamepad, "媒体"), + new("耳机", IconChar.Headphones, "媒体"), + new("音量", IconChar.VolumeUp, "媒体"), + new("静音", IconChar.VolumeMute, "媒体"), + new("麦克风", IconChar.Microphone, "媒体"), + new("电视", IconChar.Tv, "媒体"), + new("电影", IconChar.Film, "媒体"), + new("CD", IconChar.CompactDisc, "媒体"), + new("唱片", IconChar.Headphones, "媒体"), + + // ── 常用 ── + new("主页", IconChar.Home, "常用"), + new("用户", IconChar.User, "常用"), + new("用户组", IconChar.Users, "常用"), + new("时钟", IconChar.Clock, "常用"), + new("日历", IconChar.Calendar, "常用"), + new("日历检查", IconChar.CalendarCheck, "常用"), + new("日历天", IconChar.CalendarDay, "常用"), + new("日历周", IconChar.CalendarWeek, "常用"), + new("信封", IconChar.Envelope, "常用"), + new("邮件打开", IconChar.EnvelopeOpen, "常用"), + new("铃铛", IconChar.Bell, "常用"), + new("铃铛斜线", IconChar.BellSlash, "常用"), + new("书签", IconChar.Bookmark, "常用"), + new("星标", IconChar.Star, "常用"), + new("空心星", IconChar.StarHalfAlt, "常用"), + new("心形", IconChar.Heart, "常用"), + new("旗帜", IconChar.Flag, "常用"), + new("地图标记", IconChar.MapMarkerAlt, "常用"), + new("指南针", IconChar.Compass, "常用"), + new("地图", IconChar.Map, "常用"), + new("火箭", IconChar.Rocket, "常用"), + new("灯泡", IconChar.Lightbulb, "常用"), + new("标签", IconChar.Tag, "常用"), + new("标签组", IconChar.Tags, "常用"), + new("列表", IconChar.List, "常用"), + new("列表项", IconChar.ListAlt, "常用"), + new("图表", IconChar.ChartBar, "常用"), + new("折线图", IconChar.ChartLine, "常用"), + new("饼图", IconChar.ChartPie, "常用"), + new("信息", IconChar.InfoCircle, "常用"), + new("警告", IconChar.ExclamationTriangle, "常用"), + new("问题", IconChar.QuestionCircle, "常用"), + new("检查", IconChar.CheckCircle, "常用"), + new("购物车", IconChar.ShoppingCart, "常用"), + new("信用卡", IconChar.CreditCard, "常用"), + new("钱包", IconChar.Wallet, "常用"), + new("电源", IconChar.PowerOff, "常用"), + new("闪电", IconChar.Bolt, "常用"), + new("调色板", IconChar.Palette, "常用"), + new("火", IconChar.Fire, "常用"), + new("水滴", IconChar.Tint, "常用"), + new("磁铁", IconChar.Magnet, "常用"), + new("奖杯", IconChar.Trophy, "常用"), + + // ── 办公 ── + new("编辑", IconChar.Edit, "办公"), + new("笔", IconChar.Pen, "办公"), + new("注释", IconChar.Comment, "办公"), + new("评论", IconChar.Comments, "办公"), + new("邮件@", IconChar.At, "办公"), + new("地址簿", IconChar.AddressBook, "办公"), + new("便签", IconChar.StickyNote, "办公"), + new("论文", IconChar.FileAlt, "办公"), + new("文件夹+", IconChar.FolderPlus, "办公"), + new("书", IconChar.Book, "办公"), + new("书打开", IconChar.BookOpen, "办公"), + new("商务包", IconChar.Briefcase, "办公"), + new("计算器", IconChar.Calculator, "办公"), + new("图表区", IconChar.ChartArea, "办公"), + new("收件箱", IconChar.Inbox, "办公"), + new("任务列表", IconChar.Tasks, "办公"), + new("笔尖", IconChar.PenNib, "办公"), + new("签名", IconChar.Signature, "办公"), + new("印章", IconChar.Stamp, "办公"), + + // ── 系统 ── + new("Windows", IconChar.Windows, "系统"), + new("桌面", IconChar.Desktop, "系统"), + new("笔记本", IconChar.Laptop, "系统"), + new("手机", IconChar.MobileAlt, "系统"), + new("平板", IconChar.TabletAlt, "系统"), + new("命令行", IconChar.Terminal, "系统"), + new("微芯片", IconChar.Microchip, "系统"), + new("硬盘", IconChar.Hdd, "系统"), + new("内存", IconChar.Memory, "系统"), + new("USB", IconChar.Usb, "系统"), + new("插件", IconChar.Plug, "系统"), + new("电池满", IconChar.BatteryFull, "系统"), + new("鼠标", IconChar.Mouse, "系统"), + new("SD卡", IconChar.SdCard, "系统"), + new("处理器", IconChar.Microchip, "系统"), + new("风扇", IconChar.Fan, "系统"), + new("温度计", IconChar.ThermometerHalf, "系统"), + + // ── 社交 ── + new("微信", IconChar.Weixin, "社交"), + new("微博", IconChar.Weibo, "社交"), + new("QQ", IconChar.Qq, "社交"), + new("Discord", IconChar.Discord, "社交"), + new("Slack", IconChar.Slack, "社交"), + new("Telegram", IconChar.Telegram, "社交"), + new("Reddit", IconChar.Reddit, "社交"), + new("YouTube", IconChar.Youtube, "社交"), + new("Twitch", IconChar.Twitch, "社交"), + new("Steam", IconChar.Steam, "社交"), + + // ── 箭头与导航 ── + new("右箭头", IconChar.ArrowRight, "导航"), + new("左箭头", IconChar.ArrowLeft, "导航"), + new("上箭头", IconChar.ArrowUp, "导航"), + new("下箭头", IconChar.ArrowDown, "导航"), + new("循环", IconChar.Redo, "导航"), + new("刷新", IconChar.Sync, "导航"), + new("返回", IconChar.ChevronLeft, "导航"), + new("前进", IconChar.ChevronRight, "导航"), + new("位置", IconChar.LocationArrow, "导航"), + new("十字准星", IconChar.Crosshairs, "导航"), + + // ── 安全 ── + new("用户锁", IconChar.UserLock, "安全"), + new("用户盾", IconChar.UserShield, "安全"), + new("病毒", IconChar.Virus, "安全"), + new("安全盾", IconChar.ShieldVirus, "安全"), + new("指纹", IconChar.Fingerprint, "安全"), + new("ID卡", IconChar.IdCard, "安全"), + new("门禁", IconChar.DoorOpen, "安全"), + new("面具", IconChar.Mask, "安全"), + new("眼睛", IconChar.Eye, "安全"), + new("眼睛斜线", IconChar.EyeSlash, "安全"), + }; +} diff --git a/PersonalToolBox/Models/ToolItem.cs b/PersonalToolBox/Models/ToolItem.cs index df0324c..7bc89fa 100644 --- a/PersonalToolBox/Models/ToolItem.cs +++ b/PersonalToolBox/Models/ToolItem.cs @@ -18,7 +18,7 @@ public class ToolItem public string Name { get; set; } = string.Empty; /// - /// 内置图标库的字符编码 + /// 内置图标库的图标枚举名称(如 "Github", "Wrench"),用于 FontAwesome 图标渲染 /// public string IconCode { get; set; } = string.Empty; diff --git a/PersonalToolBox/PersonalToolBox.csproj b/PersonalToolBox/PersonalToolBox.csproj index 425c985..5c2981e 100644 --- a/PersonalToolBox/PersonalToolBox.csproj +++ b/PersonalToolBox/PersonalToolBox.csproj @@ -11,6 +11,7 @@ + diff --git a/PersonalToolBox/Services/ProcessExecutionService.cs b/PersonalToolBox/Services/ProcessExecutionService.cs index 8dc74d1..e9d4225 100644 --- a/PersonalToolBox/Services/ProcessExecutionService.cs +++ b/PersonalToolBox/Services/ProcessExecutionService.cs @@ -1,11 +1,14 @@ +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; using PersonalToolBox.Models; namespace PersonalToolBox.Services; /// /// 进程执行服务,负责启动外部工具进程并处理异常 -/// 支持一键多开:当 IsGroup 为 true 时批量启动子工具 +/// 支持一键多开:当 IsGroup 为 true 时批量启动子工具(支持嵌套组合,带循环检测) /// public class ProcessExecutionService : IProcessExecutionService { @@ -26,14 +29,12 @@ public class ProcessExecutionService : IProcessExecutionService return; } - // 组合卡片:遍历子工具列表逐一启动 if (tool.IsGroup) { - await ExecuteGroupAsync(tool); + await ExecuteGroupAsync(tool, new HashSet()); return; } - // 普通工具:直接启动 if (!tool.IsValid) { _logService.Warning($"无法运行工具 \"{tool.Name}\",路径失效: {tool.ExecutablePath}"); @@ -44,12 +45,17 @@ public class ProcessExecutionService : IProcessExecutionService } /// - /// 批量启动组合中的所有子工具,每次间隔 500ms 防止系统卡顿 + /// 批量启动组合中的所有子工具,支持嵌套组合,每次间隔 500ms 防止系统卡顿 /// - private async Task ExecuteGroupAsync(ToolItem group) + private async Task ExecuteGroupAsync(ToolItem group, HashSet visited) { - var subTools = new List(); + if (!visited.Add(group.Id)) + { + _logService.Warning($"组合启动跳过:检测到循环引用 ({group.Name})"); + return; + } + var subTools = new List(); foreach (var subId in group.SubToolIds) { var subTool = _dataService.Config.Tools.FirstOrDefault(t => t.Id == subId); @@ -65,13 +71,19 @@ public class ProcessExecutionService : IProcessExecutionService foreach (var subTool in subTools) { - if (!subTool.IsValid) + if (subTool.IsGroup) { - _logService.Warning($"组合启动跳过 \"{subTool.Name}\",路径失效: {subTool.ExecutablePath}"); - continue; + await ExecuteGroupAsync(subTool, visited); + } + else + { + if (!subTool.IsValid) + { + _logService.Warning($"组合启动跳过 \"{subTool.Name}\",路径失效: {subTool.ExecutablePath}"); + continue; + } + LaunchSingleTool(subTool); } - - LaunchSingleTool(subTool); await Task.Delay(500); } diff --git a/PersonalToolBox/ViewModels/GroupEditViewModel.cs b/PersonalToolBox/ViewModels/GroupEditViewModel.cs index 347bdb3..a5a725d 100644 --- a/PersonalToolBox/ViewModels/GroupEditViewModel.cs +++ b/PersonalToolBox/ViewModels/GroupEditViewModel.cs @@ -1,7 +1,9 @@ using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using PersonalToolBox.Helpers; using PersonalToolBox.Models; using PersonalToolBox.Services; @@ -30,6 +32,9 @@ public partial class GroupEditViewModel : ObservableObject [ObservableProperty] private Category? _selectedCategory; + [ObservableProperty] + private IconProvider.IconOption? _selectedIcon; + /// /// 可供勾选的普通工具列表(IsGroup == false) /// @@ -40,6 +45,11 @@ public partial class GroupEditViewModel : ObservableObject /// public ObservableCollection Categories { get; } = new(); + /// + /// 按分类分组的可用图标列表 + /// + public ListCollectionView AvailableIconsView { get; } + public Action? CloseAction { get; set; } public bool Saved { get; private set; } @@ -51,12 +61,16 @@ public partial class GroupEditViewModel : ObservableObject _logService = logService; _editingGroup = groupToEdit; + AvailableIconsView = new ListCollectionView(IconProvider.AvailableIcons); + AvailableIconsView.GroupDescriptions.Add(new PropertyGroupDescription("Category")); + foreach (var cat in _dataService.Config.Categories) Categories.Add(cat); - // 加载所有普通工具(非组合) + // 加载所有工具(排除自身及祖先组合,防止循环引用) + var excludeIds = GetAncestorIds(); var selectedIds = _editingGroup?.SubToolIds ?? new List(); - foreach (var tool in _dataService.Config.Tools.Where(t => !t.IsGroup)) + foreach (var tool in _dataService.Config.Tools.Where(t => !excludeIds.Contains(t.Id))) { AvailableTools.Add(new SelectableTool { @@ -71,6 +85,11 @@ public partial class GroupEditViewModel : ObservableObject Name = groupToEdit.Name; HotKey = groupToEdit.HotKey; SelectedCategory = Categories.FirstOrDefault(c => c.Id == groupToEdit.CategoryId); + + if (!string.IsNullOrEmpty(groupToEdit.IconCode)) + { + SelectedIcon = IconProvider.AvailableIcons.FirstOrDefault(i => i.Icon.ToString() == groupToEdit.IconCode); + } } } @@ -95,6 +114,7 @@ public partial class GroupEditViewModel : ObservableObject if (_editingGroup != null) { _editingGroup.Name = Name.Trim(); + _editingGroup.IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty; _editingGroup.HotKey = HotKey.Trim(); _editingGroup.CategoryId = SelectedCategory?.Id ?? string.Empty; _editingGroup.SubToolIds = selectedIds; @@ -106,6 +126,7 @@ public partial class GroupEditViewModel : ObservableObject _dataService.Config.Tools.Add(new ToolItem { Name = Name.Trim(), + IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty, HotKey = HotKey.Trim(), CategoryId = SelectedCategory?.Id ?? string.Empty, IsGroup = true, @@ -130,6 +151,31 @@ public partial class GroupEditViewModel : ObservableObject { CloseAction?.Invoke(false); } + + /// + /// 获取需要排除的工具 ID 集合(自身 + 所有祖先组合),防止循环引用 + /// + private System.Collections.Generic.HashSet GetAncestorIds() + { + var ancestors = new System.Collections.Generic.HashSet(); + if (_editingGroup != null) + { + ancestors.Add(_editingGroup.Id); + CollectAncestors(_editingGroup.Id, ancestors); + } + return ancestors; + } + + private void CollectAncestors(string groupId, System.Collections.Generic.HashSet ancestors) + { + foreach (var tool in _dataService.Config.Tools.Where(t => t.IsGroup)) + { + if (tool.SubToolIds.Contains(groupId) && ancestors.Add(tool.Id)) + { + CollectAncestors(tool.Id, ancestors); + } + } + } } /// diff --git a/PersonalToolBox/ViewModels/MainViewModel.cs b/PersonalToolBox/ViewModels/MainViewModel.cs index 0227d68..7b469a2 100644 --- a/PersonalToolBox/ViewModels/MainViewModel.cs +++ b/PersonalToolBox/ViewModels/MainViewModel.cs @@ -238,6 +238,25 @@ public partial class MainViewModel : ObservableObject if (editVm.Saved) RefreshData(); } + /// + /// 删除工具或组合 + /// + [RelayCommand] + private void DeleteTool(ToolItem? tool) + { + if (tool == null) return; + + var type = tool.IsGroup ? "组合" : "工具"; + if (MessageBox.Show($"确定删除{type} \"{tool.Name}\" 吗?", + "确认删除", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) + return; + + _dataService.Config.Tools.Remove(tool); + _dataService.Save(); + _logService.Info($"已删除{type}: {tool.Name}"); + RefreshData(); + } + // ───────────────────────────── 数据刷新 ───────────────────────────── /// diff --git a/PersonalToolBox/ViewModels/ToolEditViewModel.cs b/PersonalToolBox/ViewModels/ToolEditViewModel.cs index 86c124a..e396692 100644 --- a/PersonalToolBox/ViewModels/ToolEditViewModel.cs +++ b/PersonalToolBox/ViewModels/ToolEditViewModel.cs @@ -1,9 +1,11 @@ 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; @@ -38,11 +40,19 @@ public partial class ToolEditViewModel : ObservableObject [ObservableProperty] private Category? _selectedCategory; + [ObservableProperty] + private IconProvider.IconOption? _selectedIcon; + /// /// 分类下拉列表 /// public ObservableCollection Categories { get; } = new(); + /// + /// 按分类分组的可用图标列表 + /// + public ListCollectionView AvailableIconsView { get; } + /// /// 窗口关闭回调(由 View 层设置) /// @@ -61,6 +71,9 @@ public partial class ToolEditViewModel : ObservableObject _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); @@ -74,6 +87,11 @@ public partial class ToolEditViewModel : ObservableObject 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); + } } } @@ -125,6 +143,7 @@ public partial class ToolEditViewModel : ObservableObject 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(); @@ -139,6 +158,7 @@ public partial class ToolEditViewModel : ObservableObject var newTool = new ToolItem { Name = Name.Trim(), + IconCode = SelectedIcon?.Icon.ToString() ?? string.Empty, ExecutablePath = ExecutablePath.Trim(), Arguments = Arguments.Trim(), HotKey = HotKey.Trim(), diff --git a/PersonalToolBox/Views/CategoryEditWindow.xaml b/PersonalToolBox/Views/CategoryEditWindow.xaml index e144e65..c43dfa0 100644 --- a/PersonalToolBox/Views/CategoryEditWindow.xaml +++ b/PersonalToolBox/Views/CategoryEditWindow.xaml @@ -26,6 +26,7 @@ Background="{DynamicResource Theme.InputBackground}" Foreground="{DynamicResource Theme.Foreground}" BorderBrush="{DynamicResource Theme.InputBorder}" + VerticalContentAlignment="Center" Height="30" Margin="0,0,0,16" Padding="6,0"/> + + + + + + + + + @@ -22,41 +113,84 @@ - + + ItemsSource="{Binding AvailableIconsView}" + SelectedItem="{Binding SelectedIcon}" + Height="28" Margin="0,0,0,10" Padding="4,0"> + + + + + + + + + + + + + + + + + + + + + + - - - - + Margin="4,2"> + + + + + + + + + - - -