Skip to content
Closed
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
20 changes: 18 additions & 2 deletions src/Dc.App/Composition/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,31 @@ public static IServiceCollection AddDcApp(this IServiceCollection services, stri
_ => 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);
}
}
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
10 changes: 8 additions & 2 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 @@ -165,8 +168,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
16 changes: 12 additions & 4 deletions src/Dc.App/ViewModels/LiveDataViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ internal void DebounceTickForTest()
private volatile LiveFlushStats _statsSnapshot = new(0, 0, 0, 0, 0);

public ObservableCollection<LiveDataRowViewModel> Rows { get; } = new();
public ObservableCollection<string> AvailableTaskIds { get; } = new();
// 任务筛选下拉项:Id + 可读名(Display)。下拉显名、按 Id 过滤(TaskFilter 仍是 id,RowsView 不变)。
public ObservableCollection<TaskPick> AvailableTasks { get; } = new();
public ICollectionView RowsView { get; }

private bool _running;
Expand Down Expand Up @@ -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 */ }
}
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 32 additions & 3 deletions src/Dc.App/ViewModels/TagEditorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public partial class TagEditorViewModel : ObservableObject
[ObservableProperty] private string _expression = string.Empty;
[ObservableProperty] private string _outputUnit = string.Empty;

// 实时校验:无错才可保存(对齐任务编辑器),首条错误内联红字提示。
[ObservableProperty] private bool _canSave;
[ObservableProperty] private string _validationError = string.Empty;

public ObservableCollection<InputBindingRow> InputBindings { get; } = new();
public ObservableCollection<Tag> AvailableInputTags { get; } = new();

Expand Down Expand Up @@ -113,13 +117,28 @@ public TagEditorViewModel(
{
SelectedTag = AvailableInputTags.FirstOrDefault(t => t.Id == input.SourceTagId)
};
row.PropertyChanged += OnInputRowChanged;
InputBindings.Add(row);
}
}
else if (_isVirtual)
{
RebuildInputBindings();
}
Revalidate(); // 初始态:新建真实 Tag(Item 空)→ 保存禁用,直到填合法
}

// 实时校验:复用 Validate(),无错才可保存,首条错误内联提示。
private void Revalidate()
{
var errs = Validate();
CanSave = errs.Count == 0;
ValidationError = errs.Count == 0 ? string.Empty : errs[0];
}

private void OnInputRowChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(InputBindingRow.SelectedTag)) Revalidate();
}

public bool CanBrowse => _browseDialog is not null;
Expand Down Expand Up @@ -169,19 +188,26 @@ partial void OnSelectedGroupRowChanged(GroupRow? value)
// 分组定 → TaskId 定 → 刷新可选输入 Tag(同任务真实,排除自身虚拟)。
OnPropertyChanged(nameof(Group));
RefreshAvailableInputTags();
Revalidate();
}

partial void OnIsVirtualChanged(bool value)
{
if (value) RebuildInputBindings();
else InputBindings.Clear();
if (value) RebuildInputBindings(); // 内部 Revalidate
else { InputBindings.Clear(); Revalidate(); }
}

partial void OnExpressionChanged(string value)
{
if (IsVirtual) RebuildInputBindings();
if (IsVirtual) RebuildInputBindings(); // 内部 Revalidate
}

// 实时校验:这些字段变化即重算可保存性。
partial void OnItemChanged(string value) => Revalidate();
partial void OnScaleFactorChanged(string value) => Revalidate();
partial void OnOffsetChanged(string value) => Revalidate();
partial void OnFormulaNameChanged(string value) => Revalidate();

private void RefreshAvailableInputTags()
{
AvailableInputTags.Clear();
Expand All @@ -195,13 +221,16 @@ private void RebuildInputBindings()
{
var aliases = ExtractAliases(Expression);
var prevByAlias = InputBindings.ToDictionary(r => r.Alias, r => r.SelectedTag, StringComparer.OrdinalIgnoreCase);
foreach (var r in InputBindings) r.PropertyChanged -= OnInputRowChanged; // 退订旧行,避免重复触发
InputBindings.Clear();
foreach (var alias in aliases)
{
var row = new InputBindingRow(alias);
if (prevByAlias.TryGetValue(alias, out var sel)) row.SelectedTag = sel;
row.PropertyChanged += OnInputRowChanged;
InputBindings.Add(row);
}
Revalidate();
}

public IReadOnlyList<string> Validate()
Expand Down
9 changes: 8 additions & 1 deletion src/Dc.App/ViewModels/TagsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
private readonly ITagExcelService _excel;
private readonly IFilePicker _filePicker;
private readonly TaskOrchestrator _orchestrator;
private readonly Action<string>? _navigate; // 导航到其它页(空状态「浏览节点」CTA → "browse")

[ObservableProperty] private string _title = "Tag 管理";
[ObservableProperty] private bool _isLoading;
Expand Down Expand Up @@ -55,16 +56,22 @@
ITagEditorDialog editor,
ITagExcelService excel,
IFilePicker filePicker,
TaskOrchestrator orchestrator)
TaskOrchestrator orchestrator,
Action<string>? 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);

Expand Down Expand Up @@ -476,7 +483,7 @@
MessageDialog.Show("提示", "虚拟测点/公式已保存,重启任务后生效。", MessageDialogKind.Info);
}

var idx = Tags.IndexOf(SelectedTag);

Check warning on line 486 in src/Dc.App/ViewModels/TagsViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

Possible null reference argument for parameter 'item' in 'int Collection<TagRow>.IndexOf(TagRow item)'.

