返回提交历史
Modified
XFEWorkflow/XFEWorkflow/Services/UiSmokeTest.Documents.cs
+83
-20
Modified
XFEWorkflow/XFEWorkflow/Views/AppShellPage.xaml
+1
-1
Modified
XFEWorkflow/XFEWorkflow/Views/MainPage.Document.cs
+23
-0
Modified
XFEWorkflow/XFEWorkflow/Views/MainPage.Panels.cs
+2
-2
Modified
XFEWorkflow/XFEWorkflow/Views/MainPage.xaml
+3
-2
Modified
XFEWorkflow/XFEWorkflow/Views/MainPage.xaml.cs
+1
-1
Modified
XFEWorkflow/XFEWorkflow/Views/WorkflowEditorPage.cs
+19
-4
Modified
XFEWorkflow/XFEWorkflow/Views/WorkflowEditorPage.xaml
+10
-1
Modified
XFEWorkflow/XFEWorkflow/Views/WorkflowListPage.cs
+5
-2
Added
docs/screenshots/workflow-document-docking.json
+52
-0
Added
docs/screenshots/workflow-editor-connected-tabs-dark.png
+0
-0
Added
docs/screenshots/workflow-editor-connected-tabs-light.png
+0
-0
Added
docs/screenshots/workflow-list-edit-actions.png
+0
-0
Modified
docs/workbench.md
+5
-1
XFEstudio/XFEWorkflow
完善工作流列表编辑入口与编辑器标签整体布局
36a3f22
代码差异
14 个文件
+204
-34
@@ -2,6 +2,10 @@ using XFEWorkflow.Abstractions;
2
2
using XFEWorkflow.Core.Runtime;
3
3
using XFEWorkflow.ViewModels;
4
4
using XFEWorkflow.Views;
5
using Microsoft.UI.Xaml.Automation;
6
using Microsoft.UI.Xaml.Automation.Peers;
7
using Microsoft.UI.Xaml.Automation.Provider;
8
using Microsoft.UI.Xaml.Media;
5
9
6
10
namespace XFEWorkflow.Services;
7
11
@@ -10,28 +14,87 @@ internal static partial class UiSmokeTest
10
14
private static async Task SmokeWorkflowDocumentsAsync(string output)
11
15
{
12
16
var services = AppServices.Current; var navigation = AppShellPage.Current!.ViewModel.NavigationViewService;
17
var root = (FrameworkElement)App.MainWindow.Content; var originalSize = App.MainWindow.AppWindow.Size; var originalTheme = root.RequestedTheme;
18
var paletteSize = services.State.PaneSize("workflow.palette", 260);
19
var scale = root.XamlRoot.RasterizationScale; App.MainWindow.AppWindow.Resize(new((int)(1600 * scale), (int)(1000 * scale)));
13
20
var first = WorkflowTemplates.Welcome(); first.Name = "标签页 A"; first.Enabled = false; first.Triggers.Add(new() { Kind = TriggerKind.Interval, Enabled = true, IntervalSeconds = 3600 });
14
21
var second = WorkflowTemplates.Welcome(); second.Name = "标签页 B"; second.Enabled = false;
15
22
await services.Workflows.SaveAsync(first); await services.Workflows.SaveAsync(second);
16
navigation.NavigateTo<WorkflowListPage>(); await WaitManagementAsync(() => Descendants<Frame>(App.MainWindow.Content).Any(x => x.Content is WorkflowListPage));
17
await Task.Delay(300); await CaptureWindowAsync(Path.Combine(output, "workflow-list.png"));
18
navigation.NavigateTo<WorkflowEditorPage>(new WorkflowNavigationTarget(first.Id));
19
await WaitManagementAsync(() => WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == first.Id);
20
var hub = WorkflowEditorPage.Current!; var a = hub.ActiveEditor!; a.ViewModel.SelectedWorkflow!.Description = "A 独立修改";
21
var b = await hub.OpenAsync(second.Id); b.ViewModel.SelectedWorkflow!.Description = "B 独立修改";
22
await hub.OpenAsync(first.Id);
23
if (!ReferenceEquals(hub.ActiveEditor, a) || hub.Documents.TabItems.Count != 2 || a.ViewModel.SelectedWorkflow!.Description != "A 独立修改") throw new InvalidOperationException("多标签页未保留独立编辑状态或重复打开");
24
if (Descendants<CommandBar>(a).Any(x => x.Visibility == Visibility.Visible) || Descendants<Expander>(a).Any(x => x.Name == "WorkflowLibraryExpander" && x.Visibility == Visibility.Visible)) throw new InvalidOperationException("编辑器仍展示工作流列表和旧溢出工具栏");
25
if (!Descendants<MenuBar>(hub).Single().Items.Select(x => x.Title).SequenceEqual(new[] { "文件", "编辑", "工作流", "视图", "工具" })) throw new InvalidOperationException("顶部菜单不完整");
26
if (!a.ViewModel.PaletteGroups.Any(x => x.Name == "鼠标操作" && x.Glyph == "\uE962") || !a.ViewModel.PaletteGroups.Any(x => x.Name == "键盘操作" && x.Glyph == "\uE765")) throw new InvalidOperationException("鼠标或键盘分类图标错误");
27
await services.Workflows.SetActivationAsync(first.Id, true, 9); await hub.SaveAllAsync();
28
if ((await services.Workflows.GetAsync(first.Id)).Enabled != true) throw new InvalidOperationException("编辑器保存覆盖了列表的启停状态");
29
await CaptureWindowAsync(Path.Combine(output, "workflow-editor-multiple-tabs-menus.png"));
30
navigation.NavigateTo<WorkflowListPage>(); await Task.Delay(300);
31
var frame = Descendants<Frame>(App.MainWindow.Content).Single(x => x.Content is WorkflowListPage); var list = (WorkflowListPage)frame.Content;
32
await list.SetEnabledAsync(first.Id, false);
33
if (services.Scheduler.StatusFor(await services.Workflows.GetAsync(first.Id)).Any(x => x.Enabled)) throw new InvalidOperationException("列表关闭后仍在调度");
34
navigation.NavigateTo<WorkflowEditorPage>(); await Task.Delay(150); if (hub.Documents.TabItems.Count != 2) throw new InvalidOperationException("离开编辑器后标签页丢失");
35
await WorkflowEditorPage.CloseOpenAsync(first.Id); await WorkflowEditorPage.CloseOpenAsync(second.Id); navigation.NavigateTo<MainPage>();
23
try
24
{
25
navigation.NavigateTo<WorkflowListPage>(); await WaitManagementAsync(() => Descendants<Frame>(App.MainWindow.Content).Any(x => x.Content is WorkflowListPage));
26
await WaitManagementAsync(() => Descendants<Button>(App.MainWindow.Content).Any(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
27
var firstRow = Descendants<Expander>(App.MainWindow.Content).Single(x => x.Tag as string == first.Id); firstRow.IsExpanded = false;
28
await Task.Delay(300); await CaptureWindowAsync(Path.Combine(output, "workflow-list.png"));
29
await InvokeButtonAsync(Descendants<Button>(firstRow).Single(x => AutomationProperties.GetAutomationId(x) == "EditWorkflow-" + first.Id));
30
await WaitManagementAsync(() => WorkflowEditorPage.Current?.ActiveEditor?.ViewModel.SelectedWorkflow?.Id == first.Id);
31
if (firstRow.IsExpanded) throw new InvalidOperationException("列表右侧编辑按钮不应展开工作流卡片");
32
var editorNavigation = Descendants<NavigationViewItem>(AppShellPage.Current!).Single(x => x.Content as string == "工作流编辑器");
33
if (editorNavigation.Icon is not FontIcon { Glyph: "\uE943" }) throw new InvalidOperationException("工作流编辑器入口没有使用代码图标");
34
var hub = WorkflowEditorPage.Current!; var a = hub.ActiveEditor!; a.ViewModel.SelectedWorkflow!.Description = "A 独立修改";
35
var b = await hub.OpenAsync(second.Id); b.ViewModel.SelectedWorkflow!.Description = "B 独立修改";
36
await hub.OpenAsync(first.Id);
37
if (!ReferenceEquals(hub.ActiveEditor, a) || hub.Documents.TabItems.Count != 2 || a.ViewModel.SelectedWorkflow!.Description != "A 独立修改") throw new InvalidOperationException("多标签页未保留独立编辑状态或重复打开");
38
if (Descendants<CommandBar>(a).Any(x => x.Visibility == Visibility.Visible) || Descendants<Expander>(a).Any(x => x.Name == "WorkflowLibraryExpander" && x.Visibility == Visibility.Visible)) throw new InvalidOperationException("编辑器仍展示工作流列表和旧溢出工具栏");
39
if (!Descendants<MenuBar>(hub).Single().Items.Select(x => x.Title).SequenceEqual(new[] { "文件", "编辑", "工作流", "视图", "工具" })) throw new InvalidOperationException("顶部菜单不完整");
40
if (!a.ViewModel.PaletteGroups.Any(x => x.Name == "鼠标操作" && x.Glyph == "\uE962") || !a.ViewModel.PaletteGroups.Any(x => x.Name == "键盘操作" && x.Glyph == "\uE765")) throw new InvalidOperationException("鼠标或键盘分类图标错误");
41
var dockingEvidence = new List<object>(); var colors = new Dictionary<ElementTheme, Windows.UI.Color>();
42
async Task VerifyDockingAsync(string stage)
43
{
44
await Task.Delay(200);
45
var palette = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel");
46
var header = Descendants<Grid>(hub.ActiveEditor!).Single(x => x.Name == "EditorModeHeader");
47
var selected = (TabViewItem)hub.Documents.SelectedItem;
48
var stripOrigin = hub.Documents.TransformToVisual(hub).TransformPoint(new(0, 0));
49
var headerOrigin = header.TransformToVisual(hub).TransformPoint(new(0, 0));
50
var tabBottom = selected.TransformToVisual(hub).TransformPoint(new(0, selected.ActualHeight)).Y;
51
var paletteOrigin = palette.TransformToVisual(hub).TransformPoint(new(0, 0));
52
if (Math.Abs(stripOrigin.X - headerOrigin.X) > 1 || Math.Abs(tabBottom - headerOrigin.Y) > 1 || stripOrigin.X - paletteOrigin.X < 200 || Math.Abs(paletteOrigin.Y - stripOrigin.Y) > 2)
53
throw new InvalidOperationException($"工作流标签与编辑区没有对齐或存在间隙:{stage} · strip={stripOrigin} · header={headerOrigin} · tabBottom={tabBottom}");
54
var selectedPath = Descendants<Microsoft.UI.Xaml.Shapes.Path>(selected).Single(x => x.Name == "SelectedBackgroundPath");
55
var color = ((SolidColorBrush)header.Background).Color;
56
if (selectedPath.Fill is not SolidColorBrush fill || fill.Color != color || hub.DocumentSurface.Background is not SolidColorBrush surface || surface.Color != color)
57
throw new InvalidOperationException("工作流选项卡与编辑窗口未形成统一主题背景:" + stage);
58
colors[root.RequestedTheme] = color;
59
dockingEvidence.Add(new { stage, theme = root.RequestedTheme.ToString(), stripLeft = stripOrigin.X, headerLeft = headerOrigin.X, tabBottom, headerTop = headerOrigin.Y, paletteWidth = palette.ActualWidth, background = color.ToString() });
60
}
61
root.RequestedTheme = ElementTheme.Dark; await VerifyDockingAsync("dark");
62
var dockedSplitter = Descendants<Controls.PaneSplitter>(hub).Single(x => AutomationProperties.GetName(x) == "调整节点库宽度");
63
var oldPaletteWidth = Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth;
64
services.State.SetPaneSize("workflow.palette", oldPaletteWidth + 48); dockedSplitter.ReloadPreference(); await VerifyDockingAsync("resized");
65
if (Math.Abs(Descendants<Border>(hub).Single(x => x.Name == "PalettePanel").ActualWidth - oldPaletteWidth - 48) > 4) throw new InvalidOperationException("调整节点库宽度后标签与编辑器未随动");
66
await hub.OpenAsync(second.Id); await VerifyDockingAsync("second-tab");
67
await hub.OpenAsync(first.Id); await VerifyDockingAsync("first-tab-restored");
68
await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-dark.png"));
69
root.RequestedTheme = ElementTheme.Light; await VerifyDockingAsync("light");
70
await CaptureWindowAsync(Path.Combine(output, "workflow-editor-connected-tabs-light.png"));
71
if (colors[ElementTheme.Dark] == colors[ElementTheme.Light]) throw new InvalidOperationException("工作流编辑区没有适配深浅主题");
72
await Infrastructure.Storage.AtomicJson.WriteAsync(Path.Combine(output, "workflow-document-docking.json"), dockingEvidence);
73
root.RequestedTheme = originalTheme;
74
var search = Descendants<TextBox>(hub).Single(x => x.Name == "PaletteSearch"); search.Text = "输出日志";
75
await WaitManagementAsync(() => a.ViewModel.PaletteRows.OfType<PaletteItem>().Any(x => x.Descriptor.Id == "core.log"));
76
var paletteList = Descendants<ListView>(hub).Single(x => x.Name == "PaletteList"); var logItem = a.ViewModel.PaletteRows.OfType<PaletteItem>().Single(x => x.Descriptor.Id == "core.log");
77
paletteList.ScrollIntoView(logItem); await WaitManagementAsync(() => paletteList.ContainerFromItem(logItem) is FrameworkElement);
78
var nodeCount = a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count;
79
var container = (ListViewItem)paletteList.ContainerFromItem(logItem);
80
var peer = FrameworkElementAutomationPeer.FromElement(container) ?? new ListViewItemAutomationPeer(container);
81
if (peer.GetPattern(PatternInterface.Invoke) is not IInvokeProvider invoke) throw new InvalidOperationException("节点库项缺少自动化调用入口");
82
invoke.Invoke();
83
await WaitManagementAsync(() => a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count == nodeCount + 1);
84
await a.ExecuteEditorCommandAsync("undo"); if (a.ViewModel.SelectedWorkflow!.Graph.Nodes.Count != nodeCount) throw new InvalidOperationException("独立节点库添加与撤销未作用到当前工作流"); search.Text = "";
85
await services.Workflows.SetActivationAsync(first.Id, true, 9); await hub.SaveAllAsync();
86
if ((await services.Workflows.GetAsync(first.Id)).Enabled != true) throw new InvalidOperationException("编辑器保存覆盖了列表的启停状态");
87
await CaptureWindowAsync(Path.Combine(output, "workflow-editor-multiple-tabs-menus.png"));
88
navigation.NavigateTo<WorkflowListPage>(); await Task.Delay(300);
89
var frame = Descendants<Frame>(App.MainWindow.Content).Single(x => x.Content is WorkflowListPage); var list = (WorkflowListPage)frame.Content;
90
await list.SetEnabledAsync(first.Id, false);
91
if (services.Scheduler.StatusFor(await services.Workflows.GetAsync(first.Id)).Any(x => x.Enabled)) throw new InvalidOperationException("列表关闭后仍在调度");
92
navigation.NavigateTo<WorkflowEditorPage>(); await Task.Delay(150); if (hub.Documents.TabItems.Count != 2) throw new InvalidOperationException("离开编辑器后标签页丢失");
93
}
94
finally
95
{
96
await WorkflowEditorPage.CloseOpenAsync(first.Id); await WorkflowEditorPage.CloseOpenAsync(second.Id); navigation.NavigateTo<MainPage>();
97
services.State.SetPaneSize("workflow.palette", paletteSize); await services.State.SaveAsync(); root.RequestedTheme = originalTheme; App.MainWindow.AppWindow.Resize(originalSize);
98
}
36
99
}
37
100
}
@@ -56,7 +56,7 @@
56
56
<FontIcon Glyph=""/>
57
57
</NavigationViewItem.Icon>
58
58
</NavigationViewItem>
59
<NavigationViewItem Content="工作流编辑器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.WorkflowEditorPage"><NavigationViewItem.Icon><FontIcon Glyph=""/></NavigationViewItem.Icon></NavigationViewItem>
59
<NavigationViewItem Content="工作流编辑器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.WorkflowEditorPage"><NavigationViewItem.Icon><FontIcon Glyph=""/></NavigationViewItem.Icon></NavigationViewItem>
60
60
<NavigationViewItem Content="内置浏览器" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.BrowserPage" Icon="World"/>
61
61
<NavigationViewItem Content="插件中心" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.PluginsPage" Icon="Library"/>
62
62
<NavigationViewItem Content="运行记录" add:NavigationAddition.NavigateTo="XFEWorkflow.Views.RunsPage" Icon="Clock"/>
@@ -4,13 +4,35 @@ namespace XFEWorkflow.Views;
4
4
5
5
public sealed partial class MainPage
6
6
{
7
private bool documentLayout;
8
private ColumnDefinition? documentPaletteColumn;
9
private Grid? documentWorkspace;
10
private double PaletteWidth => documentPaletteColumn?.ActualWidth ?? (documentLayout ? PaletteSplitter.PreferredSize : PaletteColumn.ActualWidth);
11
private double EditorWorkspaceWidth => documentWorkspace?.ActualWidth ?? (EditorLayout.ActualWidth + (documentLayout ? PaletteWidth + 8 : 0));
7
12
internal async Task OpenDocumentAsync(WorkflowDefinition workflow)
8
13
{
9
14
initialized = true;
10
15
LegacyHeader.Visibility = WorkflowLibraryExpander.Visibility = LegacyCommands.Visibility = Visibility.Collapsed;
11
16
WorkflowPageLayout.RowSpacing = 0;
17
documentLayout = true; DocumentBackground.Visibility = Visibility.Visible;
18
EditorLayout.Children.Remove(PalettePanel); EditorLayout.Children.Remove(PaletteSplitter);
19
PaletteColumn.Width = EditorLayout.ColumnDefinitions[1].Width = new(0);
20
EditorSurface.BorderThickness = new(0); EditorSurface.CornerRadius = new(0);
12
21
await ViewModel.LoadAsync(); ViewModel.SelectedWorkflow = workflow; ready.TrySetResult(); ActivateDocument();
13
22
}
23
internal void AttachDocumentPalette(Grid workspace, ColumnDefinition width)
24
{
25
documentWorkspace = workspace; documentPaletteColumn = width;
26
workspace.Children.Add(PalettePanel); workspace.Children.Add(PaletteSplitter);
27
PaletteSplitter.ReloadPreference(); ApplyPanelSizes();
28
}
29
internal void DetachDocumentPalette()
30
{
31
if (documentWorkspace is { } workspace) { workspace.Children.Remove(PalettePanel); workspace.Children.Remove(PaletteSplitter); }
32
documentPaletteColumn = null; documentWorkspace = null;
33
}
34
private void SetPaletteWidth(double value)
35
{ if (documentPaletteColumn is { } column) column.Width = new(value); else if (!documentLayout) PaletteColumn.Width = new(value); }
14
36
internal void ActivateDocument()
15
37
{ if (disposed) return; Current = this; ViewModel.IsRunning = services.Runs.IsRunning(ViewModel.SelectedWorkflow?.Id ?? ""); CodeEditor.IsAnalysisEnabled = CodeEditor.Visibility == Visibility.Visible; }
16
38
internal void DeactivateDocument() => CodeEditor.IsAnalysisEnabled = false;
@@ -37,6 +59,7 @@ public sealed partial class MainPage
37
59
case "paste": await CanvasEditor.PasteAsync(); break;
38
60
case "delete": CanvasEditor.Delete(); break;
39
61
case "selectAll": CanvasEditor.SelectAll(); break;
62
case "search": PaletteSearch.Focus(FocusState.Keyboard); PaletteSearch.SelectAll(); break;
40
63
case "properties": Properties_Click(this, new()); break;
41
64
case "output": ToggleOutput_Click(this, new()); break;
42
65
case "center": CenterCanvas_Click(this, new()); break;
@@ -8,8 +8,8 @@ public sealed partial class MainPage
8
8
private bool updatingPaletteFilters;
9
9
private void InitializeResizablePanels()
10
10
{
11
PaletteSplitter.Bind("workflow.palette", 260, 216, 440, () => PaletteColumn.ActualWidth, value => PaletteColumn.Width = new(value),
12
() => EditorLayout.ActualWidth - 8 - 360 - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline && PropertySplit.IsPaneOpen ? PropertySplit.OpenPaneLength : 0));
11
PaletteSplitter.Bind("workflow.palette", 260, 216, 440, () => PaletteWidth, SetPaletteWidth,
12
() => EditorWorkspaceWidth - 8 - 360 - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline && PropertySplit.IsPaneOpen ? PropertySplit.OpenPaneLength : 0));
13
13
PropertySplitter.Bind("workflow.properties", 296, 248, 520, () => PropertySplit.OpenPaneLength, value => PropertySplit.OpenPaneLength = value,
14
14
() => PropertySplit.ActualWidth - (PropertySplit.DisplayMode == SplitViewDisplayMode.Inline ? 360 : 72));
15
15
OutputSplitter.Bind("workflow.output", 156, 80, 520, () => OutputPanelRow.ActualHeight, value => { if (!outputCollapsed) OutputPanelRow.Height = new(value); },
@@ -40,6 +40,7 @@
40
40
<Page.KeyboardAccelerators><KeyboardAccelerator Key="K" Modifiers="Control" Invoked="PaletteSearchShortcut_Invoked"/></Page.KeyboardAccelerators>
41
41
<Grid x:Name="WorkflowPageLayout" RowSpacing="10" SizeChanged="WorkflowPageLayout_SizeChanged">
42
42
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
43
<Border x:Name="DocumentBackground" Grid.RowSpan="5" Background="{ThemeResource TabViewItemHeaderBackgroundSelected}" Visibility="Collapsed"/>
43
44
<Grid x:Name="LegacyHeader" Margin="0,0,0,2" ColumnSpacing="16">
44
45
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
45
46
<StackPanel Spacing="3">
@@ -154,10 +155,10 @@
154
155
</Border>
155
156
</Grid>
156
157
</SplitView.Pane>
157
<Border BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
158
<Border x:Name="EditorSurface" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
158
159
<Grid>
159
160
<Grid.RowDefinitions><RowDefinition Height="48"/><RowDefinition Height="*"/></Grid.RowDefinitions>
160
<Grid Padding="8,0" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,0,0,1" ColumnSpacing="8">
161
<Grid x:Name="EditorModeHeader" Padding="8,0" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="0,0,0,1" ColumnSpacing="8" Background="{ThemeResource TabViewItemHeaderBackgroundSelected}">
161
162
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
162
163
<Border Background="{ThemeResource LayerFillColorDefaultBrush}" CornerRadius="6" Padding="2" VerticalAlignment="Center" ToolTipService.ToolTip="切换到 C# 时从当前图形生成代码;手动修改的代码会保留">
163
164
<StackPanel Orientation="Horizontal" Spacing="2">
@@ -160,7 +160,7 @@ public sealed partial class MainPage : Page
160
160
private void EditorLayout_SizeChanged(object sender, SizeChangedEventArgs e)
161
161
{
162
162
if (PropertySplit is null) return;
163
var mode = e.NewSize.Width >= 1080 ? SplitViewDisplayMode.Inline : SplitViewDisplayMode.Overlay;
163
var mode = EditorWorkspaceWidth >= 1080 ? SplitViewDisplayMode.Inline : SplitViewDisplayMode.Overlay;
164
164
if (PropertySplit.DisplayMode != mode) { PropertySplit.DisplayMode = mode; PropertySplit.IsPaneOpen = mode == SplitViewDisplayMode.Inline; }
165
165
ApplyPanelSizes();
166
166
}
@@ -10,8 +10,13 @@ namespace XFEWorkflow.Views;
10
10
public sealed partial class WorkflowEditorPage : Page
11
11
{
12
12
public static WorkflowEditorPage? Current { get; private set; }
13
internal TabView Documents { get; } = new() { TabWidthMode = TabViewWidthMode.SizeToContent, CanReorderTabs = true, AllowDrop = true };
13
internal TabView Documents { get; } = new() { Name = "WorkflowDocumentTabs", TabWidthMode = TabViewWidthMode.SizeToContent, CanReorderTabs = true, AllowDropTabs = true, VerticalAlignment = VerticalAlignment.Stretch, Padding = new(0) };
14
14
public MainPage? ActiveEditor => (Documents.SelectedItem as TabViewItem)?.Content as MainPage;
15
internal Border DocumentSurface { get; }
16
private readonly Grid workspace = new();
17
private readonly ColumnDefinition paletteColumn = new() { Width = new(0) };
18
private readonly ColumnDefinition paletteSplitterColumn = new() { Width = new(0) };
19
private MainPage? paletteOwner;
15
20
private readonly InfoBar error = new() { Severity = InfoBarSeverity.Error, IsClosable = true, Visibility = Visibility.Collapsed };
16
21
private readonly Button run = new() { Content = "▶ 运行", Style = (Style)Application.Current.Resources["AccentButtonStyle"], Padding = new(18, 7, 18, 7) };
17
22
private readonly Button stop = new() { Content = "□ 停止" };
@@ -40,12 +45,16 @@ public sealed partial class WorkflowEditorPage : Page
40
45
var quick = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Center }; quick.Children.Add(run); quick.Children.Add(stop); Grid.SetColumn(quick, 1); toolbar.Children.Add(quick); root.Children.Add(toolbar);
41
46
run.Click += async (_, _) => await GuardAsync(async () => { if (ActiveEditor is { } editor) await editor.ExecuteEditorCommandAsync("run"); });
42
47
stop.Click += async (_, _) => await GuardAsync(async () => { if (ActiveEditor is { } editor) await editor.ExecuteEditorCommandAsync("stop"); });
43
Grid.SetRow(error, 1); root.Children.Add(error); var workspace = new Grid(); workspace.Children.Add(Documents); workspace.Children.Add(empty); Grid.SetRow(workspace, 2); root.Children.Add(workspace); Content = root;
48
Grid.SetRow(error, 1); root.Children.Add(error);
49
workspace.ColumnDefinitions.Add(paletteColumn); workspace.ColumnDefinitions.Add(paletteSplitterColumn); workspace.ColumnDefinitions.Add(new());
50
var documentContent = new Grid(); documentContent.Children.Add(Documents); documentContent.Children.Add(empty);
51
DocumentSurface = new Border { Name = "WorkflowDocumentSurface", Style = (Style)Resources["WorkflowDocumentSurfaceStyle"], Child = documentContent };
52
Grid.SetColumn(DocumentSurface, 2); workspace.Children.Add(DocumentSurface); Grid.SetRow(workspace, 2); root.Children.Add(workspace); Content = root;
44
53
Documents.TabItemsChanged += (_, _) => UpdateChrome(); Documents.SelectionChanged += (_, _) => UpdateChrome();
45
54
Documents.AddTabButtonClick += async (_, _) => await GuardAsync(NewAsync);
46
55
Documents.TabCloseRequested += async (_, args) => await GuardAsync(() => CloseAsync(args.Tab));
47
56
error.Closed += (_, _) => error.Visibility = Visibility.Collapsed;
48
foreach (var pair in new[] { (VirtualKey.S, (Func<Task>)SaveAllAsync), (VirtualKey.N, (Func<Task>)NewAsync), (VirtualKey.W, (Func<Task>)(async () => { if (Documents.SelectedItem is TabViewItem item) await CloseAsync(item); })) })
57
foreach (var pair in new[] { (VirtualKey.S, (Func<Task>)SaveAllAsync), (VirtualKey.N, (Func<Task>)NewAsync), (VirtualKey.W, (Func<Task>)(async () => { if (Documents.SelectedItem is TabViewItem item) await CloseAsync(item); })), (VirtualKey.K, (Func<Task>)(() => ActiveEditor?.ExecuteEditorCommandAsync("search") ?? Task.CompletedTask)) })
49
58
{ var accelerator = new KeyboardAccelerator { Key = pair.Item1, Modifiers = VirtualKeyModifiers.Control }; accelerator.Invoked += async (_, e) => { e.Handled = true; await GuardAsync(pair.Item2); }; KeyboardAccelerators.Add(accelerator); }
50
59
Loaded += (_, _) => UpdateChrome(); Unloaded += async (_, _) => { foreach (var tab in documents.Values) ((MainPage)tab.Content).DeactivateDocument(); await GuardAsync(SaveAllAsync); }; UpdateChrome();
51
60
}
@@ -68,7 +77,7 @@ public sealed partial class WorkflowEditorPage : Page
68
77
{
69
78
if (documents.TryGetValue(id, out var existing)) { Documents.SelectedItem = existing; return (MainPage)existing.Content; }
70
79
var flow = await AppServices.Current.Workflows.GetAsync(id); var editor = new MainPage(); await editor.OpenDocumentAsync(flow);
71
var tab = new TabViewItem { Header = flow.Name, Content = editor, IconSource = new FontIconSource { Glyph = "\uEF90" }, IsClosable = true, Tag = id };
80
var tab = new TabViewItem { Header = flow.Name, Content = editor, IconSource = new FontIconSource { Glyph = "\uEF90" }, IsClosable = true, Tag = id, HorizontalContentAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Stretch };
72
81
editor.ViewModel.PropertyChanged += (_, _) =>
73
82
{ tab.Header = (editor.ViewModel.SelectedWorkflow?.Name ?? flow.Name) + (editor.ViewModel.Status.Contains("未保存", StringComparison.Ordinal) ? " •" : ""); if (ReferenceEquals(ActiveEditor, editor)) UpdateRunButtons(); };
74
83
documents[id] = tab; Documents.TabItems.Add(tab); Documents.SelectedItem = tab; UpdateChrome(); return editor;
@@ -78,6 +87,12 @@ public sealed partial class WorkflowEditorPage : Page
78
87
private void UpdateChrome()
79
88
{
80
89
empty.Visibility = documents.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
90
if (!ReferenceEquals(paletteOwner, ActiveEditor))
91
{
92
paletteOwner?.DetachDocumentPalette(); paletteOwner = ActiveEditor;
93
paletteSplitterColumn.Width = new(paletteOwner is null ? 0 : 8); paletteColumn.Width = new(paletteOwner is null ? 0 : 260);
94
paletteOwner?.AttachDocumentPalette(workspace, paletteColumn);
95
}
81
96
foreach (var tab in documents.Values) { var editor = (MainPage)tab.Content; if (ReferenceEquals(editor, ActiveEditor)) editor.ActivateDocument(); else editor.DeactivateDocument(); }
82
97
UpdateRunButtons();
83
98
}
@@ -1,3 +1,12 @@
1
1
<Page x:Class="XFEWorkflow.Views.WorkflowEditorPage"
2
2
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"/>
3
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
4
<Page.Resources>
5
<Style x:Key="WorkflowDocumentSurfaceStyle" TargetType="Border">
6
<Setter Property="Background" Value="{ThemeResource TabViewItemHeaderBackgroundSelected}"/>
7
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
8
<Setter Property="BorderThickness" Value="1"/>
9
<Setter Property="CornerRadius" Value="8"/>
10
</Style>
11
</Page.Resources>
12
</Page>
@@ -36,12 +36,15 @@ public sealed partial class WorkflowListPage : ManagementPage
36
36
var filtered = workflows.Where(x => x.Name.Contains(search.Text, StringComparison.OrdinalIgnoreCase) && (stateFilter.SelectedIndex == 0 || x.Enabled == (stateFilter.SelectedIndex == 1))).OrderByDescending(x => x.Priority).ThenByDescending(x => x.ModifiedAt).ToArray();
37
37
foreach (var workflow in filtered)
38
38
{
39
var heading = new Grid { ColumnSpacing = 14, Padding = new(0, 6, 0, 6) }; heading.ColumnDefinitions.Add(new() { Width = new(32) }); heading.ColumnDefinitions.Add(new()); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto });
39
var heading = new Grid { ColumnSpacing = 14, Padding = new(0, 6, 0, 6) }; heading.ColumnDefinitions.Add(new() { Width = new(32) }); heading.ColumnDefinitions.Add(new()); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto }); heading.ColumnDefinitions.Add(new() { Width = GridLength.Auto });
40
40
heading.Children.Add(new FontIcon { Glyph = "\uEF90", FontSize = 22 });
41
41
var name = new StackPanel { Spacing = 4 }; name.Children.Add(new TextBlock { Text = workflow.Name, FontSize = 16, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, TextTrimming = TextTrimming.CharacterEllipsis });
42
42
var statuses = Services.Scheduler.StatusFor(workflow); name.Children.Add(Caption((workflow.Enabled == true ? "已开启" : "已关闭") + " · " + (workflow.Kind == WorkflowKind.Graph ? "图形化" : "C#") + " · " + statuses.Count + " 个触发节点")); Grid.SetColumn(name, 1); heading.Children.Add(name);
43
43
var toggle = new ToggleSwitch { IsOn = workflow.Enabled == true, OnContent = "开启", OffContent = "关闭", VerticalAlignment = VerticalAlignment.Center };
44
44
AutomationProperties.SetName(toggle, "启停工作流 " + workflow.Name); Grid.SetColumn(toggle, 2); heading.Children.Add(toggle);
45
var editButton = ActionButton("编辑", () => OpenAsync(workflow.Id), "EditWorkflow-" + workflow.Id);
46
editButton.Content = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6, Children = { new FontIcon { Glyph = "\uE70F", FontSize = 14 }, new TextBlock { Text = "编辑" } } };
47
AutomationProperties.SetName(editButton, "编辑工作流 " + workflow.Name); Grid.SetColumn(editButton, 3); heading.Children.Add(editButton);
45
48
var details = new StackPanel { Spacing = 12, Padding = new(0, 0, 0, 6) };
46
49
if (workflow.Description.Length > 0) details.Children.Add(Label(workflow.Description));
47
50
var runtimeStatus = new StackPanel { Spacing = 4 }; details.Children.Add(runtimeStatus);
@@ -54,7 +57,7 @@ public sealed partial class WorkflowListPage : ManagementPage
54
57
if (statuses.Count == 0) details.Children.Add(Caption("手动运行 · 在编辑器中添加触发节点可自动执行"));
55
58
var priority = new NumberBox { Header = "优先级(较大值优先)", Value = workflow.Priority, Minimum = -10000, Maximum = 10000, SmallChange = 1, SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact, Width = 240, HorizontalAlignment = HorizontalAlignment.Left };
56
59
priority.LostFocus += async (_, _) => await Guard(async () => { if (!double.IsFinite(priority.Value)) return; var latest = await Services.Workflows.GetAsync(workflow.Id); if (latest.Priority == (int)priority.Value) return; await Services.Workflows.SetActivationAsync(workflow.Id, latest.Enabled == true, (int)priority.Value); workflow.Priority = (int)priority.Value; await Services.Scheduler.RefreshAsync(); }); details.Children.Add(priority);
57
var actions = new Controls.AdaptiveWrapPanel(); actions.Children.Add(ActionButton("编辑工作流", () => OpenAsync(workflow.Id), "EditWorkflow-" + workflow.Id, true));
60
var actions = new Controls.AdaptiveWrapPanel();
58
61
actions.Children.Add(ActionButton("运行一次", async () => { await WorkflowEditorPage.SaveOpenAsync(workflow.Id); var result = await Services.Runs.RunAsync(await Services.Workflows.GetAsync(workflow.Id)); Status.Text = workflow.Name + " · " + result.Status; }));
59
62
actions.Children.Add(ActionButton("复制", async () => { var copy = WorkflowJson.Clone(await Services.Workflows.GetAsync(workflow.Id)); copy.Id = Guid.NewGuid().ToString("N"); copy.Name += " 副本"; copy.Enabled = false; await Services.Workflows.SaveAsync(copy); await ReloadAsync(); }));
60
63
actions.Children.Add(ActionButton("删除", async () =>
@@ -0,0 +1,52 @@
1
[
2
{
3
"stage": "dark",
4
"theme": "Dark",
5
"stripLeft": 269.3333435058594,
6
"headerLeft": 269.3333435058594,
7
"tabBottom": 85.33332824707031,
8
"headerTop": 85.33332824707031,
9
"paletteWidth": 260,
10
"background": "#FF282828"
11
},
12
{
13
"stage": "resized",
14
"theme": "Dark",
15
"stripLeft": 317.3333435058594,
16
"headerLeft": 317.3333435058594,
17
"tabBottom": 85.33332824707031,
18
"headerTop": 85.33332824707031,
19
"paletteWidth": 308,
20
"background": "#FF282828"
21
},
22
{
23
"stage": "second-tab",
24
"theme": "Dark",
25
"stripLeft": 317.3333435058594,
26
"headerLeft": 317.3333435058594,
27
"tabBottom": 85.33332824707031,
28
"headerTop": 85.33332824707031,
29
"paletteWidth": 308,
30
"background": "#FF282828"
31
},
32
{
33
"stage": "first-tab-restored",
34
"theme": "Dark",
35
"stripLeft": 317.3333435058594,
36
"headerLeft": 317.3333435058594,
37
"tabBottom": 85.33332824707031,
38
"headerTop": 85.33332824707031,
39
"paletteWidth": 308,
40
"background": "#FF282828"
41
},
42
{
43
"stage": "light",
44
"theme": "Light",
45
"stripLeft": 317.3333435058594,
46
"headerLeft": 317.3333435058594,
47
"tabBottom": 85.33332824707031,
48
"headerTop": 85.33332824707031,
49
"paletteWidth": 308,
50
"background": "#FFF9F9F9"
51
}
52
]
二进制文件已变更,无法进行逐行预览。
二进制文件已变更,无法进行逐行预览。
二进制文件已变更,无法进行逐行预览。
@@ -2,7 +2,11 @@
2
2
3
3
## 页面
4
4
5
新建工作流只填写名称,编辑器保留图形化与 C# 切换。顶部“所有工作流”可展开并搜索,每个条目也可展开,查看触发器是否启用、下次执行时间或监听错误。当前工作流的触发器与下次运行时间始终显示在编辑区上方。
5
新建工作流只填写名称,工作流列表与多标签编辑器独立。列表中的每一项均可展开,查看下次执行时间或监听错误;右侧的“编辑”按钮在收起时也直接打开对应标签。工作流启停由列表控制。
6
7
编辑器入口使用代码图标。左侧节点库独立停靠,工作流标签从右侧实际编辑窗口的左边缘开始排列;标签、图形化/C# 工具栏、属性与输出属于同一个带边框的工作区。选中标签与编辑窗口使用相同的主题背景并无间隙相接。调整节点库宽度时,标签和编辑内容一同移动,切换标签保留各自的编辑状态与节点库筛选。
8
9
实机截图:[列表编辑入口](screenshots/workflow-list-edit-actions.png)、[深色编辑器](screenshots/workflow-editor-connected-tabs-dark.png)、[浅色编辑器](screenshots/workflow-editor-connected-tabs-light.png)。[布局与主题验证数据](screenshots/workflow-document-docking.json)记录标签边界、编辑区边界和面板宽度。
6
10
7
11
插件中心合并本地管理与在线目录:已安装、插件 Space、我的发布;管理员额外看到审核中心。右侧详情显示来源、版本、能力、触发器、依赖和使用此版本的工作流数量。下载、上传、审核均使用真实 Space 接口。账号与插件共用固定的本地服务器 `http://localhost:3300/space/v1/`,无需在客户端配置地址。
8
12