From 8aa25bbd5a6a4a9dedf2e25e147dfad25cbca9c1 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 12 Aug 2026 17:32:05 +0800 Subject: [PATCH] style(compute): refine Windows Node panel --- tests/test_windows_node_source.py | 10 +- .../Zcbot.WindowsNode/ConfigurationForm.cs | 198 +++++++++++++----- 2 files changed, 149 insertions(+), 59 deletions(-) diff --git a/tests/test_windows_node_source.py b/tests/test_windows_node_source.py index d37ac1f..d939c13 100644 --- a/tests/test_windows_node_source.py +++ b/tests/test_windows_node_source.py @@ -52,11 +52,11 @@ class WindowsNodeSourceTests(unittest.TestCase): self.assertNotIn("NodeToken", form) self.assertNotIn("Clipboard", form) self.assertIn("清除本机身份并重新注册", form) - self.assertIn("管理员禁用云端旧节点", form) + self.assertIn("管理后台删除或禁用云端旧节点", form) def test_configuration_window_is_resizable_and_dpi_safe(self) -> None: form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8") - self.assertIn("ClientSize = new Size(760, 690)", form) + self.assertIn("ClientSize = new Size(880, 720)", form) self.assertIn("FormBorderStyle.Sizable", form) self.assertIn("AutoScaleMode.Dpi", form) self.assertIn("AutoScroll = true", form) @@ -65,6 +65,12 @@ class WindowsNodeSourceTests(unittest.TestCase): self.assertIn("成功注册一次后立即失效", form) self.assertIn("CreateCard", form) self.assertIn("注册并连接", form) + self.assertIn("ContentWidth = 800", form) + self.assertIn("节点能力", form) + self.assertIn("Origin 绘图", form) + self.assertIn("当前 MVP 内置声明该协议,不需要手工配置", form) + self.assertIn("registrationCard.Visible = !registered", form) + self.assertIn("registeredActionsCard.Visible = registered", form) def test_startup_task_is_login_scoped_and_runs_the_fixed_node_executable(self) -> None: script = (ROOT / "install-startup.ps1").read_text(encoding="utf-8") diff --git a/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs b/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs index 84891f5..365d241 100644 --- a/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs +++ b/windows-node/Zcbot.WindowsNode/ConfigurationForm.cs @@ -2,25 +2,30 @@ namespace Zcbot.WindowsNode; internal sealed class ConfigurationForm : Form { + private const int ContentWidth = 800; + private readonly TextBox server = CreateTextBox("http://127.0.0.1:8765"); private readonly TextBox nodeName = CreateTextBox(Environment.MachineName.ToLowerInvariant()); private readonly TextBox enrollmentCode = CreateTextBox(usePassword: true); - private readonly Button register = CreateButton("注册并连接", primary: true); - private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册"); + private readonly Button register = CreateButton("注册并连接", 128, primary: true); + private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册", 220); private readonly CheckBox startAtLogin = new() { Text = "登录 Windows 后自动启动节点", - AutoSize = false, - Height = 32, - Dock = DockStyle.Top, + AutoSize = true, + Margin = new Padding(0, 4, 0, 8), }; private readonly Label state = new() { AutoSize = true, - Font = new Font("Microsoft YaHei UI", 12, FontStyle.Bold), + Font = new Font("Microsoft YaHei UI", 13, FontStyle.Bold), + Margin = new Padding(0, 2, 0, 5), }; - private readonly Label detail = new() { AutoSize = true, Dock = DockStyle.Fill, ForeColor = Color.FromArgb(71, 85, 105) }; - private readonly Label identity = new() { AutoSize = true, Dock = DockStyle.Fill, ForeColor = Color.FromArgb(71, 85, 105) }; + private readonly Label detail = CreateBodyLabel(); + private readonly Label identity = CreateBodyLabel(); + private readonly Label capabilitySummary = CreateBodyLabel(); + private readonly TableLayoutPanel registrationCard; + private readonly TableLayoutPanel registeredActionsCard; private bool changingStartup; internal event Func? RegisterRequested; @@ -30,24 +35,38 @@ internal sealed class ConfigurationForm : Form { Text = "zcbot Windows Node"; AutoScaleMode = AutoScaleMode.Dpi; - ClientSize = new Size(760, 690); - MinimumSize = new Size(720, 640); + ClientSize = new Size(880, 720); + MinimumSize = new Size(850, 680); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Microsoft YaHei UI", 9); FormBorderStyle = FormBorderStyle.Sizable; - BackColor = Color.FromArgb(245, 247, 250); + BackColor = Color.FromArgb(241, 245, 249); + + var shell = new TableLayoutPanel + { + Dock = DockStyle.Fill, + AutoScroll = true, + ColumnCount = 3, + RowCount = 1, + Padding = new Padding(0, 22, 0, 22), + BackColor = BackColor, + }; + shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + shell.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, ContentWidth)); + shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + Controls.Add(shell); var page = new TableLayoutPanel { - Dock = DockStyle.Fill, - Padding = new Padding(28, 24, 28, 24), - AutoScroll = true, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + Dock = DockStyle.Top, ColumnCount = 1, - RowCount = 7, + RowCount = 6, BackColor = BackColor, }; page.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); - Controls.Add(page); + shell.Controls.Add(page, 1, 0); var heading = new TableLayoutPanel { @@ -66,41 +85,65 @@ internal sealed class ConfigurationForm : Form heading.Controls.Add(CreateHint("连接本机科研软件与 zcbot 的受控执行节点")); page.Controls.Add(heading); - var statusCard = CreateCard(148); + var overview = new TableLayoutPanel + { + AutoSize = true, + Dock = DockStyle.Top, + ColumnCount = 2, + Margin = new Padding(0, 0, 0, 14), + }; + overview.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + overview.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50)); + + var statusCard = CreateCard(); + statusCard.Margin = new Padding(0, 0, 7, 0); statusCard.Controls.Add(CreateSectionTitle("节点状态")); statusCard.Controls.Add(state); statusCard.Controls.Add(detail); statusCard.Controls.Add(identity); - page.Controls.Add(statusCard); + overview.Controls.Add(statusCard, 0, 0); - var configCard = CreateCard(360); - configCard.Controls.Add(CreateSectionTitle("注册配置")); - configCard.Controls.Add(CreateHint( - "首次连接需要管理员在 zcbot 管理端生成注册码。注册码默认 10 分钟有效,成功注册一次后立即失效。")); - AddField(configCard, "zcbot 服务地址", server, "本机测试通常使用 http://127.0.0.1:8765"); - AddField(configCard, "节点名称", nodeName, "用于管理员识别这台 Windows 计算节点"); - AddField(configCard, "一次性注册码", enrollmentCode, "格式类似 ZCN-…;它不是日常登录密码,也不会被长期保存"); - var actions = new FlowLayoutPanel - { - AutoSize = true, - FlowDirection = FlowDirection.LeftToRight, - WrapContents = false, - Dock = DockStyle.Fill, - Margin = new Padding(0, 12, 0, 0), - }; - actions.Controls.Add(register); - actions.Controls.Add(resetIdentity); - configCard.Controls.Add(actions); - page.Controls.Add(configCard); + var capabilityCard = CreateCard(); + capabilityCard.Margin = new Padding(7, 0, 0, 0); + capabilityCard.Controls.Add(CreateSectionTitle("节点能力")); + capabilityCard.Controls.Add(CreateCapabilityBadge("Origin 绘图", "origin.plot@v1")); + capabilityCard.Controls.Add(capabilitySummary); + capabilityCard.Controls.Add(CreateHint( + "当前 MVP 内置声明该协议,不需要手工配置;它不代表已完成 Origin 安装检测。")); + overview.Controls.Add(capabilityCard, 1, 0); + page.Controls.Add(overview); - var runtimeCard = CreateCard(104); + registrationCard = CreateCard(); + registrationCard.Controls.Add(CreateSectionTitle("首次注册")); + registrationCard.Controls.Add(CreateHint( + "从 zcbot 管理后台生成注册码。注册码默认 10 分钟有效,成功注册一次后立即失效。")); + AddField(registrationCard, "zcbot 服务地址", server, "云端填写站点根地址;本机测试使用 http://127.0.0.1:8765"); + AddField(registrationCard, "节点名称", nodeName, "必须与注册码限定的节点名称完全一致"); + AddField(registrationCard, "一次性注册码", enrollmentCode, "格式类似 ZCN-…;仅用于首次注册,不会长期保存"); + var registerActions = CreateActions(); + registerActions.Controls.Add(register); + registrationCard.Controls.Add(registerActions); + page.Controls.Add(registrationCard); + + registeredActionsCard = CreateCard(); + registeredActionsCard.Controls.Add(CreateSectionTitle("节点身份")); + registeredActionsCard.Controls.Add(CreateHint( + "服务地址、节点名称和能力在首次注册时确定。需要修改时,请先在管理后台删除旧节点,再清除本机身份并重新注册。")); + var identityActions = CreateActions(); + identityActions.Controls.Add(resetIdentity); + registeredActionsCard.Controls.Add(identityActions); + page.Controls.Add(registeredActionsCard); + + var runtimeCard = CreateCard(); runtimeCard.Controls.Add(CreateSectionTitle("运行设置")); runtimeCard.Controls.Add(startAtLogin); - runtimeCard.Controls.Add(CreateHint("关闭配置窗口后,节点仍会在系统托盘运行。右键托盘图标可重连或退出。")); + runtimeCard.Controls.Add(CreateHint( + "关闭此窗口后,节点仍在系统托盘运行。右键托盘图标可以立即重连或退出。")); page.Controls.Add(runtimeCard); - var securityNote = CreateHint("安全说明:Node Token 由 Windows DPAPI 加密保存,不在界面中显示,也不会写入日志。"); - securityNote.Margin = new Padding(4, 14, 4, 0); + var securityNote = CreateHint( + "安全说明:Node Token 由 Windows DPAPI 加密保存,不在界面显示,也不会写入日志。"); + securityNote.Margin = new Padding(4, 8, 4, 0); page.Controls.Add(securityNote); register.Click += async (_, _) => await RegisterAsync(); @@ -138,16 +181,19 @@ internal sealed class ConfigurationForm : Form }; detail.Text = $"{status.Message} {status.ChangedAt:HH:mm:ss}"; identity.Text = config is null - ? $"配置文件:{NodePaths.ForCurrentMachine().ConfigPath}" + ? "尚未建立节点身份" : $"节点:{config.NodeName}\nNode ID:{config.NodeId}\n服务:{config.ServerUrl}"; - server.Enabled = config is null; - nodeName.Enabled = config is null; - enrollmentCode.Enabled = config is null; - register.Enabled = config is null && status.State != NodeState.Connecting; - resetIdentity.Visible = config is not null; - if (config is not null) + capabilitySummary.Text = config is null + ? "注册后启用" + : $"已声明 {config.Capabilities.Count} 项能力"; + + var registered = config is not null; + registrationCard.Visible = !registered; + registeredActionsCard.Visible = registered; + register.Enabled = !registered && status.State != NodeState.Connecting; + if (registered) { - server.Text = config.ServerUrl.AbsoluteUri.TrimEnd('/'); + server.Text = config!.ServerUrl.AbsoluteUri.TrimEnd('/'); nodeName.Text = config.NodeName; enrollmentCode.Clear(); } @@ -156,7 +202,7 @@ internal sealed class ConfigurationForm : Form private void ResetIdentity() { var answer = MessageBox.Show( - "这会删除本机加密身份,随后需要使用新注册码重新注册。请同时让管理员禁用云端旧节点。是否继续?", + "这会删除本机加密身份,随后需要使用新注册码重新注册。请先在管理后台删除或禁用云端旧节点。是否继续?", "清除本机节点身份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2); if (answer == DialogResult.Yes) @@ -211,16 +257,15 @@ internal sealed class ConfigurationForm : Form } } - private static TableLayoutPanel CreateCard(int minimumHeight) + private static TableLayoutPanel CreateCard() { var card = new TableLayoutPanel { AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, - MinimumSize = new Size(0, minimumHeight), Dock = DockStyle.Top, ColumnCount = 1, - Padding = new Padding(22, 18, 22, 18), + Padding = new Padding(20, 17, 20, 17), Margin = new Padding(0, 0, 0, 14), BackColor = Color.White, }; @@ -237,6 +282,15 @@ internal sealed class ConfigurationForm : Form Margin = new Padding(0, 0, 0, 8), }; + private static Label CreateBodyLabel() => new() + { + AutoSize = true, + Dock = DockStyle.Fill, + ForeColor = Color.FromArgb(71, 85, 105), + MaximumSize = new Size(340, 0), + Margin = new Padding(0, 2, 0, 5), + }; + private static Label CreateHint(string text) => new() { Text = text, @@ -246,23 +300,44 @@ internal sealed class ConfigurationForm : Form Margin = new Padding(0, 2, 0, 4), }; + private static Panel CreateCapabilityBadge(string title, string protocol) + { + var badge = new Panel + { + AutoSize = false, + Height = 54, + Dock = DockStyle.Top, + BackColor = Color.FromArgb(239, 246, 255), + Margin = new Padding(0, 2, 0, 8), + Padding = new Padding(12, 7, 12, 7), + }; + badge.Controls.Add(new Label + { + Text = $"{title}\n协议:{protocol}", + AutoSize = true, + Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold), + ForeColor = Color.FromArgb(29, 78, 216), + }); + return badge; + } + private static TextBox CreateTextBox(string text = "", bool usePassword = false) => new() { Text = text, UseSystemPasswordChar = usePassword, AutoSize = false, - Height = 36, + Height = 34, BorderStyle = BorderStyle.FixedSingle, Font = new Font("Segoe UI", 10), }; - private static Button CreateButton(string text, bool primary = false) + private static Button CreateButton(string text, int width, bool primary = false) { var button = new Button { Text = text, AutoSize = false, - Size = new Size(primary ? 128 : 220, 38), + Size = new Size(width, 36), FlatStyle = FlatStyle.Flat, BackColor = primary ? Color.FromArgb(37, 99, 235) : Color.White, ForeColor = primary ? Color.White : Color.FromArgb(51, 65, 85), @@ -274,6 +349,15 @@ internal sealed class ConfigurationForm : Form return button; } + private static FlowLayoutPanel CreateActions() => new() + { + AutoSize = true, + FlowDirection = FlowDirection.LeftToRight, + WrapContents = false, + Dock = DockStyle.Fill, + Margin = new Padding(0, 10, 0, 0), + }; + private static void AddField( TableLayoutPanel layout, string label, Control control, string hint) { @@ -283,7 +367,7 @@ internal sealed class ConfigurationForm : Form AutoSize = true, Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold), ForeColor = Color.FromArgb(51, 65, 85), - Margin = new Padding(0, 9, 0, 4), + Margin = new Padding(0, 8, 0, 4), }); control.Dock = DockStyle.Top; control.Margin = new Padding(0, 0, 0, 2);