feat(ui): 界面体验一轮打磨(快捷键/暂停滚动/会话恢复/JSON 预览)
- 全局 Ctrl+Enter 发送;订阅框 Enter 提交、Esc 退出编辑;过滤框 Esc 清空 - 消息流新增「暂停/恢复」:暂停时画面不随新消息跳动,消息仍入环形缓冲 - JSON 负载折叠态预览按原始数据美化展示(多行缩进,超 40 行兜底截断) - 标签页持久化(ui-state.json),重启恢复工作区;无可恢复标签时自动打开 首个自连连接的工作区,避免「已连上却演空态」 - 侧边栏 Error 连接行内红字显示失败原因;侧边栏/订阅面板补竖分隔线 - 消息订阅色条上下留隙,不再连成一整条竖线 - 发布被当前过滤条件隐藏时给出「已发送」提示 - 连接表单:主机+端口同行;TLS 证书三个路径字段加「浏览…」(rfd) - 主题切换/系统外观变化后强制整窗重绘,修未收敛区域的旧色残影 - 设置主题文案改「跟随系统」(跟随逻辑此前已实现)
This commit is contained in:
@@ -185,7 +185,7 @@ impl ConnectionForm {
|
||||
let ssl_client_cert = input(&c.ssl.client_cert_file, "客户端证书 PEM 路径(可选)", window, cx);
|
||||
let ssl_client_key = input(&c.ssl.client_key_file, "客户端私钥 PEM 路径(可选)", window, cx);
|
||||
|
||||
let group_new = input("", "新分组名(可选)", window, cx);
|
||||
let group_new = input("", "填写后优先于左侧所选分组", window, cx);
|
||||
|
||||
let (will_topic, will_payload, will_enabled, will_retain, will_qos_idx, will_content_type, will_response_topic) =
|
||||
match &c.last_will {
|
||||
@@ -681,6 +681,66 @@ impl ConnectionForm {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// 证书路径字段 + 「浏览…」按钮:路径仍可手输,按钮负责选文件。
|
||||
fn cert_path_row(
|
||||
&self,
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
target: Entity<InputState>,
|
||||
err: Option<SharedString>,
|
||||
danger: gpui_kit::Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.child(field_ex(label, Input::new(&target), false, err, danger)),
|
||||
)
|
||||
.child(
|
||||
Button::new(id)
|
||||
.label("浏览…")
|
||||
.outline()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.browse_cert_path(target.clone(), window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// 弹原生文件对话框并把所选路径回填到输入框。rfd 的阻塞式对话框
|
||||
/// 不可占用 UI 线程,与连接导入/导出同模式:后台线程 + smol 通道回传。
|
||||
fn browse_cert_path(
|
||||
&self,
|
||||
target: Entity<InputState>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let (tx, rx) = smol::channel::bounded::<Option<std::path::PathBuf>>(1);
|
||||
std::thread::spawn(move || {
|
||||
let picked = rfd::FileDialog::new()
|
||||
.set_title("选择证书 / 密钥文件")
|
||||
.add_filter("证书 / 密钥 (PEM)", &["pem", "crt", "cer", "key", "der"])
|
||||
.pick_file();
|
||||
let _ = tx.send_blocking(picked);
|
||||
});
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(path)) = rx.recv().await {
|
||||
let text = path.display().to_string();
|
||||
this.update_in(cx, |_form, window, cx| {
|
||||
target.update(cx, |s, cx| s.set_value(text, window, cx));
|
||||
// 回填的路径可能本就带字段错误提示,触发重绘即时更新
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn switch_row(
|
||||
&self,
|
||||
id: &'static str,
|
||||
@@ -828,18 +888,19 @@ impl Render for ConnectionForm {
|
||||
// ── 基本信息(始终展开) ──
|
||||
.child(section("基本信息", border, card_bg,
|
||||
v_flex().gap_2().child(
|
||||
// 主机与端口是一对,放同一行;名称与协议/传输同行
|
||||
h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("名称", Input::new(&self.name), true, err("name"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("主机", Input::new(&self.host), true, err("host"), danger)))
|
||||
.child(div().w(px(120.)).child(field_ex("端口", Input::new(&self.port), true, err("port"), danger)))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("主机", Input::new(&self.host), true, err("host"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("名称", Input::new(&self.name), true, err("name"), danger)))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("协议", Select::new(&self.protocol))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("传输", Select::new(&self.transport))))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("分组", Select::new(&self.group_select))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("新建分组(优先于左侧选择)", Input::new(&self.group_new))))
|
||||
.child(div().flex_1().min_w(px(0.)).child(field("新建分组", Input::new(&self.group_new))))
|
||||
)
|
||||
.child(h_flex().gap_2().w_full().items_end()
|
||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("Client ID", Input::new(&self.client_id), true, err("client_id"), danger)))
|
||||
@@ -887,10 +948,18 @@ impl Render for ConnectionForm {
|
||||
"sec-ssl", "SSL/TLS", ssl_hint, true, self.show_ssl,
|
||||
|f| f.show_ssl = !f.show_ssl, border, card_bg,
|
||||
v_flex().gap_2()
|
||||
// Phase 2 或后续接原生文件对话框;GPUI 暂无内置文件选择器,先用路径文本输入
|
||||
.child(field_ex("CA 证书 (PEM)", Input::new(&self.ssl_ca), false, err("ssl_ca"), danger))
|
||||
.child(field_ex("客户端证书 (PEM)", Input::new(&self.ssl_client_cert), false, err("ssl_client_cert"), danger))
|
||||
.child(field_ex("客户端密钥 (PEM)", Input::new(&self.ssl_client_key), false, err("ssl_client_key"), danger))
|
||||
.child(self.cert_path_row(
|
||||
"ssl-ca-browse", "CA 证书 (PEM)", self.ssl_ca.clone(),
|
||||
err("ssl_ca"), danger, cx,
|
||||
))
|
||||
.child(self.cert_path_row(
|
||||
"ssl-cert-browse", "客户端证书 (PEM)", self.ssl_client_cert.clone(),
|
||||
err("ssl_client_cert"), danger, cx,
|
||||
))
|
||||
.child(self.cert_path_row(
|
||||
"ssl-key-browse", "客户端密钥 (PEM)", self.ssl_client_key.clone(),
|
||||
err("ssl_client_key"), danger, cx,
|
||||
))
|
||||
.child(self.switch_row("ssl_ignore_ca", "忽略 CA 校验(不验证服务器证书)", self.ssl_ignore_ca, cx)),
|
||||
cx,
|
||||
)))
|
||||
|
||||
Reference in New Issue
Block a user