Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/Dc.App/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@
<DataTemplate DataType="{x:Type wsvm:WorkspaceConfigViewModel}">
<wsview:ConfigTabPanel />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:GroupsViewModel}">
<views:GroupsView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:TagsViewModel}">
<views:TagsView />
</DataTemplate>
Expand Down
26 changes: 18 additions & 8 deletions src/Dc.App/Composition/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri
// SnackbarPresenter 在 App 启动 Show 前经 SetSnackbarPresenter 接线(见 App.xaml.cs)。
services.AddSingleton<Wpf.Ui.ISnackbarService, Wpf.Ui.SnackbarService>();
services.AddSingleton<Dc.App.Services.INotificationService, Dc.App.Services.SnackbarNotificationService>();
services.AddSingleton<IGroupEditorDialog, GroupEditorDialog>();
services.AddSingleton<ITagEditorDialog, TagEditorDialog>();
services.AddSingleton<IConfigEditorDialog, ConfigEditorDialog>();
services.AddSingleton<ITagExcelService, ClosedXmlTagExcelService>();
Expand Down Expand Up @@ -199,9 +198,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri
new Dc.App.ViewModels.Workspace.WorkspaceOverviewViewModel(
sp.GetRequiredService<Dc.App.ViewModels.Dashboard.IDashboardOrchestratorView>(),
() => DateTimeOffset.UtcNow));
// GroupsViewModel 仅工作台用(无全局导航路由),复用单例即可
services.AddSingleton<Dc.App.ViewModels.Workspace.IEmbeddableGroupPanel>(
sp => sp.GetRequiredService<GroupsViewModel>());
// 全局监控分离(S4):LiveData/Diagnostics 同时挂「全局监控」导航与工作台 tab,
// 工作台用独立实例,避免工作台设的 TaskFilter/TaskScope 污染全局视图。
services.AddSingleton<Dc.App.ViewModels.Workspace.IEmbeddableLivePanel>(
Expand All @@ -224,7 +220,6 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri
sp.GetRequiredService<TagsViewModel>(),
sp.GetRequiredService<TaskOrchestrator>(),
sp.GetRequiredService<Dc.App.Services.ITaskEditorDialog>(),
sp.GetRequiredService<Dc.App.ViewModels.Workspace.IEmbeddableGroupPanel>(),
sp.GetRequiredService<Dc.App.ViewModels.Workspace.IEmbeddableLivePanel>(),
sp.GetRequiredService<Dc.App.ViewModels.Workspace.IEmbeddableDiagPanel>(),
sp.GetRequiredService<Dc.App.ViewModels.Workspace.WorkspaceConfigViewModel>(),
Expand All @@ -235,16 +230,31 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri
services.AddSingleton<System.Windows.Threading.Dispatcher>(
_ => System.Windows.Application.Current?.Dispatcher
?? System.Windows.Threading.Dispatcher.CurrentDispatcher);
services.AddSingleton<GroupsViewModel>();
services.AddSingleton<TagsViewModel>();
services.AddSingleton<TagsViewModel>(sp => new TagsViewModel(
sp.GetRequiredService<IDbContextFactory<DcDbContext>>(),
sp.GetRequiredService<ITagEditorDialog>(),
sp.GetRequiredService<ITagExcelService>(),
sp.GetRequiredService<IFilePicker>(),
sp.GetRequiredService<TaskOrchestrator>(),
// 空状态「浏览节点添加 Tag」CTA → 切到浏览页(发现→批量加 Tag 主路径)
navigate: key => sp.GetRequiredService<Dc.App.ViewModels.Shell.ShellViewModel>().NavigateCommand.Execute(key)));
services.AddSingleton<LiveDataViewModel>(sp => new LiveDataViewModel(
sp.GetRequiredService<TaskOrchestrator>(),
System.Windows.Application.Current?.Dispatcher ?? System.Windows.Threading.Dispatcher.CurrentDispatcher,
navigate: key => sp.GetRequiredService<Dc.App.ViewModels.Shell.ShellViewModel>()
.NavigateCommand.Execute(key),
showNavigateCta: true,
dbFactory: sp.GetRequiredService<IDbContextFactory<DcDbContext>>()));
services.AddSingleton<BrowseViewModel>();
services.AddSingleton<BrowseViewModel>(sp => new BrowseViewModel(
sp.GetServices<IOpcBrowserFactory>(),
sp.GetRequiredService<IDbContextFactory<DcDbContext>>(),
sp.GetRequiredService<Dc.App.Services.ITaskEditorDialog>(),
// 批量加 Tag 后:选中目标任务 + 跳到「采集任务」(发现→配置→看数据 一条线)。
onTagsAdded: taskId =>
{
sp.GetRequiredService<Dc.App.ViewModels.Workspace.TaskWorkspaceViewModel>().RequestSelect(taskId);
sp.GetRequiredService<Dc.App.ViewModels.Shell.ShellViewModel>().NavigateCommand.Execute("workspace");
}));
services.AddSingleton<DiagnosticsViewModel>(sp => new DiagnosticsViewModel(
sp.GetRequiredService<TaskOrchestrator>(),
navigate: key => sp.GetRequiredService<Dc.App.ViewModels.Shell.ShellViewModel>()
Expand Down
17 changes: 17 additions & 0 deletions src/Dc.App/Controls/EmptyState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,21 @@ public ICommand? ActionCommand
get => (ICommand?)GetValue(ActionCommandProperty);
set => SetValue(ActionCommandProperty, value);
}

// 可选次操作(幽灵按钮)。SecondaryActionText 为空时不显示。
public static readonly DependencyProperty SecondaryActionTextProperty = DependencyProperty.Register(
nameof(SecondaryActionText), typeof(string), typeof(EmptyState), new PropertyMetadata(null));
public string? SecondaryActionText
{
get => (string?)GetValue(SecondaryActionTextProperty);
set => SetValue(SecondaryActionTextProperty, value);
}

public static readonly DependencyProperty SecondaryActionCommandProperty = DependencyProperty.Register(
nameof(SecondaryActionCommand), typeof(ICommand), typeof(EmptyState), new PropertyMetadata(null));
public ICommand? SecondaryActionCommand
{
get => (ICommand?)GetValue(SecondaryActionCommandProperty);
set => SetValue(SecondaryActionCommandProperty, value);
}
}
20 changes: 0 additions & 20 deletions src/Dc.App/Services/GroupEditorDialog.cs

This file was deleted.

8 changes: 0 additions & 8 deletions src/Dc.App/Services/IGroupEditorDialog.cs

This file was deleted.

3 changes: 1 addition & 2 deletions src/Dc.App/Services/ITagEditorDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ namespace Dc.App.Services;
public interface ITagEditorDialog
{
TagEditResult? Edit(
IEnumerable<Group> availableGroups,
string taskId,
Tag? existing,
Group? defaultGroup = null,
Func<string, CollectorTask?>? taskLookup = null,
IReadOnlyCollection<Tag>? taskTags = null,
IReadOnlyCollection<Formula>? existingFormulas = null);
Expand Down
5 changes: 2 additions & 3 deletions src/Dc.App/Services/TagEditorDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ public TagEditorDialog(IBrowseDialog browseDialog, IFormulaValidator formulaVali
}

public TagEditResult? Edit(
IEnumerable<Group> availableGroups,
string taskId,
Tag? existing,
Group? defaultGroup = null,
Func<string, CollectorTask?>? taskLookup = null,
IReadOnlyCollection<Tag>? taskTags = null,
IReadOnlyCollection<Formula>? existingFormulas = null)
{
var vm = new TagEditorViewModel(
availableGroups, existing, defaultGroup, _browseDialog, taskLookup,
taskId, existing, _browseDialog, taskLookup,
taskTags, existingFormulas, _formulaValidator);
var window = new TagEditorWindow
{
Expand Down
17 changes: 17 additions & 0 deletions src/Dc.App/Theme/Tokens.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,23 @@
</Style>
</Button.Style>
</Button>
<Button Content="{TemplateBinding SecondaryActionText}"
Command="{TemplateBinding SecondaryActionCommand}"
Margin="0,8,0,0" HorizontalAlignment="Center"
MinWidth="120">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource DcBtnGhost}">
<Style.Triggers>
<DataTrigger Binding="{Binding SecondaryActionText, RelativeSource={RelativeSource AncestorType={x:Type controls:EmptyState}}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding SecondaryActionText, RelativeSource={RelativeSource AncestorType={x:Type controls:EmptyState}}}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</ControlTemplate>
</Setter.Value>
Expand Down
17 changes: 10 additions & 7 deletions src/Dc.App/ViewModels/BrowseViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public partial class BrowseViewModel : ObservableObject, IAsyncDisposable
// 可空:导航页注入(走 DI)→ 启用批量「加为 Tag」;Tag 编辑器内嵌的单点取用对话框传 null → 不启用。
private readonly IDbContextFactory<DcDbContext>? _dbFactory;
private readonly ITaskEditorDialog? _taskEditor;
private readonly Action<string>? _onTagsAdded; // 批量加 Tag 后回调(taskId):由组合根接「选中该任务+跳工作台」
private IOpcBrowser? _browser;
private readonly Stack<(string? Id, string Name)> _path = new();

Expand Down Expand Up @@ -56,11 +57,13 @@ public partial class BrowseViewModel : ObservableObject, IAsyncDisposable
public BrowseViewModel(
IEnumerable<IOpcBrowserFactory> browserFactories,
IDbContextFactory<DcDbContext>? dbFactory = null,
ITaskEditorDialog? taskEditor = null)
ITaskEditorDialog? taskEditor = null,
Action<string>? onTagsAdded = null)
{
_factories = browserFactories.ToDictionary(f => f.Protocol);
_dbFactory = dbFactory;
_taskEditor = taskEditor;
_onTagsAdded = onTagsAdded;
// 暴露注册的所有协议;DI 顺序 = UA, DA → UI 下拉同序
AvailableProtocols = _factories.Keys.ToArray();
if (_dbFactory is not null) _ = LoadTasksAsync();
Expand Down Expand Up @@ -129,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);
Expand All @@ -149,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();

Expand All @@ -165,8 +165,11 @@ private async Task AddCheckedAsTagsAsync(string taskId, string taskDisplay)
var skipped = picked.Count - toAdd.Count;
var msg = $"已添加 {toAdd.Count} 个 Tag 到「{taskDisplay}」"
+ (skipped > 0 ? $",跳过 {skipped} 个重复 Item" : "")
+ "。去「采集任务」启动即可采集。";
+ "。即将跳到「采集任务」,启动即可采集。";
MessageDialog.Show("加为 Tag", msg, MessageDialogKind.Success);

// 跳到采集任务并选中目标任务(发现→配置→看数据 一条线);组合根接线,BrowseViewModel 不依赖具体 VM。
if (toAdd.Count > 0) _onTagsAdded?.Invoke(taskId);
}

// 浏览到的数据类型名(UA: Float/Double/...; 或选项 DisplayName)→ Tag.DataType 码;认不出落 0(默认/自动)。
Expand Down
13 changes: 13 additions & 0 deletions src/Dc.App/ViewModels/ConfigEntryEditorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ public partial class ConfigEntryEditorViewModel : ObservableObject
[ObservableProperty] private string _title;
[ObservableProperty] private string _key = string.Empty;
[ObservableProperty] private string _value = string.Empty;
// 实时校验:无错才可保存 + 首条错误内联红字(对齐任务/Tag 编辑器)。
[ObservableProperty] private bool _canSave;
[ObservableProperty] private string _validationError = string.Empty;

public string? OriginalId { get; }
public bool KeyIsReadOnly => OriginalId is not null;
Expand All @@ -25,6 +28,16 @@ public ConfigEntryEditorViewModel(ConfigEntry? existing)
_key = existing.Key;
_value = existing.Value;
}
Revalidate();
}

partial void OnKeyChanged(string value) => Revalidate();

private void Revalidate()
{
var errs = Validate();
CanSave = errs.Count == 0;
ValidationError = errs.Count == 0 ? string.Empty : errs[0];
}

public IReadOnlyList<string> Validate()
Expand Down
51 changes: 0 additions & 51 deletions src/Dc.App/ViewModels/GroupEditorViewModel.cs

This file was deleted.

23 changes: 0 additions & 23 deletions src/Dc.App/ViewModels/GroupRow.cs

This file was deleted.

Loading
Loading