Check warning on line 486 in src/Dc.App/ViewModels/TagsViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

Possible null reference argument for parameter 'item' in 'int Collection<TagRow>.IndexOf(TagRow item)'.

Check warning on line 486 in src/Dc.App/ViewModels/TagsViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

Possible null reference argument for parameter 'item' in 'int Collection<TagRow>.IndexOf(TagRow item)'.

Check warning on line 486 in src/Dc.App/ViewModels/TagsViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

Possible null reference argument for parameter 'item' in 'int Collection<TagRow>.IndexOf(TagRow item)'.
if (idx >= 0)
{
var row = ToRow(entity);
Expand Down
12 changes: 9 additions & 3 deletions src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,17 @@

FilteredTasks.Refresh();

// 重建后恢复选中:按 TaskId 找回对应新行(启动/停止/刷新都经此处)
if (prevSelectedId is not null)
SelectedTask = AllTasks.FirstOrDefault(r => r.TaskId == prevSelectedId);
// 优先用外部请求的选中(浏览批量加 Tag 后跳回工作台);否则恢复原选中(启动/停止/刷新都经此处)。
var targetId = _pendingSelectId ?? prevSelectedId;
_pendingSelectId = null;
if (targetId is not null)
SelectedTask = AllTasks.FirstOrDefault(r => r.TaskId == targetId);
}

private string? _pendingSelectId;
/// <summary>请求下次 LoadAsync 时选中该任务(浏览批量加 Tag 后跳回工作台用)。</summary>
public void RequestSelect(string taskId) => _pendingSelectId = taskId;

private static string ProtocolLabel(byte type) => type switch
{
1 => "DA",
Expand Down Expand Up @@ -340,7 +346,7 @@
}
public Dc.Domain.Entities.Group? SelectedGroup => null;
public Task LoadAsync() => Task.CompletedTask;
public event Action? NavigateToTasksRequested;

Check warning on line 349 in src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

The event 'TaskWorkspaceViewModel.NullGroupPanel.NavigateToTasksRequested' is never used

Check warning on line 349 in src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

The event 'TaskWorkspaceViewModel.NullGroupPanel.NavigateToTasksRequested' is never used

Check warning on line 349 in src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

The event 'TaskWorkspaceViewModel.NullGroupPanel.NavigateToTasksRequested' is never used

Check warning on line 349 in src/Dc.App/ViewModels/Workspace/TaskWorkspaceViewModel.cs

View workflow job for this annotation

GitHub Actions / Build & Test (Windows)

The event 'TaskWorkspaceViewModel.NullGroupPanel.NavigateToTasksRequested' is never used
}

private sealed class NullLivePanel : IEmbeddableLivePanel
Expand Down
15 changes: 11 additions & 4 deletions src/Dc.App/Views/ConfigEntryEditorWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@
</Border>
<Border DockPanel.Dock="Bottom" Padding="14,14"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="取消" Margin="0,0,8,0" IsCancel="True" Style="{StaticResource DcBtnGhostSm}" />
<Button Content="保存" Click="OnSaveClick" IsDefault="True" Style="{StaticResource DcBtnPrimarySm}" />
</StackPanel>
<DockPanel LastChildFill="False">
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center"
Text="{Binding ValidationError}"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
FontSize="12" TextTrimming="CharacterEllipsis" MaxWidth="280" />
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="取消" Margin="0,0,8,0" IsCancel="True" Style="{StaticResource DcBtnGhostSm}" />
<Button Content="保存" Click="OnSaveClick" IsDefault="True"
IsEnabled="{Binding CanSave}" Style="{StaticResource DcBtnPrimarySm}" />
</StackPanel>
</DockPanel>
</Border>
<StackPanel Margin="20,18">
<Grid>
Expand Down
6 changes: 4 additions & 2 deletions src/Dc.App/Views/LiveDataView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
VerticalAlignment="Center"
Margin="0,0,6,0" />
<ComboBox Width="220"
ItemsSource="{Binding AvailableTaskIds}"
SelectedItem="{Binding TaskFilter}"
ItemsSource="{Binding AvailableTasks}"
DisplayMemberPath="Display"
SelectedValuePath="Id"
SelectedValue="{Binding TaskFilter}"
VerticalAlignment="Center" />
<Button Content="清除"
Margin="6,0,0,0"
Expand Down
16 changes: 12 additions & 4 deletions src/Dc.App/Views/TagEditorWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@
<Border DockPanel.Dock="Bottom" Padding="14,14"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="取消" Margin="0,0,8,0" IsCancel="True" Style="{StaticResource DcBtnGhostSm}" />
<Button Content="保存" Click="OnSaveClick" IsDefault="True" Style="{StaticResource DcBtnPrimarySm}" />
</StackPanel>
<DockPanel LastChildFill="False">
<!-- 实时校验:首条错误内联红字,保存按钮随 CanSave 禁用 -->
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center"
Text="{Binding ValidationError}"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
FontSize="12" TextTrimming="CharacterEllipsis" MaxWidth="320" />
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="取消" Margin="0,0,8,0" IsCancel="True" Style="{StaticResource DcBtnGhostSm}" />
<Button Content="保存" Click="OnSaveClick" IsDefault="True"
IsEnabled="{Binding CanSave}" Style="{StaticResource DcBtnPrimarySm}" />
</StackPanel>
</DockPanel>
</Border>
<StackPanel Margin="20,18">
<!-- 虚拟开关(所有模式可见) -->
Expand Down
Loading
Loading