From c8be7022cc164e08297549348f013510c6c74057 Mon Sep 17 00:00:00 2001 From: adamyu Date: Tue, 23 Jun 2026 12:38:04 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=90=9B=20fix(ui):=20=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=95=B0=E6=8D=AE=E3=80=8C=E4=BB=BB=E5=8A=A1=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E3=80=8D=E4=B8=8B=E6=8B=89=E6=98=BE=E7=A4=BA=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E5=90=8D=E8=80=8C=E9=9D=9E=20ULID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 下拉原绑 AvailableTaskIds(ULID 列表)。改为 AvailableTasks(TaskPick: Id+Display), 下拉显 DisplayName、按 Id 过滤(TaskFilter 仍是 id,RowsView 过滤不变)。名字载入后(LoadTaskNamesAsync) 回填下拉项显示名。接上次「任务列显名」,补齐筛选面。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Dc.App/ViewModels/LiveDataViewModel.cs | 16 ++++++++++++---- src/Dc.App/Views/LiveDataView.xaml | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Dc.App/ViewModels/LiveDataViewModel.cs b/src/Dc.App/ViewModels/LiveDataViewModel.cs index ccb345f..bac5576 100644 --- a/src/Dc.App/ViewModels/LiveDataViewModel.cs +++ b/src/Dc.App/ViewModels/LiveDataViewModel.cs @@ -82,7 +82,8 @@ internal void DebounceTickForTest() private volatile LiveFlushStats _statsSnapshot = new(0, 0, 0, 0, 0); public ObservableCollection Rows { get; } = new(); - public ObservableCollection AvailableTaskIds { get; } = new(); + // 任务筛选下拉项:Id + 可读名(Display)。下拉显名、按 Id 过滤(TaskFilter 仍是 id,RowsView 不变)。 + public ObservableCollection AvailableTasks { get; } = new(); public ICollectionView RowsView { get; } private bool _running; @@ -140,8 +141,14 @@ private async Task LoadTaskNamesAsync() try { _taskNames = await TaskNames.LoadAsync(_dbFactory); - // 回填已存在行的可读名(行可能先于名字加载就创建)。Start 在 UI 线程,await 续回 UI 线程,改 Rows 安全。 + // 回填已存在行的可读名(行可能先于名字加载就创建)。Start 在 UI 线程,await 续回 UI 线程,改集合安全。 foreach (var row in Rows) row.TaskName = ResolveTaskName(row.TaskId); + // 同步刷新任务筛选下拉的显示名(此前以 id 兜底加入) + for (var i = 0; i < AvailableTasks.Count; i++) + { + var name = ResolveTaskName(AvailableTasks[i].Id); + if (AvailableTasks[i].Display != name) AvailableTasks[i] = new TaskPick(AvailableTasks[i].Id, name); + } } catch { /* 名字解析失败不影响数据流,列回退显示 id */ } } @@ -239,7 +246,8 @@ private LiveFlushStats ComputeFlushStats() private void Apply(string taskId, TagValue v, int rawCount) { - if (!AvailableTaskIds.Contains(taskId)) AvailableTaskIds.Add(taskId); + if (AvailableTasks.All(t => t.Id != taskId)) + AvailableTasks.Add(new TaskPick(taskId, ResolveTaskName(taskId))); var key = $"{taskId}::{v.Item}"; if (!_rowIndex.TryGetValue(key, out var row)) @@ -269,7 +277,7 @@ private void Clear() _searchDebounce?.Stop(); // 清空后避免挂起的防抖回调刷新已清集合 _rowIndex.Clear(); Rows.Clear(); - AvailableTaskIds.Clear(); + AvailableTasks.Clear(); RowCount = 0; // 重置 flush 统计累计器,避免清空后陈旧统计(ring 数组无需清零,count=0 即 n=0) diff --git a/src/Dc.App/Views/LiveDataView.xaml b/src/Dc.App/Views/LiveDataView.xaml index 0d5c830..fd5313e 100644 --- a/src/Dc.App/Views/LiveDataView.xaml +++ b/src/Dc.App/Views/LiveDataView.xaml @@ -32,8 +32,10 @@ VerticalAlignment="Center" Margin="0,0,6,0" /> + diff --git a/src/Dc.App/ViewModels/TagsViewModel.cs b/src/Dc.App/ViewModels/TagsViewModel.cs index 9fc2881..5c10348 100644 --- a/src/Dc.App/ViewModels/TagsViewModel.cs +++ b/src/Dc.App/ViewModels/TagsViewModel.cs @@ -20,6 +20,7 @@ public partial class TagsViewModel : ObservableObject, IEmbeddableTagPanel private readonly ITagExcelService _excel; private readonly IFilePicker _filePicker; private readonly TaskOrchestrator _orchestrator; + private readonly Action? _navigate; // 导航到其它页(空状态「浏览节点」CTA → "browse") [ObservableProperty] private string _title = "Tag 管理"; [ObservableProperty] private bool _isLoading; @@ -55,16 +56,22 @@ public TagsViewModel( ITagEditorDialog editor, ITagExcelService excel, IFilePicker filePicker, - TaskOrchestrator orchestrator) + TaskOrchestrator orchestrator, + Action? navigate = null) { _dbFactory = dbFactory; _editor = editor; _excel = excel; _filePicker = filePicker; _orchestrator = orchestrator; + _navigate = navigate; _ = LoadAsync(); } + // 空状态主 CTA:跳到「浏览节点」(发现→批量加 Tag 主路径)。 + [RelayCommand] + private void BrowseNodes() => _navigate?.Invoke("browse"); + private bool IsTaskRunning(string taskId) => _orchestrator.RunningTaskIds.Contains(taskId); diff --git a/src/Dc.App/Views/TagsView.xaml b/src/Dc.App/Views/TagsView.xaml index c593f0f..def4670 100644 --- a/src/Dc.App/Views/TagsView.xaml +++ b/src/Dc.App/Views/TagsView.xaml @@ -119,13 +119,15 @@ - + From d32e4cd0fb1be68983d303ceb72baadb3df0c449 Mon Sep 17 00:00:00 2001 From: adamyu Date: Tue, 23 Jun 2026 14:01:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=E5=BD=BB?= =?UTF-8?q?=E5=BA=95=E7=A7=BB=E9=99=A4=E5=88=86=E7=BB=84=E5=B1=82=20?= =?UTF-8?q?=E2=80=94=20Tag=20=E7=9B=B4=E6=8E=A5=E6=8C=82=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?(=E5=90=AB=20SQLite=20=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3/#4 用「默认分组隐藏」折中去掉分组 UX;本次物理删除分组层。Tag 早已带 TaskId(每条创建即写), Group 实质冗余,故迁移无需复杂回填。 领域:删 Group 实体、Tag.GroupId、CollectorTask.Groups。 持久化:DcDbContext 去 Group 映射,Tag 唯一索引 (item,task_id,group_id)→(item,task_id); DbSchemaInitializer 加旧库一次性迁移 MigrateDropGroupLayer——SQLite 不能 DROP 被外键引用的列, 故整表重建 dc_tags 去 group_id(连接保持打开 + PRAGMA foreign_keys=OFF 防孤儿行中断), 防御性回填空 task_id,重建 udx_name,删 dc_groups;TaskStore 级联只删 tags。 Excel 去 GroupName 列;Backup(BackupBundle/JsonConfigBackupService)去 Groups。 App:删 GroupsView/GroupsViewModel/GroupRow/GroupEditor*/IEmbeddableGroupPanel/DefaultTaskGroup; TagEditor 由「选分组」改为固定 taskId;TagsViewModel/Browse/工作台/TagRow/导入导出去分组; 导入落当前任务;工作台删除确认改「N 个 Tag」。 测试:更新 DcDbContext(+迁移测试,造带外键旧 schema 守护重建路径)/TaskStore/Excel/Backup/ TagEditor/TaskWorkspace 测试;删 GroupEditor 测试。 验证:Infra 140(含迁移)、App 136、Integration 32 全通过;Cli 跨平台构建 0 错; office 真机:旧库(1 任务/5 Tag/含 dc_groups)启动迁移→dc_groups 与 group_id 已删、 数据保留(WAL 校验)、任务订阅 5 Tag 采集、新增 Tag(原 NOT NULL group_id 会阻塞)成功。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Dc.App/App.xaml | 3 - src/Dc.App/Composition/ServiceRegistration.cs | 6 - src/Dc.App/Services/GroupEditorDialog.cs | 20 -- src/Dc.App/Services/IGroupEditorDialog.cs | 8 - src/Dc.App/Services/ITagEditorDialog.cs | 3 +- src/Dc.App/Services/TagEditorDialog.cs | 5 +- src/Dc.App/ViewModels/BrowseViewModel.cs | 7 +- src/Dc.App/ViewModels/GroupEditorViewModel.cs | 51 ----- src/Dc.App/ViewModels/GroupRow.cs | 23 --- src/Dc.App/ViewModels/GroupsViewModel.cs | 189 ------------------ src/Dc.App/ViewModels/SettingsViewModel.cs | 4 +- src/Dc.App/ViewModels/TagEditorViewModel.cs | 84 +------- src/Dc.App/ViewModels/TagRow.cs | 9 +- src/Dc.App/ViewModels/TagsViewModel.cs | 108 +++------- .../Workspace/DbWorkspaceTaskSource.cs | 6 +- .../ViewModels/Workspace/DefaultTaskGroup.cs | 30 --- .../Workspace/IEmbeddableGroupPanel.cs | 18 -- .../Workspace/IEmbeddableTagPanel.cs | 9 - .../Workspace/IWorkspaceTaskSource.cs | 6 +- .../Workspace/TaskWorkspaceViewModel.cs | 34 +--- src/Dc.App/Views/GroupEditorWindow.xaml | 60 ------ src/Dc.App/Views/GroupEditorWindow.xaml.cs | 22 -- src/Dc.App/Views/GroupsView.xaml | 117 ----------- src/Dc.App/Views/GroupsView.xaml.cs | 15 -- src/Dc.App/Views/TagEditorWindow.xaml | 20 -- src/Dc.App/Views/TagsView.xaml | 17 -- src/Dc.App/Views/TagsView.xaml.cs | 5 - src/Dc.Domain/Entities/CollectorTask.cs | 3 - src/Dc.Domain/Entities/Group.cs | 12 -- src/Dc.Domain/Entities/Tag.cs | 1 - src/Dc.Infrastructure/Backup/BackupBundle.cs | 3 +- .../Backup/JsonConfigBackupService.cs | 11 +- .../Excel/ClosedXmlTagExcelService.cs | 12 +- .../Excel/ITagExcelService.cs | 2 +- src/Dc.Infrastructure/Excel/TagImportRow.cs | 2 +- .../Persistence/DbSchemaInitializer.cs | 53 +++++ .../Persistence/DcDbContext.cs | 12 +- .../Persistence/TaskStore.cs | 6 +- .../ViewModels/GroupEditorViewModelTests.cs | 58 ------ .../ViewModels/TagEditorViewModelTests.cs | 93 ++------- .../Workspace/TaskWorkspaceViewModelTests.cs | 23 +-- .../Backup/JsonConfigBackupTests.cs | 11 +- .../DcDbContextTests.cs | 88 ++++++-- .../Excel/TagExcelTests.cs | 29 +-- .../Persistence/TaskStoreTests.cs | 15 +- 45 files changed, 237 insertions(+), 1076 deletions(-) delete mode 100644 src/Dc.App/Services/GroupEditorDialog.cs delete mode 100644 src/Dc.App/Services/IGroupEditorDialog.cs delete mode 100644 src/Dc.App/ViewModels/GroupEditorViewModel.cs delete mode 100644 src/Dc.App/ViewModels/GroupRow.cs delete mode 100644 src/Dc.App/ViewModels/GroupsViewModel.cs delete mode 100644 src/Dc.App/ViewModels/Workspace/DefaultTaskGroup.cs delete mode 100644 src/Dc.App/ViewModels/Workspace/IEmbeddableGroupPanel.cs delete mode 100644 src/Dc.App/Views/GroupEditorWindow.xaml delete mode 100644 src/Dc.App/Views/GroupEditorWindow.xaml.cs delete mode 100644 src/Dc.App/Views/GroupsView.xaml delete mode 100644 src/Dc.App/Views/GroupsView.xaml.cs delete mode 100644 src/Dc.Domain/Entities/Group.cs delete mode 100644 tests/Dc.App.Tests/ViewModels/GroupEditorViewModelTests.cs diff --git a/src/Dc.App/App.xaml b/src/Dc.App/App.xaml index c307f5b..27cc62c 100644 --- a/src/Dc.App/App.xaml +++ b/src/Dc.App/App.xaml @@ -31,9 +31,6 @@ - - - diff --git a/src/Dc.App/Composition/ServiceRegistration.cs b/src/Dc.App/Composition/ServiceRegistration.cs index 4291c3e..0c0399d 100644 --- a/src/Dc.App/Composition/ServiceRegistration.cs +++ b/src/Dc.App/Composition/ServiceRegistration.cs @@ -142,7 +142,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri // SnackbarPresenter 在 App 启动 Show 前经 SetSnackbarPresenter 接线(见 App.xaml.cs)。 services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -199,9 +198,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri new Dc.App.ViewModels.Workspace.WorkspaceOverviewViewModel( sp.GetRequiredService(), () => DateTimeOffset.UtcNow)); - // GroupsViewModel 仅工作台用(无全局导航路由),复用单例即可 - services.AddSingleton( - sp => sp.GetRequiredService()); // 全局监控分离(S4):LiveData/Diagnostics 同时挂「全局监控」导航与工作台 tab, // 工作台用独立实例,避免工作台设的 TaskFilter/TaskScope 污染全局视图。 services.AddSingleton( @@ -224,7 +220,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), @@ -235,7 +230,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri services.AddSingleton( _ => System.Windows.Application.Current?.Dispatcher ?? System.Windows.Threading.Dispatcher.CurrentDispatcher); - services.AddSingleton(); services.AddSingleton(sp => new TagsViewModel( sp.GetRequiredService>(), sp.GetRequiredService(), diff --git a/src/Dc.App/Services/GroupEditorDialog.cs b/src/Dc.App/Services/GroupEditorDialog.cs deleted file mode 100644 index 01832ce..0000000 --- a/src/Dc.App/Services/GroupEditorDialog.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Windows; -using Dc.App.ViewModels; -using Dc.App.Views; -using Dc.Domain.Entities; - -namespace Dc.App.Services; - -public sealed class GroupEditorDialog : IGroupEditorDialog -{ - public Group? Edit(IEnumerable availableTasks, Group? existing, CollectorTask? defaultTask = null) - { - var vm = new GroupEditorViewModel(availableTasks, existing, defaultTask); - var window = new GroupEditorWindow - { - DataContext = vm, - Owner = Application.Current.MainWindow - }; - return window.ShowDialog() == true ? vm.ToEntity() : null; - } -} diff --git a/src/Dc.App/Services/IGroupEditorDialog.cs b/src/Dc.App/Services/IGroupEditorDialog.cs deleted file mode 100644 index 911db1d..0000000 --- a/src/Dc.App/Services/IGroupEditorDialog.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Dc.Domain.Entities; - -namespace Dc.App.Services; - -public interface IGroupEditorDialog -{ - Group? Edit(IEnumerable availableTasks, Group? existing, CollectorTask? defaultTask = null); -} diff --git a/src/Dc.App/Services/ITagEditorDialog.cs b/src/Dc.App/Services/ITagEditorDialog.cs index 5c8ea40..4a1814a 100644 --- a/src/Dc.App/Services/ITagEditorDialog.cs +++ b/src/Dc.App/Services/ITagEditorDialog.cs @@ -5,9 +5,8 @@ namespace Dc.App.Services; public interface ITagEditorDialog { TagEditResult? Edit( - IEnumerable availableGroups, + string taskId, Tag? existing, - Group? defaultGroup = null, Func? taskLookup = null, IReadOnlyCollection? taskTags = null, IReadOnlyCollection? existingFormulas = null); diff --git a/src/Dc.App/Services/TagEditorDialog.cs b/src/Dc.App/Services/TagEditorDialog.cs index d840eb7..7ba8c64 100644 --- a/src/Dc.App/Services/TagEditorDialog.cs +++ b/src/Dc.App/Services/TagEditorDialog.cs @@ -18,15 +18,14 @@ public TagEditorDialog(IBrowseDialog browseDialog, IFormulaValidator formulaVali } public TagEditResult? Edit( - IEnumerable availableGroups, + string taskId, Tag? existing, - Group? defaultGroup = null, Func? taskLookup = null, IReadOnlyCollection? taskTags = null, IReadOnlyCollection? existingFormulas = null) { var vm = new TagEditorViewModel( - availableGroups, existing, defaultGroup, _browseDialog, taskLookup, + taskId, existing, _browseDialog, taskLookup, taskTags, existingFormulas, _formulaValidator); var window = new TagEditorWindow { diff --git a/src/Dc.App/ViewModels/BrowseViewModel.cs b/src/Dc.App/ViewModels/BrowseViewModel.cs index df24b3f..b2c4d71 100644 --- a/src/Dc.App/ViewModels/BrowseViewModel.cs +++ b/src/Dc.App/ViewModels/BrowseViewModel.cs @@ -132,15 +132,13 @@ private async Task AddToNewTaskAsync() await AddCheckedAsTagsAsync(created.Id, created.DisplayName); } - // 把勾选的叶子节点批量建成 Tag,落到任务的默认分组(分组层隐藏);同任务已存在的 Item 跳过。 + // 把勾选的叶子节点批量建成 Tag,直接挂到任务;同任务已存在的 Item 跳过。 private async Task AddCheckedAsTagsAsync(string taskId, string taskDisplay) { if (_dbFactory is null) return; var picked = Children.Where(r => r.IsItem && r.IsChecked).ToList(); if (picked.Count == 0) return; - var group = await DefaultTaskGroup.EnsureAsync(_dbFactory, taskId); - await using var db = await _dbFactory.CreateDbContextAsync(); var existing = (await db.Tags.AsNoTracking().Where(t => t.TaskId == taskId) .Select(t => t.Item).ToListAsync()).ToHashSet(StringComparer.Ordinal); @@ -152,8 +150,7 @@ private async Task AddCheckedAsTagsAsync(string taskId, string taskDisplay) Id = UlidGenerator.NewId(), Item = r.Node.Id, DataType = MapDataType(r.DataTypeText), - TaskId = taskId, - GroupId = group.Id + TaskId = taskId }) .ToList(); diff --git a/src/Dc.App/ViewModels/GroupEditorViewModel.cs b/src/Dc.App/ViewModels/GroupEditorViewModel.cs deleted file mode 100644 index 92b3434..0000000 --- a/src/Dc.App/ViewModels/GroupEditorViewModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using Dc.Domain.Entities; - -namespace Dc.App.ViewModels; - -public partial class GroupEditorViewModel : ObservableObject -{ - [ObservableProperty] private string _title; - [ObservableProperty] private string _name = string.Empty; - [ObservableProperty] private CollectorTask? _task; - - public string? OriginalId { get; } - public bool ShowTaskSelector { get; } - public ObservableCollection AvailableTasks { get; } = new(); - - public GroupEditorViewModel(IEnumerable tasks, Group? existing, CollectorTask? defaultTask = null) - { - foreach (var t in tasks) AvailableTasks.Add(t); - - if (existing is null) - { - _task = defaultTask; - _title = _task is null ? "新建分组" : $"新建分组 · 任务:{_task.Server}"; - } - else - { - OriginalId = existing.Id; - _name = existing.Name; - _task = AvailableTasks.FirstOrDefault(t => t.Id == existing.TaskId); - _title = _task is null ? "编辑分组" : $"编辑分组 · 任务:{_task.Server}"; - } - - ShowTaskSelector = _task is null; - } - - public IReadOnlyList Validate() - { - var errors = new List(); - if (string.IsNullOrWhiteSpace(Name)) errors.Add("名称不能为空"); - if (Task is null) errors.Add("必须选择所属任务"); - return errors; - } - - public Group ToEntity() => new() - { - Id = OriginalId ?? string.Empty, - Name = Name.Trim(), - TaskId = Task!.Id - }; -} diff --git a/src/Dc.App/ViewModels/GroupRow.cs b/src/Dc.App/ViewModels/GroupRow.cs deleted file mode 100644 index e44cd1a..0000000 --- a/src/Dc.App/ViewModels/GroupRow.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Dc.Domain.Entities; - -namespace Dc.App.ViewModels; - -/// -/// 分组列表行:Group 实体 + 人类可读的任务名 + 显隐该列的标志。 -/// 此前 GroupsView「所属任务」列直接绑 TaskId(26 位 ULID);内嵌模式(工作区选了任务再看 -/// 分组 tab)该列还是冗余的。现 GroupRow 暴露 TaskName + ShowTaskColumn,实体经 .Group 透传。 -/// -public sealed class GroupListRow -{ - public Group Group { get; } - public string Id => Group.Id; - public string Name => Group.Name; - public string TaskName { get; } - public DateTime CreatedAt => Group.CreatedAt; - - public GroupListRow(Group group, string taskName) - { - Group = group; - TaskName = taskName; - } -} diff --git a/src/Dc.App/ViewModels/GroupsViewModel.cs b/src/Dc.App/ViewModels/GroupsViewModel.cs deleted file mode 100644 index e51b4f6..0000000 --- a/src/Dc.App/ViewModels/GroupsViewModel.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Dc.App.Services; -using Dc.Domain.Entities; -using Dc.App.ViewModels.Workspace; -using Dc.Infrastructure.Orchestration; -using Dc.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Dc.App.ViewModels; - -public partial class GroupsViewModel : ObservableObject, IEmbeddableGroupPanel -{ - private readonly IDbContextFactory _dbFactory; - private readonly IGroupEditorDialog _editor; - private readonly TaskOrchestrator _orchestrator; - private readonly Dictionary _taskById = new(); - - [ObservableProperty] private string _title = "分组管理"; - [ObservableProperty] private bool _isLoading; - [ObservableProperty] private GroupListRow? _selectedRow; - [ObservableProperty] private CollectorTask? _taskFilter; - [ObservableProperty] private bool _isEmbedded; - - // 嵌入主从视图时隐藏标题 + 筛选区域,避免把右侧按钮挤出视区 - public bool ShowFullToolbar => !IsEmbedded; - // 内嵌模式(工作区已选任务)下「所属任务」列冗余——上下文已定任务,隐藏。 - public bool ShowTaskColumn => !IsEmbedded; - public string EmbeddedTitle => TaskFilter is null ? "分组 ▸ (未选任务)" : $"分组 ▸ {TaskDisplayName(TaskFilter)}"; - // 无任务时无法新建分组(分组必须挂任务)。独立页无宿主可跳 → 只文字引导; - // 内嵌模式 CTA 跳「采集任务」页签(事件经宿主 TaskWorkspaceViewModel 订阅)。 - public string? CreateTaskCtaText => IsEmbedded ? "去新建任务" : null; - - /// 内嵌模式无任务时,请求宿主(采集任务工作区)切到任务列表新建任务。 - public event Action? NavigateToTasksRequested; - - /// - // IEmbeddableGroupPanel 契约暴露 Group?;实际选中行见 SelectedRow。透传实体给宿主切 Tag tab。 - public Group? SelectedGroup => SelectedRow?.Group; - - public ObservableCollection Groups { get; } = new(); - public ObservableCollection AvailableTasks { get; } = new(); - - public GroupsViewModel(IDbContextFactory dbFactory, IGroupEditorDialog editor, TaskOrchestrator orchestrator) - { - _dbFactory = dbFactory; - _editor = editor; - _orchestrator = orchestrator; - _ = LoadAsync(); - } - - partial void OnIsEmbeddedChanged(bool value) - { - OnPropertyChanged(nameof(ShowFullToolbar)); - OnPropertyChanged(nameof(ShowTaskColumn)); - OnPropertyChanged(nameof(CreateTaskCtaText)); - } - partial void OnTaskFilterChanged(CollectorTask? value) - { - OnPropertyChanged(nameof(EmbeddedTitle)); - _ = LoadAsync(); - } - - // 任务可读名:统一用 CollectorTask.DisplayName(Name → Server → Id)。 - private static string TaskDisplayName(CollectorTask t) => t.DisplayName; - - [RelayCommand] - public async Task LoadAsync() - { - if (IsLoading) return; - IsLoading = true; - try - { - await using var db = await _dbFactory.CreateDbContextAsync(); - var tasks = await db.Tasks.AsNoTracking().OrderBy(t => t.CreatedAt).ToListAsync(); - AvailableTasks.Clear(); - _taskById.Clear(); - foreach (var t in tasks) { AvailableTasks.Add(t); _taskById[t.Id] = t; } - NewCommand.NotifyCanExecuteChanged(); // 任务数变化 → 「新建」可用性 + 空状态刷新 - - var q = db.Groups.AsNoTracking().AsQueryable(); - if (TaskFilter is not null) q = q.Where(g => g.TaskId == TaskFilter.Id); - var list = await q.OrderBy(g => g.CreatedAt).ToListAsync(); - Groups.Clear(); - foreach (var g in list) - { - var taskName = _taskById.TryGetValue(g.TaskId, out var t) ? TaskDisplayName(t) : g.TaskId; - Groups.Add(new GroupListRow(g, taskName)); - } - } - finally - { - IsLoading = false; - } - } - - // 无任务时禁用(空状态引导去新建任务),双重防御。 - private bool CanNew => AvailableTasks.Count > 0; - - [RelayCommand(CanExecute = nameof(CanNew))] - private async Task NewAsync() - { - if (AvailableTasks.Count == 0) return; - var edited = _editor.Edit(AvailableTasks, null, TaskFilter); - if (edited is null) return; - - edited.Id = UlidGenerator.NewId(); - await using var db = await _dbFactory.CreateDbContextAsync(); - db.Groups.Add(edited); - await db.SaveChangesAsync(); - var taskName = _taskById.TryGetValue(edited.TaskId, out var t) ? TaskDisplayName(t) : edited.TaskId; - Groups.Add(new GroupListRow(edited, taskName)); - } - - [RelayCommand(CanExecute = nameof(HasSelection))] - private async Task EditAsync() - { - if (SelectedRow is null) return; - var edited = _editor.Edit(AvailableTasks, SelectedRow.Group); - if (edited is null) return; - - await using var db = await _dbFactory.CreateDbContextAsync(); - var entity = await db.Groups.FirstOrDefaultAsync(g => g.Id == edited.Id); - if (entity is null) return; - entity.Name = edited.Name; - entity.TaskId = edited.TaskId; - await db.SaveChangesAsync(); - - var idx = Groups.IndexOf(SelectedRow); - if (idx >= 0) - { - var taskName = _taskById.TryGetValue(entity.TaskId, out var t) ? TaskDisplayName(t) : entity.TaskId; - var row = new GroupListRow(entity, taskName); - Groups[idx] = row; - SelectedRow = row; // 替换后重选,避免下游 TagsPanel.GroupFilter 被清空 - } - } - - [RelayCommand(CanExecute = nameof(HasSelection))] - private async Task DeleteAsync() - { - var row = SelectedRow; - if (row is null) return; - var group = row.Group; - - // 确认文案不带 ULID(对用户无意义),只显分组名。 - var confirm = MessageDialog.Confirm("删除确认", - $"确定删除分组 {group.Name}?\n会同时清除该分组下的所有 Tag。", - MessageDialogKind.Warning); - if (!confirm) return; - - await using var db = await _dbFactory.CreateDbContextAsync(); - // 查出待删 Tag 的 item,热卸载用 - var tagsInGroup = await db.Tags.AsNoTracking() - .Where(t => t.GroupId == group.Id) - .Select(t => new { t.TaskId, t.Item }) - .ToListAsync(); - - await db.Groups.Where(g => g.Id == group.Id).ExecuteDeleteAsync(); - await db.Tags.Where(t => t.GroupId == group.Id).ExecuteDeleteAsync(); - - if (_orchestrator.RunningTaskIds.Contains(group.TaskId) && tagsInGroup.Count > 0) - { - try - { - await _orchestrator.RemoveTagsAsync(group.TaskId, - tagsInGroup.Select(t => t.Item).ToArray()); - } - catch { /* swallow */ } - } - - Groups.Remove(row); - } - - // 空状态 CTA:请求宿主切到任务列表。独立页无宿主 → 事件无人订阅 → 按钮不显示。 - [RelayCommand] - private void GoCreateTask() => NavigateToTasksRequested?.Invoke(); - - private bool HasSelection() => SelectedRow is not null; - - partial void OnSelectedRowChanged(GroupListRow? value) - { - DeleteCommand.NotifyCanExecuteChanged(); - EditCommand.NotifyCanExecuteChanged(); - // 接口契约 SelectedGroup 透传自 SelectedRow.Group;通知宿主(TaskWorkspaceViewModel)切 Tag tab。 - OnPropertyChanged(nameof(SelectedGroup)); - } -} diff --git a/src/Dc.App/ViewModels/SettingsViewModel.cs b/src/Dc.App/ViewModels/SettingsViewModel.cs index 0411aaa..0a1913a 100644 --- a/src/Dc.App/ViewModels/SettingsViewModel.cs +++ b/src/Dc.App/ViewModels/SettingsViewModel.cs @@ -53,7 +53,7 @@ private async Task ExportBackupAsync() var json = _backup.SerializeToJson(bundle); await File.WriteAllTextAsync(path, json); MessageDialog.Show("导出成功", - $"已导出 {bundle.Tasks.Count} 任务 / {bundle.Groups.Count} 分组 / {bundle.Tags.Count} Tag / {bundle.Configs.Count} 配置到 {path}", + $"已导出 {bundle.Tasks.Count} 任务 / {bundle.Tags.Count} Tag / {bundle.Configs.Count} 配置到 {path}", MessageDialogKind.Success); } catch (Exception ex) @@ -85,7 +85,7 @@ private async Task ImportBackupAsync() await using var db = await _dbFactory.CreateDbContextAsync(); var result = await _backup.ImportAsync(db, bundle, mode); - var msg = $"已导入 任务 {result.TasksImported} / 分组 {result.GroupsImported} / Tag {result.TagsImported} / 配置 {result.ConfigsImported}"; + var msg = $"已导入 任务 {result.TasksImported} / Tag {result.TagsImported} / 配置 {result.ConfigsImported}"; if (result.Errors.Count > 0) msg += $"\n错误 ({result.Errors.Count}):\n" + string.Join("\n", result.Errors.Take(5)); MessageDialog.Show("导入结果", msg, result.Errors.Count > 0 ? MessageDialogKind.Warning : MessageDialogKind.Success); diff --git a/src/Dc.App/ViewModels/TagEditorViewModel.cs b/src/Dc.App/ViewModels/TagEditorViewModel.cs index 80fe0a4..8e1ccda 100644 --- a/src/Dc.App/ViewModels/TagEditorViewModel.cs +++ b/src/Dc.App/ViewModels/TagEditorViewModel.cs @@ -6,12 +6,12 @@ using Dc.App.Services; using Dc.Domain.Entities; using Dc.Infrastructure.Orchestration; -using Group = Dc.Domain.Entities.Group; namespace Dc.App.ViewModels; public partial class TagEditorViewModel : ObservableObject { + private readonly string _taskId; // 分组层已去除:Tag 直接挂任务,编辑器固定此任务 private readonly IBrowseDialog? _browseDialog; private readonly Func? _taskLookup; private readonly IReadOnlyCollection? _taskTags; @@ -35,49 +35,28 @@ public partial class TagEditorViewModel : ObservableObject public ObservableCollection InputBindings { get; } = new(); public ObservableCollection AvailableInputTags { get; } = new(); - // 下拉项包装:Group 实体 + 人类可读 Display。此前 ItemTemplate 直接绑 {TaskId}(26 位 ULID), - // 折叠态显示「组名 — 01KVAS6GHVSDC5B62ZTXXY596Z」。现 Display 用任务显示名;同任务下只显组名。 - public ObservableCollection AvailableGroups { get; } = new(); - [ObservableProperty] private GroupRow? _selectedGroupRow; - - public Group? Group => SelectedGroupRow?.Group; - public string? OriginalId { get; } public IReadOnlyList DataTypeOptions => OpcDataTypeOption.All; - public bool ShowGroupSelector { get; } public TagEditorViewModel( - IEnumerable groups, + string taskId, Tag? existing, - Group? defaultGroup = null, IBrowseDialog? browseDialog = null, Func? taskLookup = null, IReadOnlyCollection? taskTags = null, IReadOnlyCollection? existingFormulas = null, IFormulaValidator? formulaValidator = null) { + _taskId = taskId; _taskTags = taskTags; _existingFormulas = existingFormulas; _formulaValidator = formulaValidator; _browseDialog = browseDialog; _taskLookup = taskLookup; - // 任务名解析:用 CollectorTask.DisplayName(Name → Server → Id);无 taskLookup 则回退空(只显组名)。 - var rows = groups.Select(g => - { - var task = taskLookup?.Invoke(g.TaskId); - var taskName = task is not null ? task.DisplayName : null; - return new GroupRow(g, taskName); - }).ToList(); - var multiTask = rows.Select(r => r.Group.TaskId).Distinct().Count() > 1; - foreach (var r in rows) r.FinalizeDisplay(multiTask); - foreach (var r in rows) AvailableGroups.Add(r); - - GroupRow? selected; if (existing is null) { - selected = defaultGroup is null ? null : rows.FirstOrDefault(r => r.Group.Id == defaultGroup.Id); - _title = "新建 Tag"; // 分组层已对用户隐藏,标题不再带分组名 + _title = "新建 Tag"; } else { @@ -88,10 +67,8 @@ public TagEditorViewModel( ? existing.ScaleFactor.Value.ToString(CultureInfo.InvariantCulture) : string.Empty; _offset = existing.Offset.HasValue ? existing.Offset.Value.ToString(CultureInfo.InvariantCulture) : string.Empty; - selected = rows.FirstOrDefault(r => r.Group.Id == existing.GroupId); - _title = "编辑 Tag"; // 分组层已对用户隐藏,标题不再带分组名 + _title = "编辑 Tag"; } - _selectedGroupRow = selected; Formula? existingVirtualFormula = null; // 编辑已存在虚拟 Tag:回填公式字段并预选输入。 @@ -107,7 +84,6 @@ public TagEditorViewModel( } } - ShowGroupSelector = selected is null; RefreshAvailableInputTags(); if (existingVirtualFormula is not null) { @@ -173,24 +149,13 @@ private void Browse() { if (_browseDialog is null) return; string? initialUri = null; - if (Group is not null && _taskLookup is not null) - { - var task = _taskLookup(Group.TaskId); - if (task is not null && !string.IsNullOrWhiteSpace(task.Node)) - initialUri = task.Node; - } + var task = _taskLookup?.Invoke(_taskId); + if (task is not null && !string.IsNullOrWhiteSpace(task.Node)) + initialUri = task.Node; var nodeId = _browseDialog.PickNodeId(initialUri); if (!string.IsNullOrWhiteSpace(nodeId)) Item = nodeId; } - partial void OnSelectedGroupRowChanged(GroupRow? value) - { - // 分组定 → TaskId 定 → 刷新可选输入 Tag(同任务真实,排除自身虚拟)。 - OnPropertyChanged(nameof(Group)); - RefreshAvailableInputTags(); - Revalidate(); - } - partial void OnIsVirtualChanged(bool value) { if (value) RebuildInputBindings(); // 内部 Revalidate @@ -211,7 +176,7 @@ partial void OnExpressionChanged(string value) private void RefreshAvailableInputTags() { AvailableInputTags.Clear(); - if (_taskTags is null || Group is null) return; + if (_taskTags is null) return; foreach (var t in _taskTags.Where(t => !t.IsVirtual && t.Id != OriginalId)) AvailableInputTags.Add(t); } @@ -236,7 +201,6 @@ private void RebuildInputBindings() public IReadOnlyList Validate() { var errors = new List(); - if (Group is null) errors.Add("必须选择所属分组"); if (!IsVirtual) { @@ -284,8 +248,7 @@ public TagEditResult ToResult() Id = OriginalId ?? string.Empty, Item = IsVirtual ? FormulaName.Trim() : Item.Trim(), DataType = DataType.Code, - GroupId = Group!.Id, - TaskId = Group!.TaskId, + TaskId = _taskId, IsVirtual = IsVirtual, ScaleFactor = IsVirtual ? null : ParseDouble(ScaleFactor), Offset = IsVirtual ? null : ParseDouble(Offset) @@ -301,7 +264,7 @@ public TagEditResult ToResult() Expression = Expression, OutputTagId = tag.Id, // 调用方在持久化时回填真实 Id OutputUnit = string.IsNullOrWhiteSpace(OutputUnit) ? null : OutputUnit.Trim(), - TaskId = Group!.TaskId + TaskId = _taskId }; var inputs = InputBindings .Where(r => r.SelectedTag is not null) @@ -320,31 +283,6 @@ public TagEditResult ToResult() => double.TryParse(s?.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null; } -/// -/// 分组下拉项:Group 实体 + 可读 Display。Display 在 FinalizeDisplay 后确定: -/// 跨任务(独立 Tag 管理页)显示「组名 — 任务名」,同任务(嵌入工作区)只显组名。 -/// -public sealed class GroupRow -{ - public Group Group { get; } - public string Display { get; private set; } - private readonly string? _taskName; - - public GroupRow(Group group, string? taskName) - { - Group = group; - _taskName = taskName; - Display = group.Name; - } - - internal void FinalizeDisplay(bool multiTask) - { - Display = multiTask && !string.IsNullOrWhiteSpace(_taskName) - ? $"{Group.Name} — {_taskName}" - : Group.Name; - } -} - /// /// 输入映射行:从表达式提取的别名(只读)+ 用户选的同任务真实 Tag。 /// diff --git a/src/Dc.App/ViewModels/TagRow.cs b/src/Dc.App/ViewModels/TagRow.cs index 6816db5..b277b2f 100644 --- a/src/Dc.App/ViewModels/TagRow.cs +++ b/src/Dc.App/ViewModels/TagRow.cs @@ -3,9 +3,8 @@ namespace Dc.App.ViewModels; /// -/// Tag 列表行:包装 Tag 实体 + 人类可读的分组名/任务名。 -/// 此前 DataGrid「分组」「任务」列直接绑 GroupId/TaskId(26 位 ULID),折叠态满屏 -/// 01KVAS6GHVSDC5B62ZTXXY596Z。现网格显名;Tag 实体经 .Tag 透传给编辑/删除/热同步。 +/// Tag 列表行:包装 Tag 实体 + 人类可读的任务名。 +/// 此前「任务」列直接绑 TaskId(26 位 ULID);现网格显名,Tag 实体经 .Tag 透传给编辑/删除/热同步。 /// public sealed class TagRow { @@ -13,14 +12,12 @@ public sealed class TagRow public string Id => Tag.Id; public string Item => Tag.Item; public int DataType => Tag.DataType; - public string GroupName { get; } public string TaskName { get; } public DateTime CreatedAt => Tag.CreatedAt; - public TagRow(Tag tag, string groupName, string taskName) + public TagRow(Tag tag, string taskName) { Tag = tag; - GroupName = groupName; TaskName = taskName; } } diff --git a/src/Dc.App/ViewModels/TagsViewModel.cs b/src/Dc.App/ViewModels/TagsViewModel.cs index 5c10348..9f913a2 100644 --- a/src/Dc.App/ViewModels/TagsViewModel.cs +++ b/src/Dc.App/ViewModels/TagsViewModel.cs @@ -25,30 +25,18 @@ public partial class TagsViewModel : ObservableObject, IEmbeddableTagPanel [ObservableProperty] private string _title = "Tag 管理"; [ObservableProperty] private bool _isLoading; [ObservableProperty] private TagRow? _selectedTag; - [ObservableProperty] private Group? _groupFilter; [ObservableProperty] private string _searchText = string.Empty; [ObservableProperty] private bool _isEmbedded; [ObservableProperty] private string? _taskScope; - /// - public event Action? NavigateToGroupsRequested; - // 嵌入主从视图时隐藏标题 + 筛选区域,避免把右侧按钮挤出视区 public bool ShowFullToolbar => !IsEmbedded; - // 分组层已对用户隐藏(Tag 直接挂任务),嵌入态只显「Tag」。 + // 分组层已去除(Tag 直接挂任务),嵌入态只显「Tag」。 public string EmbeddedTitle => "Tag"; - // 无分组时无法新建 Tag——禁用「新建」并用空状态引导,而非弹阻塞 MessageBox 把用户挡在死路上。 - // CTA 仅内嵌模式显示(有「分组」页签可跳);独立页只给文字引导。 - public string? CreateGroupCtaText => IsEmbedded ? "去创建分组" : null; - partial void OnIsEmbeddedChanged(bool value) - { - OnPropertyChanged(nameof(ShowFullToolbar)); - OnPropertyChanged(nameof(CreateGroupCtaText)); - } + partial void OnIsEmbeddedChanged(bool value) => OnPropertyChanged(nameof(ShowFullToolbar)); public ObservableCollection Tags { get; } = new(); - public ObservableCollection AvailableGroups { get; } = new(); private readonly Dictionary _taskById = new(); public TagsViewModel( @@ -101,15 +89,7 @@ public async Task LoadAsync() try { await using var db = await _dbFactory.CreateDbContextAsync(); - // 分组按当前任务 TaskScope 过滤——否则 Tag 编辑器的所属分组下拉会列出其它任务的分组, - // 选中后 tag.TaskId 取自该分组的 TaskId(见 TagEditorViewModel.Build),导致 Tag 静默落到错误任务下、永不订阅。 - var groupsQuery = db.Groups.AsNoTracking().AsQueryable(); - if (TaskScope is not null) groupsQuery = groupsQuery.Where(g => g.TaskId == TaskScope); - var groups = await groupsQuery.OrderBy(g => g.CreatedAt).ToListAsync(); - AvailableGroups.Clear(); - foreach (var g in groups) AvailableGroups.Add(g); - NewCommand.NotifyCanExecuteChanged(); // 分组数变化 → 「新建」可用性 + 空状态刷新 - var groupNameById = groups.ToDictionary(g => g.Id, g => g.Name); + NewCommand.NotifyCanExecuteChanged(); // 任务上下文变化 → 「新建」可用性 + 空状态刷新 var tasks = await db.Tasks.AsNoTracking().ToListAsync(); _taskById.Clear(); @@ -120,7 +100,6 @@ string TaskName(string tid) => var q = db.Tags.AsNoTracking().AsQueryable(); if (TaskScope is not null) q = q.Where(t => t.TaskId == TaskScope); - if (GroupFilter is not null) q = q.Where(t => t.GroupId == GroupFilter.Id); if (!string.IsNullOrWhiteSpace(SearchText)) { var pattern = $"%{SearchText.Trim()}%"; @@ -129,10 +108,7 @@ string TaskName(string tid) => var list = await q.OrderBy(t => t.Item).Take(500).ToListAsync(); Tags.Clear(); foreach (var t in list) - { - var gName = groupNameById.TryGetValue(t.GroupId, out var gn) ? gn : t.GroupId; - Tags.Add(new TagRow(t, gName, TaskName(t.TaskId))); - } + Tags.Add(new TagRow(t, TaskName(t.TaskId))); } finally { @@ -140,17 +116,14 @@ string TaskName(string tid) => } } - // Tag 直接挂任务:有任务上下文即可新建。分组层已隐藏,新建时自动落到任务的「默认分组」。 + // Tag 直接挂任务:有任务上下文即可新建。 private bool CanNew => TaskScope is not null; [RelayCommand(CanExecute = nameof(CanNew))] private async Task NewAsync() { if (TaskScope is null) return; - // 确保当前任务有「默认分组」并作为默认传给编辑器 → 编辑器隐藏分组选择器,Tag 自动落到该任务。 - var defaultGroup = await DefaultTaskGroup.EnsureAsync(_dbFactory, TaskScope); - if (AvailableGroups.All(g => g.Id != defaultGroup.Id)) AvailableGroups.Add(defaultGroup); - var result = await EditTagAsync(existing: null, defaultGroup: defaultGroup); + var result = await EditTagAsync(existing: null); if (result is null) return; await PersistNewAsync(result); } @@ -204,17 +177,23 @@ private async Task DeleteAsync() Tags.Remove(row); } - // Tag → TagRow:分组名/任务名解析与 LoadAsync 同口径。 + // Tag → TagRow:任务名解析与 LoadAsync 同口径。 private TagRow ToRow(Tag t) { - var gName = AvailableGroups.FirstOrDefault(g => g.Id == t.GroupId)?.Name ?? t.GroupId; var taskName = _taskById.TryGetValue(t.TaskId, out var tk) ? tk.DisplayName : t.TaskId; - return new TagRow(t, gName, taskName); + return new TagRow(t, taskName); } [RelayCommand] public async Task ImportAsync() { + // Tag 直接挂任务:导入落到当前任务(TaskScope)。无任务上下文(独立页)则无法导入。 + if (TaskScope is null) + { + MessageDialog.Show("无法导入", "请先在采集任务里选中一个任务再导入 Tag。", MessageDialogKind.Warning); + return; + } + var path = _filePicker.PickOpenFile("Excel 工作簿|*.xlsx", "导入 Tag"); if (path is null) return; @@ -231,27 +210,16 @@ public async Task ImportAsync() } await using var db = await _dbFactory.CreateDbContextAsync(); - var groups = await db.Groups.AsNoTracking().ToDictionaryAsync(g => g.Name, g => g); var inserted = 0; var errors = new List(); - var batch = new List(); - foreach (var row in rows) + var batch = rows.Select(row => new Tag { - if (!groups.TryGetValue(row.GroupName, out var grp)) - { - errors.Add($"未知分组: {row.GroupName} (Item={row.Item})"); - continue; - } - batch.Add(new Tag - { - Id = UlidGenerator.NewId(), - Item = row.Item, - DataType = row.DataType, - GroupId = grp.Id, - TaskId = grp.TaskId - }); - } + Id = UlidGenerator.NewId(), + Item = row.Item, + DataType = row.DataType, + TaskId = TaskScope + }).ToList(); if (batch.Count > 0) { @@ -293,10 +261,9 @@ private async Task ExportAsync() if (path is null) return; await using var db = await _dbFactory.CreateDbContextAsync(); - var groupMap = await db.Groups.AsNoTracking().ToDictionaryAsync(g => g.Id, g => g.Name); var q = db.Tags.AsNoTracking().AsQueryable(); - if (GroupFilter is not null) q = q.Where(t => t.GroupId == GroupFilter.Id); + if (TaskScope is not null) q = q.Where(t => t.TaskId == TaskScope); if (!string.IsNullOrWhiteSpace(SearchText)) { var pattern = $"%{SearchText.Trim()}%"; @@ -307,7 +274,7 @@ private async Task ExportAsync() try { await using var fs = File.Create(path); - _excel.Write(list, groupMap, fs); + _excel.Write(list, fs); MessageDialog.Show("导出成功", $"已导出 {list.Count} 条到 {path}", MessageDialogKind.Success); } catch (Exception ex) @@ -316,32 +283,18 @@ private async Task ExportAsync() } } - // 空状态 CTA:请求宿主(采集任务工作区)切到「分组」页签。独立页无宿主,事件无人订阅→按钮不显示。 - [RelayCommand] - private void GoCreateGroup() => NavigateToGroupsRequested?.Invoke(); - - partial void OnGroupFilterChanged(Group? value) - { - OnPropertyChanged(nameof(EmbeddedTitle)); - _ = LoadAsync(); - } - - // 供 NewAsync/EditAsync 调用。defaultGroup:新建时传任务的默认分组(编辑器据此隐藏分组选择器)。 - private async Task EditTagAsync(Tag? existing, Group? defaultGroup = null) + // 供 NewAsync/EditAsync 调用。 + private async Task EditTagAsync(Tag? existing) { // 任务上下文:优先当前工作台 TaskScope;独立页(无 TaskScope)编辑现有 Tag 时用其 TaskId。 - // 独立页新建虚拟测点无任务上下文 → AvailableInputTags 为空,虚拟创建受限(已知限制)。 string? taskId = TaskScope ?? existing?.TaskId; - IReadOnlyCollection? taskTags = null; + if (taskId is null) return null; // 无任务上下文无法编辑 + var taskTags = await LoadTaskTagsAsync(taskId); IReadOnlyCollection? existingFormulas = null; - if (taskId is not null) - { - taskTags = await LoadTaskTagsAsync(taskId); - if (existing is not null && existing.IsVirtual) - existingFormulas = await LoadTaskFormulasAsync(taskId); - } + if (existing is not null && existing.IsVirtual) + existingFormulas = await LoadTaskFormulasAsync(taskId); - return _editor.Edit(AvailableGroups, existing, defaultGroup ?? GroupFilter, + return _editor.Edit(taskId, existing, taskIdLookup => _taskById.TryGetValue(taskIdLookup, out var t) ? t : null, taskTags, existingFormulas); } @@ -420,7 +373,6 @@ private async Task PersistEditAsync(Tag existing, TagEditResult result) entity.Item = result.Tag.Item; entity.DataType = result.Tag.DataType; - entity.GroupId = result.Tag.GroupId; entity.TaskId = result.Tag.TaskId; entity.IsVirtual = result.Tag.IsVirtual; entity.ScaleFactor = result.Tag.ScaleFactor; diff --git a/src/Dc.App/ViewModels/Workspace/DbWorkspaceTaskSource.cs b/src/Dc.App/ViewModels/Workspace/DbWorkspaceTaskSource.cs index 1376f34..efcb9e4 100644 --- a/src/Dc.App/ViewModels/Workspace/DbWorkspaceTaskSource.cs +++ b/src/Dc.App/ViewModels/Workspace/DbWorkspaceTaskSource.cs @@ -45,12 +45,10 @@ public async Task> LoadTasksAsync() return (task, descriptors, formulas); } - public async Task<(int Groups, int Tags)> GetCountsAsync(string taskId) + public async Task GetCountsAsync(string taskId) { await using var db = await _dbFactory.CreateDbContextAsync(); - var groups = await db.Groups.AsNoTracking().CountAsync(g => g.TaskId == taskId); - var tags = await db.Tags.AsNoTracking().CountAsync(t => t.TaskId == taskId); - return (groups, tags); + return await db.Tags.AsNoTracking().CountAsync(t => t.TaskId == taskId); } public async Task> GetConfiguredTagCountsAsync(IReadOnlyCollection taskIds) diff --git a/src/Dc.App/ViewModels/Workspace/DefaultTaskGroup.cs b/src/Dc.App/ViewModels/Workspace/DefaultTaskGroup.cs deleted file mode 100644 index 41115bb..0000000 --- a/src/Dc.App/ViewModels/Workspace/DefaultTaskGroup.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Dc.Domain.Entities; -using Dc.Infrastructure.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Dc.App.ViewModels.Workspace; - -/// -/// 分组层已对用户隐藏(Tag 直接挂任务):每个任务隐式持有一个「默认分组」,承载其全部 Tag。 -/// Tag 新建(TagsViewModel)与浏览批量加 Tag(BrowseViewModel)共用此兜底,避免重复实现。 -/// -internal static class DefaultTaskGroup -{ - public const string Name = "默认分组"; - - /// 取任务最早的分组作默认;没有则建一个(SaveChanges 自动补时间戳)。 - public static async Task EnsureAsync(IDbContextFactory dbFactory, string taskId) - { - await using var db = await dbFactory.CreateDbContextAsync(); - var existing = await db.Groups.AsNoTracking() - .Where(g => g.TaskId == taskId) - .OrderBy(g => g.CreatedAt) - .FirstOrDefaultAsync(); - if (existing is not null) return existing; - - var grp = new Group { Id = UlidGenerator.NewId(), Name = Name, TaskId = taskId }; - db.Groups.Add(grp); - await db.SaveChangesAsync(); - return grp; - } -} diff --git a/src/Dc.App/ViewModels/Workspace/IEmbeddableGroupPanel.cs b/src/Dc.App/ViewModels/Workspace/IEmbeddableGroupPanel.cs deleted file mode 100644 index e35abf5..0000000 --- a/src/Dc.App/ViewModels/Workspace/IEmbeddableGroupPanel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel; -using Dc.Domain.Entities; - -namespace Dc.App.ViewModels.Workspace; - -public interface IEmbeddableGroupPanel : INotifyPropertyChanged -{ - bool IsEmbedded { get; set; } - CollectorTask? TaskFilter { get; set; } - Group? SelectedGroup { get; } - Task LoadAsync(); - - /// - /// 内嵌模式下,分组面板无任务时请求跳到任务列表(新建任务)。 - /// 独立页不触发(无宿主可跳)。 - /// - event Action? NavigateToTasksRequested; -} diff --git a/src/Dc.App/ViewModels/Workspace/IEmbeddableTagPanel.cs b/src/Dc.App/ViewModels/Workspace/IEmbeddableTagPanel.cs index bbe71aa..40b6ff7 100644 --- a/src/Dc.App/ViewModels/Workspace/IEmbeddableTagPanel.cs +++ b/src/Dc.App/ViewModels/Workspace/IEmbeddableTagPanel.cs @@ -1,18 +1,9 @@ -using Dc.Domain.Entities; - namespace Dc.App.ViewModels.Workspace; public interface IEmbeddableTagPanel { bool IsEmbedded { get; set; } string? TaskScope { get; set; } - Group? GroupFilter { get; set; } Task LoadAsync(); Task ImportAsync(); - - /// - /// 内嵌模式下,Tag 面板无分组时请求跳转到「分组」页签创建分组。 - /// 独立页不触发(无 Groups tab 可跳)。 - /// - event Action? NavigateToGroupsRequested; } diff --git a/src/Dc.App/ViewModels/Workspace/IWorkspaceTaskSource.cs b/src/Dc.App/ViewModels/Workspace/IWorkspaceTaskSource.cs index 267f872..be233eb 100644 --- a/src/Dc.App/ViewModels/Workspace/IWorkspaceTaskSource.cs +++ b/src/Dc.App/ViewModels/Workspace/IWorkspaceTaskSource.cs @@ -19,11 +19,11 @@ public interface IWorkspaceTaskSource /// Persist edits to an existing task (preserves CreatedAt). Task UpdateTaskAsync(CollectorTask task); - /// Delete a task and cascade-delete its groups and tags in one transaction. + /// Delete a task and cascade-delete its tags in one transaction. Task DeleteTaskCascadeAsync(string taskId); - /// 选中任务的分组数 / Tag 数(tab 计数 badge 用)。默认 0,便于测试 fake 不必实现。 - Task<(int Groups, int Tags)> GetCountsAsync(string taskId) => Task.FromResult((0, 0)); + /// 选中任务的 Tag 数(tab 计数 badge 用)。默认 0,便于测试 fake 不必实现。 + Task GetCountsAsync(string taskId) => Task.FromResult(0); /// /// 批量取各任务的「已配置 Tag 数」(DB 口径,与运行状态无关)。用于任务列表 badge—— diff --git a/src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs b/src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs index 3a1d272..4b288d9 100644 --- a/src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs +++ b/src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs @@ -27,7 +27,6 @@ public sealed partial class TaskWorkspaceViewModel : ObservableObject public WorkspaceOverviewViewModel Overview { get; } public IEmbeddableTagPanel TagsPanel { get; } - public IEmbeddableGroupPanel GroupsPanel { get; } public IEmbeddableLivePanel LivePanel { get; } public IEmbeddableDiagPanel DiagPanel { get; } public WorkspaceConfigViewModel Config { get; } @@ -38,7 +37,6 @@ public sealed partial class TaskWorkspaceViewModel : ObservableObject [ObservableProperty] private int _runningCount; [ObservableProperty] private int _stoppedCount; [ObservableProperty] private int _alertCount; - [ObservableProperty] private int _selectedGroupCount; // tab badge:分组 [ObservableProperty] private int _selectedTagCount; // tab badge:Tag [ObservableProperty] private string _selectedTab = "overview"; [ObservableProperty] private object? _currentTabContent; @@ -54,7 +52,6 @@ public TaskWorkspaceViewModel( IEmbeddableTagPanel tagsPanel, TaskOrchestrator? orchestrator = null, ITaskEditorDialog? editor = null, - IEmbeddableGroupPanel? groupsPanel = null, IEmbeddableLivePanel? livePanel = null, IEmbeddableDiagPanel? diagPanel = null, WorkspaceConfigViewModel? config = null, @@ -73,13 +70,11 @@ public TaskWorkspaceViewModel( TagsPanel = tagsPanel; TagsPanel.IsEmbedded = true; - // 分组层已对用户隐藏(Tag 直接挂任务):不再有「分组」页签,GroupsPanel 仅保留为隐式默认分组的载体。 - GroupsPanel = groupsPanel ?? new NullGroupPanel(); + // 分组层已去除:Tag 直接挂任务,不再有「分组」页签。 LivePanel = livePanel ?? new NullLivePanel(); DiagPanel = diagPanel ?? new NullDiagPanel(); Config = config ?? new WorkspaceConfigViewModel(editor ?? new NullTaskEditorDialog()); - GroupsPanel.IsEmbedded = true; // 任务编辑持久化走 VM(main 的任务 CRUD);PersistEditedAsync 内部已 LoadAsync 刷新列表。 Config.Edited += async edited => await PersistEditedAsync(edited); @@ -104,21 +99,17 @@ partial void OnSelectedTaskChanged(TaskMasterRow? value) Overview.Sample(); var task = value is null ? null : _tasksById.GetValueOrDefault(value.TaskId); - GroupsPanel.TaskFilter = task; - _ = GroupsPanel.LoadAsync(); LivePanel.TaskFilter = value?.TaskId; DiagPanel.TaskScope = value?.TaskId; Config.SetTask(task); - if (value is null) { SelectedGroupCount = 0; SelectedTagCount = 0; } + if (value is null) SelectedTagCount = 0; else _ = LoadCountsAsync(value.TaskId); } private async Task LoadCountsAsync(string taskId) { - var (g, t) = await _source.GetCountsAsync(taskId); - SelectedGroupCount = g; - SelectedTagCount = t; + SelectedTagCount = await _source.GetCountsAsync(taskId); } partial void OnSelectedTabChanged(string value) => UpdateTabContent(); @@ -306,9 +297,9 @@ public async Task DeleteSelectedAsync() var name = SelectedTask.Name; // 先确认,确认后才产生任何副作用(取消 = 真 no-op,不停不删) - var (g, t) = await _source.GetCountsAsync(id); + var t = await _source.GetCountsAsync(id); if (!_confirm.Confirm("删除任务", - $"将删除任务「{name}」及其 {g} 个分组、{t} 个 Tag,不可恢复。确定删除?")) + $"将删除任务「{name}」及其 {t} 个 Tag,不可恢复。确定删除?")) return; // 确认后:运行中先停,再级联删 @@ -334,21 +325,6 @@ public async Task ImportAsync() // ── Null-object stubs for optional panels ────────────────────────────── - private sealed class NullGroupPanel - : CommunityToolkit.Mvvm.ComponentModel.ObservableObject, IEmbeddableGroupPanel - { - public bool IsEmbedded { get; set; } - private Dc.Domain.Entities.CollectorTask? _taskFilter; - public Dc.Domain.Entities.CollectorTask? TaskFilter - { - get => _taskFilter; - set => SetProperty(ref _taskFilter, value); - } - public Dc.Domain.Entities.Group? SelectedGroup => null; - public Task LoadAsync() => Task.CompletedTask; - public event Action? NavigateToTasksRequested; - } - private sealed class NullLivePanel : IEmbeddableLivePanel { public string? TaskFilter { get; set; } diff --git a/src/Dc.App/Views/GroupEditorWindow.xaml b/src/Dc.App/Views/GroupEditorWindow.xaml deleted file mode 100644 index 45812bd..0000000 --- a/src/Dc.App/Views/GroupEditorWindow.xaml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - -