style(compute): refine Windows Node panel

This commit is contained in:
caoqianming 2026-08-12 17:32:05 +08:00
parent b555059bc4
commit 8aa25bbd5a
2 changed files with 149 additions and 59 deletions

View File

@ -52,11 +52,11 @@ class WindowsNodeSourceTests(unittest.TestCase):
self.assertNotIn("NodeToken", form) self.assertNotIn("NodeToken", form)
self.assertNotIn("Clipboard", form) self.assertNotIn("Clipboard", form)
self.assertIn("清除本机身份并重新注册", form) self.assertIn("清除本机身份并重新注册", form)
self.assertIn("管理禁用云端旧节点", form) self.assertIn("管理后台删除或禁用云端旧节点", form)
def test_configuration_window_is_resizable_and_dpi_safe(self) -> None: def test_configuration_window_is_resizable_and_dpi_safe(self) -> None:
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8") 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("FormBorderStyle.Sizable", form)
self.assertIn("AutoScaleMode.Dpi", form) self.assertIn("AutoScaleMode.Dpi", form)
self.assertIn("AutoScroll = true", form) self.assertIn("AutoScroll = true", form)
@ -65,6 +65,12 @@ class WindowsNodeSourceTests(unittest.TestCase):
self.assertIn("成功注册一次后立即失效", form) self.assertIn("成功注册一次后立即失效", form)
self.assertIn("CreateCard", form) self.assertIn("CreateCard", form)
self.assertIn("注册并连接", 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: 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") script = (ROOT / "install-startup.ps1").read_text(encoding="utf-8")

View File

@ -2,25 +2,30 @@ namespace Zcbot.WindowsNode;
internal sealed class ConfigurationForm : Form internal sealed class ConfigurationForm : Form
{ {
private const int ContentWidth = 800;
private readonly TextBox server = CreateTextBox("http://127.0.0.1:8765"); private readonly TextBox server = CreateTextBox("http://127.0.0.1:8765");
private readonly TextBox nodeName = CreateTextBox(Environment.MachineName.ToLowerInvariant()); private readonly TextBox nodeName = CreateTextBox(Environment.MachineName.ToLowerInvariant());
private readonly TextBox enrollmentCode = CreateTextBox(usePassword: true); private readonly TextBox enrollmentCode = CreateTextBox(usePassword: true);
private readonly Button register = CreateButton("注册并连接", primary: true); private readonly Button register = CreateButton("注册并连接", 128, primary: true);
private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册"); private readonly Button resetIdentity = CreateButton("清除本机身份并重新注册", 220);
private readonly CheckBox startAtLogin = new() private readonly CheckBox startAtLogin = new()
{ {
Text = "登录 Windows 后自动启动节点", Text = "登录 Windows 后自动启动节点",
AutoSize = false, AutoSize = true,
Height = 32, Margin = new Padding(0, 4, 0, 8),
Dock = DockStyle.Top,
}; };
private readonly Label state = new() private readonly Label state = new()
{ {
AutoSize = true, 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 detail = CreateBodyLabel();
private readonly Label identity = new() { AutoSize = true, Dock = DockStyle.Fill, ForeColor = Color.FromArgb(71, 85, 105) }; private readonly Label identity = CreateBodyLabel();
private readonly Label capabilitySummary = CreateBodyLabel();
private readonly TableLayoutPanel registrationCard;
private readonly TableLayoutPanel registeredActionsCard;
private bool changingStartup; private bool changingStartup;
internal event Func<EnrollOptions, Task>? RegisterRequested; internal event Func<EnrollOptions, Task>? RegisterRequested;
@ -30,24 +35,38 @@ internal sealed class ConfigurationForm : Form
{ {
Text = "zcbot Windows Node"; Text = "zcbot Windows Node";
AutoScaleMode = AutoScaleMode.Dpi; AutoScaleMode = AutoScaleMode.Dpi;
ClientSize = new Size(760, 690); ClientSize = new Size(880, 720);
MinimumSize = new Size(720, 640); MinimumSize = new Size(850, 680);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9); Font = new Font("Microsoft YaHei UI", 9);
FormBorderStyle = FormBorderStyle.Sizable; 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 var page = new TableLayoutPanel
{ {
Dock = DockStyle.Fill, AutoSize = true,
Padding = new Padding(28, 24, 28, 24), AutoSizeMode = AutoSizeMode.GrowAndShrink,
AutoScroll = true, Dock = DockStyle.Top,
ColumnCount = 1, ColumnCount = 1,
RowCount = 7, RowCount = 6,
BackColor = BackColor, BackColor = BackColor,
}; };
page.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); page.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
Controls.Add(page); shell.Controls.Add(page, 1, 0);
var heading = new TableLayoutPanel var heading = new TableLayoutPanel
{ {
@ -66,41 +85,65 @@ internal sealed class ConfigurationForm : Form
heading.Controls.Add(CreateHint("连接本机科研软件与 zcbot 的受控执行节点")); heading.Controls.Add(CreateHint("连接本机科研软件与 zcbot 的受控执行节点"));
page.Controls.Add(heading); 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(CreateSectionTitle("节点状态"));
statusCard.Controls.Add(state); statusCard.Controls.Add(state);
statusCard.Controls.Add(detail); statusCard.Controls.Add(detail);
statusCard.Controls.Add(identity); statusCard.Controls.Add(identity);
page.Controls.Add(statusCard); overview.Controls.Add(statusCard, 0, 0);
var configCard = CreateCard(360); var capabilityCard = CreateCard();
configCard.Controls.Add(CreateSectionTitle("注册配置")); capabilityCard.Margin = new Padding(7, 0, 0, 0);
configCard.Controls.Add(CreateHint( capabilityCard.Controls.Add(CreateSectionTitle("节点能力"));
"首次连接需要管理员在 zcbot 管理端生成注册码。注册码默认 10 分钟有效,成功注册一次后立即失效。")); capabilityCard.Controls.Add(CreateCapabilityBadge("Origin 绘图", "origin.plot@v1"));
AddField(configCard, "zcbot 服务地址", server, "本机测试通常使用 http://127.0.0.1:8765"); capabilityCard.Controls.Add(capabilitySummary);
AddField(configCard, "节点名称", nodeName, "用于管理员识别这台 Windows 计算节点"); capabilityCard.Controls.Add(CreateHint(
AddField(configCard, "一次性注册码", enrollmentCode, "格式类似 ZCN-…;它不是日常登录密码,也不会被长期保存"); "当前 MVP 内置声明该协议,不需要手工配置;它不代表已完成 Origin 安装检测。"));
var actions = new FlowLayoutPanel overview.Controls.Add(capabilityCard, 1, 0);
{ page.Controls.Add(overview);
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 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(CreateSectionTitle("运行设置"));
runtimeCard.Controls.Add(startAtLogin); runtimeCard.Controls.Add(startAtLogin);
runtimeCard.Controls.Add(CreateHint("关闭配置窗口后,节点仍会在系统托盘运行。右键托盘图标可重连或退出。")); runtimeCard.Controls.Add(CreateHint(
"关闭此窗口后,节点仍在系统托盘运行。右键托盘图标可以立即重连或退出。"));
page.Controls.Add(runtimeCard); page.Controls.Add(runtimeCard);
var securityNote = CreateHint("安全说明Node Token 由 Windows DPAPI 加密保存,不在界面中显示,也不会写入日志。"); var securityNote = CreateHint(
securityNote.Margin = new Padding(4, 14, 4, 0); "安全说明Node Token 由 Windows DPAPI 加密保存,不在界面显示,也不会写入日志。");
securityNote.Margin = new Padding(4, 8, 4, 0);
page.Controls.Add(securityNote); page.Controls.Add(securityNote);
register.Click += async (_, _) => await RegisterAsync(); register.Click += async (_, _) => await RegisterAsync();
@ -138,16 +181,19 @@ internal sealed class ConfigurationForm : Form
}; };
detail.Text = $"{status.Message} {status.ChangedAt:HH:mm:ss}"; detail.Text = $"{status.Message} {status.ChangedAt:HH:mm:ss}";
identity.Text = config is null identity.Text = config is null
? $"配置文件:{NodePaths.ForCurrentMachine().ConfigPath}" ? "尚未建立节点身份"
: $"节点:{config.NodeName}\nNode ID{config.NodeId}\n服务{config.ServerUrl}"; : $"节点:{config.NodeName}\nNode ID{config.NodeId}\n服务{config.ServerUrl}";
server.Enabled = config is null; capabilitySummary.Text = config is null
nodeName.Enabled = config is null; ? "注册后启用"
enrollmentCode.Enabled = config is null; : $"已声明 {config.Capabilities.Count} 项能力";
register.Enabled = config is null && status.State != NodeState.Connecting;
resetIdentity.Visible = config is not null; var registered = config is not null;
if (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; nodeName.Text = config.NodeName;
enrollmentCode.Clear(); enrollmentCode.Clear();
} }
@ -156,7 +202,7 @@ internal sealed class ConfigurationForm : Form
private void ResetIdentity() private void ResetIdentity()
{ {
var answer = MessageBox.Show( var answer = MessageBox.Show(
"这会删除本机加密身份,随后需要使用新注册码重新注册。请同时让管理员禁用云端旧节点。是否继续?", "这会删除本机加密身份,随后需要使用新注册码重新注册。请先在管理后台删除或禁用云端旧节点。是否继续?",
"清除本机节点身份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, "清除本机节点身份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2); MessageBoxDefaultButton.Button2);
if (answer == DialogResult.Yes) 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 var card = new TableLayoutPanel
{ {
AutoSize = true, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSizeMode = AutoSizeMode.GrowAndShrink,
MinimumSize = new Size(0, minimumHeight),
Dock = DockStyle.Top, Dock = DockStyle.Top,
ColumnCount = 1, ColumnCount = 1,
Padding = new Padding(22, 18, 22, 18), Padding = new Padding(20, 17, 20, 17),
Margin = new Padding(0, 0, 0, 14), Margin = new Padding(0, 0, 0, 14),
BackColor = Color.White, BackColor = Color.White,
}; };
@ -237,6 +282,15 @@ internal sealed class ConfigurationForm : Form
Margin = new Padding(0, 0, 0, 8), 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() private static Label CreateHint(string text) => new()
{ {
Text = text, Text = text,
@ -246,23 +300,44 @@ internal sealed class ConfigurationForm : Form
Margin = new Padding(0, 2, 0, 4), 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() private static TextBox CreateTextBox(string text = "", bool usePassword = false) => new()
{ {
Text = text, Text = text,
UseSystemPasswordChar = usePassword, UseSystemPasswordChar = usePassword,
AutoSize = false, AutoSize = false,
Height = 36, Height = 34,
BorderStyle = BorderStyle.FixedSingle, BorderStyle = BorderStyle.FixedSingle,
Font = new Font("Segoe UI", 10), 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 var button = new Button
{ {
Text = text, Text = text,
AutoSize = false, AutoSize = false,
Size = new Size(primary ? 128 : 220, 38), Size = new Size(width, 36),
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
BackColor = primary ? Color.FromArgb(37, 99, 235) : Color.White, BackColor = primary ? Color.FromArgb(37, 99, 235) : Color.White,
ForeColor = primary ? Color.White : Color.FromArgb(51, 65, 85), ForeColor = primary ? Color.White : Color.FromArgb(51, 65, 85),
@ -274,6 +349,15 @@ internal sealed class ConfigurationForm : Form
return button; 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( private static void AddField(
TableLayoutPanel layout, string label, Control control, string hint) TableLayoutPanel layout, string label, Control control, string hint)
{ {
@ -283,7 +367,7 @@ internal sealed class ConfigurationForm : Form
AutoSize = true, AutoSize = true,
Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold), Font = new Font("Microsoft YaHei UI", 9, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 65, 85), 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.Dock = DockStyle.Top;
control.Margin = new Padding(0, 0, 0, 2); control.Margin = new Padding(0, 0, 0, 2);